Subtitle: If you treat transport as the last mile, MCP projects tend to fail in the ugliest places. Mature remote servers design transport, proxying, sessions, and client compatibility from day one.
When people first meet MCP, transport is often imagined like this:
- get it working locally first
- worry about exposure later
- it is all just HTTP anyway
- add a proxy if needed
- plug it into ChatGPT at the end
That instinct is understandable.
It is also dangerous.
Because remote deployment in MCP is not just “wrap the local demo in a URL”.
What it actually changes is:
- who maintains the session
- how client and server negotiate transport
- which headers, paths, auth modes, and streaming behaviours must align
- how reverse proxies affect connection semantics
- whether the server is really ready to be accessed by multiple clients, over real networks, for real periods of time
In other words, transport is not the last mile. It is part of the protocol design.
My conclusion first: if the server is meant for ChatGPT, API clients, or any cross-network host, transport is not an appendix
The MCP ecosystem itself does not treat transport as a footnote.
The 2025-03-26 MCP changelog explicitly replaced the older HTTP+SSE transport with the more flexible Streamable HTTP transport. FastMCP now recommends HTTP transport for production remote deployments and keeps SSE mainly for backward compatibility. OpenAI is equally explicit: for ChatGPT Developer Mode and the Responses API, remote MCP servers currently need to support either SSE or streaming/streamable HTTP. citeturn677151view5turn677151view4turn677151view3turn677151view1turn677151view2
Put those together and you get one very practical engineering truth:
If your MCP server is meant to live at a remote URL rather than as a local stdio process, transport needs to be treated as a first-class design decision from the start.
Separate the three deployment shapes first
Transport conversations get muddled very quickly unless these are separated.
1. Local stdio server
Best for:
- local development
- a single client
- local agents or CLI workflows
- no public URL requirement
The advantage is speed and simplicity.
The limitation is obvious: this is not a public remote service.
2. Remote server over Streamable HTTP
This is the direction that makes the most sense for new remote deployments.
Best for:
- multiple clients
- public or controlled URL access
- cloud-based hosts
- transport, auth, and reverse proxies that need to be governed together
FastMCP’s own docs describe HTTP transport as the recommended path for production deployments because the server runs as an independent web service and manages its own lifecycle. citeturn677151view4turn677151view3
3. SSE transport
SSE is not unusable.
But if you are building a new system, control the infrastructure, and do not carry unusual compatibility constraints, I would not make it the default choice.
FastMCP’s current wording is very clear: SSE is there for backward compatibility. citeturn677151view4
Why “it runs locally” and “it is remote-ready” are completely different milestones
This is one of the most important distinctions in the whole topic.
Many engineers instinctively define success like this:
- the
@toolfunctions are written - it runs locally
- deployment is the final step
For MCP, that is too optimistic.
Because “it runs locally” proves only that:
- the Python code is alive
- tool registration succeeded
- some transport works on the local machine
It does not prove that:
- your reverse proxy preserves paths correctly
- auth headers are forwarded the way you think
- the host and server agree on transport expectations
- the session and connection behaviour are suitable across real networks
- Cloudflare, nginx, TLS, and origin settings will not distort the outcome
So the first real milestone for a remote MCP server should not be:
it runs
It should be:
it survives a real client over a real transport through a real network edge
Why remote deployment amplifies problems you could ignore locally
A local stdio server can avoid a lot of reality.
For example:
- no DNS
- no TLS
- no reverse proxy
- no cross-network latency
- no edge or CDN
- no pressure on session persistence
- no variation between different hosts or clients
As soon as you move to remote deployment, all of those become real.
Problem 1: path is no longer just a path
Locally, you may only care about localhost:8000.
Remotely, the path may become:
https://mcp.example.com/mcp
with all of this in the middle:
- Cloudflare
- nginx
- path rewriting
- trailing slash differences
At that point, path is no longer just an internal route. It becomes part of the end-to-end contract.
Problem 2: headers and auth really do cross multiple layers
In remote deployments, request headers are not just “whatever the app receives”. They depend on:
- what the edge modifies
- what the proxy forwards
- whether auth is bearer, OAuth, or mixed auth
- whether any header leaks downstream where it should not
That is exactly why I respect the amount of production work FastMCP has been putting into auth and header handling. This is not cosmetic infrastructure. citeturn677151view10turn677151view11
Problem 3: session and streaming stop being local assumptions
As soon as you have cross-network access, long-lived connections, and multiple clients, the session and streaming properties of the transport start to shape reliability.
MCP’s roadmap is already discussing horizontal scaling, stateless operation, and middleware patterns around Streamable HTTP. That alone is enough evidence that transport is not some side concern. citeturn635383search0
My current rule of thumb: transport choice is really a decision about how you want the server to live
That sentence has become more useful to me than “which protocol is newer”.
If you need:
- local development
- a single client
- rapid validation
- almost no network edge
then stdio is entirely reasonable.
If you need:
- a real host like ChatGPT or an API client
- a public or semi-public URL
- governance around reverse proxy, TLS, and auth
- reuse across multiple clients or environments
then remote HTTP transport belongs in the design from the beginning.
That is why I now think of transport less as a connection method and more as:
the server’s mode of existence
A minimal shape that already feels more production-like
If I want a remote server shape that I can live with, I now prefer something like this:
from fastmcp import FastMCP
from fastmcp.server.http import create_streamable_http_app
mcp = FastMCP("Example Gateway")
@mcp.tool
def healthcheck() -> dict:
return {"ok": True}
app = create_streamable_http_app(
server=mcp,
streamable_http_path="/mcp",
)
Then I run it behind uvicorn on loopback:
uvicorn mcp_server.app.server:app --host 127.0.0.1 --port 8000
and expose it publicly through nginx at:
https://mcp.example.com/mcp
I like this shape not because it is flashy, but because the roles are clean:
- FastMCP: capability surface
- uvicorn: app process
- nginx: public entrypoint, TLS, reverse proxy
- Cloudflare / DNS: edge and name resolution
Why reverse proxies are not optional decoration
A common question is: “Why not just expose the app port directly?”
In theory, you can.
In practice, I increasingly dislike that choice.
A reverse proxy gives you cleaner control over:
- TLS termination
- a stable path such as
/mcp - future health endpoints
- keeping the app bound to loopback
- separating public networking from app lifecycle
So the proxy is not just “another layer”.
It is how you separate the protocol entrypoint from the application process.
Why transport confusion often gets misdiagnosed as a model problem
This is another reason I wanted this article early in the series.
If transport and deployment are not thought through properly, people often conclude that the problem is:
- the model chose the wrong tool
- the skill instructions are too weak
- the description is poor
- ChatGPT is being inconsistent
But the actual cause may be:
- the endpoint is not reliably reachable
- the path rewrite is wrong
- the proxy is not forwarding what you expect
- the auth mode is mismatched
- the client and server are not compatible on transport
So in MCP systems, what looks like a prompt problem can easily be a transport problem.
That is precisely why I wanted transport to be the first article in the engineering deep dive series.
If this layer is shaky, security, contracts, and skills are all built on a floor that moves.
The counterexample I want to leave behind
I also want to keep one counterexample in the article so it does not become another “everyone should remote-deploy immediately” post.
Not every MCP server should start life as a remote deployment.
If you are still:
- validating tool surface
- cleaning up contracts
- working entirely locally
- serving a single client
then stdio is often the smarter choice.
The value of remote deployment is not that it is more advanced.
Its value begins when you truly need:
- a public URL
- host integration
- authentication
- transport governance
- reverse proxies
- stable access for real clients
My current working principle
If I had to compress the article into one sentence that actually changes design decisions, it would be this:
If your target is remote MCP, transport is not a deployment appendix. It is an architectural decision.
It affects:
- how you define the server entrypoint
- how the path is fixed
- whether a proxy exists
- where auth lives
- how host compatibility is validated
- what “ready for real clients” even means
Next in the series: security, not more frameworks
Once transport is clear, the next natural topic is security, auth, and public server hardening.
Because once the server is truly reachable from outside, the questions are no longer only:
- can it connect?
- can it stream?
- can tools be listed?
They become:
- who is allowed to connect?
- who can call which tool?
- which headers and tokens cross which layers?
- how do you stop a public entrypoint from becoming a new attack surface?
Transport keeps the server alive.
Security decides whether it can stay alive safely.