FastMCP read_resource() returns incorrect error code when resource not found
Initial Checks
- [x] I confirm that I'm using the latest version of MCP Python SDK
- [x] I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
Description
It appears to be unclear as to how an MCP server should respond to a read resource request if the resource is not found, however, FastMCP returns error code 0, which seems likely incorrect regardless.
In the Error Handling part of the protocol specification it states:
Resource not found:
-32002
However, the python SDK's FastMCP attempts to raise a ResourceError, except it appears that code would never get executed because a ValueError is raised in ResourceManager first.
The net result of this is that servers implemented using FastMCP return the following for resource-not-found:
{"jsonrpc":"2.0","id":7,"error":{"code":0,"message":"Unknown resource: https://example.com/does-not-exist"}}
For the purposes of comparison, I compared this to the (presumably reference) typescript implementation, which raises an MCP error with InvalidParams: code -32602 - see here
-32602 is notably similar to -32002, but also seemingly incorrect (though in a different way ☹).
As things stand today, it is rather difficult to reliably write clients that behave predictably due to this. I understand this may be more of a protocol specification problem than a python-sdk problem, but error code 0 seems incorrect either way, so thought I'd report it.
Example Code
Basic server (anything will do):
from mcp.server.fastmcp import FastMCP
mcp = FastMCP('Pydantic AI MCP Server')
log_level = 'unset'
@mcp.resource('resource://user_name.txt', mime_type='text/plain')
async def user_name_resource() -> str:
return 'Alice'
if __name__ == '__main__':
mcp.run(transport='streamable-http')
Then try to fetch any resource via whatever MCP client you like, eg (manual curl):
MCP_URL="http://localhost:8000/mcp"
SESSION_ID=""
# Step 1: Initialize and get session ID
echo "1. Initializing MCP server..."
INIT_RESPONSE=$(curl -s -D - -X POST "$MCP_URL" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "test-mcp-script",
"version": "1.0.0"
}
},
"id": 1
}')
# Extract session ID from headers
SESSION_ID=$(echo "$INIT_RESPONSE" | grep -i 'mcp-session-id:' | cut -d' ' -f2 | tr -d '\r')
# Try to read resource:
curl -s -X POST "$MCP_URL" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID" \
-d '{
"jsonrpc": "2.0",
"method": "resources/read",
"params": {
"uri": "https://example.com/does-not-exist"
},
"id": 7
}'
# > event: message
# > data: {"jsonrpc":"2.0","id":7,"error":{"code":0,"message":"Unknown resource: https://example.com/does-not-exist"}}
Python & MCP Python SDK
`main`
Thank you for the report! It would be great if you could provide some example code for the server/client with which shows the errors and what you'd expect, or hope to be consistent.
Thank you for the report! It would be great if you could provide some example code for the server/client with which shows the errors and what you'd expect, or hope to be consistent.
Done!
Wait, before we go further on this, I just found this: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1545
It looks like the spec itself is wrong ☹️. I haven't read the whole thread but this may change the answer as to what the right solution is.
I am getting the same error after updating FastMCP from v2.12.5 to v2.13.0.2. The error originates in mcp/shared/session.py → BaseSession → send_reques()
jsonrpc='2.0' id=8 error=ErrorData(code=0, message="Unknown resource: 'canvasapi://courses/DATA101-F25/syllabus?sis_user_id=123456789'", data=None)
Here is our resource template declaration:
@courses_mcp.resource(
f"canvasapi://courses/{{sis_course_id}}/syllabus?sis_user_id={{sis_user_id}}",
name="get_syllabus",
description="Fetch the syllabus_body field for a course. Returns the syllabus text for the course.",
tags={ToolPermission.COURSE, ToolPermission.SYLLABUS, ContentTag.SYLLABUS},
)
async def get_syllabus(
sis_course_id: str,
sis_user_id: str,
) -> str:
"""
Fetch the syllabus_body for a course from Canvas.
"""
try:
logging.info("Fetching syllabus for course.")
# Use get_course_for_user to get course details
course = self._canvas.courses.get_course_for_user(
sis_course_id=sis_course_id,
sis_user_id=sis_user_id,
include=["syllabus_body", "term"],
)
# Use Pythonic fallback to empty string if syllabus_body is falsy
syllabus_body = course.syllabus_body or ""
return syllabus_body
except Exception as e:
logging.error(f"Error in get_syllabus: {e}")
return {"error": str(e), "sis_course_id": sis_course_id}