mcpgate
mcpgate
MCP servers exist everywhere. The production layer doesn't.
MCP (Model Context Protocol) servers are easy to write and easy to expose — and that's the problem. A raw MCP server has no authentication, no rate limiting, no caching, no circuit breaking, and no observability. Put one behind your product and the first abusive client, the first slow backend, or the first retry storm takes it down.
mcpgate is the production layer: a gateway that sits in front of your MCP servers and adds auth, per-key rate limits, response caching, circuit breakers, structured logs, and Prometheus metrics — without touching the servers themselves.
┌─────────────────────────────────────────────┐
MCP client ──────▶│ mcpgate │
(any MCP client) │ │
│ ┌─────────┐ ┌──────────┐ ┌───────────┐ │
│ │ API key │─▶│ token │─▶│ cache │ │
│ │ auth │ │ bucket │ │ (TTL) │ │
│ └─────────┘ └──────────┘ └─────┬─────┘ │
│ ┌───────────┐ ┌───────────┐ │ │
│ │ circuit │─▶│ backend │◀────┘ │
│ │ breaker │ │ stdio / │ │
│ └───────────┘ │ HTTP │ │
│ /metrics /healthz └───────────┘ │
└─────────────────────────────────────────────┘
│ │
stdio child Streamable HTTP
MCP server MCP serverQuickstart
# Build
go build -o mcpgate ./cmd/mcpgate
go build -o echo-backend ./examples/echo-backend
# Run (examples/config.yaml uses the demo key "demo-key-123" — rotate it)
./mcpgate -config examples/config.yamlList tools through the gateway:
curl -X POST http://localhost:8080/mcp/echo \
-H 'Content-Type: application/json' \
-H 'X-API-Key: demo-key-123' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Call a tool:
curl -X POST http://localhost:8080/mcp/echo \
-H 'Content-Type: application/json' \
-H 'X-API-Key: demo-key-123' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":3}}}'
# {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"5"}]}}Or run the full guided demo (auth failure, cache MISS/HIT, rate-limit 429s, health + metrics):
bash demo/demo.shRelated MCP server: mcp-streamable-http-bridge
How it works
POST /mcp/{backend} runs one JSON-RPC 2.0 request through this pipeline:
Auth —
X-API-Keyheader orAuthorization: Bearer <key>for API keys; whenoidcis configured, a Bearer JWT signed by the issuer is also accepted (identityoidc:<sub>). Missing/invalid credentials get401. Key values and token contents are never logged.Rate limit — token bucket per API key. Exhausted buckets get
429with a JSON-RPC error. Per-key overrides in config.Backend lookup — unknown backends get
404/ JSON-RPC-32601.Tool filter — a backend's
tool_filterhides tools: denied tools are removed fromtools/listresults, andtools/callfor a filtered tool is rejected with403/ JSON-RPC-32602before touching the backend, cache, or breaker.Circuit breaker — an open breaker fails fast with
503and never touches the backend. After the cooldown, one probe is allowed; success closes the breaker, failure re-opens it.Cache —
tools/listresponses are cached per backend.tools/callresponses are cached only for tools explicitly listed incacheable_tools, because tool calls can have side effects. Cache hits returnX-Cache: HIT; the cached payload is re-wrapped with the current request'sid, because JSON-RPC requires the response id to match.Forward — the request goes to the backend (stdio child process or Streamable HTTP POST). Configured static
headersare injected into HTTP backend requests. Transport failures return502and count toward the breaker. JSON-RPC error objects from the backend are forwarded verbatim and are not cached.
Notifications (messages without an id) are forwarded to the backend
fire-and-forget and the client gets 202 Accepted — the client asked for
acceptance, not delivery confirmation, so a backend that can't be reached
still gets a 202, and the failure is logged and counted instead of
surfacing to the client. An open circuit breaker drops notifications
without attempting the forward.
Configuration
listen: ":8080"
api_keys:
- key: "demo-key-123" # X-API-Key or Bearer token
name: "demo" # appears in logs/metrics, never the key itself
rate_limit: # optional per-key override
requests_per_second: 5
burst: 10
default_rate_limit:
requests_per_second: 10
burst: 20
# Optional: accept OIDC JWT bearer tokens as well as API keys.
# oidc:
# issuer: "https://accounts.example.com" # discovery via /.well-known/openid-configuration
# audience: "mcpgate" # required aud claim
# # jwks_url: "https://accounts.example.com/jwks" # override discovery
# # clock_skew: 60s # exp/nbf tolerance (default 60s)
backends:
- name: "echo"
transport: "stdio" # or "http"
command: ["./echo-backend"] # stdio: executable + args
env: {} # stdio: extra env vars for the child
# url: "http://localhost:9000/mcp" # http: Streamable HTTP endpoint
# auth: {type: "bearer", token: "..."} # http: gateway -> backend auth
# headers: # http: static headers injected into
# X-Gateway: "mcpgate" # every request to this backend
timeout: 10s # per-request backend timeout (default 10s)
tool_filter: # which tools clients may see and call
# allow: ["echo", "add"] # non-empty = only these tools are exposed
deny: ["drop_table"] # hidden from tools/list, rejected on tools/call
cache:
tools_list_ttl: 60s
tools_call_ttl: 30s # 0 disables tools/call caching
cacheable_tools: ["echo", "add"]
breaker:
failure_threshold: 5 # consecutive failures to trip open (0 = never)
cooldown: 30s # open duration before a probe is allowedField | Required | Notes |
| yes | Address the gateway binds, e.g. |
| yes | At least one; keys must be unique |
| yes | Applies to keys without an override |
| with | OIDC issuer URL; JWKS discovered via |
| with | Required |
| no | Overrides the discovered JWKS endpoint |
| no |
|
| yes | At least one; names must be unique |
| yes |
|
| stdio | Executable + args for the child process |
| http | Streamable HTTP endpoint |
| no |
|
| no | http only: static headers injected into every backend request; backend auth wins on name clashes |
| no | Non-empty: only these tools are exposed |
| no | Hidden from |
| no | Default |
| no |
|
| no |
|
| no |
|
| no | Time before a probe is allowed |
Observability
GET /healthz→{"status":"ok"}(liveness).GET /metrics→ Prometheus exposition:
Metric | Labels | Meaning |
| backend, method, status | Requests by outcome ( |
| backend | Backend call latency histogram |
| backend, method | Cache behavior |
| backend | Transport failures |
| backend |
|
| key_name | Rejected requests per API key name |
| backend, method, status | Notifications by outcome ( |
Logs are structured JSON (via log/slog) with request_id, api_key
(name only), backend, method, cache outcome, and latency. Backend
credentials and API key values never appear in logs.
Development
gofmt -l . # must be clean
go build ./...
go vet ./...
go test ./... # unit tests: rate limiter, cache, breaker, config,
# backend transports (incl. stdio against echo-backend),
# gateway routing (auth, rate limit, cache, breaker)CI (GitHub Actions) runs format, build, vet, and test on every push/PR.
Roadmap — what's NOT in v0.1
Honest gaps, in rough priority order:
SSE streaming: HTTP backends that answer with
text/event-streamhave the stream read and the JSON-RPC response with the matching request id returned; events are not relayed to the client as a live stream.OAuth / OIDC: OIDC JWT bearer-token client auth is supported (RS/ES/PS signature validation against the issuer's JWKS via discovery, strict
iss/aud/expchecks; JWKS refreshes periodically and on key rotation). Still missing: OAuth2 token introspection, scopes, and per-tool authorization.Request/response transformation: per-backend
tool_filter(allow/deny lists: hidden fromtools/list, rejected ontools/call) and staticheadersinjection for HTTP backends are supported. Still missing: schema rewriting.Backend health checks / load balancing: one backend per name, no pooling, no active probing — the breaker is the only failure signal.
Distributed rate limiting / shared cache: both are in-process only. Multi-replica deployments need Redis-backed implementations.
TLS termination: run behind a TLS-terminating reverse proxy.
Admin API: no runtime config reload or cache invalidation endpoint; restart to pick up config changes.
License
MIT. See LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP Gateway: wrap any MCP server with cold-start retries, uptime SLA, and per-execution MPP billing.
REST-to-MCP for UK hospitality. Safety proxy: circuit-breakers, rate limits, whitelists. Apache 2.0.
Governed MCP gateway: one endpoint for your tools, with credential custody and audit log.
Stateless MCP gateway and OTel span-streaming bridge for hosted MCP servers.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceAggregates multiple upstream MCP servers (stdio, streamable-http, sse, websocket) behind a single streamable-HTTP endpoint with declarative configuration, namespacing, filtering, and auto-reload.MIT
- AlicenseNot gradedqualityDmaintenanceBridges stdio Model Context Protocol (MCP) servers to MCP Streamable HTTP behind a single gateway, enabling multi-tenant, multi-user deployment with per-tenant environment variables via HTTP headers.MIT

AISIX AI Gatewayofficial
AlicenseAqualityAmaintenanceSelf-hosted MCP gateway that registers upstream MCP servers and fronts them behind one governed Streamable HTTP endpoint: per-tool access control by caller API key, guardrails over tool arguments and results, rate limits, and usage logs. The same Rust gateway also proxies LLM and A2A agent traffic.1200Apache 2.0- AlicenseNot gradedqualityBmaintenanceHigh-performance Go gateway and reverse proxy for Model Context Protocol servers with rate-limiting, authorization, and pay-per-result metering.33 npm15MIT