Advanced MCP Patterns
Remote MCP over Streamable HTTP
stdio only reaches a server running on the same machine as the host. To serve a team, or anything you deploy, you need Streamable HTTP — a single HTTP endpoint that upgrades to a stream only when a response needs one.
On HTTP+SSE: the older two-endpoint HTTP+SSE transport was deprecated in the 2025-03-26 revision and replaced by Streamable HTTP. It is still listed in the specification's deprecated features registry, which is where to check its status rather than trusting any date written here — deprecated features document an earliest-removal date and are removed on a maintainer decision after that. Do not start a new project on it. This lesson teaches Streamable HTTP throughout.
Choosing a transport
Three options exist in the wild, and one of them is only there for compatibility. Transports evolve faster than course material does — check the MCP specification for the current state before starting anything new.
stdio vs Streamable HTTP vs legacy HTTP+SSE
stdio
- Nothing to deploy, nothing to secure
- No network hop, so latency is negligible
- One user, one machine
- stdout is the protocol channel — a stray print breaks it
Streamable HTTP
- A single endpoint that upgrades to streaming only when needed
- Ordinary HTTP infrastructure applies: proxies, TLS, rate limits
- You now own an exposed service and everything that implies
- Needs CORS handling for browser-based hosts
HTTP+SSE
- Existing servers keep functioning
- The application-level patterns carry over unchanged
- Two endpoints to route, secure, and keep in sync
- Do not start a new project on it
The patterns in the rest of this lesson — multi-client handling, CORS, auth — apply to both HTTP transports. What changes between them is the transport primitive your SDK hands you, not the shape of your code.
Setting Up a Remote Server
from mcp.server import MCPServer
mcp = MCPServer(name="remote-mcp")
@mcp.tool()
def ping(msg: str = "hi") -> str:
"""Echo a message back."""
return f"pong: {msg}"
if __name__ == "__main__":
mcp.run("streamable-http")
That is the entire difference from the stdio server you have been writing: one argument to
run(). The server now listens on /mcp and speaks the same protocol to the same handlers.
When you need to mount MCP inside an existing web application rather than run it standalone, ask for the ASGI app instead and route to it yourself:
app = mcp.streamable_http_app() # a Starlette app exposing POST/GET /mcp
streamable_http_app() takes the interesting knobs — streamable_http_path,
stateless_http, json_response, max_request_body_size — so reach for it when you are
integrating, and for mcp.run("streamable-http") when you are not.
Client Configuration
Configure a remote MCP server in Claude Desktop:
{
"mcpServers": {
"remote-kb": {
"transport": "sse",
"url": "https://your-server.com/mcp"
}
}
}
Message Flow
The asymmetry is the thing to understand: the stream carries everything from the server, while anything going to the server travels as a separate POST. Responses do not come back on the POST — they arrive on the stream you opened earlier.
Why SSE needs two channels
This split is exactly what Streamable HTTP removes by collapsing both directions onto one endpoint that upgrades to a stream only when a response actually needs streaming. If the diagram above feels like more moving parts than the problem warrants, that reaction is the reason the spec moved on.
Handling Multiple Clients
SSE naturally supports multiple concurrent clients:
from contextlib import asynccontextmanager
from collections import defaultdict
class MultiClientServer:
def __init__(self):
self.clients = defaultdict(dict)
@asynccontextmanager
async def client_session(self, client_id: str):
self.clients[client_id] = {"connected": True}
try:
yield
finally:
del self.clients[client_id]
async def broadcast(self, message):
for client_id in self.clients:
await self.send_to_client(client_id, message)
CORS Configuration
For browser-based clients, configure CORS:
from starlette.middleware.cors import CORSMiddleware
app = mcp.streamable_http_app()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"], # name them; do not ship "*"
allow_methods=["GET", "POST"],
allow_headers=["*"],
expose_headers=["Mcp-Session-Id"],
)
allow_origins=["*"] on a server that carries credentials is how a browser-based host on
any origin gets to call your tools. Name the origins you actually serve. Mcp-Session-Id
has to be in expose_headers or a browser client cannot read it back, which breaks session
resumption in a way that looks like a server bug.
Next, we'll add authentication to protect your MCP endpoints. :::
Sign in to rate