CLI reference

mcpgw is a single binary. The CLI plus YAML config is the only administrative surface — there is no web UI. The commands are grouped into:

Group Purpose
Process lifecycle serve / no args, stdio, version, healthcheck, reload
Diagnostics doctor
Config config validate, config show, config init
Keys key generate, key list, key revoke, key rotate
License license info, license verify
Policy policy lint, policy test, policy explain, policy replay
Upstream upstream list, upstream ping
Tool index tools list, tools search
Telemetry trace probe
Audit audit tail, audit follow, audit stats
Shell completion bash, completion zsh

Every command supports --help / -h. Every read-only command supports --json for machine-parseable output where it makes sense (the JSON form is documented per-command below).

Exit codes

Stable contract — scripts can branch on these.

Code Meaning
0 Success
1 Generic failure (network error, denied policy decision, etc.)
64 Usage error (bad flags) — EX_USAGE
70 Internal error — EX_SOFTWARE
74 I/O error — EX_IOERR
78 Config invalid or check failed — EX_CONFIG

Process lifecycle

serve

Run the gateway as a long-running HTTP server. This is the default action — mcpgw with no arguments is equivalent to mcpgw serve.

mcpgw [serve] [--config=PATH] [--pidfile=PATH]
Flag Default Notes
--config /etc/mcpgw/mcpgw.yaml Path to the YAML config. Validation errors are fatal at startup.
--pidfile — Optional path to write the server PID. Required if you want to use mcpgw reload. Refuses to clobber an existing pidfile that names a live process.

Signals

Signal Action
SIGHUP Re-read config; atomically swap policy, auth store, OAuth verifier, CIMD fetcher, tool index, guardrail registry, TLS reloaders.
SIGTERM, SIGINT Graceful shutdown. Flush audit + OTLP.

Endpoints

Path Method Purpose
/mcp POST The MCP JSON-RPC endpoint.
/healthz GET Liveness.
/readyz GET Readiness (license validity).
/.well-known/oauth-protected-resource GET RFC 9728 metadata (when OAuth enabled).

stdio

Bridge a stdio-based MCP client (Claude Desktop, Cursor) to a remote gateway.

mcpgw stdio --upstream=URL
Flag Default Notes
--upstream — Required. Base URL of the gateway. /mcp is appended automatically.

version

mcpgw version [--json]

Without --json: prints mcpgw <version>.

With --json: emits a JSON object with version, go_version, goos, goarch, vcs_commit, vcs_time, vcs_dirty, and license_pubkey_fingerprint (first 8 hex chars of the embedded Ed25519 pubkey). Use it from scripts that need to assert the running binary is what you think it is.

healthcheck

Probe a running gateway’s /healthz and exit non-zero on failure. Used by docker HEALTHCHECK.

mcpgw healthcheck [--url=URL] [--timeout=DUR]
Flag Default Notes
--url http://127.0.0.1:7332/healthz Endpoint to probe.
--timeout 2s Request deadline.

reload

Send SIGHUP to a running gateway so it re-reads config and atomically swaps policy, auth store, OAuth verifier, CIMD fetcher, tool index, guardrail registry, and TLS reloaders. The server must have been started with --pidfile=PATH.

mcpgw reload --pidfile=PATH
mcpgw reload --pid=N
Flag Default Notes
--pidfile — Path to pidfile written by mcpgw serve --pidfile=PATH.
--pid — Explicit PID (override). Mutually exclusive with --pidfile.

Exit 1 if the target PID is unsignalable (dead process, foreign uid).


Diagnostics

doctor

Comprehensive preflight check. Exercises every external dependency the gateway will touch at startup, plus the local prerequisites. Never starts the listener — safe to run repeatedly during config iteration.

mcpgw doctor [--config=PATH] [--timeout=DUR] [--offline] [--json]
Flag Default Notes
--config /etc/mcpgw/mcpgw.yaml Path to config.
--timeout 10s Per-check network timeout.
--offline false Skip checks that need network (upstreams, JWKS, OTLP, sinks).
--json false Emit a JSON list of check results.

