Gate CI on it
Exit codes are a contract, so a pipeline can fail on capability drift, on a server that accepts what its own schema forbids, or on a gateway that answered without actually serving.
Every one-shot verb takes --json and prints its result document as the only
thing on stdout. That, plus exit codes that mean something, is what makes these
usable as gates rather than as things a human reads.
Exit codes
| Code | Meaning |
|---|---|
0 | ok |
1 | usage |
2 | connect / probe failed |
3 | auth required |
4 | unreachable |
5 | the operation failed, or the report found something |
5 is the interesting one. It separates "I could not ask" from "I asked, and
the answer is bad" — a distinction a pipeline needs, because the first is
usually infrastructure and the second is usually your change.
Fail a PR when the tool surface drifts
mcpg inspector snapshot https://gw.example/mcp --json > baseline.json
mcpg inspector diff https://gw.example/mcp --against baseline.json --mode strict
diff exits 5 when the diff fails its mode. Commit the baseline, and a
config change that quietly adds, removes or reshapes a tool stops being
something you discover from a client.
Fail when a server accepts what it forbids
mcpg inspector fuzz https://gw.example/mcp
fuzz derives its cases from each tool's own inputSchema — 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 calls 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.
Fail when the gateway answered but is not serving
mcpg inspector gateway https://gw.example/mcp --json \
| jq '.plugins[] | select(.state != "active")'
The verb exits 5 when a readiness check is not passing or a plugin loaded but
is not active. A plain HTTP health check cannot see either — the gateway
answers fine while the plugin behind half its tools sits degraded.
Measure latency
mcpg inspector bench https://gw.example/mcp dev.mock.echo -n 50
bench calls one tool -n times and reports min / p50 / p90 / p99 / max in
milliseconds, fractional — a local stdio server answers well inside a
millisecond. The first calls are warmup (--warmup), so a cold connection is
not reported as the server's latency.
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.
Conformance in a pipeline
mcpg inspector check https://gw.example/mcp
Checks that do not apply to the negotiated revision are skipped, not failed, so this stays meaningful across both wires without a per-revision matrix.
Unattended runs
A server can ask the client to sample a model, elicit a value, or list roots —
and the call that triggered one waits for an answer. In CI there is nobody to
answer, so set responder on the target to auto-decline or mock. Under
those policies nothing ever queues.
The inspector never generates a model answer on your behalf under any policy.
A worked example
- name: The tool surface has not drifted
run: |
mcpg inspector diff "$GW" --against .ci/tools-baseline.json --mode strict
- name: The gateway is serving, not merely answering
run: mcpg inspector gateway "$GW"
- name: Tools enforce their own schemas
run: mcpg inspector fuzz "$GW"
env:
GW: ${{ secrets.GATEWAY_URL }}
Each step fails the job on 5 without any parsing, because the exit code
already carries the verdict. Add --json and redirect when you want the
document as an artifact too.
Next steps
- Debug a server — when a gate goes red.
- Recordings — attach the frames to the report.
mcpg inspectorreference — every verb and flag.