Add session-scoped state persistence (#2873)

This commit is contained in:
Jeremiah Lowin 2026-01-16 14:11:21 -05:00 committed by GitHub
commit c8c84ff911
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 744 additions and 167 deletions

View file

@ -274,6 +274,50 @@ The environment variable for controlling the server banner has been renamed:
This change reflects that the setting now applies to all server startup methods, not just the CLI. The banner is now suppressed when running `python server.py` directly, not just when using `fastmcp run`.
### Context State Methods Are Async
<Warning>
**Breaking Change:** `ctx.set_state()` and `ctx.get_state()` are now async methods. Synchronous calls will fail.
</Warning>
Context state has changed from request-scoped to session-scoped, persisting across multiple tool calls within the same MCP session. The methods are now async because they interact with a pluggable storage backend.
<CodeGroup>
```python Before
@mcp.tool
def my_tool(ctx: Context) -> str:
ctx.set_state("key", "value")
value = ctx.get_state("key")
return value
```
```python After
@mcp.tool
async def my_tool(ctx: Context) -> str:
await ctx.set_state("key", "value")
value = await ctx.get_state("key")
return value
```
</CodeGroup>
**What changed:**
- State now persists across requests within a session (not just within a single request)
- Different clients have isolated state (keyed by session ID)
- State expires after 1 day to prevent unbounded memory growth
- New method: `await ctx.delete_state(key)`
**Custom storage backends:**
By default, state uses an in-memory store. For distributed deployments, provide a custom backend:
```python
from key_value.aio.stores.redis import RedisStore
mcp = FastMCP("server", session_state_store=RedisStore(...))
```
See [Session State](/servers/context#session-state) for full documentation.
## v2.14.0
### OpenAPI Parser Promotion

View file

@ -150,6 +150,37 @@ Documentation: `docs/servers/providers/transforms.mdx`, `docs/servers/visibility
---
## Session-Scoped State
v3.0 changes context state from request-scoped to session-scoped. State now persists across multiple tool calls within the same MCP session.
```python
@mcp.tool
async def increment_counter(ctx: Context) -> int:
count = await ctx.get_state("counter") or 0
await ctx.set_state("counter", count + 1)
return count + 1
```
State is automatically keyed by session ID, ensuring isolation between different clients. The implementation uses [pykeyvalue](https://github.com/strawgate/py-key-value) for pluggable storage backends:
```python
from key_value.aio.stores.redis import RedisStore
# Use Redis for distributed deployments
mcp = FastMCP("server", session_state_store=RedisStore(...))
```
**Key details:**
- Methods are now async: `await ctx.get_state()`, `await ctx.set_state()`, `await ctx.delete_state()`
- State expires after 1 day (TTL) to prevent unbounded memory growth
- Works during `on_initialize` middleware when using the same session object
- For distributed HTTP, session identity comes from the `mcp-session-id` header
Documentation: `docs/servers/context.mdx`
---
## Visibility System
Components can be dynamically enabled/disabled at runtime using the visibility system ([#2708](https://github.com/jlowin/fastmcp/pull/2708)).
@ -727,3 +758,19 @@ See `docs/development/v3-notes/auth-provider-env-vars.mdx` for rationale.
`FASTMCP_SHOW_CLI_BANNER` → `FASTMCP_SHOW_SERVER_BANNER` ([#2771](https://github.com/jlowin/fastmcp/pull/2771))
Now applies to all server startup methods, not just the CLI.
### Context State Methods Are Async
`ctx.set_state()` and `ctx.get_state()` are now async and session-scoped:
```python
# v2.x
ctx.set_state("key", "value")
value = ctx.get_state("key")
# v3.0
await ctx.set_state("key", "value")
value = await ctx.get_state("key")
```
State now persists across requests within a session. See "Session-Scoped State" above.