Checks performed

Name Verifies
config.load YAML parses and passes config.Validate.
license License JWT exists, signature matches embedded pubkey, days-to-expiry. Warning at < 14 days, error past grace.
tls.keypair Cert + key load; leaf certificate’s not_after. Warning at < 30 days, error if expired.
tls.client_ca (if mTLS) Bundle contains parseable PEM certificates.
auth.keys Argon2id hashes parse; auth.NewStore succeeds.
audit.path Audit directory exists and is writable (probed with a temp file).
upstream.<name> One tools/list POST per upstream; reports tool count + latency.
oauth.verifier oauth.NewVerifier constructs successfully (resolves JWKS).
telemetry.customer / telemetry.operator TCP/TLS handshake against OTLP endpoint (no spans sent).
audit.sinks[].<type> Webhook/Kafka TCP reachability; S3/GCS metadata sanity.
guardrail.<name> One plain GET per guardrail webhook endpoint; any HTTP status < 500 counts as reachable. Reports TLS validity and round-trip latency. An unreachable endpoint is an error when fail_open: false (every matched request would be denied at runtime), a warning when fail_open: true (traffic would flow with the guardrail bypassed). Configured static headers are not attached to the probe, so a 401 from an auth-gated endpoint still counts as reachable. guardrails reports skipped when none are configured.

Exit codes

  • 0 — all checks ok or warning
  • 78 — one or more checks errored

config validate

Parse + validate a config file without starting the server. Identical to the config.load step doctor runs.

mcpgw config validate [--config=PATH | PATH]

The path can be given either as --config or as a bare positional argument; passing both is a usage error.

config show

Print the effective config with defaults applied, secrets masked. Useful for confirming env-var substitution and unset fields.

mcpgw config show [--config=PATH | PATH] [--unmask]

Like config validate, the path can be passed positionally instead of via --config.

Flag Default Notes
--unmask false Include argon2id hashes and webhook header values verbatim. Off by default so the output is safe to share.

config init

Write a starter mcpgw.yaml to stdout (or a path). Refuses to overwrite an existing file.

mcpgw config init [--out=PATH]
Flag Default Notes
--out - Output path; - for stdout. Refuses to overwrite existing files.

Keys

key generate

Mint a new API key and emit a YAML stanza for auth.keys[]. The raw key is printed once.

mcpgw key generate [--label=ID] [--expires=DUR]
Flag Default Notes
--label key-1 Human-readable key id stored in audit + spans.
--expires 2160h (90d) Key lifetime. 0 for no expiry.

key list

List configured key ids, scopes, created_at, expires_at. Hashes are never printed.

mcpgw key list [--config=PATH]

Status is EXPIRED when expires_at < now. If auth.enabled: false, prints a notice and exits 0.

key revoke

Emit an updated YAML stanza with one key removed. Does not mutate the config file — paste the output back yourself. This keeps the CLI safe to run on production configs.

mcpgw key revoke --id=KEY_ID [--config=PATH]

Exit non-zero if the id is not found.

key rotate

The canonical rotation flow: mint a fresh key, set the old key’s expires_at to now + grace so clients have a window to switch over, and emit the combined auth.keys stanza. As with revoke, the config file is not mutated.

mcpgw key rotate --id=ID [--new-id=ID] [--grace=DUR] [--new-expires=DUR] [--config=PATH]
Flag Default Notes
--id — Required. Id of the key to rotate.
--new-id <old>-next Id for the new key.
--grace 24h How long the old key remains valid after rotation.
--new-expires 2160h (90d) Lifetime of the new key; 0 = no expiry.

The raw new key is printed once. After clients have switched, run mcpgw key revoke --id=<old> to drop the old key entirely.


License

license info

Print claims, days-to-expiry, grace-period status. Verifies the JWT signature against the embedded pubkey.

mcpgw license info --path=PATH [--json]

JSON shape:

{
  "subject": "acme",
  "tier": "enterprise",
  "seat_count": 50,
  "issued_at": "2026-01-01T00:00:00Z",
  "expires_at": "2027-01-01T00:00:00Z",
  "grace_days": 30,
  "grace_until": "2027-01-31T00:00:00Z",
  "days_to_expiry": 220,
  "expired": false,
  "grace_period": false
}

