Advanced MCP Patterns

SSE Transport for Remote MCP

5 min read

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

Local default

stdio

Where it runsChild process of the host
Reachable fromThe same machine only
AuthNone needed — OS process boundary
Use forDesktop integrations, CLI tools
Pros
  • Nothing to deploy, nothing to secure
  • No network hop, so latency is negligible
Cons
  • One user, one machine
  • stdout is the protocol channel — a stray print breaks it
Remote default

Streamable HTTP

Where it runsAnywhere you can host HTTP
Reachable fromAnywhere you allow
AuthYours to build — start here, not later
Use forHosted servers, shared team access
Pros
  • A single endpoint that upgrades to streaming only when needed
  • Ordinary HTTP infrastructure applies: proxies, TLS, rate limits
Cons
  • You now own an exposed service and everything that implies
  • Needs CORS handling for browser-based hosts
Legacy

HTTP+SSE

StatusSuperseded by Streamable HTTP
ShapeTwo endpoints: GET stream, POST messages
Still worksYes, for existing deployments
Use forMaintaining something already shipped
Pros
  • Existing servers keep functioning
  • The application-level patterns carry over unchanged
Cons
  • 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

open onceheld opentools/callresponse arrives hereClientHolds the stream open for the w…GET /mcpLong-lived SSE stream. Server →…POST /mcpOne request per message. Client…ServerCorrelates POSTs to the right o…

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. :::

Quiz

Module 4 Quiz: Advanced MCP Patterns

Take Quiz
Was this lesson helpful?

Sign in to rate