Reference
Referencebeta

mcpg inspector

beta

Inspect any MCP server — web UI, terminal UI and scriptable CLI in one binary, pre-wired when run against a gateway.

mcpg inspector — inspect an MCP server

Every verb and flag. For the task-oriented walk-through — what to reach for when a tool is missing, a call is refused, or you need to know what the wire actually said — start with Inspect an MCP server.

One binary, three faces over the same engine:

text
mcpg inspector serve         Web UI + HTTP API on one origin (default 127.0.0.1:7846).
mcpg inspector tui           Terminal UI, same engine, for SSH and terminal-first work.
mcpg inspector list …        One-shot verbs that print JSON: list / call / read /
mcpg inspector call …        prompt / complete / auth / login / check / snapshot /
mcpg inspector check …       diff / gateway / bench / fuzz. Exit codes are a
                             contract, so CI can gate on them.

The web UI has a pane per surface — tools, resources (and templates), prompts, subscriptions, diagnose (the auth lab, the protocol checks and the gateway behind the target) and wire.

The TUI covers the same surfaces: targets, tools, resources, prompts, subs, pending, diagnose and wire. tab (or h/l) moves between them, j/k selects, c connects, a edits the arguments for whatever is selected, and enter runs whatever the screen is about — calling a tool, reading a resource, rendering a prompt, starting or stopping the push stream, running the protocol checks. The argument buffer reseeds per screen, so a tool's arguments never follow you to a prompt; the subscription screen keeps its own watch list, which survives the screens you visit while it is listening.

Where a tool declares an input schema, arguments are a form: one control per field, typed the way the schema declares it, required fields first, with descriptions and enum choices in place. That typing is the point — a control hands back text for everything, and {"count": "7"} is a different call from {"count": 7}. The raw JSON is one toggle away and edits the same value, in both faces (f in the TUI), because sending what a schema does not describe is half of what an inspector is for. Nothing is ever blocked: a value the schema disagrees with is reported and still sent, since finding out how a server handles it is the job.

Where the protocol defines completions — prompt arguments and resource-template variables, not tool arguments — the form can ask the server what fits: suggest beside a field in the web UI, s in the TUI's form. The values are the server's own, narrowed by whatever is already typed; nothing is guessed locally, which is why the control only appears where a completion surface exists.

A resource template's variables are fields too, and they keep the order the URI names them in — {year}/{id} offers year first, because a template carries its own order even though a schema's properties do not.

Results read the same way. A tool that declares an output schema gets its structuredContent shown as labelled rows, content blocks are rendered as the kind they say they are — text, image, embedded resource — and the raw envelope stays one click (or J) away. Prompt arguments and resource-template variables get the same form, shaped from what those surfaces declare instead of from JSON Schema.

MCP runs in both directions: a server may answer a call by asking the client to sample a model, elicit a value, or list its roots. Those land in pending, and the call that triggered one waits for the answer. Both faces surface it where you already are — the web UI as a banner above whatever pane is open, the TUI by switching to the pending screen and badging the tab — because a request filed behind a tab you have not opened is holding a call open that you cannot see. enter sends the answer, x declines; declining is a legitimate answer, not an error.

The answer is seeded per request kind and never generated: the inspector will not call a model on your behalf. Set responder on a target to auto-decline or mock to answer without a human, which is what the CLI and CI want — under those policies nothing ever queues.

mcpg-inspector tui --attach <url> points the terminal at a running inspector instead of dialing targets itself — its targets, its sessions, its wire log. That is the difference that matters against a gateway sidecar: the sidecar holds the frames of the gateway's own traffic, and a TUI dialing the gateway directly opens a fresh session and sees none of them. Pass the token in MCPG_INSPECTOR_TOKEN rather than in the URL; a command line is readable by every process on the box.

On subs, an empty watch list is a real subscription — it follows tools, prompts and resources list-changed notifications. Add comma-separated resource URIs with a to watch those resources' contents as well. diagnose runs the portable protocol checks against an HTTP target and reports each as passed, failed, or skipped; skipped means the check does not apply to the revision this target negotiated, which is not a failure.

