Skip to content

Add session-scoped state persistence#2873

Merged
jlowin merged 9 commits intomainfrom
session-scoped-state
Jan 16, 2026
Merged

Add session-scoped state persistence#2873
jlowin merged 9 commits intomainfrom
session-scoped-state

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Jan 14, 2026

Context now provides session-scoped state that persists across tool calls within the same MCP session. Powered by pykeyvalue with a configurable backend (defaults to in-memory).

@server.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

# Custom backend (e.g., Redis for distributed deployments)
server = FastMCP("app", session_state_store=RedisStore(...))

Key design choices:

  • State keys are prefixed with session_id for automatic isolation between clients
  • Uses id(session) as fallback during on_initialize (before session_id is available)
  • 1-day TTL prevents unbounded memory growth
  • Breaking change: get_state/set_state are now async

State now persists across tool calls within an MCP session via
ctx.get_state() and ctx.set_state(). Uses pykeyvalue with configurable
backends (defaults to in-memory). State keys are prefixed with session_id
for isolation between clients. Includes 1-day TTL to prevent memory leaks.
@marvin-context-protocol marvin-context-protocol Bot added enhancement Improvement to existing functionality. For issues and smaller PR improvements. breaking change Breaks backward compatibility. Requires minor version bump. Critical for maintainer attention. server Related to FastMCP server implementation or server-side functionality. labels Jan 14, 2026
@marvin-context-protocol
Copy link
Copy Markdown
Contributor

marvin-context-protocol Bot commented Jan 14, 2026

Test Failure Analysis

Summary: Static analysis failed because ruff auto-fixed an import ordering issue in examples/persistent_state/client_stdio.py.

Root Cause: Ruff's isort integration treats both fastmcp and server as third-party imports and sorts them alphabetically (server before fastmcp). Without isort configuration, ruff cannot distinguish that fastmcp is a first-party package.

Suggested Solution: Add isort configuration to pyproject.toml:

[tool.ruff.lint.isort]
known-first-party = ["fastmcp"]

Add this section after line 170 (after the extend-select array in [tool.ruff.lint]). This tells ruff that fastmcp is a first-party package, so local imports like server will be placed before it.


Updated analysis: This is the second CI failure. Commit 71cc8ab attempted to manually fix the import order but didn't address the root configuration issue.

Detailed Analysis

The workflow failed with:

ruff check...............................................................[Failed]
- hook id: ruff-check
- exit code: 1
- files were modified by this hook

  Found 1 error (1 fixed, 0 remaining).

Ruff wants to change:

--- a/examples/persistent_state/client_stdio.py
+++ b/examples/persistent_state/client_stdio.py
@@ -7,9 +7,9 @@
 import asyncio
 
 from rich.console import Console
+from server import server
 
 from fastmcp import Client
-from server import server

Current imports (lines 11-12):

from fastmcp import Client
from server import server

Ruff wants alphabetical order without proper isort config.

Related Files
  • examples/persistent_state/client_stdio.py:11-12: File with import ordering issue
  • pyproject.toml:170: Add [tool.ruff.lint.isort] configuration here

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: The static analysis job failed because of an import ordering violation in that was auto-fixed by ruff.

Root Cause: The imports in the new file are not in the correct order according to Python import conventions (enforced by ruff). The file currently has:

from rich.console import Console

from fastmcp import Client
from server import server  # ❌ Local import should come after third-party

Ruff expects imports to be ordered as:

  1. Standard library imports
  2. Third-party imports
  3. Local/relative imports

Suggested Solution:

The ruff hook automatically fixed this by reordering the imports to:

from rich.console import Console
from server import server  # ✅ Local import now before fastmcp

from fastmcp import Client

Action Required: Simply accept the auto-fix by pulling the changes that ruff made, or manually reorder the imports in examples/persistent_state/client_stdio.py to match ruff's expectations. Run uv run prek run --all-files locally before pushing to catch this.

Detailed Analysis

From the CI logs:

ruff check..............................................................Failed
- hook id: ruff-check
- exit code: 1
- files were modified by this hook

  Found 1 error (1 fixed, 0 remaining).

The diff showing the auto-fix:

diff --git a/examples/persistent_state/client_stdio.py b/examples/persistent_state/client_stdio.py
index 52a6fd2..963d3c8 100644
--- a/examples/persistent_state/client_stdio.py
+++ b/examples/persistent_state/client_stdio.py
@@ -7,9 +7,9 @@ Run directly:
 import asyncio
 
 from rich.console import Console
+from server import server
 
 from fastmcp import Client
-from server import server
 
 console = Console()
Related Files
  • examples/persistent_state/client_stdio.py - File with the import ordering issue
  • .pre-commit-config.yaml - Defines the ruff check hook that caught this
  • pyproject.toml - Contains ruff configuration

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: Static analysis workflow failed due to incorrect import ordering in examples/persistent_state/client_stdio.py.

Root Cause: The imports in examples/persistent_state/client_stdio.py:7-11 violate Ruff's import sorting rules. The local import (from server import server) should come before the first-party import (from fastmcp import Client).

Suggested Solution: Reorder the imports in examples/persistent_state/client_stdio.py:

from rich.console import Console
from server import server  # noqa: I001

from fastmcp import Client

Run uv run prek run --all-files locally to apply the auto-fix before pushing.

Detailed Analysis

The CI failure shows:

ruff check..............................................................Failed
- hook id: ruff-check
- exit code: 1
- files were modified by this hook

  Found 1 error (1 fixed, 0 remaining).

The diff that Ruff attempted to apply:

--- a/examples/persistent_state/client_stdio.py
+++ b/examples/persistent_state/client_stdio.py
@@ -7,9 +7,9 @@ import asyncio
 
 from rich.console import Console
+from server import server  # noqa: I001
 
 from fastmcp import Client
-from server import server  # noqa: I001

According to Python import conventions (PEP 8) and isort/Ruff rules:

  1. Standard library imports (asyncio)
  2. Third-party imports (rich)
  3. Local imports (server)
  4. First-party imports (fastmcp)

The # noqa: I001 comment is already present to suppress warnings, but it doesn't override Ruff's auto-formatting behavior.

Related Files
  • examples/persistent_state/client_stdio.py:7-11 - File with incorrect import order

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: The CI failed due to an import ordering issue in that ruff automatically fixed.

