Advanced MCP Patterns

Remote MCP over Streamable HTTP

5 min read

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

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

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

Quiz

Module 4 Quiz: Advanced MCP Patterns

Take Quiz
Was this lesson helpful?

Sign in to rate