Reached as mcpg inspector … through the front door, or invoked directly as mcpg-inspector. It speaks both MCP revisions mcpg speaks — the sessionful 2025-11-25 wire and the stateless 2026-07-28 wire — using the gateway's own client, so what the inspector negotiates is what the gateway would negotiate.

Attached to a gateway

bash
mcpg --config server.yml --inspector

The gateway supervises a sibling inspector process: readiness-polled, killed with its parent, and pre-registered against that gateway — nothing to copy, paste or configure. It hands the child:

  • this gateway as a target, at the scheme and path it actually serves, with a per-boot identity that clears the default trust floor. On a stock config it lists tools immediately, where an anonymous caller would see none;
  • every federated upstream, so one session can compare what mcpg re-serves against what the upstream itself offers — the question behind most "why is this tool missing" reports. Each carries the wire the federation pinned and the same private-address posture the gateway uses for it.

No credential rides on the command line. The gateway's own token travels by environment, because argv is readable by every process on the box; a federation's credential stays with the gateway, since the inspector dialling that upstream directly is a different caller.

The URL is printed once the gateway itself is answering, not when the sidecar comes up. The sidecar is ready in milliseconds while the gateway still has plugins to load, so announcing early would put the first click on a refused connection to the pre-wired target — which looks like the inspector being broken. If the gateway does not come up within a minute, the URL is printed anyway, with a note that its own target will not connect yet.

--inspector-bind, --inspector-auth-none and the rest pass through to the child.

Getting around

The server you are looking at is a picker in the header, not a column — which server, the wire it negotiated, and whether it is up, in the width of its own label. Adding one lives inside it.

history keeps every tool call, resource read and prompt render: what was sent, what came back, how long it took, and whether it failed. "Load into tools" puts an old call's arguments back in the pane that sent them rather than re-running it, because re-running silently is how you lose the difference you were comparing.

A subscription outlives the tab that started it. The stream belongs to the target, so leaving for the tools pane is navigation rather than cancellation, and the subscriptions tab carries a count of what arrived while you were away. The pane says what is being watched and since when; pushes are grouped by what they are about, with the raw sequence one click away.

The TUI has the same two: a history screen (enter loads an entry back into the screen that sent it) and a count on the subs tab.

MCP Apps

A tool may ship an HTML app (SEP-1865): _meta.ui.resourceUri names a ui:// resource whose contents are the page, and whose own _meta.ui declares the network it wants and the browser permissions it asks for. The web UI renders it; the TUI names it and points at the resource, since a terminal cannot run a page.

The inspector is a host pointed at servers nobody trusts, so three rules hold:

  • Nothing runs until you ask. The page renders on a click, and the click says what it is agreeing to.
  • The frame is sandboxed without allow-same-origin. That one attribute is what keeps the page out of this document, its storage, and your session token — with it, an untrusted server's HTML would have all three. The CSP starts at default-src 'none' and adds only what the app declared, so a page that declares nothing reaches nothing.
  • Everything the app asks for is shown. Its tools/call requests run against the same target under the same identity, so they appear in the wire pane like any other call — and the ones this host declines are listed too, with what was asked, because that is the more interesting half.

Recordings

Any target's exchange can be written out as a recording — the header button in the web UI, w in the TUI, or GET /api/v1/targets/{id}/export. It is NDJSON: one header line saying which server and which wire, then the frames as they crossed.

Replay one anywhere a target goes:

bash
mcpg-inspector tui --target recording:./mcpg-inspector-gateway-1786.jsonl
mcpg-inspector list recording:./that-file.jsonl tools

Every surface works on it — the catalog, a call, the wire pane — because nothing above the session knows it is replaying. It dials nothing, so it works with the server gone, which is the point. A call the recording does not contain says so rather than inventing an answer.

