MCP proxy vs MCP gateway: what a pass-through proxy can't do

An MCP proxy passes messages between a client and a server unchanged, and a gateway is a proxy that also decides which agent may call which tool.

An MCP proxy sits between an MCP client and an MCP server and passes messages through. It may translate between stdio and HTTP, merge several servers into one, or sign requests, but it does not decide whether a given agent should be allowed to call a given tool. A gateway is a proxy that does decide, and that also holds the keys and writes the log.

What does an MCP proxy do

It relays. A client sends tools/list or tools/call, the proxy hands it to the real server, and the answer comes back the same way. The reasons to put one in the path are practical ones: the client speaks HTTP and the server speaks stdio, you want one endpoint instead of six, or the server's cloud wants requests signed.

None of those reasons is about trust. A proxy that does only what its job says treats every caller and every tool the same, because it has no way to tell them apart.

Which kinds of MCP proxy will you run into

The word covers at least four different jobs. They look similar in a config file and are unrelated in what they protect.

Four jobs that share the word proxy

  • Transport bridge

    Exposes a stdio server over HTTP, or an HTTP server over stdio.

    Source: FastMCP proxy docs

  • Aggregator

    Mounts several servers behind one, with prefixes such as weather_get_forecast.

    Source: FastMCP proxy docs

  • Signing proxy

    Signs requests to remote servers with AWS SigV4; has a read-only mode.

    Source: MCP Proxy for AWS announcement

  • OAuth proxy server

    Fronts a third-party API as a single OAuth client for every MCP client.

    Source: MCP security best practices

Each description comes from the linked project's or the MCP specification's own page. A product can do more than one of these.

FastMCP's docs show the first two. Its create_proxy() takes a URL, a transport or a config of several servers, and its docs say a local stdio server can be exposed over HTTP and an HTTP backend over stdio. Mounted servers get prefixes, so a get_forecast tool from a server named weather appears as weather_get_forecast. Amazon's MCP Proxy for AWS is the third kind: it lets clients reach AWS-hosted MCP servers with SigV4 signing, and the announcement lists a read-only mode among its features.

The fourth kind has a different origin. The MCP specification uses "MCP proxy server" for one specific thing: an MCP server that connects clients to a third-party API while acting as a single OAuth client to that API. Its security best practices page lists the conditions under which that setup lets an attacker skip user consent, and says such proxies must keep per-client consent. That is the confused deputy problem, which has its own tutorial. It is a flaw in how one kind of proxy handles OAuth, not a statement about proxies in general.

How little code is a pass-through proxy

Seven lines of Node. I wrote this one and a fake upstream server with two tools, list_issues and delete_repository, to see what a bare relay does. The proxy spawns the upstream and copies every line of JSON-RPC in both directions.

// proxy.mjs
import { spawn } from 'node:child_process';
import readline from 'node:readline';
const up = spawn('node', ['upstream.mjs']);
readline.createInterface({ input: process.stdin }).on('line', (l) => up.stdin.write(l + '\n'));
up.stdout.pipe(process.stdout);
process.stdin.on('end', () => setTimeout(() => process.exit(0), 300));

I sent it a tool list, a call to list_issues and a call to delete_repository:

node proxy.mjs < calls.txt
{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"list_issues"},{"name":"delete_repository"}]}}
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"ran list_issues"}]}}
{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"ran delete_repository"}]}}

The proxy did its job. It also ran delete_repository for a caller who only wanted to read issues, because nothing in it knows the difference. Real proxies are far more careful than this one, but FastMCP's documentation describes the same property: tool results are relayed unchanged, and incoming headers including Authorization are forwarded to HTTP backends by default. A proxy that forwards your credentials onward is doing what a proxy is for.

Does adding a deny list fix it

Partly. The obvious next step is a set of tool names the proxy refuses. Here is the same proxy with one added check, 14 lines in total:

// proxy-deny.mjs (the lines that differ)
const DENY = new Set(['delete_repository']);
readline.createInterface({ input: process.stdin }).on('line', (l) => {
  const m = JSON.parse(l);
  if (m.method === 'tools/call' && DENY.has(m.params.name)) {
    console.log(JSON.stringify({ jsonrpc: '2.0', id: m.id, error: { code: -32001, message: 'denied' } }));
    return;
  }
  up.stdin.write(l + '\n');
});
{"jsonrpc":"2.0","id":3,"error":{"code":-32001,"message":"denied"}}
{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"list_issues"},{"name":"delete_repository"}]}}
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"ran list_issues"}]}}

