MCP proxy server: bridges, gateways and web egress
What an MCP proxy server does: stdio to HTTP bridges, aggregators and gateways, what the 2026-07-28 spec changed for them, security risks and web egress.

Quick summary · TL;DR
- An MCP proxy server speaks MCP on both sides. It acts as a server to the client and as a client to one or more MCP servers, so it can bridge transports, merge servers behind one endpoint and enforce policy on tool calls.
- Three different things share the name. A transport bridge, an aggregator or gateway, and a network proxy under a tool server that fetches the web. Only the last one changes what a website sees.
- The 2026-07-28 MCP revision rewrote the intermediary's job. Sessions are gone, every POST carries an Mcp-Method header, tool calls add Mcp-Name, and a header that disagrees with the body is rejected with error -32020.
- The spec names MCP proxy servers in its threat list. Confused deputy, token passthrough and stdio spawn escalation all apply to a proxy that holds credentials or launches servers.
An MCP proxy server sits between an MCP client and one or more MCP servers, acting as a server to the client and a client to the servers. It bridges transports such as stdio and Streamable HTTP, puts many servers behind one endpoint, and enforces auth and policy on tool calls. It never touches the web traffic those tools send.
The name causes most of the confusion. A developer searching for one might want a bridge so a desktop client can reach a remote server, a gateway so fifteen engineers stop pasting tokens into config files, or a way to send a browser tool’s traffic out through a residential IP. Those are three layers with three different failure modes.
Two things changed the job recently. The 2026-07-28 revision of the MCP specification made the protocol stateless and added routing headers built for intermediaries, and the MCP security best practices (2026-07-28 edition) name MCP proxy servers in their threat list.
What an MCP proxy server does
MCP messages are JSON-RPC 2.0, and on the wire a proxy works with two transports. Over stdio, the client launches the server as a child process and writes messages to its stdin. Over Streamable HTTP, the client POSTs each message to one endpoint and reads back either a JSON response or an SSE stream. A proxy terminates one of those connections and opens another.
On the client side it answers tools/list, resources/list and prompts/list as if it were the server. On the server side it forwards each tools/call to whichever downstream server owns the tool, then relays the result. In between it can rewrite: prefix tool names, drop tools, attach credentials. What it does not understand, it passes on. The Streamable HTTP transport (2026-07-28) says an intermediate server that does not recognize an Mcp-Param-{Name} header must forward it and otherwise ignore it.
What it never sees. When a fetch tool downloads a page, that request leaves from the machine where the fetch server runs, over that machine’s network. The MCP proxy carries the tool call and the result, nothing below them. A proxy for the tool’s web traffic is a separate component, configured on the tool server itself.
Three jobs share one name
Aggregation, translation and control cover the MCP layer. An operator needs one more column, because “which MCP proxy do I need” often means “why does the agent’s browser get challenge pages”, and no MCP-level component answers that.
Bridging stdio and remote servers
Transport bridging runs in two directions. A client that only launches local processes needs a bridge that it can spawn over stdio and that dials a remote server over HTTP. A server that only speaks stdio needs a bridge that listens on a port and spawns it. The Python mcp-proxy by sparfenyuk (README checked 2026-10-07, v0.12.0) does both.
# stdio-only client -> remote Streamable HTTP server
mcp-proxy --transport=streamablehttp https://MCP_HOST/mcp
# local stdio server -> reachable over HTTP on port 8080, bound to 127.0.0.1
mcp-proxy --port=8080 uvx mcp-server-fetch
In the client’s config the bridge replaces the server command. The access token goes in an environment variable, so it never sits in the arguments that process listings show:
{
"mcpServers": {
"remote-tools": {
"command": "mcp-proxy",
"args": ["--transport=streamablehttp", "https://MCP_HOST/mcp"],
"env": { "API_ACCESS_TOKEN": "TOKEN" }
}
}
}
The server-side mode listens on 127.0.0.1 by default, which matches the spec: a locally running server should bind to localhost, and every server must validate the Origin header. Passing --host=0.0.0.0 hands anyone who can reach that port the tools of a process running under your user. Put an authenticating proxy in front before doing that. Authorization is optional in MCP, but a server that supports it must publish OAuth 2.0 Protected Resource Metadata (RFC 9728, 2025-04), and several open-source auth proxies exist to add exactly that in front of a server that has none.
One transport is on its way out. HTTP+SSE, the 2024-11-05 transport, has been deprecated since 2025-03-26, and the 2026-07-28 changelog moves it into the formal Deprecated state with a minimum twelve-month window. New bridges should default to Streamable HTTP; the mcp-proxy README tells users to force it with --transport=streamablehttp when the server uses it.
Aggregating servers behind one endpoint
An aggregator mounts several MCP servers and serves them from one address, so a client config lists one entry instead of ten. TBXark/mcp-proxy (Go, configuration docs checked 2026-10-07) is a common example. Each server key becomes a route under the base URL, and each can carry its own tool filter and access token:
{
"mcpProxy": {
"baseURL": "https://MCP_HOST",
"addr": ":9090",
"type": "streamable-http",
"options": { "authTokens": ["CLIENT_TOKEN"] }
},
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "GITHUB_TOKEN" },
"options": { "toolFilter": { "mode": "block", "list": ["create_or_update_file"] } }
},
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"],
"timeout": "60s"
}
}
}
Two details bite in production. A tool filter is policy at its cheapest. Once the proxy blocks a write tool, no prompt can talk the model into calling it. And the project’s docs warn that timeout wants a duration string, because a bare number is read as nanoseconds, so "timeout": 30 fails every call.
MCP proxy vs MCP gateway
A proxy moves MCP traffic to the right place. A gateway decides who is calling, on whose behalf, what they may do, and records it. A gateway binds each call to a user identity, authorizes per call, rate-limits, and writes an audit log. An aggregator with one shared bearer token is a proxy. Once it knows which person is behind each call and logs the decision, it is a gateway. For the audit half, the 2026-07-28 revision documents OpenTelemetry trace context (traceparent, tracestate, baggage) in each request’s _meta, so a proxy can attach every tool call to the agent’s existing trace.
Do you need an MCP proxy?
A bridge is needed when a client and a server share no transport, or a local server has to be reachable from a container or a colleague’s machine. An aggregator earns its place at about the point where several servers are copied into several people’s configs. A gateway is warranted with many teams, per-tenant isolation or a compliance duty to show who called what.
No proxy is needed when the client speaks Streamable HTTP natively and the remote server runs its own OAuth. An extra hop adds a failure point and one more place where tokens live.
What the 2026-07-28 revision changed
Five changes in the current specification reach an intermediary, and every MCP proxy server built against 2025 behavior needs checking against them.
No sessions. The initialize handshake and the Mcp-Session-Id header are gone. Every request carries its protocol version and client capabilities in _meta, and servers that need state across calls mint handles that travel as ordinary tool arguments. For a proxy this removes the need for sticky load balancing on a session header, since any replica can answer any request.
Routing headers. Every POST carries Mcp-Method, and tools/call, resources/read and prompts/get also carry Mcp-Name. A proxy can now route, rate-limit or block a tool from the headers without parsing the JSON body. Names that are not plain ASCII arrive Base64-encoded in a =?base64?...?= wrapper, and an intermediary that inspects them must decode first.
Header and body must agree. A server that reads the body must reject a request whose headers disagree with it, with 400 Bad Request and error -32020 (HeaderMismatch). The rule exists because a load balancer that routes on the header while the server executes the body would let the two disagree about which tool runs. So a proxy that prefixes tool names has to rewrite the header and the body together, or neither.
Caching hints. List results and resources/read now carry ttlMs and cacheScope. A shared proxy may cache a public tools list for every caller. A private one belongs to the caller who asked.
No stream resumability. Last-Event-ID redelivery is removed. A response stream that breaks loses the in-flight request, and the client must send it again with a new ID. Idle timeouts on load balancers now decide whether a long tool call survives.
Servers on protocol versions 2025-03-26 through 2025-11-25 may still assign sessions, so a proxy in front of mixed versions keeps passing Mcp-Session-Id for them.
Security risks the spec names
The MCP security best practices page treats MCP proxy servers as a named risk class. Four items apply to anyone running one.
Confused deputy. The page defines an MCP proxy server as one that connects clients to a third-party API while acting as a single OAuth client with a static client ID. Combined with dynamic client registration and consent cookies, a malicious client can obtain an authorization code without the user’s consent. The mitigation is a per-user record of approved client IDs, checked before the proxy starts any third-party authorization.
Token passthrough. Accepting a token that was not issued to the MCP server and forwarding it downstream is explicitly forbidden. It breaks audience checks, hides which client acted, and turns the proxy into an exfiltration route for a stolen token.
Spawn escalation. A local proxy that launches stdio servers as child processes turns a client-side bug into code execution. The chain in the spec: XSS in the client, theft of the proxy’s auth token, an authenticated request that spawns an arbitrary command. Mitigations: sandbox or containerize spawned servers, restrict their file access, log every stdio launch, and require extra authorization for dangerous commands. A fixed list of allowed commands in the proxy’s own config beats commands supplied by the client.
SSRF. A client deployed server-side fetches OAuth metadata URLs that an attacker can influence. The spec recommends blocking private and link-local ranges, including 169.254.0.0/16 where cloud metadata lives, and routing those fetches through an egress proxy that enforces network policy, naming Stripe’s Smokescreen as an example. So the specification itself recommends the third kind of proxy, for the client’s own outbound requests.
Network proxies for MCP tool servers
Tools that read the web exit from their host. On a cloud VM that is a hosting ASN, and consumer sites that score network origin classify it as automated before reading a header. Types of proxies sorts the options by what the target sees; the MCP-specific part is where to set one.
Browser servers. Playwright MCP (README checked 2026-10-07) takes --proxy-server with an http:// or socks5:// URL and a --proxy-bypass list. A proxy with a username and password goes in its config file, loaded with --config, as a Playwright proxy object:
{
"browser": {
"isolated": true,
"launchOptions": {
"proxy": { "server": "http://HOST:PORT", "username": "USERNAME", "password": "PASSWORD" }
}
}
}
The reference fetch server. The fetch server (README checked 2026-10-07) takes --proxy-url. By default it sends ModelContextProtocol/1.0 (Autonomous; ...) as its user agent on model-initiated requests and obeys robots.txt for them. A proxy changes the network origin, not that declaration, and a robots.txt disallow still means stop.
Node-based servers. Node’s fetch ignores HTTP_PROXY unless told otherwise. The Node.js docs (checked 2026-10-07) list NODE_USE_ENV_PROXY=1, still marked active development, which routes requests through HTTP_PROXY, HTTPS_PROXY and NO_PROXY: fetch since v24.0.0, http.request since v24.5.0, and both since v22.21.0 on the 22 line. Put it in that one server’s env block:
{
"mcpServers": {
"web-reader": {
"command": "node",
"args": ["server.js"],
"env": {
"NODE_USE_ENV_PROXY": "1",
"HTTPS_PROXY": "http://USERNAME:PASSWORD@HOST:PORT",
"NO_PROXY": "localhost,127.0.0.1"
}
}
}
}
Three rules keep this clean. Scope the proxy to the tool server, never to the whole host, or the client’s LLM API calls go out through a per-GB exit for no reason. Keep proxy credentials in the server’s environment, never in a tool argument or a resource the model can read back. And point at the proxy with http:// or socks5://; SOCKS5 vs HTTP proxy covers which to pick. HTTPS sites stay encrypted end to end through the tunnel either scheme opens.
Which category fits which tool. Tools that read APIs, docs and lenient sites need no proxy, or datacenter proxies when the job is polling or monitoring from many addresses. A browser tool working on consumer sites that score the network fits rotating residential proxies, with a sticky session for multi-step tasks, because a new IP mid-flow breaks a login or a cart. The residential proxy guide covers how those sessions behave. Recurring work in accounts the team owns fits static residential ISP proxies, one fixed address per account. A site that blocks or challenges agents has made a choice, and a proxy is not a way around it.
Deployment and troubleshooting
Both projects above publish container images, and a container gives spawned servers a sandbox with little setup. Terminate TLS at a reverse proxy in front of the bridge, keep the bridge on localhost behind it, and set idle timeouts longer than the slowest tool call. The spec asks servers to send X-Accel-Buffering: no on SSE responses so nginx does not buffer events, and suggests SSE comment lines as keep-alives on quiet streams.
| Symptom | Layer | Likely cause | Fix |
|---|---|---|---|
| 400 with error -32020 | MCP proxy | Proxy rewrote a tool name in the body but not in Mcp-Name, or the reverse | Rewrite both or neither |
| 403 on every request | MCP server | Origin check rejects the proxy’s origin | Set allowed origins on the server or bridge |
| Streamed events arrive in one burst | Reverse proxy | Response buffering | Honor X-Accel-Buffering: no or turn buffering off |
| Long tool calls die at the same second | Load balancer | Idle timeout shorter than the call; since 2026-07-28 the request is lost | Raise the timeout, keep the stream alive |
| Every call to one server fails at once | Aggregator | timeout written as a bare number (nanoseconds) | Use a duration string such as ”60s” |
| 407 from a fetch or browser tool | Network proxy | Wrong username or password in the server’s env | Fix the credentials in that server’s config |
| Tool reads a challenge page | Network origin | Hosting ASN, or a site that refuses agents | If the site refuses agents, stop; if it permits them, match the category to the site |
| Model calls slow, proxy bandwidth high | Environment | Global HTTPS_PROXY sends LLM API calls through the exit | Scope the proxy to the tool server |
Which MCP proxy server to run
If the client speaks only stdio and the server is remote, run a transport bridge and nothing else. If one team runs a handful of servers, run an aggregator with a token per client, a credential per downstream server and tool filters on anything that writes. If many teams share servers and someone will ask who called what, run a gateway that binds identity to every call. Whichever it is, check that it handles the 2026-07-28 headers and never passes a client’s token downstream.
If the problem is what a website sees, no MCP proxy server fixes it. Put a network proxy on that one tool server, choose the category by the target’s rules, and leave the MCP layer alone.
Frequently asked questions
An MCP proxy server is a process that acts as an MCP server toward the client and as an MCP client toward one or more real servers. It is used to bridge transports such as stdio and Streamable HTTP, to serve several servers from one endpoint, and to apply authentication, tool filters and logging. It carries tool calls and results, not the web traffic the tools themselves send.
An MCP proxy forwards and translates MCP traffic so it reaches the right server. An MCP gateway adds identity and policy: it binds each call to a user, authorizes it per call, applies rate limits and writes an audit log. An aggregator with one shared token is a proxy; one that knows who made each call and records the decision is a gateway.
A proxy is needed when a client and a server share no transport, when a local stdio server must be reachable over the network, or when several servers should sit behind one address with shared auth. A gateway is worth it for many teams, per-tenant isolation or compliance. A client that speaks Streamable HTTP natively and connects to a server with its own OAuth does not need one.
Run a transport bridge that the client launches over stdio and that dials the remote server over HTTP. With the Python mcp-proxy package, the client config runs mcp-proxy with the flag --transport=streamablehttp and the server's endpoint URL, and passes the access token in the API_ACCESS_TOKEN environment variable rather than in the arguments.
It can be, because it holds credentials and often launches servers. The MCP security best practices describe a confused deputy attack on proxies that act as a single OAuth client, forbid token passthrough, and describe how a proxy that spawns stdio servers can turn a client-side bug into code execution. Per-user consent records, credentials issued to the proxy itself, sandboxed servers and a localhost binding address those risks.
Yes, if the server supports it. Playwright MCP takes a --proxy-server flag with an http or socks5 URL, or a proxy object with a username and password in its config file. The reference fetch server takes --proxy-url, and Node-based servers honor HTTPS_PROXY when NODE_USE_ENV_PROXY=1 is set in their environment.
Streamable HTTP can still return a response as a Server-Sent Events stream, and long-lived subscriptions use one. The older HTTP+SSE transport from 2024-11-05 has been deprecated since 2025-03-26 and entered the formal Deprecated state in the 2026-07-28 revision, and stream resumability through Last-Event-ID was removed in that revision.
Researched and drafted with AI tools, then checked against our publishing rules before it went live: 9 outside links, 6 links to other proxymint pages, no prices and a fact-check pass against our product records. How we write.