Repository navigation
Description
Initial Checks
- I confirm that I'm using the newest release of my line (the latest 2.x, or the latest 1.x if I'm still on v1)
- I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
Release line
2.x (current stable)
Description
A resource template handler can take a parameter with a non-str annotation, such as user_id: int. The "Type conversion" section of docs/servers/uri-templates.md teaches this with orders://{order_id}. When a URI carries a value that doesn't convert, the SDK treats it as a server crash. Reading users://abc/profile returns -32603 Error creating resource from template users://abc/profile, and the server logs Resource 'users://abc/profile' raised an unexpected exception at ERROR with the full pydantic ValidationError traceback. Any client can trigger this, and it happens on every such request.
Expected: that URI names no resource, so it should get the same answer as an unmatched URI or a path-safety rejection: -32602 Unknown resource: users://abc/profile, logged once at INFO. Tools already treat a schema rejection as anticipated (one INFO line, no traceback).
Cause: ResourceTemplate.from_function (src/mcp/server/mcpserver/resources/templates.py) wraps the handler in validate_call, so validating the URI parameters and running the body happen in one call. In ResourceTemplate.create_resource, the ValidationError from a bad parameter lands in except Exception and becomes UnexpectedResourceError. #3314 lists this under "Not in here": "A resource template parameter that fails its type annotation is still logged as a crash: templates run through validate_call, which fuses validation with the call. Not a regression. Follow-up." No issue tracks it yet.
Proposed fix: from_function already builds the handler's argument model with func_metadata. Keep it on the template as a private attribute and, inside the existing try of create_resource, validate the extracted parameters against it before calling the handler, raising ResourceNotFoundError(f"Unknown resource: {uri}") on failure. A ValidationError from the handler body, or a validator raising something other than ValueError/AssertionError, stays a crash. I have this on a local branch (about 20 lines, two tests and one docs sentence) and would like to send the PR if the direction works for you.
Two choices I'd like your view on:
- The parameters are validated twice (here and again by
validate_call), so a custom validator runs twice per read, and a stateful one that passes the first time and fails the second surfaces as a crash. Validating once means calling the undecorated function with the model's output, which either changes what the publicResourceTemplate.fnholds or keeps a second, private reference to the function. ReusingFuncMetadata.validate_arguments()asTool.rundoes would also add itspre_parse_jsonstep, which JSON-decodes string values for non-strparameters. - Like the path-safety check, a value that fails stops at the first matching template. With
items://{item_id}/x(item_id: int) registered beforeitems://{slug}/x,items://hello/xgets-32602and does not fall through to the second template. Should it fall through instead?
Disclosure: Claude Code found this, wrote the fix and tests, ran the example and drafted this issue; I reviewed the output on main and on the branch, and the duplicate search, before filing.
Example Code
import anyio
from mcp import Client, MCPError
from mcp.server import MCPServer
mcp = MCPServer("repro")
@mcp.resource("users://{user_id}/profile")
def profile(user_id: int) -> str:
return f"user {user_id}"
async def main() -> None:
async with Client(mcp) as client:
print((await client.read_resource("users://42/profile")).contents[0].text)
try:
await client.read_resource("users://abc/profile")
except MCPError as e:
print(e.error.code, e.error.message)
anyio.run(main)
# user 42
# -32603 Error creating resource from template users://abc/profile
# stderr: ERROR Resource 'users://abc/profile' raised an unexpected exception
# + the pydantic ValidationError traceback (int_parsing, input_value='abc')Python & MCP Python SDK
Python 3.11.14, pydantic 2.12.5, macOS
mcp main at 77001c9. src/mcp/server/mcpserver/resources/templates.py is unchanged since v2.3.0 (2118f14).
Activity
tharunsridhar commentedon Nov 8, 2026
Reproduced on mcp 2.3.0 with the example from the report:
users://42/profilereturnsuser 42, andusers://abc/profilelogs "Resource 'users://abc/profile' raised an unexpected exception" with the full pydantic traceback instead of a clean "unknown resource" answer.Since the reporter has first call on this, I'm happy to take the PR only if you don't plan to open it yourself. If a maintainer assigns it to me, I'd follow the proposed fix (validate the extracted parameters against the stored argument model inside
create_resourceand raiseResourceNotFoundError) and add tests for both the invalid and the valid int cases.Disclosure: I used Claude Code to run the reproduction; I read the output and wrote this comment.
rodrigogaraujo commentedon Nov 8, 2026
Thanks for confirming on 2.3.0. Yes, I plan to open the PR: the branch is ready, with the check inside the existing try of create_resource and tests for both the invalid and the valid values. I'm waiting on a maintainer's call on the two questions above (single validation and fall-through) before sending it.
Drafted with Claude Code.
media-ctrl commentedon Nov 9, 2026
Confirming the mechanism against main at 77001c9, because the two places that matter are both in the code you cited.
ResourceTemplate.from_functionbuilds the argument model at line 153 (func_arg_metadata = func_metadata(...)) and then rebinds the handler throughvalidate_callat line 159, so parameter validation and the body share one call.create_resourcethen catchesExceptionat line 247 and raisesUnexpectedResourceErrorat 248. Since the parameterValidationErrorarrives from inside the same call, it takes the crash branch. Nothing in the template path can distinguish a parameter rejection from a body failure today.On your question 1, the double validation is real, and there is a way to avoid it without changing what
ResourceTemplate.fnholds. Measured on pydantic 2.13.5 and 2.14.3 with anAnnotated[int, AfterValidator(...)]parameter, counting validator invocations:validate_callwrapper with those validated arguments: 1 more run, so 2 per readwrapper.__wrapped__instead: 0 more runs, so 1 per readvalidate_callkeeps the original function on__wrapped__in both versions, so the pre-validated path can call that and skip the second pass. It also keeps the publicfnattribute as it is today. Your concern about reusingFuncMetadata.validate_arguments()is right as well: it addspre_parse_json, so"42"is JSON-decoded before the model sees it, which is a second interpretation of the same value and not whatfrom_functiondoes today.On question 2, I would keep the stop-at-first-match behaviour and not add fall-through. Path safety stops at the first match because the URI names a specific template, and a type rejection is a property of the value that matched, not of the route. Falling through would make routing depend on how many templates happen to collide, and it would resolve
items://hello/xto a different resource thanitems://42/xdoes, which is harder to reason about than a clean-32602. It also gives a resource a second chance to match on the same URI, so a client sees one template answerUnknown resourceand another answer success for the same string shape.The strictest version of the fix is the one you proposed: validate inside the existing
try, raiseResourceNotFoundErroronValidationErrorfrom argument validation only, and let a body-raisedValidationErrorkeep the crash path. That means the narrowexcept ValidationErrorhas to wrap just the parameter validation and not the handler call, which is what makes the single-validation variant worth having.