Deploying and scaling remote servers

Model Context Protocol (MCP) · Expert · 15 min

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 with RequestStateSecurity(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-Method and Mcp-Name headers, 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 ttlMs and cacheScope hints; 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 balancer

Remember

  • 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

Check yourself

  1. After scaling to 4 replicas, elicitation retries randomly fail with invalid request state. Likely cause?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: py.sdk.modelcontextprotocol.io/run/deploy/