mcpgateway
Provides a Prometheus-compatible /metrics endpoint exposing per-tool call counts, latency histograms, server health, policy denials, rate-limit hits, and audit errors.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
mcpgateway
One process fronting all your MCP servers. MCP works in demos; production is a
mess — dozens of tools across servers, duplicated config, fragmented audit
logs, tool name collisions, no fleet-wide policy, zero visibility into what
gets called. mcpgateway is the missing production layer:
Unified tool namespace — one
tools/listacross all servers, with collision resolution (prefixdefault,priority,explicit)Fleet-wide policy — allow/deny/hide, argument substring rules, response redact/block/warn, observe mode (mcp-jail semantics, fleet scope)
Per-tool rate limits — token buckets, bounded memory
Centralized audit log — append-only JSONL, secret redaction, rotation
Prometheus metrics — per-tool calls, latency histograms, server health
Auth at the edge — bearer keys with scopes; the gateway holds downstream credentials (no more per-server secret sprawl)
Curated discovery —
mcpgateway namespacepreviews the unified table;/v1/registryand/v1/namespaceon the admin API
Drop-in: point your agent at the gateway instead of N servers — stdio (swap the server command) or Streamable HTTP.
Install
npm install -g mcpgateway
# or run from source:
npm install && npm run build && node dist/src/index.js --helpRequires Node ≥ 20.
Related MCP server: MCP Proxy Server
Quickstart
mcpgateway init # writes ./mcpgateway.yaml and prints a fresh random key
export GATEWAY_KEY=<the printed key> # the key is printed, never written to the file
# edit mcpgateway.yaml: add your servers
mcpgateway validate -c mcpgateway.yaml
mcpgateway namespace -c mcpgateway.yaml # preview the unified tool table
mcpgateway serve -c mcpgateway.yamlAlready have a .mcp.json? Convert it instead of retyping servers:
mcpgateway import .mcp.json > servers.yaml # paste the servers: block into mcpgateway.yamlimport reads mcpServers (or servers, or a top-level map). Entries with
command become transport: stdio (with args and env); entries with url
become transport: http (with headers). Names are lowercased and other
characters become -; clashes get -2, -3. Any env or headers value
whose name looks secret (same list as the audit redactKeys default: token,
secret, password, authorization, ...) is replaced with ${NAME} and a
note on stderr tells you which variable to export, so the secret is never
written to the YAML. Values under other names are copied as-is, so check the
output before committing it. Invalid JSON exits 2.
Point your agent at it (stdio drop-in):
{
"mcpServers": {
"gateway": { "command": "mcpgateway", "args": ["serve", "-c", "/path/to/mcpgateway.yaml"] }
}
}Or serve Streamable HTTP (upstream.http.enabled: true in config, or the
--http flag: mcpgateway serve -c mcpgateway.yaml --http) and point remote
agents at http://host:3000/mcp with a bearer key.
How it works
agent ──stdio / Streamable HTTP──▶ mcpgateway ──┬──stdio──▶ server A
(one session) auth at edge │ ├──stdio──▶ server B
unified namespace policy engine│ └──http───▶ server C
rate limits audit+metrics│Each downstream server gets an independent MCP session with bounded
concurrency (stdio defaults to 1 — most stdio servers are single-threaded),
per-call timeouts, a circuit breaker (5 consecutive failures → 30s open, fail
fast, half-open ping probe), and periodic health checks. Catalog changes
(tools/list_changed) rebuild the unified namespace without dropping
in-flight calls.
Namespace & collisions
MCP tool names must match ^[a-zA-Z0-9_-]{1,64}$. When two servers expose the
same tool name:
strategy | behavior |
|
|
| highest-priority server keeps the bare name; losers are prefixed (still reachable). Every shadowing is logged. |
|
|
Non-colliding tools keep their bare names. Aliases ({"server:tool": "name"})
override anything. A renamed tool longer than 64 chars, or a public name that
collides with another tool's natural name, is a hard error with a suggested
fix — run mcpgateway namespace to preview before serving.
Policy (fleet scope)
policy.allowTools / denyTools / hideTools are glob patterns over public
tool names (* supported); deny wins, hidden tools vanish from listings
and are denied on call — the caller sees the same unknown tool error as
for unmapped names, so probing can't confirm a hidden tool exists (the audit
record keeps the real hidden reason for operators). argRules are
substring matchers over the JSON-encoded arguments (deliberately not
regex — no ReDoS). responseRedact / responseBlock / responseWarn scan
the JSON-encoded result (first 64KB). mode: observe logs would-be verdicts
and forwards everything — safe onboarding.
This is the fleet layer. For per-server deep policy (argument shapes, human approvals), pair it with mcp-jail: gateway decides which tools exist and who may call them; the jail decides how each server's tools may be used.
Tool-definition pinning (rug-pull guard)
A server can change a tool's description or schema after you approved it.
The gateway fingerprints each downstream tool (sha256 over the canonical JSON
of name, title, description, inputSchema, outputSchema,
annotations; key order does not matter) and compares it with a pin on
startup and on every list_changed.
pinning:
mode: warn # off | warn (default) | quarantine
file: pins.json # optional; without it, tools are pinned on first sight at startup (in memory)warn: a changed tool is still served; onepin_mismatchaudit record (old and new hash, and the changed field names when the old definition was seen in this process) andmcpgateway_pin_mismatch_total{server}. A tool with no pin gets apin_newrecord.quarantine: the same records, and the changed or unpinned tool is removed fromtools/list; calls get the ordinaryunknown toolerror./healthzreportspinMismatches.mcpgateway pin -c mcpgateway.yaml -o pins.jsonwrites the pins (sorted, stable bytes). To accept a change, re-run it and review the file diff. There is no accept API.
Pinning detects definition changes. It cannot detect a server that keeps its
definition and changes behaviour. Without file, pins live in memory only, so
a restart re-trusts whatever the servers serve at that moment.
Invisible-character check
On every catalog build, each tool's name, title, description and every
string inside its inputSchema are scanned for characters a human reviewer
cannot see but a model reads: the Unicode tag block (U+E0000 to U+E007F),
zero-width and direction marks (U+200B to U+200F), bidi embeddings and
overrides (U+202A to U+202E), invisible operators (U+2060 to U+2064), bidi
isolates (U+2066 to U+2069) and U+FEFF. A flagged tool gets one
hidden_chars audit record (code points and their JSON path), and
mcpgateway_hidden_chars_tools counts flagged tools. By default the tool is
still served; policy.hideSuspicious: true hides it and calls get the
unknown tool error.
It flags invisible characters; it does not judge visible text. Zero-width joiners inside legitimate emoji sequences are flagged too.
Audit & metrics
One JSONL record per call: timestamp, request id, session, key id, tool,
server, original tool name, duration, status, error. Argument values whose key
looks secret (token, secret, password, … — configurable, substring,
case-insensitive, recursive) become [REDACTED]; serialized args are capped
(8KB default, visible truncation marker); set logArgs: false to store only a
SHA-256 of the args. Size-based rotation with a bounded file count — the log
can never fill the disk. The writer never throws into the request path;
failures are counted and surfaced on /healthz.
Prometheus text on the admin port: mcpgateway_tool_calls_total,
mcpgateway_tool_call_duration_seconds (histogram), mcpgateway_server_up,
mcpgateway_server_inflight, mcpgateway_policy_denials_total,
mcpgateway_rate_limit_hits_total, mcpgateway_audit_errors_total,
mcpgateway_http_rejected_total, mcpgateway_pin_mismatch_total, mcpgateway_hidden_chars_tools.
Admin API (default http://127.0.0.1:3001)
GET /healthz— open (load-balancer convention): status, per-server health, audit errorsGET /metrics— Prometheus exposition (adminscope)GET /v1/registry— servers, tool counts, health (any key)GET /v1/namespace— unified tool table + collision report (any key)POST /v1/reload— refresh all downstream catalogs (adminscope)
The admin API refuses to bind a public address unless
admin.allowPublicAdmin: true is set explicitly.
Configuration
See mcpgateway.example.yaml — every option is
commented. Highlights:
${VAR}/${VAR:-default}env interpolation everywhere (unset without a default = startup error naming the variable)stdio servers spawn with
shell: falseand an argv array; env is a small safe-default set plus yourenvblockvalidate/serverefuse any key containingchangeme(the example file's fallback), so a gateway started withoutGATEWAY_KEYfails instead of running on a key that is public in this repo; keys under 16 chars are refused toobearer keys carry scopes:
tools:call,tools:list,resources:*,prompts:*,admin(compared in constant time)a Streamable HTTP session id only works with the key that opened it
auth.keys[].tools(optional glob list over public tool names) limits what one key can list and call; a call outside the list gets the sameunknown toolerror as a missing tool and is audited asdeniedwith reasonkey_scope. Withouttools, a key sees every tool. The stdio upstream has no key, so per-key lists apply to the HTTP upstream onlythe config file is trusted operator input (like
sshd_config) —chmod 600
CLI
mcpgateway serve [-c config.yaml] # stdio upstream by default; logs to stderr
mcpgateway validate [-c config.yaml] # config checks; --network adds reachability
mcpgateway namespace [-c config.yaml] # connect + print the unified tool table
mcpgateway init [-o out.yaml] # write the starter config
mcpgateway pin [-c config.yaml] [-o pins.json] # connect + write tool-definition pins
mcpgateway import .mcp.json # print a servers: block from a Claude-style configLimitations (honest)
No sampling/elicitation proxying. If a downstream server initiates
sampling/createMessageorelicitation/create, the gateway answers "method not found". Forwarding server→client requests across two independent sessions is a correctness swamp we chose not to wade into for v1.No roots. The gateway advertises an empty roots list downstream.
Prefix-on-collision only. Non-colliding tools keep bare names, so adding a server later can rename a previously bare tool (it becomes prefixed). Pin names you depend on with
aliases.Single process. No clustering or HA in v1; run one gateway per host and put it behind your own supervisor. In-flight calls are capped (
globalMaxInflight, default 256) so the gateway degrades loudly, not silently.Config is trusted. A malicious config file owns the machine (it names commands to spawn). This is by design — like
sshd_config— but don't generate configs from untrusted input.HTTP URLs are operator-trusted. Server
urls are called as configured; the gateway does not SSRF-filter them (your servers usually are localhost). Don't point it at URLs you don't control.No request/response transformation. The gateway routes, guards, and observes; it does not rewrite payloads. No result caching either.
Audit is local JSONL. Rotation is bounded, but shipping to a SIEM is your job (filebeat/vector sidecar).
Metrics are Prometheus-text only. No OTel exporter in v1.
Origin/Host checks are allow-lists, nothing more. Before auth, the HTTP upstream answers 403 when an
Originheader is present and not inupstream.http.allowedOrigins(exact match; default empty, so any browser origin is refused), and when theHostheader is not inupstream.http.allowedHosts(an entry without a port matches any port). A loopback bind defaultsallowedHoststolocalhost,127.0.0.1,[::1]; a non-loopback bind defaults to no Host check andvalidatewarns. Requests with noOrigin(CLI and SDK clients) pass the Origin check. Rejections are counted inmcpgateway_http_rejected_total{reason}and logged to stderr.No SSE resumability. No event store: if a Streamable HTTP connection drops, clients reconnect and start a new session. Sessions are capped (
maxSessions, default 128) so abandoned ones can't exhaust memory.Wedged stdio children are SIGTERM'd, not SIGKILL'd, on gateway shutdown; a child that ignores SIGTERM will linger as an orphan.
Downstream error messages are scrubbed of any secret values seen in the call's args (same redact-key list as the audit log), then truncated — but a server that leaks secrets through other channels (e.g. inside a returned document) is only caught if you configure
responseRedactpatterns.
Development
npm install
npm test # build + unit and in-process integration tests
npm run lint # tsc --noEmit (strict)License
MIT — see LICENSE.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
Related MCP Connectors
Toolstile is a managed MCP gateway that connects remote MCP servers behind one endpoint, with per-agent permissions, shared accounts and activity logs. Connect at https://mcp.toolstile.com/mcp over Streamable HTTP with OAuth; sign in at https://toolstile.com, select a workspace and connect your upstream servers. Available tools, resources and prompts depend on that workspace and the agent's granted access.
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.
MCP federation gateway: route, govern and observe agent tools across clouds and MCP servers.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceAggregates and routes requests across multiple backend MCP servers, exposing their combined tools and resources through unified SSE, Streamable HTTP, or Stdio endpoints. Supports optional Web UI management, granular tool control, and endpoint authentication.120 npmMIT
- AlicenseNot gradedqualityAmaintenanceAggregates multiple backend MCP resource servers (Stdio, SSE, and Streamable HTTP) into a single unified endpoint exposed over SSE, Streamable HTTP, or Stdio. Enables routing of tool calls, resource access, and per-tool enable/disable or rename overrides through an optional web admin UI with configurable API key or bearer-token authentication.120 npmMIT
- AlicenseNot gradedqualityAmaintenanceEnables clients to connect to, manage, and aggregate multiple backend MCP servers and access their combined tools and resources through unified SSE or Streamable HTTP endpoints, or as a single Stdio server. It provides request routing, optional authentication, granular tool controls, and an optional web admin interface.120 npmMIT
- FlicenseNot gradedqualityAmaintenanceAggregates multiple backend MCP servers (Stdio, SSE, and Streamable HTTP) and exposes their combined tools and resources through a single unified SSE or Streamable HTTP endpoint, or as a standalone Stdio server. It routes tool calls and resource requests to the right backend and offers optional web-based admin, authentication, and per-tool control.-