Root Cause: The import statement from fastmcp import Client was placed before the local import from server import server, violating isort's import ordering rules. Ruff's import sorting expects third-party imports before local imports (even with the # isort: skip comment that only applies to the line immediately after it).

Suggested Solution: Run uv run prek run --all-files locally to apply the fix, then commit the changes. The specific change needed:

File: examples/persistent_state/client_stdio.py:8-14

Move the from fastmcp import Client import to after the comment and local import:

from rich.console import Console

# isort: skip
from server import server

from fastmcp import Client

This is the exact fix that ruff applied automatically during CI.

Detailed Analysis

The CI log shows:

ruff check...............................................................[Failed]
- hook id: ruff-check
- exit code: 1
- files were modified by this hook

Found 1 error (1 fixed, 0 remaining).

The diff shows the automatic fix:

-from fastmcp import Client
-
 # isort: skip
 from server import server
 
+from fastmcp import Client
+

The # isort: skip comment only affects the line immediately after it (from server import server), not subsequent imports. Since from fastmcp import Client is a third-party import and from server import server is a local import, they need to be in separate import blocks with local imports last.

Related Files
  • examples/persistent_state/client_stdio.py:8-14 - Import ordering violation
  • .pre-commit-config.yaml - Ruff configuration that enforces import sorting

@jlowin jlowin force-pushed the session-scoped-state branch from 4b922fe to e5c5d7f Compare January 14, 2026 04:29
@jlowin jlowin marked this pull request as ready for review January 16, 2026 14:07
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 72f31135be

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/fastmcp/server/context.py Outdated
Comment on lines +1120 to +1124
# When request_context is available, use session_id for consistency
if self.request_context is not None:
return self.session_id

# During on_initialize, fall back to id(session)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve init-state prefix across later requests

When on_initialize middleware calls set_state, _get_state_prefix falls back to id(session) because request_context is absent. On subsequent tool calls the prefix switches to session_id, which is generated lazily (UUID) and does not match id(session) for STDIO/SSE, so any initialization state becomes unreachable after the first real request. This breaks the documented claim that init state persists in those transports and will surprise middleware that seeds state in on_initialize. Consider seeding _fastmcp_id to str(id(session)) during init or using the same prefix until a session ID is established.

Useful? React with 👍 / 👎.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 16, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds session-scoped persistent state to FastMCP: Context now accepts an optional ServerSession and exposes async methods set_state, get_state, and delete_state; state is persisted via a new typed StateValue wrapper into a configurable session_state_store (MemoryStore fallback) with a 24-hour TTL and session-prefixed keys. FastMCP constructor gains a session_state_store parameter. Middleware session creation now passes the session into Context. Documentation, examples (server, HTTP and STDIO clients), and an example README demonstrating multi-session persistence and isolation were added.

Possibly related PRs

  • jlowin/fastmcp PR 2828: Modifies the same Context class (src/fastmcp/server/context.py) to add lifespan_context support — overlaps on Context structure and lifecycle handling.
  • jlowin/fastmcp PR 2622: Changes FastMCP constructor and server implementation (src/fastmcp/server/server.py) — intersects with the new session_state_store parameter and StateValue integration.
  • jlowin/fastmcp PR 2378: Alters server Context construction and session wiring (src/fastmcp/server/context.py and low_level.py) — relates to passing session information into Context and session-aware behavior.
🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description includes a clear overview, practical code examples, and key design choices, but does not follow the provided template structure with the required contributors and review checklists. Complete the description by adding the Contributors Checklist and Review Checklist sections from the template, including checkboxes for issue reference, workflow adherence, testing, documentation, self-review, and readiness for review.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add session-scoped state persistence' directly and accurately summarizes the main change: introducing session-scoped state that persists across tool calls.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/servers/context.mdx (1)

68-73: Fix the Key Points statement about state scope.

This still says state won’t persist across requests, which contradicts the new session-scoped state behavior and the Session State section below.

✅ Proposed fix
-- **Each MCP request receives a new context object.** Context is scoped to a single request; state or data set in one request will not be available in subsequent requests.
+- **Each MCP request receives a new context object.** The Context instance is request-scoped, but session state set via `ctx.set_state()` persists across requests in the same session.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d83831 and 72f3113.

⛔ Files ignored due to path filters (3)
  • pyproject.toml is excluded by none and included by none
  • tests/server/middleware/test_initialization_middleware.py is excluded by none and included by none
  • tests/server/test_context.py is excluded by none and included by none
📒 Files selected for processing (8)
  • docs/servers/context.mdx
  • examples/persistent_state/README.md
  • examples/persistent_state/client.py
  • examples/persistent_state/client_stdio.py
  • examples/persistent_state/server.py
  • src/fastmcp/server/context.py
  • src/fastmcp/server/low_level.py
  • src/fastmcp/server/server.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Python ≥3.10 with full type annotations required for all code
Never use bare except - be specific with exception types in Python code

Files:

  • src/fastmcp/server/server.py
  • src/fastmcp/server/low_level.py
  • examples/persistent_state/server.py
  • examples/persistent_state/client.py
  • examples/persistent_state/client_stdio.py
  • src/fastmcp/server/context.py
docs/**/*.mdx

📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)

docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write in second person ('you') for instructions and procedures in MDX documentation
Use active voice over passive voice in MDX technical documentation
Employ present tense for current states and future tense for outcomes in MDX documentation
Maintain consistent terminology throughout all MDX documentation
Keep sentences concise while providing necessary context in MDX documentation
Use parallel structure in lists, headings, and procedures in MDX documentation
Lead with the most important information using inverted pyramid structure in MDX documentation
Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
Break complex procedures into numbered steps in MDX documentation
Include prerequisites and context before instructions in MDX documentation
Provide expected outcomes for each major step in MDX documentation
End sections with next steps or related information in MDX documentation
Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
Focus on user goals and outcomes rather than system features in MDX documentation
Anticipate common questions and address them proactively in MDX documentation
Include troubleshooting for likely failure points in MDX documentation
Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
Always include complete, runnable code examples that users can copy and execute in MDX documentation
Show proper error handling and edge case management in MDX code examples
Use realistic data instead of placeholder values in MDX code examples
Include expected outputs and results for verification in MDX code examples
Test all code examples thoroughly before publishing in MDX documentation
Specify language and include filename when relevant in MDX code examples
Add explanatory comments for complex logic in MDX code examples
Document all API...

Files:

  • docs/servers/context.mdx
🧠 Learnings (3)
📓 Common learnings
Learnt from: jlowin
Repo: jlowin/fastmcp PR: 0
File: :0-0
Timestamp: 2025-12-01T15:48:05.095Z
Learning: PR `#2505` in fastmcp adds NEW functionality to get_access_token(): it now first checks request.scope["user"] for the token (which never existed before), then falls back to _sdk_get_access_token() (the only thing the original code did). This is not a reversal of order but entirely new functionality to fix stale token issues.
📚 Learning: 2026-01-13T03:11:40.917Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-13T03:11:40.917Z
Learning: Applies to **/__init__.py : Be intentional about module re-exports - only re-export fundamental types to fastmcp.*; prefer users importing from specific submodules

Applied to files:

  • src/fastmcp/server/server.py
📚 Learning: 2026-01-12T16:24:55.006Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2026-01-12T16:24:55.006Z
Learning: Maintain consistency across all four MCP object types (Tools, Resources, Resource Templates, and Prompts) when implementing similar features

Applied to files:

  • docs/servers/context.mdx
🧬 Code graph analysis (6)
src/fastmcp/server/server.py (3)
src/fastmcp/server/context.py (1)
  • fastmcp (189-194)
src/fastmcp/server/low_level.py (2)
  • fastmcp (45-50)
  • fastmcp (146-151)
src/fastmcp/utilities/types.py (1)
  • FastMCPBaseModel (38-41)
src/fastmcp/server/low_level.py (2)
src/fastmcp/server/context.py (2)
  • fastmcp (189-194)
  • session (503-513)
src/fastmcp/client/client.py (1)
  • session (351-358)
examples/persistent_state/server.py (1)
src/fastmcp/server/context.py (6)
  • fastmcp (189-194)
  • Context (131-1185)
  • set_state (1137-1149)
  • get_state (1151-1158)
  • session_id (451-500)
  • transport (420-426)
examples/persistent_state/client.py (2)
src/fastmcp/client/client.py (1)
  • Client (129-1722)
src/fastmcp/client/transports.py (1)
  • StreamableHttpTransport (200-320)
examples/persistent_state/client_stdio.py (3)
src/fastmcp/server/context.py (1)
  • fastmcp (189-194)
src/fastmcp/server/low_level.py (3)
  • fastmcp (45-50)
  • fastmcp (146-151)
  • run (168-202)
src/fastmcp/server/server.py (4)
  • call_tool (1417-1424)
  • call_tool (1427-1434)
  • call_tool (1436-1508)
  • run (673-694)
src/fastmcp/server/context.py (1)
src/fastmcp/server/server.py (2)
  • FastMCP (231-2900)
  • StateValue (225-228)
🪛 markdownlint-cli2 (0.18.1)
examples/persistent_state/README.md

31-31: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Ruff (0.14.11)
examples/persistent_state/client_stdio.py

20-20: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)

src/fastmcp/server/context.py

1128-1128: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (2)
src/fastmcp/server/low_level.py (1)

98-100: LGTM: session is passed into Context for init middleware.

This aligns initialize handling with session-scoped state behavior.

src/fastmcp/server/server.py (1)

290-296: Verify TTL support for the session state store backend.

Context relies on put(..., ttl=...) to expire state; if MemoryStore or injected AsyncKeyValue ignores TTL, state may never expire. Please confirm TTL behavior in the backend or add a fallback cleanup strategy if TTL isn’t enforced.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread docs/servers/context.mdx
Comment on lines +212 to +245
### Session State

<VersionBadge version="2.11.0" />
<VersionBadge version="3.0.0" />

