Skip to content

Commit 0bc072e

Browse files
committed
Document warm-process HTTP app lifespans
1 parent 91941ed commit 0bc072e

2 files changed

Lines changed: 26 additions & 0 deletions

File tree

‎docs/run/deploy.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,30 @@ Nothing about the fan-out cares which server object a stream is attached to. Two
170170
* 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.
171171
* A server that needs no change notifications skips the bus: **[turn them off](../handlers/subscriptions.md#turning-it-off)**.
172172

173+
## Runtimes that reuse a process
174+
175+
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:
176+
177+
```python title="server.py"
178+
from starlette.applications import Starlette
179+
180+
from mcp.server import MCPServer
181+
from mcp.server.transport_security import TransportSecuritySettings
182+
183+
mcp = MCPServer("example")
184+
security = TransportSecuritySettings(allowed_hosts=["mcp.example.com", "mcp.example.com:*"])
185+
186+
187+
def app_for_invocation() -> Starlette:
188+
return mcp.streamable_http_app(stateless_http=True, transport_security=security)
189+
```
190+
191+
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.
192+
193+
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).
194+
195+
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.
196+
173197
## What the SDK does not give you
174198

175199
An `MCPServer` is a protocol implementation, not an application server. The deployment knobs you go looking for next are missing on purpose:
@@ -187,6 +211,7 @@ An `MCPServer` is a protocol implementation, not an application server. The depl
187211
* 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"*.
188212
* 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.
189213
* 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.
214+
* If an adapter restarts ASGI lifespan on each warm-process invocation, call `streamable_http_app()` for each invocation. A manager cannot enter `run()` twice.
190215
* There is no `workers=`, no health route, no production settings object. Bring your own ASGI server.
191216

192217
The other thing a real hostname needs in front of it is a token: **[Authorization](authorization.md)**.

‎docs/troubleshooting.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif
256256
**[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:
257257

258258
* `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.
259+
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).
259260
* `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.
260261

261262
## `MCPError: Session not found`

0 commit comments

Comments
 (0)