A recording is a file; sharing one is sending it. The inspector stores none — no share links, no retention, no custody of anyone's traffic (see RFC 0003). Frames are run through the same credential redaction the gateway applies to audit records, and the target is described rather than copied, so a bearer token never reaches the file. What remains is your own traffic — including arguments you chose to send — so treat a recording as the traffic it is.

Standalone

bash
mcpg inspector serve --target http://127.0.0.1:8787/mcp --open

The URL it prints carries a per-boot session token. The API can spawn processes for stdio targets, so it binds loopback and requires that token; --auth-none drops it but only on a loopback bind, and reaching beyond this host needs the deliberately ugly --dangerously-bind-all-interfaces.

Targets take three forms anywhere one is accepted:

bash
http://host:port/mcp                     # Streamable HTTP
stdio:npx -y @modelcontextprotocol/server-everything
'{"url": "https://gw.example/mcp", "bearer": "…"}'   # JSON, for the full spec

Scripting

Every one-shot verb takes --json and prints its result document as the only thing on stdout:

bash
mcpg inspector list http://127.0.0.1:8787/mcp tools --json | jq '.tools[].name'
mcpg inspector call http://127.0.0.1:8787/mcp dev.mock.echo --args '{"text":"hi"}'
mcpg inspector read http://127.0.0.1:8787/mcp 'docs://runbook'
mcpg inspector prompt http://127.0.0.1:8787/mcp greet --args '{"who":"world"}'
mcpg inspector complete http://127.0.0.1:8787/mcp prompt:greet who w
mcpg inspector snapshot http://127.0.0.1:8787/mcp --json > baseline.json
mcpg inspector diff http://127.0.0.1:8787/mcp --against baseline.json --mode strict
mcpg inspector gateway http://127.0.0.1:8787/mcp --json | jq '.plugins[] | select(.state != "active")'
mcpg inspector bench http://127.0.0.1:8787/mcp dev.mock.echo --args '{"text":"hi"}' -n 50
mcpg inspector fuzz http://127.0.0.1:8787/mcp

Exit codes: 0 ok, 1 usage, 2 connect/probe, 3 auth required, 4 unreachable, 5 operation or tool error. diff exits 5 when the diff fails its mode, which is how you gate a PR on capability drift.

check runs the portable protocol checks — the ones any conformant server should pass — and reports each as pass or fail with the frame that decided it.

When the tool is missing and the config looks right

gateway reads what the mcpg gateway serving this endpoint says about itself: readiness, which plugins loaded, and which of them are not running. A binding can resolve perfectly in config and still answer nothing because the plugin behind it came up degraded — and the MCP surface shows only the consequence, a tool that is not there.

bash
mcpg inspector gateway http://127.0.0.1:8787/mcp

It exits 5 when something wants attention — a readiness check that is not passing, or a plugin that loaded but is not active — so a smoke test can gate on "the gateway is actually serving", not merely "it answered".

The report comes from the gateway's /runtime, which mcpg serves without authentication and with operator configuration deliberately left out of it (sink kinds, not sink settings). Against a server that is not an mcpg gateway, the verb says so rather than guessing.

In the web UI it is the third diagnose tab; in the TUI, g on the diagnose screen. Both lead with what is wrong: a gateway running twelve healthy plugins lists none of them, because the one that matters is the one that is not healthy.

How fast, and how strict

bench calls one tool -n times and reports min / p50 / p90 / p99 / max in milliseconds. Calls go out one at a time: concurrency measures something else, and an inspector pointed at someone else's server should not be the thing that decides to load-test it. The first couple of calls are warmup (--warmup), so a cold connection is not reported as the server's latency. Timings are fractional, because a local stdio server answers well inside a millisecond.

fuzz sends a tool the input its own schema forbids — a missing required property, the wrong type, an oversized string, an unknown property — and reports what came back. A server that enforces its schema rejects the invalid cases and accepts the valid one; anything else is flagged surprising, and the run exits 5.

It only touches tools that declare readOnlyHint: true. Everything else is skipped by name, with the reason, because these are real calls:

