Advanced MCP Patterns
SSE Transport for Remote MCP
While stdio works great for local servers, Server-Sent Events (SSE) and its successor Streamable HTTP enable remote MCP servers accessible over HTTP.
Important: As of the MCP spec update on 2025-03-26, HTTP+SSE has been deprecated in favor of Streamable HTTP, which consolidates the two-endpoint SSE model into a single HTTP endpoint that can optionally upgrade to SSE streaming. Legacy SSE servers still work, but new projects should use Streamable HTTP. The patterns in this lesson apply to both — the transport primitive in your SDK is the main thing that changes.
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 an SSE Server
from mcp.server import Server
from mcp.server.sse import sse_server
import uvicorn
from starlette.applications import Starlette
from starlette.routing import Route
server = Server(name="remote-mcp")
# Register your tools and resources
@server.list_tools()
async def list_tools():
return [...]
# Create the SSE handler
async def handle_sse(request):
async with sse_server() as (read, write):
await server.run(read, write)
# Create Starlette app
app = Starlette(
routes=[Route("/mcp", handle_sse)],
debug=True
)
# Run with uvicorn
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
Client Configuration
Configure a remote MCP server in Claude Desktop:
{
"mcpServers": {
"remote-kb": {
"transport": "sse",
"url": "https://your-server.com/mcp"
}
}
}
SSE 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.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["GET", "POST"],
allow_headers=["*"],
)
Next, we'll add authentication to protect your MCP endpoints. :::
Sign in to rate