go build -o bin/mcpproxyd ./cmd/mcpproxyd # the gateway daemon
go build -o bin/mcpsmoke ./cmd/mcpsmoke # scripted MCP test client
go build -o bin/configcheck ./cmd/configcheck # config validatorRequires Go 1.26+. For stdio backends using npx/uvx, Node/Python must
be on the daemon's PATH.
bin/mcpproxyd -config examples/01-minimal-stdio.yaml -debug# another terminal: full round-trip, initialize → tools/list → tools/call
bin/mcpsmoke -url http://127.0.0.1:8000/mcp -tool echo -args '{"message":"hi"}'
# or only the catalog
bin/mcpsmoke -url http://127.0.0.1:8000/mcp -list-onlyConnect a real client (Claude Code, Cursor, MCP Inspector) by adding a
streamable-HTTP MCP server pointing at http://127.0.0.1:8000/mcp.
One YAML file; ${VAR} expands from the environment, which keeps secrets
out of the file. Validate without starting:
bin/configcheck examples/*.yamlStart from an example in examples/, numbered 01 to 13
across every option; examples/remote/ lists known public remote MCP
servers.
Flags: -config <path> (default config.yaml), -debug.
| Endpoint | Purpose |
|---|---|
POST /mcp |
one JSON-RPC message |
GET /mcp |
SSE stream (server-initiated messages) |
DELETE /mcp |
end session |
GET /healthz |
liveness |
GET /metrics |
Prometheus (if configured) |
GET /approvals/ + POST /approvals/{id}/approve|reject |
review API |
export T=$MCPPROXY_APPROVAL_TOKEN
curl -s -H "Authorization: Bearer $T" http://127.0.0.1:8000/approvals/ | jq
curl -X POST -H "Authorization: Bearer $T" http://127.0.0.1:8000/approvals/<id>/approveA held call blocks its client until a reviewer resolves it, or until
approvals.timeout auto-rejects.
With wal_dir set, each session appends JSONL audit events:
jq -r '[.time, .event, .tool // "", .reason // ""] | @tsv' sessions/<sid>.jsonl$MCPPROXY_STATE_DIR(default~/.mcpproxy): outbound OAuth tokens, AES-256-GCM encrypted.tokens.keyis the keyfile, so protect it and back it up alongsidetokens.enc. Losing the key while keeping the ciphertext makes the daemon fail to start.- DCR client registrations are not persisted: the daemon leaves the client store unset, so each restart re-registers with the upstream authorization server.
- The embedded auth server keeps sessions and codes in memory, so clients re-authenticate after a restart.
- SIGINT/SIGTERM starts a graceful shutdown: HTTP drains, then the gateway closes every session's backends and process-group kills stdio children.
- The reaper ends sessions idle for 30 minutes.
- A dying backend (stdio child exit, remote failure) ends that session, and the client re-initializes to get a fresh one.
-debuglogs every gateway decision plus stdio children's stderr.- Seeing
tool_deniedon a tool that looks allowed points at two causes: deny beat allow, or overrides un-aliased the name before policy ran. Check the REAL tool name in the audit event'sruleandreason. - For a stuck OAuth backend, watch the daemon log for the authorize URL;
grants land in
~/.mcpproxy. Delete that directory to force re-authorization. mcpsmokeprints raw JSON-RPC for every exchange, which is the quickest way to see what the gateway returned.- Orphan check after hard kills:
pgrep -f <your mcp server command>should return nothing once the daemon is down.
- Run one daemon per trust boundary; a single instance multiplexes many sessions and backends.
- Put TLS in front with a reverse proxy; the daemon listens plaintext.
- Set
inbound.modeto something real (oidc,github,awssts) before exposing beyond localhost. Setapprovals.api_tokentoo, or the review API is open. - Alert on
mcp.session_killed(rug pull),mcp.guardrail_block, andmcpproxy_messages_total{decision="kill"}.