Skip to content

Commit 9a38150

Browse files
g97iulio1609Copilot
andcommitted
fix: detect Context parameter in callable class instances
find_context_parameter() uses typing.get_type_hints(fn) to find Context-typed parameters. However, get_type_hints() doesn't introspect the __call__ method of callable class instances, so ctx: Context is exposed as a visible tool parameter instead of being injected by the framework. The fix checks if fn is a callable class instance (not a function or method) and inspects fn.__call__ instead. Fixes #1974 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 62575ed commit 9a38150

2 files changed

Lines changed: 43 additions & 1 deletion

File tree

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,15 @@ 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 inspecting __call__ method
26+
target = fn
27+
if not (inspect.isfunction(fn) or inspect.ismethod(fn)):
28+
if callable(fn) and hasattr(fn, "__call__"):
29+
target = fn.__call__
30+
2531
# Get type hints to properly resolve string annotations
2632
try:
27-
hints = typing.get_type_hints(fn)
33+
hints = typing.get_type_hints(target)
2834
except Exception: # pragma: lax no cover
2935
# If we can't resolve type hints, we can't find the context parameter
3036
return None

tests/server/mcpserver/test_tool_manager.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,42 @@ 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_detection_callable_class(self):
417+
"""Test that context parameters are detected in callable class instances."""
418+
419+
class MyTool:
420+
def __init__(self, name: str):
421+
self.__name__ = name
422+
423+
async def __call__(self, query: str, ctx: Context[ServerSessionT, None]) -> str: # pragma: no cover
424+
return f"Result: {query}"
425+
426+
manager = ToolManager()
427+
tool = manager.add_tool(MyTool(name="my_tool"), name="my_tool", description="A tool")
428+
assert tool.context_kwarg == "ctx"
429+
# ctx should not appear in the JSON schema
430+
assert "ctx" not in json.dumps(tool.parameters)
431+
432+
@pytest.mark.anyio
433+
async def test_context_injection_callable_class(self):
434+
"""Test that context is injected into callable class instances."""
435+
436+
class MyTool:
437+
def __init__(self, name: str):
438+
self.__name__ = name
439+
440+
async def __call__(self, x: int, ctx: Context[ServerSessionT, None]) -> str:
441+
assert isinstance(ctx, Context)
442+
return str(x)
443+
444+
manager = ToolManager()
445+
manager.add_tool(MyTool(name="my_tool"), name="my_tool", description="A tool")
446+
447+
mcp = MCPServer()
448+
ctx = mcp.get_context()
449+
result = await manager.call_tool("my_tool", {"x": 42}, context=ctx)
450+
assert result == "42"
451+
416452

417453
class TestToolAnnotations:
418454
def test_tool_annotations(self):

0 commit comments

Comments
 (0)