# CLI

> MCP Inspect CLI reference for snapshot, diff, check, watch and push. Compare MCP contracts locally or in CI without an account.

```bash
mcp-inspect <command> [options]
```

## Commands

### init

```bash
mcp-inspect init --snapshot
mcp-inspect init --server search --snapshot
mcp-inspect init --workflow
```

Discover a server and capture its baseline in one step with `--snapshot`.
Use `--server` to select a named server when a client config contains several.
`--workflow` generates a repository-appropriate GitHub workflow without replacing
an existing workflow. Review its build command and configure server secrets in CI.

A capture establishes a baseline; it does not check compatibility. After changing
and rebuilding your server, compare locally with
`mcp-inspect check --baseline-file .mcp-inspect/surface.json`.

### snapshot

Capture the MCP surface.

```bash
mcp-inspect snapshot
mcp-inspect snapshot --stdio "node dist/server.js"
mcp-inspect snapshot --url https://api.acme.com/mcp --header "Authorization: Bearer $T"
mcp-inspect snapshot --out surface.json --pretty --push
```

Only `server/discover` and the `*/list` methods are ever called — all read-only
by specification. **The inspector never invokes a tool.** Pagination is
exhaustive: on hitting a page cap it fails rather than recording a truncated
surface, because a truncated list reads as a mass removal on the next diff.

An HTTP server that answers `401` starts the MCP authorization flow: resource
metadata, authorization server metadata, PKCE, a browser tab, a redirect back
to a listener on `127.0.0.1`. It runs when a terminal is attached (or with
`--oauth`); in CI a `401` is an error that names the alternatives, such as
`--header "Authorization: Bearer …"` or `--oauth-client-id`. **The token stays in
memory for one process and is never written to a snapshot, a config, or a file.**

Use `--out - --raw capture.json` to pipe the normalized JSON while retaining raw
protocol evidence in a file. Explicit `--out` or `--raw` paths require
`--server <name>` when capturing several configured servers.

### diff

```bash
mcp-inspect diff old.json new.json --format markdown --fail-on POTENTIALLY_BREAKING
```

Formats: `text` (default), `markdown`, `json`, `sarif`. The JSON report is a
supported integration surface and is versioned.

### check

Snapshot the working tree's server, resolve a baseline, diff, apply ignores, exit.

```bash
mcp-inspect check --baseline origin/main
mcp-inspect check --format markdown --output comment.md
```

Baseline resolution, in order: `--baseline-file`, then a git ref
(`git show <ref>:<path>`), then the hosted API, then nothing. A missing baseline is explicitly reported as **comparison
skipped**, with exit 0 so the setup PR can merge. It is not evidence of a passing
comparison.

For automation, `--format json --output report.json` writes a report even on
exit 1. A missing baseline reports `status: "skipped"` and `compared: false`.
Checks covering several servers emit one JSON document with
`{ formatVersion: 1, reports: [{ server, report }] }`; inspect each named outcome.
SARIF contains one run per server with `runs[].properties.server` and retains
skipped-comparison metadata. Selecting `--server <name>` gives the existing
single-report shape. The process exits with the worst code across the checks;
a skipped entry still needs a baseline before its compatibility can be assessed.

Under the summary, one advisory line compares the declared version with what
the diff found — `breaking changes suggest a major bump to 3.0.0` — when both
surfaces declare semver. It never changes the verdict.

### watch

```bash
mcp-inspect watch --path src
```

The develop loop: snapshot, diff against the baseline, print, then re-run on
every save under the watched paths. What it prints is what `check` prints in
CI. Ctrl-C stops it.

### doctor

Config, server reachability, what it advertises, baseline, hosted status. The
command to run when `check` did something unexpected.

### keys

```bash
mcp-inspect keys list
mcp-inspect keys create --kind api --name github-actions
mcp-inspect keys rotate key_01k4…
mcp-inspect keys revoke key_01k4…
```

The plaintext goes to **stdout alone** and everything else to stderr, so
`mcp-inspect keys create … > key.txt` captures the key and nothing else.

Rotation mints a replacement and leaves the previous key working for a week. A
rotation that breaks every build the instant it happens is one nobody performs
twice. `revoke` stops a key immediately.

### push and usage

`push` records a snapshot as a deployment. `usage` prints observed usage for a
tool, including argument presence and the observation window. Both need an
account; `push` exits 0 even when the upload fails, because losing a snapshot
must never break a build.

## Exit codes

| Code | Meaning                                                              |
| ---- | -------------------------------------------------------------------- |
| `0`  | Passed, or there was nothing to compare.                             |
| `1`  | The change exceeded the fail threshold.                              |
| `2`  | The command could not run: server unreachable, bad config, bad JSON. |

`2` is never `0`. A snapshot that could not be taken is not "no changes".

| Command    | `0`                                       | `1`      | `2`                                        |
| ---------- | ----------------------------------------- | -------- | ------------------------------------------ |
| `snapshot` | captured                                  | —        | unreachable, bad config                    |
| `diff`     | at or below `fail_on`                     | above it | missing, corrupt, or wrong `formatVersion` |
| `check`    | at or below `fail_on`, **or no baseline** | above it | unreachable, bad config                    |
| `push`     | uploaded, **or the upload failed**        | —        | the snapshot could not be read             |
| `keys`     | done                                      | —        | no token, or a missing argument            |
| `doctor`   | every check passed or warned              | —        | a check failed                             |

## Configuration

`.mcp-inspect.yml`, resolved from the working directory upward.
`.mcp-inspect.json` is accepted identically.

```yaml
server:
  command: node dist/server.js # or: url: https://api.acme.com/mcp
  env:
    DATABASE_URL: ${DATABASE_URL} # ${VAR} interpolates; an unset one is an error
  # headers:
  #   Authorization: Bearer ${MCP_TOKEN}
  timeout_ms: 30000

baseline:
  source: git # git | file | api
  ref: origin/main
  path: .mcp-inspect/surface.json

fail_on: BREAKING

ignore:
  - rule: tool.icons.changed
severity_overrides:
  tool.annotations.safetyWeakened: BREAKING

project: acme/search-mcp # hosted; everything above works without it
```

Several servers in one repository: replace `server:` with a `servers:` map,
one named entry each, and every command runs against each in turn with its own
baseline at `.mcp-inspect/<name>.json`; `--server <name>` picks one.

```yaml
servers:
  api:
    command: node dist/api.js
  internal:
    url: https://internal.example/mcp
    oauth:
      client_id: ${INTERNAL_OAUTH_CLIENT} # optional; `oauth: false` disables the browser flow
```

An unset `${VAR}` is a hard error rather than an empty string. An empty auth
header produces a smaller surface, which the next diff reports as a mass
removal — a loud failure is the only safe behaviour.

A top-level key within two edits of a real one (`sever:`, `failon:`) is an error
naming what you probably meant. A genuinely unrelated key is ignored, so a config
written for a newer version still loads.
