The 2026 spec was redesigned so MCP servers scale like ordinary web services, if you deploy them correctly.
Because 2026-07-28 requests carry their own version and capabilities and there are no sessions, any replica can answer any request. You can put several server processes behind a plain round-robin load balancer with no sticky routing. What still needs care:
- Legacy clients. Clients still on the 2025 protocol open sessions, and those sessions live in one process's memory, so they still need sticky routing. SDK v2 serves both eras from one app.
- Multi round-trip state. When a tool returns "input required", the SDK seals your
request_state(encrypts and signs it) so the client carries it to the retry. The default key is generated per process, so in a multi-replica deployment pass the same key to every replica withRequestStateSecurity(keys=[...])and rotate it like any secret. - Notifications across replicas. A tool-list or resource change published on one replica must reach clients listening on another; plug a shared
SubscriptionBus(for example backed by Redis) into the server. - Cross-call state belongs to you. If a workflow spans calls, mint an explicit handle (like a job ID) returned from one tool and passed as an argument to the next, and store the data in a database, not process memory.
- Proxies and gateways. HTTP requests now carry
Mcp-MethodandMcp-Nameheaders, so gateways can route and rate-limit without reading bodies. Behind a TLS-terminating proxy, set the allowed Host list to your public hostname. - Caching. List and read results carry
ttlMsandcacheScopehints; set them (cache_hints=) so clients stop re-polling stable lists.
Long-running jobs that outlive one HTTP request are handled by the official tasks extension, which the Python SDK does not implement yet.
Example
import os
from mcp.server.mcpserver import MCPServer, RequestStateSecurity
from mcp.server.transport_security import TransportSecuritySettings
# Every replica: same name, same request_state key (from your secret store).
mcp = MCPServer(
"billing",
request_state_security=RequestStateSecurity(keys=[os.environ["MCP_STATE_KEY"]]),
)
@mcp.tool()
def start_export(account_id: str) -> str:
"""Start an export and return a job ID to pass to export_status."""
job_id = f"job-{account_id}" # real code: write the job to a shared database
return job_id
app = mcp.streamable_http_app(
transport_security=TransportSecuritySettings(allowed_hosts=["mcp.example.com"]),
)
# gunicorn/uvicorn workers x N behind a round-robin load balancerRemember
- Current-spec traffic needs no sticky sessions; legacy clients still do
- Share the request_state key across replicas
- Use a shared subscription bus for cross-replica notifications
- Keep workflow state in explicit handles + shared storage