Skip to content

Commit 6fe7727

Browse files
fix(mcpserver): ignore the return annotation when finding the context parameter
`find_context_parameter` iterated every entry from `typing.get_type_hints`, which includes the return annotation under the special `"return"` key. A tool, prompt, or resource function annotated `-> Context` (or a union containing it) was therefore recorded as having a context parameter named `"return"`, and the handler failed at invocation with `got an unexpected keyword argument 'return'`. Drop the `"return"` key before scanning, so only real parameters are considered. Fixes #3298 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 6e30452 commit 6fe7727

2 files changed

Lines changed: 94 additions & 0 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ def find_context_parameter(fn: Callable[..., Any]) -> str | None:
2929
# If we can't resolve type hints, we can't find the context parameter
3030
return None
3131

32+
# `get_type_hints` reports the return annotation under the "return" key, which is
33+
# not a parameter and must not be mistaken for one.
34+
hints.pop("return", None)
35+
3236
# Check each parameter's type hint
3337
for param_name, annotation in hints.items():
3438
# Handle direct Context type
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Tests for context parameter discovery and injection."""
2+
3+
from mcp_types import TextContent, TextResourceContents
4+
5+
from mcp.client.client import Client
6+
from mcp.server.mcpserver import Context, MCPServer
7+
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter
8+
9+
10+
def test_context_parameter_is_found_by_its_annotation() -> None:
11+
"""SDK-defined: a parameter annotated with `Context` is the one context is injected into."""
12+
13+
def fn(value: int, ctx: Context) -> str:
14+
raise NotImplementedError
15+
16+
assert find_context_parameter(fn) == "ctx"
17+
18+
19+
def test_context_return_annotation_is_not_reported_as_a_parameter() -> None:
20+
"""SDK-defined: a `Context` return annotation is not a parameter, so nothing is injected."""
21+
22+
def fn(value: int) -> Context:
23+
raise NotImplementedError
24+
25+
assert find_context_parameter(fn) is None
26+
27+
28+
def test_context_in_a_union_return_annotation_is_not_reported_as_a_parameter() -> None:
29+
"""SDK-defined: a union return annotation mentioning `Context` is still not a parameter."""
30+
31+
def fn(value: int) -> Context | None:
32+
raise NotImplementedError
33+
34+
assert find_context_parameter(fn) is None
35+
36+
37+
def test_context_parameter_wins_over_a_context_return_annotation() -> None:
38+
"""SDK-defined: the real parameter is found even when the return annotation is also `Context`."""
39+
40+
def fn(ctx: Context) -> Context:
41+
raise NotImplementedError
42+
43+
assert find_context_parameter(fn) == "ctx"
44+
45+
46+
async def test_tool_returning_context_is_called_with_only_its_own_arguments() -> None:
47+
"""A tool whose return annotation mentions `Context` runs without a spurious `return` argument."""
48+
server = MCPServer("test")
49+
50+
@server.tool()
51+
def maybe_context(value: int) -> Context | str:
52+
return f"got {value}"
53+
54+
async with Client(server) as client:
55+
result = await client.call_tool("maybe_context", {"value": 7})
56+
57+
assert result.is_error is False
58+
assert result.structured_content == {"result": "got 7"}
59+
60+
61+
async def test_prompt_returning_context_is_called_with_only_its_own_arguments() -> None:
62+
"""A prompt whose return annotation mentions `Context` runs without a spurious `return` argument."""
63+
server = MCPServer("test")
64+
65+
@server.prompt()
66+
def maybe_context(value: str) -> Context | str:
67+
return f"got {value}"
68+
69+
async with Client(server) as client:
70+
result = await client.get_prompt("maybe_context", {"value": "seven"})
71+
72+
content = result.messages[0].content
73+
assert isinstance(content, TextContent)
74+
assert content.text == "got seven"
75+
76+
77+
async def test_resource_template_returning_context_is_called_with_only_its_own_arguments() -> None:
78+
"""A resource template whose return annotation mentions `Context` runs without a spurious `return`."""
79+
server = MCPServer("test")
80+
81+
@server.resource("res://{value}")
82+
def maybe_context(value: str) -> Context | str:
83+
return f"got {value}"
84+
85+
async with Client(server) as client:
86+
result = await client.read_resource("res://seven")
87+
88+
contents = result.contents[0]
89+
assert isinstance(contents, TextResourceContents)
90+
assert contents.text == "got seven"

0 commit comments

Comments
 (0)