fix: Fix logic that determines standard resource vs. resource template to account for context param (#1635)
- Fix template detection logic to exclude context when determining if a resource is a template or not
- Add support for FunctionResources to be read with a context param, allowing resources registered with a context param to read from ccontext objects
- Use
func_metadata.call_fn_with_arg_validation()to validate arguments in resources while ignoring injected context param, to avoid Pydantic model validation issues - Extracted
is_async_callable()as a public utility
Motivation and Context
This solves the issue of context not being properly injected into regular resources. Per #1635, when registering a resource with @mcp.resource(), the logic that determines whether to create a standard resource or a resource template was incorrectly counting the Context parameter as a function parameter. This caused functions with only a Context parameter (no URI parameters) to be incorrectly treated as templates.
How Has This Been Tested?
I added unit tests plus used the following test application to ensure the fix worked as expected and didn't affect existing functionality around resources and resource templates. I tested reading resources and resource templates with and without context params
from contextlib import asynccontextmanager
from datetime import datetime
from mcp import ServerSession
from mcp.server import FastMCP
from mcp.server.fastmcp import Context
class AppContext:
"""Application context with some shared state."""
def __init__(self):
self.server_start_time = datetime.now()
self.request_count = 0
def increment_requests(self):
self.request_count += 1
@asynccontextmanager
async def lifespan(server: FastMCP):
"""Initialize application context."""
print("Server starting up...")
ctx = AppContext()
yield ctx
print(f"Server shutting down. Total requests: {ctx.request_count}")
mcp = FastMCP("Context Test Demo", lifespan=lifespan)
@mcp.resource("time://current")
async def current_time(ctx: Context[ServerSession, AppContext]) -> str:
return f"Current time: {datetime.now().isoformat()}, request {ctx.request_id}"
@mcp.resource("time://format")
async def time_format() -> str:
return "Supported formats: ISO8601, RFC3339, Unix timestamp"
@mcp.resource("greeting://{name}")
async def personalized_greeting(name: str, ctx: Context[ServerSession, AppContext]) -> str:
uptime = (datetime.now() - ctx.request_context.lifespan_context.server_start_time).total_seconds()
return f"Hello {name}! Server uptime: {uptime:.1f}s"
@mcp.resource("math://{operation}/{a}/{b}")
async def math_operation(operation: str, a: int, b: int) -> str:
operations = {
"add": a + b,
"subtract": a - b,
"multiply": a * b,
"divide": a / b if b != 0 else "Error: Division by zero"
}
result = operations.get(operation, "Error: Unknown operation")
return f"{operation}({a}, {b}) = {result}"
if __name__ == "__main__":
import asyncio
mcp.run()
Breaking Changes
Technically yes, because the signature of the read() function on the Resource type now includes an additional optional context param, but this shouldn't break at runtime since the arg is optional
Types of changes
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation update
Checklist
- [x] I have read the MCP Documentation
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [x] I have added or updated documentation as needed