Jeremiah Lowin commited on
Commit
80b9b2c
·
unverified ·
2 Parent(s): 10aafd86f561a8

Merge pull request #13 from jlowin/context

Browse files
src/fastmcp/server.py CHANGED
@@ -119,12 +119,16 @@ class FastMCP:
119
  for info in tools
120
  ]
121
 
122
- def get_context(self) -> Optional["Context"]:
 
 
 
 
123
  try:
124
  request_context = self._mcp_server.request_context
125
- return Context(request_context=request_context, fastmcp=self)
126
  except LookupError:
127
- return None
 
128
 
129
  async def call_tool(
130
  self, name: str, arguments: dict
@@ -457,7 +461,11 @@ class Context(BaseModel):
457
  _fastmcp: FastMCP
458
 
459
  def __init__(
460
- self, *, request_context: RequestContext, fastmcp: FastMCP, **kwargs: Any
 
 
 
 
461
  ):
462
  super().__init__(**kwargs)
463
  self._request_context = request_context
@@ -466,11 +474,15 @@ class Context(BaseModel):
466
  @property
467
  def fastmcp(self) -> FastMCP:
468
  """Access to the FastMCP server."""
 
 
469
  return self._fastmcp
470
 
471
  @property
472
  def request_context(self) -> RequestContext:
473
  """Access to the underlying request context."""
 
 
474
  return self._request_context
475
 
476
  async def report_progress(
 
119
  for info in tools
120
  ]
121
 
122
+ def get_context(self) -> "Context":
123
+ """
124
+ Returns a Context object. Note that the context will only be valid
125
+ during a request; outside a request, most methods will error.
126
+ """
127
  try:
128
  request_context = self._mcp_server.request_context
 
129
  except LookupError:
130
+ request_context = None
131
+ return Context(request_context=request_context, fastmcp=self)
132
 
133
  async def call_tool(
134
  self, name: str, arguments: dict
 
461
  _fastmcp: FastMCP
462
 
463
  def __init__(
464
+ self,
465
+ *,
466
+ request_context: RequestContext = None,
467
+ fastmcp: FastMCP = None,
468
+ **kwargs: Any,
469
  ):
470
  super().__init__(**kwargs)
471
  self._request_context = request_context
 
474
  @property
475
  def fastmcp(self) -> FastMCP:
476
  """Access to the FastMCP server."""
477
+ if self._fastmcp is None:
478
+ raise ValueError("Context is not available outside of a request")
479
  return self._fastmcp
480
 
481
  @property
482
  def request_context(self) -> RequestContext:
483
  """Access to the underlying request context."""
484
+ if self._request_context is None:
485
+ raise ValueError("Context is not available outside of a request")
486
  return self._request_context
487
 
488
  async def report_progress(
src/fastmcp/tools.py CHANGED
@@ -71,7 +71,7 @@ class Tool(BaseModel):
71
  """Run the tool with arguments."""
72
  try:
73
  # Inject context if needed
74
- if self.context_kwarg and context:
75
  arguments[self.context_kwarg] = context
76
 
77
  # Call function with proper async handling
 
71
  """Run the tool with arguments."""
72
  try:
73
  # Inject context if needed
74
+ if self.context_kwarg:
75
  arguments[self.context_kwarg] = context
76
 
77
  # Call function with proper async handling
tests/test_tool_manager.py CHANGED
@@ -1,6 +1,7 @@
1
  import logging
2
  import pytest
3
  from pydantic import BaseModel
 
4
 
5
  from fastmcp.exceptions import ToolError
6
  from fastmcp.tools import ToolManager
@@ -153,3 +154,84 @@ class TestCallTools:
153
  manager = ToolManager()
154
  with pytest.raises(ToolError):
155
  await manager.call_tool("unknown", {"a": 1})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import logging
2
  import pytest
3
  from pydantic import BaseModel
4
+ from typing import Optional
5
 
6
  from fastmcp.exceptions import ToolError
7
  from fastmcp.tools import ToolManager
 
154
  manager = ToolManager()
155
  with pytest.raises(ToolError):
156
  await manager.call_tool("unknown", {"a": 1})
157
+
158
+
159
+ class TestContextHandling:
160
+ """Test context handling in the tool manager."""
161
+
162
+ def test_context_parameter_detection(self):
163
+ """Test that context parameters are properly detected in Tool.from_function()."""
164
+ from fastmcp import Context
165
+
166
+ def tool_with_context(x: int, ctx: Context) -> str:
167
+ return str(x)
168
+
169
+ manager = ToolManager()
170
+ tool = manager.add_tool(tool_with_context)
171
+ assert tool.context_kwarg == "ctx"
172
+
173
+ def tool_without_context(x: int) -> str:
174
+ return str(x)
175
+
176
+ tool = manager.add_tool(tool_without_context)
177
+ assert tool.context_kwarg is None
178
+
179
+ async def test_context_injection(self):
180
+ """Test that context is properly injected during tool execution."""
181
+ from fastmcp import Context, FastMCP
182
+
183
+ def tool_with_context(x: int, ctx: Context) -> str:
184
+ assert isinstance(ctx, Context)
185
+ return str(x)
186
+
187
+ manager = ToolManager()
188
+ tool = manager.add_tool(tool_with_context)
189
+
190
+ mcp = FastMCP()
191
+ ctx = mcp.get_context()
192
+ result = await manager.call_tool("tool_with_context", {"x": 42}, context=ctx)
193
+ assert result == "42"
194
+
195
+ async def test_context_injection_async(self):
196
+ """Test that context is properly injected in async tools."""
197
+ from fastmcp import Context, FastMCP
198
+
199
+ async def async_tool(x: int, ctx: Context) -> str:
200
+ assert isinstance(ctx, Context)
201
+ return str(x)
202
+
203
+ manager = ToolManager()
204
+ tool = manager.add_tool(async_tool)
205
+
206
+ mcp = FastMCP()
207
+ ctx = mcp.get_context()
208
+ result = await manager.call_tool("async_tool", {"x": 42}, context=ctx)
209
+ assert result == "42"
210
+
211
+ async def test_context_optional(self):
212
+ """Test that context is optional when calling tools."""
213
+ from fastmcp import Context
214
+
215
+ def tool_with_context(x: int, ctx: Optional[Context] = None) -> str:
216
+ return str(x)
217
+
218
+ manager = ToolManager()
219
+ tool = manager.add_tool(tool_with_context)
220
+ # Should not raise an error when context is not provided
221
+ result = await manager.call_tool("tool_with_context", {"x": 42})
222
+ assert result == "42"
223
+
224
+ async def test_context_error_handling(self):
225
+ """Test error handling when context injection fails."""
226
+ from fastmcp import Context, FastMCP
227
+
228
+ def tool_with_context(x: int, ctx: Context) -> str:
229
+ raise ValueError("Test error")
230
+
231
+ manager = ToolManager()
232
+ tool = manager.add_tool(tool_with_context)
233
+
234
+ mcp = FastMCP()
235
+ ctx = mcp.get_context()
236
+ with pytest.raises(ToolError, match="Error executing tool tool_with_context"):
237
+ await manager.call_tool("tool_with_context", {"x": 42}, context=ctx)