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
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/liststill advertisesdelete_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
| Question | Pass-through proxy | Proxy with a deny list | Gateway |
|---|---|---|---|
| Call delete_repository | ran delete_repository | error -32001 denied | 403 before any credential is fetched |
| Does tools/list still show delete_repository? | Yes | Yes | Not for a caller without the grant |
| Who is the rule written for? | No rule | Every caller alike | A named agent |
| Is the refused call recorded? | No | Only if you add logging | Yes, with the agent that made it |
| Where does the upstream key sit? | With whoever launched it | With whoever launched it | In the gateway; the agent holds one token |
Each row maps to something a gateway adds to the relay:
- 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.
- 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.
- A record. Audit logs hold refusals as well as successes, which a proxy that does not know the caller cannot write usefully.
- 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?
Nothing ticked: a pass-through proxy covers a single agent talking to a server that only reads.
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
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.