Document session state isolation across mount boundaries (#3801)

This commit is contained in:
Jeremiah Lowin 2026-04-09 16:15:57 -04:00 committed by GitHub
commit 0194c6e8ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 39 additions and 0 deletions

View file

@ -277,6 +277,43 @@ mcp = FastMCP("distributed-app", session_state_store=RedisStore(...))
Any backend compatible with the [py-key-value-aio](https://github.com/strawgate/py-key-value) `AsyncKeyValue` protocol works. See [Storage Backends](/servers/storage-backends) for more options including Redis, DynamoDB, and MongoDB.
#### State and Mounted Servers
Each `FastMCP` instance has its own session state store. When you `mount()` a child server, state set on the parent is not visible to tools on the child, and vice versa:
```python
from fastmcp import FastMCP, Context
from fastmcp.server.middleware import Middleware, MiddlewareContext
parent = FastMCP("Parent")
child = FastMCP("Child")
parent.mount(child, namespace="child")
class Stasher(Middleware):
async def on_call_tool(self, context: MiddlewareContext, call_next):
await context.fastmcp_context.set_state("user", "alice")
return await call_next(context)
parent.add_middleware(Stasher())
@child.tool
async def whoami(ctx: Context) -> str:
return await ctx.get_state("user") or "unknown" # returns "unknown"
```
To share state across the mount boundary, pass the same store to both servers:
```python
from key_value.aio.stores.memory import MemoryStore
store = MemoryStore()
parent = FastMCP("Parent", session_state_store=store)
child = FastMCP("Child", session_state_store=store)
parent.mount(child, namespace="child")
```
Alternatively, state set with `serializable=False` lives on the request context and is inherited by mounted children automatically — use it when the value is request-scoped and does not need to persist across tool calls.
#### State During Initialization
State set during `on_initialize` middleware persists to subsequent tool calls when using the same session object (STDIO, SSE, single-server HTTP). For distributed/serverless HTTP deployments where different machines handle init and tool calls, state is isolated by the `mcp-session-id` header.

View file

@ -84,6 +84,8 @@ parent.mount(child, namespace="child")
Requests to `child_tool` flow through the parent's `AuthMiddleware` first, then through the child's `LoggingMiddleware`.
Middleware-stored state does not automatically cross mount boundaries. If `AuthMiddleware` on the parent calls `ctx.set_state("user_id", ...)`, a tool on the child server calling `ctx.get_state("user_id")` will get `None` — each `FastMCP` instance owns its own session state store. To share state across the mount, either pass the same `session_state_store` to both servers or use `serializable=False` for request-scoped values. See [State and Mounted Servers](/servers/context#state-and-mounted-servers) for details.
## Hooks
Rather than processing every message identically, FastMCP provides specialized hooks at different levels of specificity. Multiple hooks fire for a single request, going from general to specific: