Gateway
The gateway is how mcpgw works, not a mode you turn on. mcpgw sync points
every client at it: each client talks to mcpgw, and mcpgw talks to the servers.
Claude Code ─┐ ┌─ github (stdio)
Cursor ─┼─→ mcpgw serve ─────→ ├─ linear (http)
VS Code ─┤ :8137/mcp └─ postgres (stdio)
Claude Desktop┘ (mcpgw connect)
Two things fall out of that shape: one connection per upstream instead of one per client, and a single place where all the traffic is visible. A third is the cost — the gateway has to be running for a client to reach anything, which is what Running as a daemon is for, and one process now holds every server's credentials, which is what the Trust model is about.
Earlier versions could also write each server into each client directly,
gateway or no gateway. That choice is gone: entries written straight at the
servers were a second shape to test, a second thing for doctor to reason
about, and the source of a silent breakage on clients that cannot hold an HTTP
entry at all. mcpgw sync writes gateway entries, and mcpgw eject writes the
originals back if you want out.
Serving
mcpgw serve
Every enabled server, behind http://127.0.0.1:8137/mcp. Ctrl-C shuts it down
and reaps the child processes.
mcpgw serve --port 9000
mcpgw serve --server github --server linear # a subset (repeatable)
mcpgw serve --no-capture # don't write the traffic log
Binding
--bind defaults to 127.0.0.1. Anything else prints a warning, because
there is no authentication yet — whoever can reach the address can call
your MCP servers:
mcpgw serve --bind 0.0.0.0 # warns loudly; keep it behind something
A gateway under a service manager refuses the same address outright — see Running as a daemon. What loopback does and does not buy you is spelled out in the Trust model.
Tool names
Tools are exposed as server__tool — github__create_issue,
linear__list_issues. Namespacing every tool up front means adding a server
can never rename an existing one, which would otherwise silently break a prompt
that referenced it.
The exception: --server with exactly one name turns the gateway into a plain
pipe to that server, tool names untouched.
Per-server endpoints
Alongside the aggregate, every served server gets its own endpoint, where its tools appear under their own names. No flag — serving one implies serving all of them:
mcpgw serve
# http://127.0.0.1:8137/mcp — everything, as server__tool
# http://127.0.0.1:8137/s/github — github only, tools unprefixed
# http://127.0.0.1:8137/s/linear — linear only, tools unprefixed
A per-server endpoint is a plain pipe, so it forwards everything an MCP server
can offer — tools, resources, resource templates, prompts and argument
completion — with names, URIs and errors untouched. Answers are handed back as
the server wrote them: caching metadata, _meta, and pagination cursors all
survive the hop, and a client pages through a long tools/list against the
server's own cursors instead of being handed one list the gateway assembled.
The one thing the gateway does adjust is the protocol revision, because the two
sides of it need not agree. A current client speaks MCP 2026-07-28, where every
result carries resultType and lists carry ttlMs and cacheScope; the server
behind the gateway may predate all three. The gateway fills in what the client's
revision requires and never overwrites what the server actually said, so each
client gets a reply that is valid for the revision it negotiated. A server that
gave no freshness hint is reported as ttlMs: 0, cacheScope: private — "ask
again, and do not share it": the gateway will not invent a caching policy on a
server's behalf, and the answer was fetched with your credentials.
/mcp serves tools only, and that is deliberate. Tools can be namespaced
(github__create_issue); resource URIs and prompt names cannot. Two servers
can both offer file:///README.md — one name, two different documents — and
rewriting the URIs would break every link inside the contents that points at
them. So the aggregate merges what it can merge honestly, and /s/<name> is
where the rest lives.
One caveat: an endpoint reports its server's capabilities as of the last time
it reached that server. A client connecting to a freshly started gateway,
before anything has talked to the server yet, is told "tools" — the
conservative answer — because working it out for real would mean starting the
server in the middle of a handshake. Anything that connects after the first
request through the endpoint sees the full set. The name an endpoint reports at
initialize follows the same rule: once the gateway has met the server it
answers with that server's own name and version (Context7 4.0.4), which is
what a client shows the user; before first contact, and on /mcp, it is
mcpgw.
The endpoints share one process and one set of upstream connections, so a client can take the whole gateway, a single server, or both at once without starting anything twice. A stdio-only client reaches one the same way:
mcpgw connect --server github
Upstream lifecycle
Each server gets one connection, multiplexed across every client session — no
process per session. If an upstream dies it's restarted with backoff. If it
keeps failing, calls to it return a loud error rather than the tool quietly
vanishing from tools/list, which is the failure mode that costs you an
afternoon.
stdio and HTTP upstreams run through the same lifecycle; from a client's side they're indistinguishable.
Config reload
A running gateway follows the config file. mcpgw add, remove, enable and
disable take effect within a couple of seconds — no restart, and nothing
disconnected:
mcpgw add github -- npx -y @modelcontextprotocol/server-github
# a moment later, without touching the gateway:
curl http://127.0.0.1:8137/s/github
An added server gets its endpoint and joins /mcp; a removed or disabled one
loses both and its process is stopped. A server the edit didn't mention is left
completely alone — same connection, same child process — so adding one server
never interrupts the others. Only a change to a server's own transport (its
command, args, env or URL) restarts that server.
Nothing is torn down under a request in flight: a tools/call that was already
running when the config changed still gets its answer from the process it
started on.
On Unix, kill -HUP the gateway to reload immediately rather than waiting for
the next check. A config file that doesn't parse changes nothing — the gateway
says so and keeps serving what it already had, so a typo can't take your servers
down.
Pointing clients at it
mcpgw sync
Every enabled server keeps its entry and its name; only the transport changes,
to that server's own /s/<name> endpoint. So the client's list of servers
looks the same before and after, tools stay unprefixed, and anything the client
keeps beside the entry — Cline's off switch, its auto-approved tools — survives
the move, because it is still the same entry.
"github": { "type": "http", "url": "http://127.0.0.1:8137/s/github" }
The shape is per client: httpUrl for Gemini, serverUrl for Windsurf,
type: "remote" for opencode, and so on. Any running mcpgw serve answers on
those endpoints.
mcpgw sync --gateway-url http://127.0.0.1:9000/mcp
mcpgw sync --dry-run
mcpgw sync --rollback # back to whatever was there before
The other shape: one entry for the whole gateway
mcpgw sync --aggregate
The alternative to one entry per server: a single mcpgw entry pointing at
/mcp, where every tool is namespaced server__tool. One entry per client
instead of a dozen, at the price of prefixed tool names and no resources or
prompts (see above). Worth it when a client charges you per entry — a UI that
lists servers, a harness with a low limit — and not otherwise, which is why
per-server is the default.
Switching between the two is a normal sync either way: the entries the other shape wrote are mcpgw's own, so they are replaced rather than left behind. Aggregate mode also does not need the canonical config to be readable, since the one entry it writes says nothing about which servers exist.
Checking the path clients take
mcpgw doctor --probe
A server that answers when mcpgw spawns it directly tells you nothing about
whether a client can reach it through the gateway, so --probe reports the two
separately:
probes — direct to each server
✓ github (canonical): github-mcp-server 0.19.1, 41 tools
probes — through the gateway at http://127.0.0.1:8137/mcp
✓ http://127.0.0.1:8137/s/github ← Cursor "github", Zed "github": github-mcp-server 0.19.1, 41 tools
The second section takes every entry mcpgw wrote into a client, keeps the ones
aimed at the gateway, and runs a real initialize and tools/list against
that endpoint — the same request the client makes. Entries you wrote by hand
are left alone, and entries pointing somewhere else are not the gateway's
business.
Two failures it exists to name. A gateway that isn't running is one error, not one per client:
✗ not reachable — start it with `mcpgw serve` (3 endpoint(s) not checked)
And an entry left over from a server that has since been renamed or disabled, which is the failure that otherwise shows up as a client silently missing its tools:
✗ Cursor "ghost" points at http://127.0.0.1:8137/s/ghost, which the running
gateway does not serve — no server endpoint named "ghost" — known
endpoints: /s/github, /s/linear
--gateway-url points the check at a gateway on another port, matching
sync --gateway-url.
stdio-only clients
Claude Desktop only speaks stdio, so it can't be handed a URL. mcpgw connect
is the bridge — a stdio server on one side, an HTTP client to the gateway on
the other:
mcpgw connect
mcpgw connect --url http://127.0.0.1:9000/mcp
mcpgw connect --server github # one server's endpoint, tools unprefixed
mcpgw connect --server github --url http://127.0.0.1:9000/mcp
--url alone is the gateway, verbatim. With --server it says where the
gateway is and the server's endpoint is resolved on it — which is what sync
writes for a stdio-only client.
sync writes this for you; you rarely type it. If the gateway isn't running, the client sees a plain message saying so and telling you to start it
— not a transport error.