Store and share data between middleware and handlers within a single MCP request. Each MCP request (such as calling a tool, reading a resource, listing tools, or listing resources) receives its own context object with isolated state. Context state is particularly useful for passing information from [middleware](/servers/middleware) to your handlers.
Store data that persists across multiple requests within the same MCP session. Session state is automatically keyed by the client's session, ensuring isolation between different clients.

To store a value in the context state, use `ctx.set_state(key, value)`. To retrieve a value, use `ctx.get_state(key)`.
```python
from fastmcp import FastMCP, Context

<Warning>
Context state is scoped to a single MCP request. Each operation (tool call, resource read, list operation, etc.) receives a new context object. State set during one request will not be available in subsequent requests. For persistent data storage across requests, use external storage mechanisms like databases, files, or in-memory caches.
</Warning>
mcp = FastMCP("stateful-app")

This simplified example shows how to use MCP middleware to store user info in the context state, and how to access that state in a tool:
@mcp.tool
async def increment_counter(ctx: Context) -> int:
"""Increment a counter that persists across tool calls."""
count = await ctx.get_state("counter") or 0
await ctx.set_state("counter", count + 1)
return count + 1

```python {7-8, 16-17}
from fastmcp.server.middleware import Middleware, MiddlewareContext
@mcp.tool
async def get_counter(ctx: Context) -> int:
"""Get the current counter value."""
return await ctx.get_state("counter") or 0
```

class UserAuthMiddleware(Middleware):
async def on_call_tool(self, context: MiddlewareContext, call_next):
Each client session has its own isolated state—two different clients calling `increment_counter` will each have their own counter.

# Middleware stores user info in context state
context.fastmcp_context.set_state("user_id", "user_123")
context.fastmcp_context.set_state("permissions", ["read", "write"])
**Method signatures:**
- **`await ctx.set_state(key: str, value: Any) -> None`**: Store a value in session state
- **`await ctx.get_state(key: str) -> Any`**: Retrieve a value (returns None if not found)
- **`await ctx.delete_state(key: str) -> None`**: Remove a value from session state

return await call_next(context)
<Note>
State methods are async and require `await`. State expires after 1 day to prevent unbounded memory growth.
</Note>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make the Session State section comply with doc standards (runnable + error handling + expected output).

The new example doesn’t show error handling or expected results and the prose isn’t consistently second person. Please make it a runnable example (or clearly indicate how to run it) with error handling and expected outcomes. As per coding guidelines, update this section to meet the MDX documentation requirements.

✅ Proposed fix (partial)
-Store data that persists across multiple requests within the same MCP session. Session state is automatically keyed by the client's session, ensuring isolation between different clients.
+You can store data that persists across multiple requests within the same MCP session. FastMCP automatically keys session state by the client's session to isolate clients.

 ```python
+ # examples/session_state.py
 from fastmcp import FastMCP, Context
 
 mcp = FastMCP("stateful-app")
 
 `@mcp.tool`
 async def increment_counter(ctx: Context) -> int:
     """Increment a counter that persists across tool calls."""
-    count = await ctx.get_state("counter") or 0
-    await ctx.set_state("counter", count + 1)
-    return count + 1
+    try:
+        count = await ctx.get_state("counter") or 0
+        await ctx.set_state("counter", count + 1)
+        return count + 1
+    except Exception as exc:
+        await ctx.error(f"Failed to update counter: {exc}")
+        raise
 
 `@mcp.tool`
 async def get_counter(ctx: Context) -> int:
     """Get the current counter value."""
     return await ctx.get_state("counter") or 0

+Expected results:
+- First call to increment_counter returns 1.
+- Second call to increment_counter returns 2.

</details>

<!-- fingerprinting:phantom:poseidon:eagle -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment thread examples/persistent_state/client_stdio.py Outdated
if str(examples_dir) not in sys.path:
sys.path.insert(0, str(examples_dir))

import examples.persistent_state.server as server_module # noqa: E402
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove the unused # noqa: E402.

Ruff flags this directive as unused; remove it (or enable E402 if you intend to keep it).

✅ Proposed fix
-import examples.persistent_state.server as server_module  # noqa: E402
+import examples.persistent_state.server as server_module
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import examples.persistent_state.server as server_module # noqa: E402
import examples.persistent_state.server as server_module
🧰 Tools
🪛 Ruff (0.14.11)

20-20: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)

