Skip to content

Commit df8f49a

Browse files
committed
docs: cover reused-process runtimes (AWS Lambda) in deploy.md
StreamableHTTPSessionManager.run() can only run once per instance. docs/troubleshooting.md already explains this for a Mount swallowing a lifespan and for several long-running workers. It doesn't cover a serverless runtime that reuses one warm process across separate invocations, which hits the same error the moment a container is reused, the normal case in production. Adds that as a third cause in deploy.md, with the per-invocation fix and a SnapStart-specific note, and links it from troubleshooting.md. Fixes #3590
1 parent f1b6589 commit df8f49a

2 files changed

Lines changed: 25 additions & 1 deletion

File tree

‎docs/run/deploy.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,29 @@ Nothing about the fan-out cares which server object a stream is attached to. Two
169169
* The bus carries four small typed events, never JSON-RPC. Acknowledgment, filtering, and stream lifecycle stay in the SDK, so your bus cannot break the protocol; it can only move events between processes.
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

172+
## Reused-process runtimes (Lambda, and similar)
173+
174+
A different problem from having many workers: having one process that gets reused, sequentially, across calls that don't share a lifespan. AWS Lambda is the common case. A container that handled one invocation can be frozen and woken up for the next one, often minutes or hours later, with the same Python process and the same objects still in memory.
175+
176+
Build `mcp.streamable_http_app()` once, at import time, the way you would for a normal server, and the first invocation works fine. The second invocation against that same warm container fails on every request:
177+
178+
```text
179+
RuntimeError: StreamableHTTPSessionManager .run() can only be called once per instance. Create a new instance if you need to run again.
180+
```
181+
182+
The manager's lifespan already ran and finished at the end of the first invocation. **[Troubleshooting](../troubleshooting.md)** covers this same error for two other causes; a reused process is the third, and it's easy to miss because a cold container only ever sees one request during local testing.
183+
184+
The fix is to build the app inside the handler, per invocation, instead of once at import time:
185+
186+
```python
187+
def handler(event, context):
188+
app = mcp.streamable_http_app() # fresh instance, every invocation
189+
...
190+
```
191+
192+
!!! warning "SnapStart"
193+
SnapStart's snapshot is taken before any request arrives and deliberately excludes live network connections and running event loops. An app built once at import time and cached across invocations is exactly what that model assumes you won't do. Build fresh per invocation here too.
194+
172195
## What the SDK does not give you
173196

174197
An `MCPServer` is a protocol implementation, not an application server. The deployment knobs you go looking for next are missing on purpose:
@@ -186,6 +209,7 @@ An `MCPServer` is a protocol implementation, not an application server. The depl
186209
* 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"*.
187210
* 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.
188211
* 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.
212+
* A process reused across invocations (Lambda, and similar) hits the same single-use-manager error as a `Mount` swallowing a lifespan or several workers. Build the app fresh inside the handler, per invocation, not once at import time.
189213
* There is no `workers=`, no health route, no production settings object. Bring your own ASGI server.
190214

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

‎docs/troubleshooting.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif
247247

248248
**[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:
249249

250-
* `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.
250+
* `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. A process reused sequentially across separate invocations (AWS Lambda, and similar) hits this too, once a warm container serves its second request. **[Reused-process runtimes](run/deploy.md#reused-process-runtimes-lambda-and-similar)** is that case specifically.
251251
* `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.
252252

253253
## `MCPError: Session not found`

0 commit comments

Comments
 (0)