Conversation
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.
Test Failure AnalysisSummary: Static analysis failed because ruff auto-fixed an import ordering issue in Root Cause: Ruff's isort integration treats both Suggested Solution: Add isort configuration to [tool.ruff.lint.isort]
known-first-party = ["fastmcp"]Add this section after line 170 (after the Updated analysis: This is the second CI failure. Commit Detailed AnalysisThe workflow failed with: 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 serverCurrent imports (lines 11-12): from fastmcp import Client
from server import serverRuff wants alphabetical order without proper isort config. Related Files
|
Test Failure AnalysisSummary: 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-partyRuff expects imports to be ordered as:
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 ClientAction Required: Simply accept the auto-fix by pulling the changes that ruff made, or manually reorder the imports in Detailed AnalysisFrom the CI logs: 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
|
Test Failure AnalysisSummary: Static analysis workflow failed due to incorrect import ordering in Root Cause: The imports in Suggested Solution: Reorder the imports in from rich.console import Console
from server import server # noqa: I001
from fastmcp import ClientRun Detailed AnalysisThe CI failure shows: 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: I001According to Python import conventions (PEP 8) and isort/Ruff rules:
The Related Files
|
Test Failure AnalysisSummary: The CI failed due to an import ordering issue in that ruff automatically fixed. Root Cause: The import statement Suggested Solution: Run File: Move the from rich.console import Console
# isort: skip
from server import server
from fastmcp import ClientThis is the exact fix that ruff applied automatically during CI. Detailed AnalysisThe CI log shows: The diff shows the automatic fix: -from fastmcp import Client
-
# isort: skip
from server import server
+from fastmcp import Client
+The Related Files
|
4b922fe to
e5c5d7f
Compare
There was a problem hiding this comment.
💡 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".
| # 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) |
There was a problem hiding this comment.
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 👍 / 👎.
|
Note Other AI code review bot(s) detectedCodeRabbit 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. WalkthroughAdds session-scoped persistent state to FastMCP: Context now accepts an optional ServerSession and exposes async methods Possibly related PRs
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (3)
pyproject.tomlis excluded by none and included by nonetests/server/middleware/test_initialization_middleware.pyis excluded by none and included by nonetests/server/test_context.pyis excluded by none and included by none
📒 Files selected for processing (8)
docs/servers/context.mdxexamples/persistent_state/README.mdexamples/persistent_state/client.pyexamples/persistent_state/client_stdio.pyexamples/persistent_state/server.pysrc/fastmcp/server/context.pysrc/fastmcp/server/low_level.pysrc/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 bareexcept- be specific with exception types in Python code
Files:
src/fastmcp/server/server.pysrc/fastmcp/server/low_level.pyexamples/persistent_state/server.pyexamples/persistent_state/client.pyexamples/persistent_state/client_stdio.pysrc/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; ifMemoryStoreor injectedAsyncKeyValueignores 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.
| ### 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> |
There was a problem hiding this comment.
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 -->
| 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 |
There was a problem hiding this comment.
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.
| 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)
| @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, | ||
| } |
There was a problem hiding this comment.
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,
}| 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)) | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find all assignments and usages of _fastmcp_id
rg -n '_fastmcp_id' -C 3Repository: 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 10Repository: 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 2Repository: jlowin/fastmcp
Length of output: 40
🏁 Script executed:
#!/bin/bash
# Check transport initialization for stdio/sse
rg -n 'transport.*=.*stdio|transport.*=.*sse' -C 3Repository: 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 2Repository: 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 5Repository: 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 3Repository: 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 3Repository: 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 -100Repository: 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.pyRepository: 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 10Repository: 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 2Repository: 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 5Repository: 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.pyRepository: 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 -80Repository: 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 5Repository: 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 -20Repository: 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.pyRepository: 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.pyRepository: 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.pyRepository: 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.pyRepository: 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 2Repository: 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.pyRepository: 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 3Repository: 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 -40Repository: 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.pyRepository: 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 -5Repository: 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 -60Repository: 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.
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.
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
tests/server/test_context.pyis excluded by none and included by none
📒 Files selected for processing (5)
examples/persistent_state/README.mdexamples/persistent_state/client.pyexamples/persistent_state/client_stdio.pyexamples/persistent_state/server.pysrc/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 bareexcept- be specific with exception types in Python code
Files:
examples/persistent_state/client_stdio.pysrc/fastmcp/server/context.pyexamples/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.pyexamples/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 betweenon_initializeand 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.
| URL = "http://127.0.0.1:8000/mcp" | ||
| console = Console() |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate the file
find . -type f -name "client.py" | grep -E "persistent_state|examples" | head -20Repository: 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
fiRepository: 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.
| URL = "http://127.0.0.1:8000/mcp" | |
| console = Console() | |
| URL: str = "http://127.0.0.1:8000/mcp" | |
| console: Console = Console() |
Test Failure AnalysisSummary: 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 The issue appears to be in how the MCP server's Suggested Solution:
Detailed AnalysisFailure LogClient 2's Relevant Code PathsSession 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_idState storage uses session-prefixed keys: def _make_state_key(self, key: str) -> str:
return f"{self._get_state_prefix()}:{key}" # ← Prefix with session_idIf two clients get the same session ID, their keys collide: Test ExpectationsThe test expects:
But the failure shows client 2 IS seeing client 1's data, meaning session_id_1 == session_id_2. Related Files
|
Test Failure AnalysisSummary: The test Root Cause: When using The issue appears to be in how the MCP server's Suggested Solution:
Detailed AnalysisFailure LogClient 2's Relevant Code PathsSession 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_idState storage uses session-prefixed keys: def _make_state_key(self, key: str) -> str:
return f"{self._get_state_prefix()}:{key}" # ← Prefix with session_idIf two clients get the same session ID, their keys collide: Test ExpectationsThe test expects:
But the failure shows client 2 IS seeing client 1's data, meaning session_id_1 == session_id_2. Related Files
|
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.
There was a problem hiding this comment.
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 forsession_idproperty.The documentation states
ctx.session_id -> str | None, but the implementation raisesRuntimeErrorwhen the MCP session is unavailable rather than returningNone. This should be-> strwith 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
⛔ Files ignored due to path filters (1)
tests/server/test_context.pyis excluded by none and included by none
📒 Files selected for processing (2)
docs/servers/context.mdxsrc/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 bareexcept- 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
StateValuefromfastmcp.server.serveris correctly added to support the new state persistence functionality.
180-186: LGTM!The
__init__method correctly accepts an optionalsessionparameter to enable state operations during initialization (whenrequest_contextis not yet available). The weak reference pattern forfastmcpis preserved.
482-500: LGTM!The
session_idproperty correctly shares the cached_fastmcp_state_prefixattribute 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:
- Falls back to
self._sessionwhenrequest_contextis unavailable (duringon_initialize)- Caches the generated prefix on the session object (
_fastmcp_state_prefix)- 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)andsession_id.
1156-1182: LGTM!The state methods are well-implemented:
set_statecorrectly wraps values inStateValueand applies the 1-day TTLget_statesafely unwraps the value or returnsNonedelete_stateis straightforwardThe methods correctly delegate to the
_state_storeand 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 inlow_level.py(line 98), and_get_state_prefixcaches the state prefix on the session object. Tool calls then retrieve the session via the MCP SDK'srequest_ctxContextVar 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 thatrequest_ctxreferences 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.
There was a problem hiding this comment.
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 toincrement_counterreturns1.
+- Second call toincrement_counterreturns2.</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#2505in 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 -->
| #### 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(...)) | ||
| ``` |
There was a problem hiding this comment.
🧩 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:
- 1: https://github.com/strawgate/py-key-value
- 2: https://fastmcp.mintlify.app/servers/storage-backends?utm_source=openai
- 3: https://api.python.langchain.com/en/latest/community/storage/langchain_community.storage.redis.RedisStore.html?utm_source=openai
🌐 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 1Repository: 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).
There was a problem hiding this comment.
Actionable comments posted: 4
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
docs/development/upgrade-guide.mdxdocs/development/v3-notes/v3-features.mdxsrc/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.mdxdocs/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 bareexcept- 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.ServerSessionfrom themcppackage (version >=1.24.0,<2.0) supports dynamic attribute assignment. This pattern is already used successfully elsewhere in the codebase—specificallysession._subscription_task_groupinsrc/fastmcp/server/low_level.py—and is verified by tests intests/server/test_context.py, which explicitly assert thatsession._fastmcp_state_prefixis 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.
| ### 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. |
There was a problem hiding this comment.
🧩 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 3Repository: 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 2Repository: 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 fRepository: jlowin/fastmcp
Length of output: 94
🏁 Script executed:
# Search for RedisStore in Python files
rg "RedisStore" -t py -B 2 -A 5Repository: 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 3Repository: 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 || trueRepository: 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.mdxRepository: 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 -5Repository: 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 -10Repository: 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 -20Repository: 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 fRepository: 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 -20Repository: 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 -200Repository: 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.pyRepository: 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.
| ## 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 | ||
|
|
There was a problem hiding this comment.
🧩 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.
| ### 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") | ||
| ``` |
There was a problem hiding this comment.
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, ...
| 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." | ||
| ) |
There was a problem hiding this comment.
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)
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).
Key design choices:
session_idfor automatic isolation between clientsid(session)as fallback duringon_initialize(before session_id is available)get_state/set_stateare now async