Skip to content

Commit 2ed8c3f

Browse files
committed
fix: handle callable class instances in find_context_parameter()
When registering a callable class instance as an MCP tool via FastMCP.add_tool(), the ctx: Context parameter was incorrectly exposed as an externally visible tool parameter instead of being injected by the framework. Root cause: typing.get_type_hints() was called on the class instance rather than its __call__ method, so Context type hints were not resolved. Fix: detect non-function/non-method callables and target their __call__ method, mirroring the existing pattern in _is_async_callable(). Fixes #1974
1 parent d6d3ad9 commit 2ed8c3f

File tree

2 files changed

+61
-1
lines changed

2 files changed

+61
-1
lines changed

src/mcp/server/mcpserver/utilities/context_injection.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,17 @@ def find_context_parameter(fn: Callable[..., Any]) -> str | None:
2222
"""
2323
from mcp.server.mcpserver.server import Context
2424

25+
# Handle callable class instances by using __call__ method,
26+
# since typing.get_type_hints() doesn't introspect __call__
27+
# on class instances.
28+
target = fn
29+
if not (inspect.isfunction(fn) or inspect.ismethod(fn)):
30+
if callable(fn) and hasattr(fn, "__call__"):
31+
target = fn.__call__
32+
2533
# Get type hints to properly resolve string annotations
2634
try:
27-
hints = typing.get_type_hints(fn)
35+
hints = typing.get_type_hints(target)
2836
except Exception: # pragma: lax no cover
2937
# If we can't resolve type hints, we can't find the context parameter
3038
return None

tests/server/mcpserver/test_tool_manager.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,58 @@ def tool_with_context(x: int, ctx: Context[ServerSessionT, None]) -> str:
413413
with pytest.raises(ToolError, match="Error executing tool tool_with_context"):
414414
await manager.call_tool("tool_with_context", {"x": 42}, context=ctx)
415415

416+
def test_context_parameter_detection_callable_class(self):
417+
"""Test that context parameters are detected in callable class instances."""
418+
419+
class MyTool:
420+
def __init__(self):
421+
self.__name__ = "my_tool"
422+
423+
def __call__(self, x: int, ctx: Context[ServerSessionT, None]) -> str: # pragma: no cover
424+
return str(x)
425+
426+
manager = ToolManager()
427+
tool = manager.add_tool(MyTool())
428+
assert tool.context_kwarg == "ctx"
429+
assert "ctx" not in json.dumps(tool.parameters)
430+
assert "Context" not in json.dumps(tool.parameters)
431+
432+
def test_context_parameter_detection_async_callable_class(self):
433+
"""Test that context parameters are detected in async callable class instances."""
434+
435+
class MyAsyncTool:
436+
def __init__(self):
437+
self.__name__ = "my_async_tool"
438+
439+
async def __call__(self, x: int, ctx: Context[ServerSessionT, None]) -> str: # pragma: no cover
440+
return str(x)
441+
442+
manager = ToolManager()
443+
tool = manager.add_tool(MyAsyncTool())
444+
assert tool.context_kwarg == "ctx"
445+
assert tool.is_async is True
446+
assert "ctx" not in json.dumps(tool.parameters)
447+
448+
@pytest.mark.anyio
449+
async def test_context_injection_callable_class(self):
450+
"""Test that context is properly injected in callable class tools."""
451+
452+
class MyTool:
453+
def __init__(self):
454+
self.__name__ = "my_tool"
455+
456+
def __call__(self, x: int, ctx: Context[ServerSessionT, None]) -> str:
457+
assert isinstance(ctx, Context)
458+
return str(x)
459+
460+
manager = ToolManager()
461+
manager.add_tool(MyTool())
462+
463+
mcp = MCPServer()
464+
ctx = mcp.get_context()
465+
result = await manager.call_tool("my_tool", {"x": 42}, context=ctx)
466+
assert result == "42"
467+
416468

417469
class TestToolAnnotations:
418470
def test_tool_annotations(self):

0 commit comments

Comments
 (0)