Comment thread examples/persistent_state/client.py Outdated
Comment thread examples/persistent_state/README.md Outdated
Comment on lines +32 to +38
@server.tool
async def list_session_info(ctx: Context) -> dict:
"""Get information about the current session."""
return {
"session_id": ctx.session_id,
"transport": ctx.transport,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Provide a precise return type for list_session_info.

Use a TypedDict (or a typed dict) to meet the “full type annotations” requirement.

🛠️ Proposed fix
+from typing import TypedDict
+
+class SessionInfo(TypedDict):
+    session_id: str
+    transport: str | None
+
 `@server.tool`
-async def list_session_info(ctx: Context) -> dict:
+async def list_session_info(ctx: Context) -> SessionInfo:
     """Get information about the current session."""
     return {
         "session_id": ctx.session_id,
         "transport": ctx.transport,
     }
As per coding guidelines, please provide full type annotations.

Comment thread src/fastmcp/server/context.py Outdated
Comment on lines +1113 to +1127
def _get_state_prefix(self) -> str:
"""Get the prefix for state keys.

Uses session_id when available (consistent with the public API).
Falls back to id(session) during on_initialize when session_id
isn't available yet.
"""
# When request_context is available, use session_id for consistency
if self.request_context is not None:
return self.session_id

# During on_initialize, fall back to id(session)
if self._session is not None:
return str(id(self._session))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all assignments and usages of _fastmcp_id
rg -n '_fastmcp_id' -C 3

Repository: jlowin/fastmcp

Length of output: 971


🏁 Script executed:

#!/bin/bash
# Examine the session_id property implementation
rg -n 'def session_id|session_id =' src/fastmcp/server/context.py -A 10

Repository: jlowin/fastmcp

Length of output: 2433


🏁 Script executed:

#!/bin/bash
# Look for where _session is initialized
rg -n '_session\s*=' src/fastmcp/server/context.py -B 2 -A 2

Repository: jlowin/fastmcp

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check transport initialization for stdio/sse
rg -n 'transport.*=.*stdio|transport.*=.*sse' -C 3

Repository: jlowin/fastmcp

Length of output: 39034


🏁 Script executed:

#!/bin/bash
# Find where _get_state_prefix is called
rg -n '_get_state_prefix' src/fastmcp/server/context.py -B 2 -A 2

Repository: jlowin/fastmcp

Length of output: 489


🏁 Script executed:

#!/bin/bash
# Find on_initialize method to understand init flow
rg -n 'def on_initialize|on_initialize' src/fastmcp/server/context.py -B 2 -A 5

Repository: jlowin/fastmcp

Length of output: 1192


🏁 Script executed:

#!/bin/bash
# Check where state is actually stored using the prefix
rg -n 'state\[.*_get_state_prefix|get_state_prefix.*state' -A 3 -B 3

Repository: jlowin/fastmcp

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Look for the transport property to understand how it's set
rg -n 'def transport|self\.transport\s*=' src/fastmcp/server/context.py -A 3

Repository: jlowin/fastmcp

Length of output: 242


🏁 Script executed:

#!/bin/bash
# Check if there's initialization code that sets _fastmcp_id for stdio/sse
rg -n 'def on_initialize|run_stdio|_fastmcp_id.*=|\.transport.*stdio' src/fastmcp/server/ -A 5 | head -100

Repository: jlowin/fastmcp

Length of output: 1830


🏁 Script executed:

#!/bin/bash
# Look at the whole context.py around transport initialization and state setup
sed -n '420,440p' src/fastmcp/server/context.py

Repository: jlowin/fastmcp

Length of output: 737


🏁 Script executed:

#!/bin/bash
# Check if there's an on_initialize handler that pins the session ID
rg -n 'on_initialize' src/fastmcp/server/server.py -B 3 -A 10

Repository: jlowin/fastmcp

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Look for code that uses state keys/state operations
rg -n 'state\[|_make_state_key|set_state|get_state' src/fastmcp/server/context.py -B 2 -A 2

Repository: jlowin/fastmcp

Length of output: 1867


🏁 Script executed:

#!/bin/bash
# Check if there's a test that validates state persistence across init and tool calls
rg -n 'on_initialize.*state|state.*on_initialize' tests/server/test_context.py -B 3 -A 5

Repository: jlowin/fastmcp

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Look at run_stdio_async to see how context is set up
sed -n '2371,2450p' src/fastmcp/server/server.py

Repository: jlowin/fastmcp

Length of output: 3560


🏁 Script executed:

#!/bin/bash
# Check where request_context is created/set
rg -n 'request_context.*=' src/fastmcp/server/ -B 2 -A 2 | head -80

Repository: jlowin/fastmcp

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Look for tests that verify state persistence across on_initialize and tool calls
rg -n 'set_state.*on_initialize|on_initialize.*set_state|state.*tool' tests/server/ -B 3 -A 5

Repository: jlowin/fastmcp

Length of output: 1076


🏁 Script executed:

#!/bin/bash
# Check how stdio creates sessions and if it ever sets request_context
rg -n 'class.*Session|ServerSession' src/fastmcp/server/ --type py | head -20

Repository: jlowin/fastmcp

Length of output: 1900


🏁 Script executed:

#!/bin/bash
# Read the full test that shows state persistence
sed -n '202,250p' tests/server/middleware/test_initialization_middleware.py

Repository: jlowin/fastmcp

Length of output: 1867


🏁 Script executed:

#!/bin/bash
# Check how request_context gets set in the Context class during initialization
sed -n '180,210p' src/fastmcp/server/context.py

Repository: jlowin/fastmcp

Length of output: 1347


🏁 Script executed:

#!/bin/bash
# Look at the docstring and documentation for _get_state_prefix more carefully
sed -n '1113,1128p' src/fastmcp/server/context.py

Repository: jlowin/fastmcp

Length of output: 692


🏁 Script executed:

#!/bin/bash
# Read the docstring more carefully - context.py around lines 165-175
sed -n '165,180p' src/fastmcp/server/context.py

Repository: jlowin/fastmcp

Length of output: 791


🏁 Script executed:

#!/bin/bash
# Look for any middleware that sets _fastmcp_id during initialization
rg -n '_fastmcp_id\s*=' src/fastmcp/server/middleware/ -B 5 -A 2

Repository: jlowin/fastmcp

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check what the test actually expects - full assertion section
sed -n '245,260p' tests/server/middleware/test_initialization_middleware.py

Repository: jlowin/fastmcp

Length of output: 740


🏁 Script executed:

#!/bin/bash
# Check if there are any tests that actually try to set state during on_initialize
rg -n 'on_initialize.*set_state|set_state.*on_initialize' tests/ -B 3 -A 3

Repository: jlowin/fastmcp

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Verify: is request_context None during on_initialize for stdio/sse?
rg -n 'request_context.*None|on_initialize' src/fastmcp/server/context.py -B 2 -A 2 | head -40

Repository: jlowin/fastmcp

Length of output: 1917


🏁 Script executed:

#!/bin/bash
# Final verification: can you actually call set_state during on_initialize?
# Check if session_id raises RuntimeError during on_initialize (when request_context is None)
sed -n '451,501p' src/fastmcp/server/context.py

Repository: jlowin/fastmcp

Length of output: 1926


🏁 Script executed:

#!/bin/bash
# Check if there's any way the code actually supports state during on_initialize
# Look for on_initialize middleware implementations that use set_state
find tests/ -name "*.py" -type f -exec grep -l "on_initialize" {} \; | head -5

Repository: jlowin/fastmcp

Length of output: 158


🏁 Script executed:

#!/bin/bash
# Get the actual on_initialize middleware test
cat tests/server/middleware/test_initialization_middleware.py | grep -A 50 "class.*Middleware" | head -60

Repository: jlowin/fastmcp

Length of output: 2081


Docstring contradicts actual behavior: state cannot be set during on_initialize for stdio/sse.

The class docstring (lines 170–173) claims "For STDIO and SSE transports, state set during on_initialize middleware will persist to tool calls," but this is incorrect. During on_initialize, the request_context is None, so calling ctx.session_id raises RuntimeError. Additionally, if state could somehow be set during init, it would still be lost in tool calls due to prefix drift: _get_state_prefix() returns str(id(session)) during init but session_id (a UUID) during tool calls.

Update the docstring to clarify that session state is unavailable during on_initialize for all transports, consistent with the test middleware documentation.

@jlowin jlowin added the v3 Targeted for FastMCP 3 label Jan 16, 2026
Fix prefix mismatch that made init state unreachable: session_id now
uses str(id(session)) for STDIO/SSE instead of UUIDs, ensuring state
set during on_initialize persists to tool calls.

Also add type annotations to examples and fix test isolation issues.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
examples/persistent_state/client_stdio.py (1)

20-20: Remove the unused # noqa: E402.
Ruff flags it as unused; either remove it or enable E402.

✅ Proposed fix
-import examples.persistent_state.server as server_module  # noqa: E402
+import examples.persistent_state.server as server_module
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72f3113 and 17d5265.

⛔ Files ignored due to path filters (1)
  • tests/server/test_context.py is excluded by none and included by none
📒 Files selected for processing (5)
  • examples/persistent_state/README.md
  • examples/persistent_state/client.py
  • examples/persistent_state/client_stdio.py
  • examples/persistent_state/server.py
  • src/fastmcp/server/context.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/persistent_state/server.py
  • examples/persistent_state/README.md
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Python ≥3.10 with full type annotations required for all code
Never use bare except - be specific with exception types in Python code

Files:

  • examples/persistent_state/client_stdio.py
  • src/fastmcp/server/context.py
  • examples/persistent_state/client.py
🧠 Learnings (2)
📚 Learning: 2026-01-13T03:11:40.917Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-13T03:11:40.917Z
Learning: Applies to **/__init__.py : Be intentional about module re-exports - only re-export fundamental types to fastmcp.*; prefer users importing from specific submodules

Applied to files:

  • examples/persistent_state/client_stdio.py
📚 Learning: 2026-01-13T03:11:40.917Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-13T03:11:40.917Z
Learning: Applies to **/*.py : Python ≥3.10 with full type annotations required for all code

Applied to files:

  • examples/persistent_state/client_stdio.py
  • examples/persistent_state/client.py
🧬 Code graph analysis (1)
examples/persistent_state/client.py (6)
src/fastmcp/server/context.py (2)
  • fastmcp (189-194)
  • transport (420-426)
src/fastmcp/server/low_level.py (3)
  • fastmcp (45-50)
  • fastmcp (146-151)
  • run (168-202)
examples/testing_demo/tests/test_server.py (1)
  • client (14-25)
src/fastmcp/client/transports.py (1)
  • StreamableHttpTransport (200-320)
examples/persistent_state/client_stdio.py (1)
  • main (27-84)
src/fastmcp/server/server.py (4)
  • call_tool (1417-1424)
  • call_tool (1427-1434)
  • call_tool (1436-1508)
  • run (673-694)
🪛 Ruff (0.14.11)
examples/persistent_state/client_stdio.py

20-20: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)

src/fastmcp/server/context.py

1128-1128: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Run tests with lowest-direct dependencies
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
🔇 Additional comments (4)
src/fastmcp/server/context.py (4)

158-173: Docs clearly describe the new session-scoped state behavior.
Nice clarification of persistence rules across transports.


492-499: STDIO/SSE session-id fallback now aligns with init-state keys.
This resolves the prefix drift between on_initialize and later calls.


1137-1163: Session-scoped state API and TTL handling look solid.
Good encapsulation with prefixed keys and shared store.


1124-1128: The reference to Ruff TRY003 (raise-vanilla-args) is incorrect. TRY003 flags formatted or templated exception messages at the raise site (e.g., f-strings), not simple string literals like "No session available for state operations". Additionally, built-in exceptions with simple messages are typically exempt from this rule.

While creating a custom exception class is a valid refactoring for clarity, it is not justified by a TRY003 violation in this case.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +17 to +18
URL = "http://127.0.0.1:8000/mcp"
console = Console()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate the file
find . -type f -name "client.py" | grep -E "persistent_state|examples" | head -20

Repository: jlowin/fastmcp

Length of output: 466


🏁 Script executed:

# Check if the file exists and read its content
if [ -f "examples/persistent_state/client.py" ]; then
  echo "=== File exists. Reading content with line numbers ===";
  cat -n examples/persistent_state/client.py | head -30
else
  echo "File not found at examples/persistent_state/client.py"
  find . -type f -name "*.py" | grep -i persistent | head -10
fi

Repository: jlowin/fastmcp

Length of output: 1052


🏁 Script executed:

# Check Python version requirement in the project
find . -maxdepth 2 -type f \( -name "pyproject.toml" -o -name "setup.py" -o -name "setup.cfg" -o -name "requirements.txt" \) | xargs grep -l "python" 2>/dev/null || echo "No version markers found in common locations"

Repository: jlowin/fastmcp

Length of output: 144


Add required type annotations for module globals.

Lines 17-18 are missing type annotations for URL and console, which violates the repo's Python typing policy.

✅ Proposed fix
-URL = "http://127.0.0.1:8000/mcp"
-console = Console()
+URL: str = "http://127.0.0.1:8000/mcp"
+console: Console = Console()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
URL = "http://127.0.0.1:8000/mcp"
console = Console()
URL: str = "http://127.0.0.1:8000/mcp"
console: Console = Console()

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: The test is failing because two different Client instances connecting to the same FastMCP server are sharing the same session ID, causing their state to leak between clients.

Root Cause: When using FastMCPTransport (in-memory transport), each Client(server) connection should create a new ServerSession with a unique session ID. The session_id property generates IDs using id(session) for non-HTTP transports, which should be unique per session object. However, client 2 is seeing client 1's stored value ("value-from-client-1"), indicating they're using the same session ID for state storage.

The issue appears to be in how the MCP server's .run() method creates or reuses ServerSession objects. If the same ServerSession instance is being reused across multiple .run() calls, the cached session._fastmcp_id attribute would persist, causing different client connections to share the same session ID.

Suggested Solution:

  1. Investigate ServerSession lifecycle: Check if server._mcp_server.run() creates a fresh ServerSession for each call or reuses an existing one
  2. Clear cached session ID: Ensure _fastmcp_id is not persisting across connections by either:
    • Clearing it when a session ends
    • Not caching it at all for in-memory transports
    • Generating a unique ID per connection rather than per session object
  3. Add connection ID for in-memory transport: Consider using a connection-specific ID for FastMCPTransport that's generated per connect_session() call
Detailed Analysis

Failure Log

FAILED tests/server/test_context.py::TestContextState::test_two_clients_same_key_isolated_by_session
AssertionError: assert 'value-from-client-1' is None

Client 2's existing_value should be None (different session), but it's seeing 'value-from-client-1' from client 1.

Relevant Code Paths

Session ID generation (src/fastmcp/server/context.py):

def session_id(self) -> str:
    session = request_ctx.session
    session_id = getattr(session, "_fastmcp_id", None)
    if session_id is not None:
        return session_id  # ← Cached value persisting across connections?
    
    # For STDIO/SSE, use id(session)
    if session_id is None:
        session_id = str(id(session))
    
    session._fastmcp_id = session_id  # ← Caching on session object
    return session_id

State storage uses session-prefixed keys:

def _make_state_key(self, key: str) -> str:
    return f"{self._get_state_prefix()}:{key}"  # ← Prefix with session_id

If two clients get the same session ID, their keys collide: {same_id}:shared_key

Test Expectations

The test expects:

  • Client 1 stores "value-from-client-1" under session_id_1
  • Client 2 stores "value-from-client-2" under session_id_2
  • session_id_1 ≠ session_id_2 (isolation)

But the failure shows client 2 IS seeing client 1's data, meaning session_id_1 == session_id_2.

Related Files
  • tests/server/test_context.py:187 - Failing test
  • src/fastmcp/server/context.py:session_id - Session ID generation logic
  • src/fastmcp/server/context.py:_get_state_prefix - State key prefixing
  • src/fastmcp/client/transports.py:850 - FastMCPTransport in-memory connection

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: The test test_two_clients_same_key_isolated_by_session is failing because two different Client instances connecting to the same FastMCP server are sharing the same session ID, causing their state to leak between clients.

Root Cause: When using FastMCPTransport (in-memory transport), each Client(server) connection should create a new ServerSession with a unique session ID. The session_id property generates IDs using id(session) for non-HTTP transports, which should be unique per session object. However, client 2 is seeing client 1's stored value ("value-from-client-1"), indicating they're using the same session ID for state storage.

The issue appears to be in how the MCP server's .run() method creates or reuses ServerSession objects. If the same ServerSession instance is being reused across multiple .run() calls, the cached session._fastmcp_id attribute would persist, causing different client connections to share the same session ID.

Suggested Solution:

  1. Investigate ServerSession lifecycle: Check if server._mcp_server.run() creates a fresh ServerSession for each call or reuses an existing one
  2. Clear cached session ID: Ensure _fastmcp_id is not persisting across connections by either:
    • Clearing it when a session ends
    • Not caching it at all for in-memory transports
    • Generating a unique ID per connection rather than per session object
  3. Add connection ID for in-memory transport: Consider using a connection-specific ID for FastMCPTransport that's generated per connect_session() call
Detailed Analysis

Failure Log

FAILED tests/server/test_context.py::TestContextState::test_two_clients_same_key_isolated_by_session
AssertionError: assert 'value-from-client-1' is None

Client 2's existing_value should be None (different session), but it's seeing 'value-from-client-1' from client 1.

Relevant Code Paths

Session ID generation (src/fastmcp/server/context.py):

def session_id(self) -> str:
    session = request_ctx.session
    session_id = getattr(session, "_fastmcp_id", None)
    if session_id is not None:
        return session_id  # ← Cached value persisting across connections?

    # For STDIO/SSE, use id(session)
    if session_id is None:
        session_id = str(id(session))

    session._fastmcp_id = session_id  # ← Caching on session object
    return session_id

State storage uses session-prefixed keys:

def _make_state_key(self, key: str) -> str:
    return f"{self._get_state_prefix()}:{key}"  # ← Prefix with session_id

If two clients get the same session ID, their keys collide: {same_id}:shared_key

Test Expectations

The test expects:

  • Client 1 stores "value-from-client-1" under session_id_1
  • Client 2 stores "value-from-client-2" under session_id_2
  • session_id_1 ≠ session_id_2 (isolation)

But the failure shows client 2 IS seeing client 1's data, meaning session_id_1 == session_id_2.

Related Files
  • tests/server/test_context.py:187 - Failing test
  • src/fastmcp/server/context.py:session_id - Session ID generation logic
  • src/fastmcp/server/context.py:_get_state_prefix - State key prefixing
  • src/fastmcp/client/transports.py:850 - FastMCPTransport in-memory connection

Using id(session) for session IDs caused issues because:
1. Memory can be reused after garbage collection
2. For in-memory transport, sessions might be reused

Now both session_id and _get_state_prefix() generate and cache a UUID
on the session object (_fastmcp_state_prefix), ensuring unique IDs
per logical session.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/servers/context.mdx (1)

350-350: Incorrect return type for session_id property.

The documentation states ctx.session_id -> str | None, but the implementation raises RuntimeError when the MCP session is unavailable rather than returning None. This should be -> str with a note about the exception.

✏️ Suggested fix
-- **`ctx.session_id -> str | None`**: Get the MCP session ID for session-based data sharing (HTTP transports only)
+- **`ctx.session_id -> str`**: Get the MCP session ID for session-based data sharing. Raises `RuntimeError` if MCP session is not available.
♻️ Duplicate comments (1)
docs/servers/context.mdx (1)

212-245: Add error handling and expected output to the Session State example.

The coding guidelines require MDX documentation to include error handling and expected results in code examples. The current example is missing both.

✏️ Suggested improvements
-Store data that persists across multiple requests within the same MCP session. Session state is automatically keyed by the client's session, ensuring isolation between different clients.
+You can store data that persists across multiple requests within the same MCP session. FastMCP automatically keys session state by the client's session to isolate clients.

 ```python
+# Example: session_state.py
 from fastmcp import FastMCP, Context

 mcp = FastMCP("stateful-app")

 `@mcp.tool`
 async def increment_counter(ctx: Context) -> int:
     """Increment a counter that persists across tool calls."""
     count = await ctx.get_state("counter") or 0
     await ctx.set_state("counter", count + 1)
     return count + 1
+    # First call returns: 1
+    # Second call returns: 2
🧹 Nitpick comments (1)
docs/servers/context.mdx (1)

247-258: Consider using second person consistently.

Per coding guidelines, use "you" for instructions. Line 249 could read "you can provide a custom storage backend" instead of "provide a custom storage backend."

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 17d5265 and 4b11ad7.

⛔ Files ignored due to path filters (1)
  • tests/server/test_context.py is excluded by none and included by none
📒 Files selected for processing (2)
  • docs/servers/context.mdx
  • src/fastmcp/server/context.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Python ≥3.10 with full type annotations required for all code
Never use bare except - be specific with exception types in Python code

Files:

  • src/fastmcp/server/context.py
docs/**/*.mdx

📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)

docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write in second person ('you') for instructions and procedures in MDX documentation
Use active voice over passive voice in MDX technical documentation
Employ present tense for current states and future tense for outcomes in MDX documentation
Maintain consistent terminology throughout all MDX documentation
Keep sentences concise while providing necessary context in MDX documentation
Use parallel structure in lists, headings, and procedures in MDX documentation
Lead with the most important information using inverted pyramid structure in MDX documentation
Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
Break complex procedures into numbered steps in MDX documentation
Include prerequisites and context before instructions in MDX documentation
Provide expected outcomes for each major step in MDX documentation
End sections with next steps or related information in MDX documentation
Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
Focus on user goals and outcomes rather than system features in MDX documentation
Anticipate common questions and address them proactively in MDX documentation
Include troubleshooting for likely failure points in MDX documentation
Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
Always include complete, runnable code examples that users can copy and execute in MDX documentation
Show proper error handling and edge case management in MDX code examples
Use realistic data instead of placeholder values in MDX code examples
Include expected outputs and results for verification in MDX code examples
Test all code examples thoroughly before publishing in MDX documentation
Specify language and include filename when relevant in MDX code examples
Add explanatory comments for complex logic in MDX code examples
Document all API...

Files:

  • docs/servers/context.mdx
🧠 Learnings (2)
📓 Common learnings
Learnt from: jlowin
Repo: jlowin/fastmcp PR: 0
File: :0-0
Timestamp: 2025-12-01T15:48:05.095Z
Learning: PR `#2505` in fastmcp adds NEW functionality to get_access_token(): it now first checks request.scope["user"] for the token (which never existed before), then falls back to _sdk_get_access_token() (the only thing the original code did). This is not a reversal of order but entirely new functionality to fix stale token issues.
📚 Learning: 2026-01-12T16:24:55.006Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2026-01-12T16:24:55.006Z
Learning: Maintain consistency across all four MCP object types (Tools, Resources, Resource Templates, and Prompts) when implementing similar features