license verify

Exit 0 if the signature matches; exit 1 if it does not. Quieter than info — for scripting.

mcpgw license verify --path=PATH

Policy

The policy CLI mirrors the same engine the proxy uses. There is no shadow engine — policy test and the live gateway always agree.

policy lint

Structural sanity over rules. Reports:

  • Errors: missing rule id, duplicate rule id.
  • Warnings: unreachable rules (an earlier universal matcher would short-circuit), conflicting actions for the same matcher, a legacy MRTR method rule with no result_type: input_required counterpart (legacy-mrtr-method), and a guardrail rule with direction: server_to_client (guardrail-sse-direction — the callout fires on buffered JSON responses only; it does not apply to SSE frames in v1).
mcpgw policy lint [--config=PATH]

Exit 0 if no errors (warnings allowed); 78 if any error.

policy test

Simulate one request through the engine and print the decision.

mcpgw policy test [--config=PATH] [--method=METHOD] [--tool=NAME] \
                  [--direction=client_to_server|server_to_client] \
                  [--session=ID] [--body=FILE] \
                  [--subject=SUB] [--client-id=ID] \
                  [--scopes=S1,S2] [--groups=G1,G2] \
                  [--guardrail-stub=pass|mask|reject] [--json]
Flag Default Notes
--method tools/call JSON-RPC method.
--tool — Tool name. Required when --method=tools/call.
--direction client_to_server Stream direction (matters for SSE-frame rules).
--session test-session Synthetic principal used by rate_limit rules. Historical flag name.
--body — Optional path to a raw JSON request body. Use this to test redact patterns against captured traffic.
--subject — Synthetic JWT sub for when.claims.subject_in.
--client-id — Synthetic OAuth client_id for identity-aware policy debugging.
--scopes — Comma-separated synthetic OAuth scopes for when.claims.scopes_in.
--groups — Comma-separated synthetic OAuth groups for when.claims.groups and groups_in.
--guardrail-stub — Force every guardrail decision to this verdict offline: pass (→ allow), mask (→ redact), or reject (→ deny). No network call is ever made. Any other value is a usage error (exit 64).

When a guardrail rule matches without --guardrail-stub, the output names the endpoint and notes it cannot be resolved offline. With the stub, the effective outcome is printed (a mask stub passes the body through unchanged — the webhook’s mutated body is unknowable offline — with a note on stderr). --json output gains "guardrail": "<endpoint>" whenever the decision named one, plus a "guardrail_stub": "<verdict>" provenance field when stubbed, so a stub-forced outcome is distinguishable from an engine-derived one.

Exit codes

  • 0 on allow, redact, strip_app
  • 1 on deny, rate_limit_blocked

policy explain

Show every rule that would match a given request, mark the winner and the shadowed ones.

mcpgw policy explain [--config=PATH] --tool=NAME [--method=METHOD] [--direction=...]

Useful for “why is my rule not firing?” — anything tagged SHADOW is matched but bypassed because an earlier rule already decided the request.

policy replay

Re-evaluate captured audit records against the current policy. Use this to answer “what would change if I deployed this policy candidate against last hour of traffic?” before you ship the change.

mcpgw policy replay [--config=PATH] (--audit=PATH | --stdin) [--limit=N] [--diff-only] [--json]
Flag Default Notes
--audit — Path to JSONL audit log to replay. Mutually exclusive with --stdin.
--stdin false Read records from stdin (pair with tail, grep, etc.).
--limit 0 (no limit) Stop after N records.
--diff-only false Skip the recorded-vs-replayed breakdown; print only divergences.
--json false Emit a structured summary + full divergence list.

Output

  • Action histograms — how the records break down by recorded action vs by replayed action under the current policy.
  • Rule firing counts — how many records each rule in the current policy would fire on.
  • Divergences — every record whose replayed action differs from its recorded action, with timestamp, method/tool, the previous rule_id, and the new rule_id.

