Mukul Murthy Jeremiah Lowin commited on
Commit
62aab99
·
unverified ·
1 Parent(s): ef833ac

Add state dict to Context (#1118) (#1160)

Browse files

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>

src/fastmcp/server/context.py CHANGED
@@ -1,6 +1,7 @@
1
  from __future__ import annotations
2
 
3
  import asyncio
 
4
  import warnings
5
  from collections.abc import Generator
6
  from contextlib import contextmanager
@@ -83,9 +84,19 @@ class Context:
83
  request_id = ctx.request_id
84
  client_id = ctx.client_id
85
 
 
 
 
 
86
  return str(x)
87
  ```
88
 
 
 
 
 
 
 
89
  The context parameter name can be anything as long as it's annotated with Context.
90
  The context is optional - tools that don't need it can omit the parameter.
91
 
@@ -95,9 +106,15 @@ class Context:
95
  self.fastmcp = fastmcp
96
  self._tokens: list[Token] = []
97
  self._notification_queue: set[str] = set() # Dedupe notifications
 
98
 
99
  async def __aenter__(self) -> Context:
100
  """Enter the context manager and set this context as the current context."""
 
 
 
 
 
101
  # Always set this context and save the token
102
  token = _current_context.set(self)
103
  self._tokens.append(token)
@@ -455,6 +472,14 @@ class Context:
455
 
456
  return fastmcp.server.dependencies.get_http_request()
457
 
 
 
 
 
 
 
 
 
458
  def _queue_tool_list_changed(self) -> None:
459
  """Queue a tool list changed notification."""
460
  self._notification_queue.add("notifications/tools/list_changed")
 
1
  from __future__ import annotations
2
 
3
  import asyncio
4
+ import copy
5
  import warnings
6
  from collections.abc import Generator
7
  from contextlib import contextmanager
 
84
  request_id = ctx.request_id
85
  client_id = ctx.client_id
86
 
87
+ # Manage state across the request
88
+ ctx.set_state_value("key", "value")
89
+ value = ctx.get_state_value("key")
90
+
91
  return str(x)
92
  ```
93
 
94
+ State Management:
95
+ Context objects maintain a state dictionary that can be used to store and share
96
+ data across middleware and tool calls within a request. When a new context
97
+ is created (nested contexts), it inherits a copy of its parent's state, ensuring
98
+ that modifications in child contexts don't affect parent contexts.
99
+
100
  The context parameter name can be anything as long as it's annotated with Context.
101
  The context is optional - tools that don't need it can omit the parameter.
102
 
 
106
  self.fastmcp = fastmcp
107
  self._tokens: list[Token] = []
108
  self._notification_queue: set[str] = set() # Dedupe notifications
109
+ self._state: dict[str, Any] = {}
110
 
111
  async def __aenter__(self) -> Context:
112
  """Enter the context manager and set this context as the current context."""
113
+ parent_context = _current_context.get(None)
114
+ if parent_context is not None:
115
+ # Inherit state from parent context
116
+ self._state = copy.deepcopy(parent_context._state)
117
+
118
  # Always set this context and save the token
119
  token = _current_context.set(self)
120
  self._tokens.append(token)
 
472
 
473
  return fastmcp.server.dependencies.get_http_request()
474
 
475
+ def set_state(self, key: str, value: Any) -> None:
476
+ """Set a value in the context state."""
477
+ self._state[key] = value
478
+
479
+ def get_state(self, key: str) -> Any:
480
+ """Get a value from the context state. Returns None if the key is not found."""
481
+ return self._state.get(key)
482
+
483
  def _queue_tool_list_changed(self) -> None:
484
  """Queue a tool list changed notification."""
485
  self._notification_queue.add("notifications/tools/list_changed")
tests/server/test_context.py CHANGED
@@ -123,3 +123,51 @@ class TestSessionId:
123
  "fastmcp.server.dependencies.get_http_headers", return_value=mock_headers
124
  ):
125
  assert context.session_id == "" # Empty string is still returned as-is
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  "fastmcp.server.dependencies.get_http_headers", return_value=mock_headers
124
  ):
125
  assert context.session_id == "" # Empty string is still returned as-is
126
+
127
+
128
+ class TestContextState:
129
+ """Test suite for Context state functionality."""
130
+
131
+ @pytest.mark.asyncio
132
+ async def test_context_state(self):
133
+ """Test that state modifications in child contexts don't affect parent."""
134
+ mock_fastmcp = MagicMock()
135
+
136
+ async with Context(fastmcp=mock_fastmcp) as context:
137
+ assert context.get_state("test1") is None
138
+ assert context.get_state("test2") is None
139
+ context.set_state("test1", "value")
140
+ context.set_state("test2", 2)
141
+ assert context.get_state("test1") == "value"
142
+ assert context.get_state("test2") == 2
143
+ context.set_state("test1", "new_value")
144
+ assert context.get_state("test1") == "new_value"
145
+
146
+ @pytest.mark.asyncio
147
+ async def test_context_state_inheritance(self):
148
+ """Test that child contexts inherit parent state."""
149
+ mock_fastmcp = MagicMock()
150
+
151
+ async with Context(fastmcp=mock_fastmcp) as context1:
152
+ context1.set_state("key1", "key1-context1")
153
+ context1.set_state("key2", "key2-context1")
154
+ async with Context(fastmcp=mock_fastmcp) as context2:
155
+ # Override one key
156
+ context2.set_state("key1", "key1-context2")
157
+ assert context2.get_state("key1") == "key1-context2"
158
+ assert context1.get_state("key1") == "key1-context1"
159
+ assert context2.get_state("key2") == "key2-context1"
160
+
161
+ async with Context(fastmcp=mock_fastmcp) as context3:
162
+ # Verify state was inherited
163
+ assert context3.get_state("key1") == "key1-context2"
164
+ assert context3.get_state("key2") == "key2-context1"
165
+
166
+ # Add a new key and verify parents were not affected
167
+ context3.set_state("key-context3-only", 1)
168
+ assert context1.get_state("key-context3-only") is None
169
+ assert context2.get_state("key-context3-only") is None
170
+ assert context3.get_state("key-context3-only") == 1
171
+
172
+ assert context1.get_state("key1") == "key1-context1"
173
+ assert context1.get_state("key-context3-only") is None