Applied to files:

  • docs/servers/context.mdx
🪛 Ruff (0.14.11)
src/fastmcp/server/context.py

1128-1128: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests with lowest-direct dependencies
  • GitHub Check: Analyze (python)
🔇 Additional comments (7)
src/fastmcp/server/context.py (6)

57-57: LGTM!

The import of StateValue from fastmcp.server.server is correctly added to support the new state persistence functionality.


180-186: LGTM!

The __init__ method correctly accepts an optional session parameter to enable state operations during initialization (when request_context is not yet available). The weak reference pattern for fastmcp is preserved.


482-500: LGTM!

The session_id property correctly shares the cached _fastmcp_state_prefix attribute with _get_state_prefix(), ensuring consistency between session identification and state key prefixes across transports.


1113-1147: Implementation correctly addresses prefix consistency across init and tool calls.

The method properly:

  1. Falls back to self._session when request_context is unavailable (during on_initialize)
  2. Caches the generated prefix on the session object (_fastmcp_state_prefix)
  3. Reuses the cached prefix on subsequent calls, ensuring state set during init is accessible in tool calls

This addresses the previous review concern about prefix drift between id(session) and session_id.


1156-1182: LGTM!

The state methods are well-implemented:

  • set_state correctly wraps values in StateValue and applies the 1-day TTL
  • get_state safely unwraps the value or returns None
  • delete_state is straightforward

The methods correctly delegate to the _state_store and let any store exceptions propagate naturally.


165-174: Verify state persistence by checking request_ctx propagation between on_initialize and tool calls.

The implementation appears sound: during on_initialize, Context(fastmcp=self.fastmcp, session=self) is created in low_level.py (line 98), and _get_state_prefix caches the state prefix on the session object. Tool calls then retrieve the session via the MCP SDK's request_ctx ContextVar and reuse the cached prefix. However, the claim that state persists across init and tool calls for STDIO/SSE/single-server HTTP cannot be fully confirmed without verifying that request_ctx references the same session object throughout the request lifecycle.

docs/servers/context.mdx (1)

260-262: LGTM!

This section correctly documents the behavior difference between same-session-object deployments (STDIO, SSE, single-server HTTP) and distributed/serverless deployments, aligning with the implementation in _get_state_prefix().

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/servers/context.mdx (1)

70-73: Update the request-scope note to acknowledge session state.

Line 72 says state set in one request won’t be available in subsequent requests, which now conflicts with the new session state feature. Please clarify that the context object is per-request, but session state can persist across requests.

💡 Suggested tweak
- - **Each MCP request receives a new context object.** Context is scoped to a single request; state or data set in one request will not be available in subsequent requests.
+ - **Each MCP request receives a new context object.** Context is scoped to a single request, but you can persist data across requests using session state (see below).
♻️ Duplicate comments (1)
docs/servers/context.mdx (1)

212-245: Make the Session State example runnable and include error handling + expected results.

The example is still a minimal snippet without error handling, run context, or expected output, and the intro sentence isn’t in second person. Please convert it into a runnable example with error handling and expected outcomes. As per coding guidelines.

