Skip to content

Commit 9c807f7

Browse files
Varun SharmaCopilot
andcommitted
fix: resource with only Context param no longer classified as template
When a @mcp.resource() handler has only a Context parameter (no URI template params), it was incorrectly classified as a resource template because bool(sig.parameters) counted the Context param. Now the Context parameter is excluded before checking has_func_params. Also adds context injection support to FunctionResource so that context-only resources can access the server context when read. Fixes #1635 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 62575ed commit 9c807f7

File tree

3 files changed

+47
-7
lines changed

3 files changed

+47
-7
lines changed

src/mcp/server/mcpserver/resources/types.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,18 @@ class FunctionResource(Resource):
5151
"""
5252

5353
fn: Callable[[], Any] = Field(exclude=True)
54+
context_kwarg: str | None = Field(default=None, exclude=True)
5455

55-
async def read(self) -> str | bytes:
56+
async def read(self, context: Any | None = None) -> str | bytes:
5657
"""Read the resource by calling the wrapped function."""
5758
try:
59+
# Inject context if needed
60+
kwargs: dict[str, Any] = {}
61+
if self.context_kwarg is not None and context is not None:
62+
kwargs[self.context_kwarg] = context
63+
5864
# Call the function first to see if it returns a coroutine
59-
result = self.fn()
65+
result = self.fn(**kwargs)
6066
# If it's a coroutine, await it
6167
if inspect.iscoroutine(result):
6268
result = await result
@@ -84,12 +90,19 @@ def from_function(
8490
icons: list[Icon] | None = None,
8591
annotations: Annotations | None = None,
8692
meta: dict[str, Any] | None = None,
93+
context_kwarg: str | None = None,
8794
) -> "FunctionResource":
8895
"""Create a FunctionResource from a function."""
96+
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter
97+
8998
func_name = name or fn.__name__
9099
if func_name == "<lambda>": # pragma: no cover
91100
raise ValueError("You must provide a name for lambda functions")
92101

102+
# Detect context parameter
103+
if context_kwarg is None:
104+
context_kwarg = find_context_parameter(fn)
105+
93106
# ensure the arguments are properly cast
94107
fn = validate_call(fn)
95108

@@ -103,6 +116,7 @@ def from_function(
103116
icons=icons,
104117
annotations=annotations,
105118
meta=meta,
119+
context_kwarg=context_kwarg,
106120
)
107121

108122

src/mcp/server/mcpserver/server.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,10 @@ async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContent
448448
raise ResourceError(f"Unknown resource: {uri}")
449449

450450
try:
451-
content = await resource.read()
451+
if isinstance(resource, FunctionResource):
452+
content = await resource.read(context=context)
453+
else:
454+
content = await resource.read()
452455
return [ReadResourceContents(content=content, mime_type=resource.mime_type, meta=resource.meta)]
453456
except Exception as exc:
454457
logger.exception(f"Error getting resource {uri}")
@@ -686,12 +689,12 @@ def decorator(fn: _CallableT) -> _CallableT:
686689
# Check if this should be a template
687690
sig = inspect.signature(fn)
688691
has_uri_params = "{" in uri and "}" in uri
689-
has_func_params = bool(sig.parameters)
690692

691-
if has_uri_params or has_func_params:
692-
# Check for Context parameter to exclude from validation
693-
context_param = find_context_parameter(fn)
693+
# Exclude Context parameter when checking for function params
694+
context_param = find_context_parameter(fn)
695+
has_func_params = bool({p for p in sig.parameters if p != context_param})
694696

697+
if has_uri_params or has_func_params:
695698
# Validate that URI params match function params (excluding context)
696699
uri_params = set(re.findall(r"{(\w+)}", uri))
697700
# We need to remove the context_param from the resource function if

tests/server/mcpserver/test_server.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,29 @@ def resource_custom_ctx(id: str, my_ctx: Context[ServerSession, None]) -> str:
11891189
)
11901190
)
11911191

1192+
async def test_resource_with_context_only_not_classified_as_template(self):
1193+
"""Test that a resource with only a Context param is registered as a resource, not a template."""
1194+
mcp = MCPServer()
1195+
1196+
@mcp.resource("time://current")
1197+
def current_time(ctx: Context[ServerSession, None]) -> str:
1198+
"""Resource with only context parameter."""
1199+
return "11:41"
1200+
1201+
# Should be registered as a regular resource, not a template
1202+
resources = await mcp.list_resources()
1203+
templates = mcp._resource_manager.list_templates()
1204+
assert len(resources) == 1
1205+
assert str(resources[0].uri) == "time://current"
1206+
assert len(templates) == 0
1207+
1208+
async with Client(mcp) as client:
1209+
result = await client.read_resource("time://current")
1210+
assert len(result.contents) == 1
1211+
content = result.contents[0]
1212+
assert isinstance(content, TextResourceContents)
1213+
assert content.text == "11:41"
1214+
11921215
async def test_prompt_with_context(self):
11931216
"""Test that prompts can receive context parameter."""
11941217
mcp = MCPServer()

0 commit comments

Comments
 (0)