mcpgate
Summary: This MCP server exposes two tools, echo and add, and can be fronted by mcpgate for production-style HTTP/stdio access with auth, limits, caching, and observability.
echo: returns the providedtextunchanged.add: adds two numeric arguments,aandb.Call tools via JSON-RPC
tools/listandtools/call, either through mcpgate’s HTTP endpoint or its stdio aggregation mode.Apply mcpgate features: API-key/OIDC/OAuth auth, per-key rate limits, tool filtering, response caching, circuit breaking, structured logs, Prometheus metrics, and health/readiness endpoints.
Use admin cache invalidation endpoints to clear cached responses at runtime.
Accepts OIDC JWT bearer tokens as an alternative to API keys, validating them against an issuer's discovery document (/.well-known/openid-configuration) and JWKS with required audience and clock-skew checks, then using the token subject as the request identity.
Exposes gateway and per-backend telemetry in Prometheus exposition format at /metrics, including request counts by outcome, backend latency histograms, cache hit/miss counters, backend transport errors, circuit breaker state, and rate-limit rejections.
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 /readyz └───────────┘ │
└─────────────────────────────────────────────┘
│ │
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.shStdio mode
With -stdio, mcpgate itself runs as a stdio MCP server, aggregating
every configured backend behind one stdin/stdout stream — for
stdio-only clients (Claude Desktop, IDE extensions, MCP inspectors)
and server directories:
./mcpgate -config examples/config.yaml -stdioThe gateway answers initialize/ping itself, merges all backends'
tool lists into one tools/list (a tool name claimed by more than
one backend is listed once — calls can only route to the first
owner, in sorted backend-name order), and routes each tools/call to the
backend that owns the tool. Per backend, the tool filter, circuit breaker, and
response cache apply exactly as on the HTTP path. Two HTTP concerns do
not apply: stdio is a local trust boundary, so there is no API-key auth
or rate limiting — whoever can spawn the process already controls it.
Logs go to stderr; stdout carries protocol traffic only.
Related 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>); whenoauthis configured, an opaque Bearer token is checked with the authorization server's introspection endpoint (identityoauth:<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. Operators can drop cached entries without restarting the gateway:POST /admin/cache/invalidateclears every backend's cache,POST /admin/cache/invalidate/{backend}clears one (API key required, answered with{"status":"ok","backends":[...],"cleared":N}).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. A Streamable HTTP backend that answers withtext/event-streamhas its stream parsed incrementally: the JSON-RPC response matching the request id is returned as soon as its event arrives, instead of waiting for the stream to close.
Clients that send Accept: text/event-stream get the backend's SSE
events relayed live: progress notifications and other server messages
arrive as data: events as the backend sends them, and the stream
ends after the JSON-RPC response matching the request id. Streaming
responses are never cached, and the per-request timeout bounds the
whole stream. A backend that cannot stream (stdio) answers with a
single SSE event carrying the response. Clients that only accept JSON
keep the single-response behavior.
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)
# Optional: accept opaque OAuth2 bearer tokens via RFC 7662 introspection.
# oauth:
# introspection_url: "https://auth.example.com/oauth/introspect"
# client_id: "gateway" # gateway credential at the endpoint
# client_secret: "s3cret" # never logged; rotate before real use
# # cache_ttl: 60s # successful-introspection cache (default 60s, never past exp)
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 |
|
| with | RFC 7662 introspection endpoint |
| with | Gateway credential at the introspection endpoint (HTTP Basic) |
| with | Gateway credential at the introspection endpoint; never logged |
| no | Successful-introspection cache; default |
| yes | At least one; names must be unique |
| yes |
|
| stdio | Executable + args for the child process |
| http | Absolute |
| 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 /readyz→{"status":"ready","backends":{"demo":"closed"}}(readiness: per-backend circuit-breaker state;503 {"status":"degraded",...}when no backend can serve — use this for load-balancer health checks, not/healthz, which stays green while every backend is down).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 (live relay): supported — a client that sends
Accept: text/event-streamgets the backend's SSE events relayed live (progress notifications arrive before the response, instead of being swallowed), and the stream ends after the matching JSON-RPC response. Streaming responses are never cached; stdio backends answer with a single SSE event. Still missing: relaying events that arrive after the response (the stream closes at the response, matching the single-response path).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). OAuth2 token introspection (RFC 7662) is supported too: opaque Bearer tokens are checked with the authorization server (HTTP Basic client auth,active/sub/expenforced, successful results cached no longer than the token's remaining lifetime). Still missing: 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.
GET /readyzat least surfaces that signal per backend for load balancers and operators.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:
POST /admin/cache/invalidate[/{backend}]clears backend response caches at runtime (API-key authenticated); no config reload or other admin operations yet — restart to pick up config changes.
License
MIT. See LICENSE.
Available Tools
2 toolsTool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
2 tool updates
v0.1.0- First observed
add - First observed
echo
TDQS
Scored across 2 tools
echo and add target completely different operations (returning text vs arithmetic), so there is no risk of misselection. Each tool has a single, clearly distinct purpose.
Both names are single lowercase verbs (echo, add), following a clean and predictable pattern. The convention is minimal but perfectly consistent.
Two tools is very thin even for a gateway server; the surface feels like a demo rather than a functional set. It earns a borderline score rather than being egregiously mismatched.
There is no discernible domain to cover—echo and add are unrelated utility stubs with no lifecycle or workflow. This leaves significant gaps for almost any real use case.
Maintenance
Related MCP Connectors
Streamable HTTP MCP gateway for NV oOS fleets — one API key, namespaced tools, quotas and policy.
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.
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
AlicenseAqualityBmaintenanceSelf-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.1216Apache 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