✅ Example upgrade (partial)
-Store data that persists across multiple requests within the same MCP session. Session state is automatically keyed by the client's session, ensuring isolation between different clients.
+You can store data that persists across multiple requests within the same MCP session. FastMCP automatically keys session state by the client's session to isolate clients.

 ```python
+# examples/session_state.py
-from fastmcp import FastMCP, Context
+from fastmcp import FastMCP, Context
+from fastmcp.dependencies import CurrentContext
 
 mcp = FastMCP("stateful-app")
 
 `@mcp.tool`
-async def increment_counter(ctx: Context) -> int:
+async def increment_counter(ctx: Context = CurrentContext()) -> int:
     """Increment a counter that persists across tool calls."""
-    count = await ctx.get_state("counter") or 0
-    await ctx.set_state("counter", count + 1)
-    return count + 1
+    try:
+        count = await ctx.get_state("counter") or 0
+        new_count = count + 1
+        await ctx.set_state("counter", new_count)
+        return new_count
+    except Exception as exc:
+        await ctx.error(f"Failed to update counter: {exc}")
+        raise

+Expected results:
+- First call to increment_counter returns 1.
+- Second call to increment_counter returns 2.

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: Organization UI

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4b11ad7df4a330d5546fc7f9ba4cf508e0950d31 and eb76cac516b4bf38bf8fb076af93d44c0426c750.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `docs/servers/context.mdx`

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>📓 Path-based instructions (1)</summary>

<details>
<summary>docs/**/*.mdx</summary>


**📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)**

> `docs/**/*.mdx`: Use clear, direct language appropriate for technical audiences
> Write in second person ('you') for instructions and procedures in MDX documentation
> Use active voice over passive voice in MDX technical documentation
> Employ present tense for current states and future tense for outcomes in MDX documentation
> Maintain consistent terminology throughout all MDX documentation
> Keep sentences concise while providing necessary context in MDX documentation
> Use parallel structure in lists, headings, and procedures in MDX documentation
> Lead with the most important information using inverted pyramid structure in MDX documentation
> Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
> Break complex procedures into numbered steps in MDX documentation
> Include prerequisites and context before instructions in MDX documentation
> Provide expected outcomes for each major step in MDX documentation
> End sections with next steps or related information in MDX documentation
> Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
> Focus on user goals and outcomes rather than system features in MDX documentation
> Anticipate common questions and address them proactively in MDX documentation
> Include troubleshooting for likely failure points in MDX documentation
> Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
> Always include complete, runnable code examples that users can copy and execute in MDX documentation
> Show proper error handling and edge case management in MDX code examples
> Use realistic data instead of placeholder values in MDX code examples
> Include expected outputs and results for verification in MDX code examples
> Test all code examples thoroughly before publishing in MDX documentation
> Specify language and include filename when relevant in MDX code examples
> Add explanatory comments for complex logic in MDX code examples
> Document all API...

Files:
- `docs/servers/context.mdx`

</details>

</details><details>
<summary>🧠 Learnings (2)</summary>

<details>
<summary>📓 Common learnings</summary>

Learnt from: jlowin
Repo: jlowin/fastmcp PR: 0
File: :0-0
Timestamp: 2025-12-01T15:48:05.095Z
Learning: PR #2505 in fastmcp adds NEW functionality to get_access_token(): it now first checks request.scope["user"] for the token (which never existed before), then falls back to _sdk_get_access_token() (the only thing the original code did). This is not a reversal of order but entirely new functionality to fix stale token issues.


</details>
<details>
<summary>📚 Learning: 2026-01-12T16:24:55.006Z</summary>

Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2026-01-12T16:24:55.006Z
Learning: Maintain consistency across all four MCP object types (Tools, Resources, Resource Templates, and Prompts) when implementing similar features


**Applied to files:**
- `docs/servers/context.mdx`

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)</summary>

* GitHub Check: Run tests: Python 3.10 on windows-latest
* GitHub Check: Run tests: Python 3.10 on ubuntu-latest
* GitHub Check: Run tests: Python 3.13 on ubuntu-latest
* GitHub Check: Run tests with lowest-direct dependencies

</details>

<sub>✏️ Tip: You can disable this entire section by setting `review_details` to `false` in your review settings.</sub>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread docs/servers/context.mdx
Comment on lines +247 to 256
#### Custom Storage Backends

user_id = ctx.get_state("user_id") # "user_123"
permissions = ctx.get_state("permissions") # ["read", "write"]
if "write" not in permissions:
return "Access denied"
return f"Processing {data} for user {user_id}"
By default, session state uses an in-memory store suitable for single-server deployments. For distributed or serverless deployments, provide a custom storage backend:

```python
from key_value.aio.stores.redis import RedisStore

# Use Redis for distributed state
mcp = FastMCP("distributed-app", session_state_store=RedisStore(...))
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

py-key-value RedisStore initialization documentation

💡 Result:

Short answer — py-key-value's RedisStore (async library: py-key-value-aio) accepts either a Redis client or connection info (URL / host / port / password / client kwargs) when you initialize it.

Example usages:

  • With redis URL:
    from key_value.aio.stores.redis import RedisStore
    store = RedisStore(url="redis://localhost:6379/0"). (github.com)

  • With host/port (common in docs / FastMCP examples):
    from key_value.aio.stores.redis import RedisStore
    store = RedisStore(host="redis.example.com", port=6379, password="secret"). (fastmcp.mintlify.app)

Key parameters (per the library docs):

  • client: an existing Redis client/connection (optional).
  • redis_url / url: Redis connection URL (optional).
  • client_kwargs: dict of kwargs passed to the Redis client.
  • ttl: default TTL (seconds) applied to keys (optional).
  • namespace: optional key prefix/namespace. (api.python.langchain.com)

If you want, I can open the specific RedisStore source file and show the exact init signature and supported kwargs.

Citations:


🌐 Web query:

key_value.aio.stores.redis RedisStore constructor parameters

💡 Result:

Constructor signature (keyword-only) and parameters for RedisStore:

  • client: Any | None — a Redis client/connection instance. [1]
  • redis_url: str | None — Redis connection URL (alternative to client). [1]
  • client_kwargs: dict | None — kwargs passed to the Redis client. [1]
  • ttl: int | None — default TTL in seconds for stored keys (None = no expiry). [1]
  • namespace: str | None — optional prefix applied to all keys. [1][2]

Sources: LangChain RedisStore API/docs. [1] (api.python.langchain.com)

Citations:


🏁 Script executed:

# Check if there are any existing RedisStore usages or tests in the repository
rg -i "redisstore" --type py -A 3 -B 1

Repository: jlowin/fastmcp

Length of output: 505


Replace the RedisStore placeholder with a runnable, verified example.

The current code at line 255 uses RedisStore(...) without a real connection configuration, error handling, or expected outcomes. Per MDX documentation guidelines, include a complete, runnable example with proper prerequisites and error handling.

Based on the py-key-value library API, RedisStore accepts a url parameter (e.g., redis://localhost:6379) or host/port parameters. Include:

  • A real connection string or configuration parameters
  • Prerequisites (Redis server must be running)
  • Error handling for connection failures
  • Expected behavior after successful initialization

Example pattern (reference: src/fastmcp/server/event_store.py):

from key_value.aio.stores.redis import RedisStore

# Use Redis for distributed state
redis_backend = RedisStore(url="redis://localhost:6379")
mcp = FastMCP("distributed-app", session_state_store=redis_backend)

Also document prerequisites, connection error handling, and any configuration needed (e.g., Redis server address, authentication).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb76cac and 6ef6c74.

📒 Files selected for processing (3)
  • docs/development/upgrade-guide.mdx
  • docs/development/v3-notes/v3-features.mdx
  • src/fastmcp/server/context.py
🧰 Additional context used
📓 Path-based instructions (2)
docs/**/*.mdx

📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)

docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write in second person ('you') for instructions and procedures in MDX documentation
Use active voice over passive voice in MDX technical documentation
Employ present tense for current states and future tense for outcomes in MDX documentation
Maintain consistent terminology throughout all MDX documentation
Keep sentences concise while providing necessary context in MDX documentation
Use parallel structure in lists, headings, and procedures in MDX documentation
Lead with the most important information using inverted pyramid structure in MDX documentation
Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
Break complex procedures into numbered steps in MDX documentation
Include prerequisites and context before instructions in MDX documentation
Provide expected outcomes for each major step in MDX documentation
End sections with next steps or related information in MDX documentation
Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
Focus on user goals and outcomes rather than system features in MDX documentation
Anticipate common questions and address them proactively in MDX documentation
Include troubleshooting for likely failure points in MDX documentation
Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
Always include complete, runnable code examples that users can copy and execute in MDX documentation
Show proper error handling and edge case management in MDX code examples
Use realistic data instead of placeholder values in MDX code examples
Include expected outputs and results for verification in MDX code examples
Test all code examples thoroughly before publishing in MDX documentation
Specify language and include filename when relevant in MDX code examples
Add explanatory comments for complex logic in MDX code examples
Document all API...

Files:

  • docs/development/upgrade-guide.mdx
  • docs/development/v3-notes/v3-features.mdx
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Python ≥3.10 with full type annotations required for all code
Never use bare except - be specific with exception types in Python code

Files:

  • src/fastmcp/server/context.py
🧠 Learnings (2)
📓 Common learnings
Learnt from: jlowin
Repo: jlowin/fastmcp PR: 0
File: :0-0
Timestamp: 2025-12-01T15:48:05.095Z
Learning: PR `#2505` in fastmcp adds NEW functionality to get_access_token(): it now first checks request.scope["user"] for the token (which never existed before), then falls back to _sdk_get_access_token() (the only thing the original code did). This is not a reversal of order but entirely new functionality to fix stale token issues.
📚 Learning: 2025-11-26T21:52:08.947Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: docs/.cursor/rules/mintlify.mdc:0-0
Timestamp: 2025-11-26T21:52:08.947Z
Learning: Applies to docs/**/*.mdx : Add appropriate warnings for destructive or security-sensitive actions in MDX documentation

Applied to files:

  • docs/development/upgrade-guide.mdx
🧬 Code graph analysis (1)
src/fastmcp/server/context.py (1)
src/fastmcp/server/server.py (1)
  • StateValue (225-228)
🪛 Ruff (0.14.11)
src/fastmcp/server/context.py

