Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/fastmcp/server/context.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import asyncio
import copy
import warnings
from collections.abc import Generator
from contextlib import contextmanager
Expand Down Expand Up @@ -83,9 +84,19 @@ def my_tool(x: int, ctx: Context) -> str:
request_id = ctx.request_id
client_id = ctx.client_id

# Manage state across the request
ctx.set_state_value("key", "value")
value = ctx.get_state_value("key")

return str(x)
```

State Management:
Context objects maintain a state dictionary that can be used to store and share
data across middleware and tool calls within a request. When a new context
is created (nested contexts), it inherits a copy of its parent's state, ensuring
that modifications in child contexts don't affect parent contexts.
Comment on lines +97 to +98
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Discussed the semantics of this a little in the PR description. If you think child state should affect the parent, we could do it that way.


The context parameter name can be anything as long as it's annotated with Context.
The context is optional - tools that don't need it can omit the parameter.

Expand All @@ -95,9 +106,15 @@ def __init__(self, fastmcp: FastMCP):
self.fastmcp = fastmcp
self._tokens: list[Token] = []
self._notification_queue: set[str] = set() # Dedupe notifications
self._state: dict[str, Any] = {}

async def __aenter__(self) -> Context:
"""Enter the context manager and set this context as the current context."""
parent_context = _current_context.get(None)
if parent_context is not None:
# Inherit state from parent context
self._state = copy.deepcopy(parent_context._state)

# Always set this context and save the token
token = _current_context.set(self)
self._tokens.append(token)
Expand Down Expand Up @@ -455,6 +472,14 @@ def get_http_request(self) -> Request:

return fastmcp.server.dependencies.get_http_request()

def set_state(self, key: str, value: Any) -> None:
"""Set a value in the context state."""
self._state[key] = value

def get_state(self, key: str) -> Any:
"""Get a value from the context state. Returns None if the key is not found."""
return self._state.get(key)

def _queue_tool_list_changed(self) -> None:
"""Queue a tool list changed notification."""
self._notification_queue.add("notifications/tools/list_changed")
Expand Down
48 changes: 48 additions & 0 deletions tests/server/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,51 @@ def test_session_id_with_empty_header(self, context):
"fastmcp.server.dependencies.get_http_headers", return_value=mock_headers
):
assert context.session_id == "" # Empty string is still returned as-is


class TestContextState:
"""Test suite for Context state functionality."""

@pytest.mark.asyncio
async def test_context_state(self):
"""Test that state modifications in child contexts don't affect parent."""
mock_fastmcp = MagicMock()

async with Context(fastmcp=mock_fastmcp) as context:
assert context.get_state("test1") is None
assert context.get_state("test2") is None
context.set_state("test1", "value")
context.set_state("test2", 2)
assert context.get_state("test1") == "value"
assert context.get_state("test2") == 2
context.set_state("test1", "new_value")
assert context.get_state("test1") == "new_value"

@pytest.mark.asyncio
async def test_context_state_inheritance(self):
"""Test that child contexts inherit parent state."""
mock_fastmcp = MagicMock()

async with Context(fastmcp=mock_fastmcp) as context1:
context1.set_state("key1", "key1-context1")
context1.set_state("key2", "key2-context1")
async with Context(fastmcp=mock_fastmcp) as context2:
# Override one key
context2.set_state("key1", "key1-context2")
assert context2.get_state("key1") == "key1-context2"
assert context1.get_state("key1") == "key1-context1"
assert context2.get_state("key2") == "key2-context1"

async with Context(fastmcp=mock_fastmcp) as context3:
# Verify state was inherited
assert context3.get_state("key1") == "key1-context2"
assert context3.get_state("key2") == "key2-context1"

# Add a new key and verify parents were not affected
context3.set_state("key-context3-only", 1)
assert context1.get_state("key-context3-only") is None
assert context2.get_state("key-context3-only") is None
assert context3.get_state("key-context3-only") == 1

assert context1.get_state("key1") == "key1-context1"
assert context1.get_state("key-context3-only") is None