mcpg inspector
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:
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
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 atdefault-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/callrequests 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:
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
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:
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:
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.
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:
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
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:
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:
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:
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:
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-Digestis signed by default. Every MCP call isPOST /mcp, so without it the signature would say nothing about which call was made.EdDSAvsEd25519. The JOSEalgis the fully-specifiedEd25519. The polymorphicEdDSAis forbidden by the draft and refused by mcpg, so there is no switch for it. Not to be confused with the RFC 9421alginsideSignature-Input, a different registry spelled lowercaseed25519— 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
- Inspect an MCP server — the guide this page is the reference for
- Quickstart — bring up a gateway and point the inspector at it
mcpggateway CLI