The destructive call is refused. Three things are still wrong, and none of them can be fixed by adding more lines to a rule:

  • tools/list still advertises delete_repository, so the agent sees a tool it will be refused on and spends a turn finding out.
  • The rule applies to everyone. If a second agent should be allowed to delete, there is nobody to write that exception for, because the proxy has no idea who is calling.
  • The refusal printed to the client and nowhere else. No record says which agent tried it.

Where a pass-through proxy stops

Those three, plus the key, are the whole gap. This is the same set of calls against three setups.

Same three calls, three setups

QuestionPass-through proxyProxy with a deny listGateway
Call delete_repositoryran delete_repositoryerror -32001 denied403 before any credential is fetched
Does tools/list still show delete_repository?YesYesNot for a caller without the grant
Who is the rule written for?No ruleEvery caller alikeA named agent
Is the refused call recorded?NoOnly if you add loggingYes, with the agent that made it
Where does the upstream key sit?With whoever launched itWith whoever launched itIn the gateway; the agent holds one token
The first two columns are the output of the two scripts above. The gateway column is how Statio behaves, as described on /security and /mcp-permissions.

Each row maps to something a gateway adds to the relay:

  1. A rule that can allow one tool and deny another. Per-tool access is checked on every call, and a refused call never reaches the upstream or its credential.
  2. A caller the rule can name. Without an identity per agent, a rule can only say "everyone". With one, the same server can be open to a triage agent and shut to a deploy agent.
  3. A record. Audit logs hold refusals as well as successes, which a proxy that does not know the caller cannot write usefully.
  4. A holder for the key. A pass-through proxy leaves the upstream key in the config of whoever launched it. A gateway keeps it in one place and attaches it when the call goes out, so the agent holds a single token for the gateway.

What is an MCP gateway? covers each of these in more depth, and best MCP gateways puts six questions to the products that exist.

Do you need a gateway or is a proxy enough

Tick what is true for you. The figure lists what a proxy alone would have to grow into to cover it.

Is a proxy enough for your setup?

Tick what is true

Nothing ticked: a pass-through proxy covers a single agent talking to a server that only reads.

    Five situations, each tied to one gap from the section above. Nothing you tick is stored.

    If you have ticked more than one, you are describing a gateway whether or not you build it yourself. Building it is possible: the deny list above is the start of one. What grows from there is a table of grants, a token per agent, a vault and a log writer, and that is the part to weigh against buying or running someone else's. Statio can run in your own cloud if the hosting is what keeps you from a managed one, which the self-hosted page explains.

    When is a proxy the right answer

    Often. A proxy is the better tool when:

    • You have one agent, one server, and the server only reads. Nothing here needs a decision.
    • The problem is plumbing: stdio to HTTP, six endpoints into one, a signing requirement. A gateway would do that too, but a library does it with less to run.
    • You are on a single developer machine and the config file never leaves it.

    And a proxy does not replace the controls it sits next to. Neither a proxy nor a gateway stops a model from being talked into a bad call. A gateway narrows what that call can reach and records that it happened, which is a smaller promise than "secure". If you want to see where your setup stands, the MCP security checklist scores it in a few minutes.

    Questions

    No. A proxy forwards MCP messages and may translate between transports or combine servers. A gateway is a proxy that also decides which agent may call which tool, holds the credentials and records each call. Every gateway is a proxy; most proxies are not gateways.

    Only if someone writes the rule, and a bare proxy has no caller identity to write it against. My 14-line example refused a tool by name for every caller, but it still listed the tool and left no record. Per-agent rules need identities, which is what a gateway adds.

    An MCP server that connects clients to a third-party API and acts as a single OAuth client to that API. The specification's security page warns that such a proxy must obtain consent for each client, because a static client ID plus a consent cookie can let an attacker skip it.

    Not by itself. FastMCP's docs say incoming headers, including Authorization, are forwarded to HTTP backends by default, so a proxy can carry a credential straight through. Keeping a key away from the agent takes something that stores the key and attaches it at call time.

    You need something that bridges the transport. FastMCP's proxy does it in either direction, and many clients can also launch a stdio server locally. For a shared remote server with several users, a gateway also handles the access side.

    If you have worked out that you need the decisions and not only the relay, Statio is the gateway I build: per-tool grants, an identity per agent, upstream keys held in the gateway and a log of every call.

    Deny one tool and watch the call fail. Three servers and 10,000 calls a month, free.