bash
mcpg inspector fuzz https://gw.example/mcp                 # read-only tools
mcpg inspector fuzz https://gw.example/mcp incident.file --include-writes

--include-writes is deliberately awkward to reach for. The tool that forgot to annotate itself is often the one that moves something.

Working out why a server said no

bash
mcpg inspector auth https://gw.example/mcp

auth sends one deliberately credential-free request and reports what came back: the WWW-Authenticate challenge, every step of the RFC 9728 → RFC 8414 discovery walk including the one that failed, and a one-line verdict naming the next action. A half-walked chain reads as "it advertises OAuth but its authorization server could not be read", never as "no OAuth here" — those are different problems with different fixes.

It also reports AAuth posture, which lives in a separate channel: AAuth-Requirement, Signature-Error, and the target's /.well-known/aauth-resource.json.

Getting a token

Once auth finds an authorization server, login walks the rest:

bash
mcpg inspector login https://gw.example/mcp

It registers a client dynamically (RFC 7591, which is how MCP clients work — you cannot pre-register everywhere), opens a browser for an authorization-code grant with PKCE, catches the redirect on loopback, and prints the access token on stdout. Pass it straight back:

bash
TOKEN="$(mcpg inspector login https://gw.example/mcp)"
mcpg inspector list https://gw.example/mcp tools --bearer "$TOKEN"

--client-id uses a client you registered yourself; --scope overrides what to ask for (the default is whatever the challenge named); and --no-browser prints the URL instead of opening one, for SSH sessions. The token is audience-bound to this server via RFC 8707 resource indicators, so it is not replayable at another resource that trusts the same authorization server.

AAuth

AAuth gives an agent a portable cryptographic identity — aauth:local@domain bound to an Ed25519 key — and signs every request with it, so a captured credential is useless without the key. mcpg verifies it via the dev.mcpg.identity.aauth plugin; the inspector can present it.

There is no registration step. Trust is domain control plus a published key, so getting an identity means generating one and serving two static documents:

bash
mcpg inspector aauth-keygen \
  --agent aauth:inspector@yourdomain.example \
  --publish ./site

That writes ./site/.well-known/aauth-agent.json and ./site/.well-known/jwks.json, and prints the private seed. Serve the two documents under https://yourdomain.example, then sign with:

bash
mcpg inspector list https://gw.example/mcp tools \
  --aauth-key "$MCPG_INSPECTOR_AAUTH_KEY" \
  --aauth-agent aauth:inspector@yourdomain.example \
  --aauth-issuer https://yourdomain.example

--aauth-key reads MCPG_INSPECTOR_AAUTH_KEY so the seed need not appear in a process listing. If you already have an agent token from an agent provider, pass it with --aauth-token and the token's own sub names the agent.

The inspector implements the identity-based access mode — the agent token is the credential — which is the mode mcpg verifies. The modes that require a person server and a human approval are reported by auth but not driven.

Two details worth knowing:

  • Content-Digest is signed by default. Every MCP call is POST /mcp, so without it the signature would say nothing about which call was made.
  • EdDSA vs Ed25519. The JOSE alg is the fully-specified Ed25519. The polymorphic EdDSA is forbidden by the draft and refused by mcpg, so there is no switch for it. Not to be confused with the RFC 9421 alg inside Signature-Input, a different registry spelled lowercase ed25519 — this is the most common interop failure in AAuth.

Hosted

The hosted inspector is free to use — there is no plan, tier or licence behind it, and the same is true of the CLI.

--hosted is the profile inspector.mcpg.cloud runs. It is a profile rather than one switch: no stdio targets, no private-address egress (overridden, not merely validated), per-address rate limits, and OIDC sign-in — a hosted instance refuses to boot without an issuer, and refuses --auth-none and --session-token outright, because one shared credential would mean one shared workspace.

Each signed-in user gets their own workspace: targets, and the credentials attached to them, are never visible to anyone else. Nothing is persisted, so a restart signs everyone out.

Point it at a public server, or at a local one through mcpg --tunnel.

See also