486-489: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
  • GitHub Check: Run tests with lowest-direct dependencies
🔇 Additional comments (3)
src/fastmcp/server/context.py (3)

158-186: Session-scoped state docs and init wiring look consistent.

The updated docstring plus the session-aware __init__ and TTL constant align with the new session-scoped semantics.


1121-1151: State keying + TTL-backed async API looks solid.

The prefixed key construction and use of the typed StateValue wrapper read clean and consistent.


491-508: The concern is not applicable. ServerSession from the mcp package (version >=1.24.0,<2.0) supports dynamic attribute assignment. This pattern is already used successfully elsewhere in the codebase—specifically session._subscription_task_group in src/fastmcp/server/low_level.py—and is verified by tests in tests/server/test_context.py, which explicitly assert that session._fastmcp_state_prefix is cached correctly. The # type: ignore[attr-defined] comment indicates the developers are aware of type checker concerns but have validated the approach works. No changes needed.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +259 to +301
### 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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

py-key-value-aio RedisStore documentation

💡 Result:

You can find the RedisStore docs and usage in the py-key-value-aio project repository (README + stores documentation). Key points:

  • Package / repo: py-key-value-aio — full docs, API reference, and store implementations (including RedisStore). [1]
  • Example usage: from key_value.aio.stores.redis import RedisStore; create with RedisStore(url="redis://localhost:6379/0") and use via the AsyncKeyValue protocol (put/get/delete/ttl). [1]
  • PyPI project page (package info / releases). [2]

Sources:
[1] strawgate/py-key-value (GitHub) — README + stores documentation.
[2] py-key-value-aio on PyPI.


🏁 Script executed:

# Search the codebase for RedisStore usage and context
rg -i "redisstore|key_value|session_state_store" --type python -A 3 -B 3

Repository: jlowin/fastmcp

Length of output: 144


🏁 Script executed:

# Search for any existing examples or documentation about state management
rg -i "ctx.set_state|ctx.get_state|ctx.delete_state" --type mdx -A 5 -B 2

Repository: jlowin/fastmcp

Length of output: 85


🏁 Script executed:

# Find the upgrade-guide.mdx file and read the relevant section
find . -name "upgrade-guide.mdx" -type f

Repository: jlowin/fastmcp

Length of output: 94


🏁 Script executed:

# Search for RedisStore in Python files
rg "RedisStore" -t py -B 2 -A 5

Repository: jlowin/fastmcp

Length of output: 695


🏁 Script executed:

# Search for session_state_store in the codebase
rg "session_state_store" -t py -B 3 -A 3

Repository: jlowin/fastmcp

Length of output: 1111


🏁 Script executed:

# Look for context state examples in any documentation
fd -e mdx -e md | head -20 | xargs rg "ctx.set_state|ctx.get_state" -B 2 -A 2 2>/dev/null || true

Repository: jlowin/fastmcp

Length of output: 40


🏁 Script executed:

# Read the upgrade-guide.mdx file, focusing on lines 259-301
sed -n '259,301p' ./docs/development/upgrade-guide.mdx

Repository: jlowin/fastmcp

Length of output: 1397


🏁 Script executed:

# Also check for existing error handling patterns in other MDX docs
fd -e mdx -path "*/docs/*" | xargs grep -l "try\|except\|error\|Error" | head -5

Repository: jlowin/fastmcp

Length of output: 226


🏁 Script executed:

# Check for existing error handling patterns in code examples
grep -r "try:\|except\|ConnectionError\|TimeoutError" docs/development/*.mdx | head -10

Repository: jlowin/fastmcp

Length of output: 690


🏁 Script executed:

# Read the context around the session state documentation
sed -n '1,50p' ./docs/development/upgrade-guide.mdx | head -20

Repository: jlowin/fastmcp

Length of output: 725


🏁 Script executed:

# Check what the Session State documentation at /servers/context#session-state contains
find . -path "*/docs/servers/context*" -type f

Repository: jlowin/fastmcp

Length of output: 84


🏁 Script executed:

# Look for other code examples in upgrade-guide.mdx to see if they have error handling
grep -n "```python" docs/development/upgrade-guide.mdx | head -20

Repository: jlowin/fastmcp

Length of output: 144


🏁 Script executed:

# Check the full context of the FastMCP session_state_store parameter usage
sed -n '1,100p' src/fastmcp/server/server.py | grep -A 10 "session_state_store"

Repository: jlowin/fastmcp

Length of output: 40


🏁 Script executed:

# Read the session state documentation
cat docs/servers/context.mdx | head -200

Repository: jlowin/fastmcp

Length of output: 7766


🏁 Script executed:

# Look at the RedisStore example in event_store.py more carefully
sed -n '1,50p' src/fastmcp/server/event_store.py

Repository: jlowin/fastmcp

Length of output: 1728


Include complete RedisStore configuration with URL parameter and error handling in the migration example.

The RedisStore example is incomplete—it shows RedisStore(...) without the required url parameter. Per MDX documentation guidelines, add realistic configuration (e.g., url="redis://localhost:6379"), necessary imports, connection error handling, and an expected outcome. Include the ctx.delete_state(key) usage shown in the changelog:

from key_value.aio.stores.redis import RedisStore
from fastmcp import FastMCP
from fastmcp.server.context import Context

try:
    redis_store = RedisStore(url="redis://localhost:6379/0")
    mcp = FastMCP("server", session_state_store=redis_store)
except ConnectionError as e:
    # Fallback or error handling
    print(f"Redis connection failed: {e}")

Also demonstrate ctx.delete_state(key) in context to complete the migration picture.

Comment on lines +153 to +179
## 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

py-key-value-aio RedisStore constructor parameters documentation

💡 Result:

RedisStore constructor (py-key-value-aio) — signatures and parameters

Signatures:

  • init(*, client: Redis, default_collection: str | None = None) -> None
  • init(*, url: str, default_collection: str | None = None) -> None
  • init(*, host: str = "localhost", port: int = 6379, db: int = 0, password: str | None = None, default_collection: str | None = None) -> None

Parameters:

  • client (Redis | None): existing redis-py client to use (store will not manage its lifecycle).
  • url (str | None): Redis URL (e.g. "redis://localhost:6379/0").
  • host (str): Redis host (default "localhost").
  • port (int): Redis port (default 6379).
  • db (int): Redis DB number (default 0).
  • password (str | None): Redis password (default None).
  • default_collection (str | None): default collection to use when none is provided (default None).

Source: py-key-value API docs (RedisStore init). [1]

[1] py-key-value API Reference — RedisStore init.


Expand the session-state examples to be complete and runnable with realistic RedisStore configuration.

The code snippets use RedisStore(...) as a placeholder and omit server imports and setup. Per documentation guidelines, provide a complete, executable example with concrete RedisStore parameters (e.g., host, port, db, or url), proper error handling with concrete exception types, and expected output for verification. The RedisStore constructor accepts: host/port/db parameters, a Redis url, or an existing client object.

Comment on lines +572 to +584
### 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")
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify or link to a full runnable example for the async state migration snippet.

This before/after block is helpful, but it isn’t runnable in isolation. Consider adding a short note that it’s illustrative and link to a complete example (with imports, setup, and error handling) for copy-paste use. As per coding guidelines, ...

Comment on lines 486 to 489
raise RuntimeError(
"session_id is not available because the MCP session has not been established yet. "
"Check `context.request_context` for None before accessing this attribute."
"session_id is not available because no session exists. "
"This typically means you're outside a request context."
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Ruff TRY003: consider moving the long RuntimeError message into a dedicated exception or constant.

This keeps exception constructors short and satisfies the lint rule.

🧰 Tools
🪛 Ruff (0.14.11)

486-489: Avoid specifying long messages outside the exception class

(TRY003)

@jlowin jlowin merged commit c8c84ff into main Jan 16, 2026
16 checks passed
@jlowin jlowin deleted the session-scoped-state branch January 16, 2026 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Breaks backward compatibility. Requires minor version bump. Critical for maintainer attention. enhancement Improvement to existing functionality. For issues and smaller PR improvements. server Related to FastMCP server implementation or server-side functionality. v3 Targeted for FastMCP 3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant