Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/run/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,30 @@ Nothing about the fan-out cares which server object a stream is attached to. Two
* Streams are **not** resumable and events are **not** replayed. Losing a replica drops its streams; the clients re-listen and re-fetch. There is no event store to share and nothing else to configure. This is the one place where scaling out is genuinely just more of the same.
* A server that needs no change notifications skips the bus: **[turn them off](../handlers/subscriptions.md#turning-it-off)**.

## Runtimes that reuse a process

Some serverless adapters, including AWS Lambda adapters, start and stop the ASGI lifespan for each invocation while keeping the Python process warm. Keep your registered `MCPServer`, but call `streamable_http_app()` inside each invocation before passing the returned app to the adapter:

```python title="server.py"
from starlette.applications import Starlette

from mcp.server import MCPServer
from mcp.server.transport_security import TransportSecuritySettings

mcp = MCPServer("example")
security = TransportSecuritySettings(allowed_hosts=["mcp.example.com", "mcp.example.com:*"])


def app_for_invocation() -> Starlette:
return mcp.streamable_http_app(stateless_http=True, transport_security=security)
```

Call `app_for_invocation()` from your adapter's handler on every invocation. If you mount the MCP app in a host Starlette or FastAPI app, build the host app, its lifespan, and the mount inside that handler too. Each new HTTP app has a new `StreamableHTTPSessionManager`; re-entering the old app's lifespan fails because its manager can run only once.

Replace `mcp.example.com` with your public Host header. If browsers call the endpoint, also configure `allowed_origins`; see [the Host allowlist](#before-anything-else-the-host-allowlist).

The example uses `stateless_http=True` so legacy clients do not try to resume a session that ended with the previous invocation. It gives up the legacy server-to-client back-channel; see [Serving legacy clients](legacy-clients.md). Your server's own `lifespan=` also runs on every invocation, so keep resources that must outlive an invocation outside it. Modern requests are sessionless, but they still need a fresh app when the adapter restarts its lifespan. If your adapter keeps one lifespan open across invocations, keep the app for that lifespan instead.

## What the SDK does not give you

An `MCPServer` is a protocol implementation, not an application server. The deployment knobs you go looking for next are missing on purpose:
Expand All @@ -187,6 +211,7 @@ An `MCPServer` is a protocol implementation, not an application server. The depl
* The default `requestState` key is `os.urandom(32)`, minted per process. A multi-round-trip retry that reaches a different worker fails with `-32602` *"Invalid or expired requestState"*.
* The fix is `RequestStateSecurity(keys=[...])` **and** the same server name on every instance. The name is the token's default audience claim. Same keys, same name.
* Change notifications cross replicas through one shared `SubscriptionBus`. The SDK's only implementation is in-process; the two-method `Protocol` over your own pub/sub is yours to write.
* If an adapter restarts ASGI lifespan on each warm-process invocation, call `streamable_http_app()` for each invocation. A manager cannot enter `run()` twice.
* There is no `workers=`, no health route, no production settings object. Bring your own ASGI server.

The other thing a real hostname needs in front of it is a token: **[Authorization](authorization.md)**.
1 change: 1 addition & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif
**[Add to an existing app](run/asgi.md)** is the page for this, including several servers in one app and FastAPI. Two neighbouring strings from the same class:

* `StreamableHTTPSessionManager .run() can only be called once per instance. Create a new instance if you need to run again.` The manager is single-use; entering the same app's lifespan twice hits it.
In a warm AWS Lambda process, an adapter that starts and stops the lifespan for each invocation hits this on the second invocation. Build a fresh HTTP app inside each invocation; see [Runtimes that reuse a process](run/deploy.md#runtimes-that-reuse-a-process).
* `mcp.session_manager` only exists **after** `streamable_http_app()` has been called, so build the routes first and touch the manager only inside the lifespan.

## `MCPError: Session not found`
Expand Down
Loading