Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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__toolgithub__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.