Caveats

  • The synthetic envelope is reconstructed from method + tool_name + session_id. The original request body is not in the audit log (by design), so redact rules are evaluated at the matcher level only — body-content matches won’t fire.
  • Records whose audited decision came from a guardrail webhook callout (guardrail_name is set on the audit line) cannot be re-derived offline — the verdict is not in the log and bodies are never audited. They are counted as guardrail_skipped, reported on stderr, shown inline in the text summary (, N guardrail when non-zero) and as guardrail_skipped in --json, and excluded from the divergence gate. Divergences on any other record still fail the gate.
  • rate_limit rules are keyed by the recorded principal (falling back to session_id for older logs without the field), so per-caller throttling is reproduced faithfully across replay.
  • when.claims rules are evaluated with no synthetic identity during replay. Claim-gated rules therefore fail closed unless a future replay input includes claim snapshots.

Exit codes

  • 0 if no divergences (current policy reproduces every recorded decision).
  • 1 if any divergences are found. Useful as a CI gate: mcpgw policy replay --audit=last-hour.jsonl || refuse-deploy.

Upstream

upstream list

Print configured upstreams, their URLs, and which one is default_upstream.

mcpgw upstream list [--config=PATH] [--json]

With --json: emits {"upstreams": [...]} where each entry has name, url, and default (true for the configured default_upstream).

upstream ping

POST a tools/list to one upstream. Confirms reachability and MCP compliance without booting the full gateway.

mcpgw upstream ping --name=NAME [--config=PATH] [--timeout=DUR] [--json]

Prints latency, tool count, and up to five sample tool names. Exit 1 on any transport failure or non-200 response.


Tool index

Both subcommands refresh the index synchronously (one tools/list per upstream) then answer locally.

tools list

Print every tool the gateway would see, with its upstream and (truncated) description.

mcpgw tools list [--config=PATH] [--timeout=DUR] [--json]

Run the mcp_search ranker offline. Use this to predict what mcp_search would return for a query without enabling tool-search in config.

mcpgw tools search [--config=PATH] [--limit=N] [--timeout=DUR] [--json] <query>

Telemetry

trace probe

Emit one synthetic span (mcpgw.cli.trace_probe) via every enabled OTLP exporter, then flush. Verifies the exporter is reachable end-to-end without running the gateway.

mcpgw trace probe [--config=PATH] [--timeout=DUR]

Exit 1 if either exporter’s flush returns an error.


Audit

The audit log is line-delimited JSON. These commands operate on the file directly — no live gateway needed.

audit tail

Pretty-print records with optional filters. Bad lines (torn writes) are skipped with a stderr warning rather than aborting.

mcpgw audit tail [--path=PATH] [--tool=NAME] [--method=METHOD] \
                 [--action=allow|deny|redact|...] [--since=DUR] [--json]

--json emits the original JSONL lines verbatim (post-filter).

audit follow

tail -f equivalent: stream new audit records as they’re appended. Seeks to end of file at start, polls every 200ms for new bytes, follows log rotation (inode swap or truncate). SIGINT/SIGTERM stops cleanly.

mcpgw audit follow [--path=PATH] [--tool=NAME] [--method=METHOD] [--action=...] [--json]

Filters match audit tail. Polls rather than using fsnotify to stay reliable on Docker bind-mounts (where filesystem-watch events are dropped).

audit stats

Summarize calls/minute, top tools, top denials over a time window.

mcpgw audit stats [--path=PATH] [--since=DUR] [--json]
Flag Default Notes
--since 1h Aggregate records newer than now - since.

Shell completion

Generated directly from the live command tree, so new subcommands and flags appear in completion as soon as you upgrade the binary.

completion bash

# System-wide:
mcpgw completion bash | sudo tee /etc/bash_completion.d/mcpgw

# Per-user:
mcpgw completion bash > ~/.local/share/bash-completion/completions/mcpgw

completion zsh

mcpgw completion zsh > "${fpath[1]}/_mcpgw"
# Then in ~/.zshrc:
autoload -U compinit && compinit