From 64e70e5753b49d79a9c8810df5a0a721b21a3f05 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Sun, 7 Jun 2026 17:03:21 +0800 Subject: [PATCH] fix: return resource-not-found code from MCPServer --- src/mcp/server/mcpserver/exceptions.py | 4 ++++ src/mcp/server/mcpserver/server.py | 10 +++++++--- src/mcp/types/__init__.py | 2 ++ src/mcp/types/jsonrpc.py | 3 +++ tests/interaction/_requirements.py | 6 ------ tests/interaction/mcpserver/test_resources.py | 7 +++++-- tests/server/mcpserver/test_server.py | 9 +++++++-- 7 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/mcp/server/mcpserver/exceptions.py b/src/mcp/server/mcpserver/exceptions.py index dd1b75e829..cc48c85bdd 100644 --- a/src/mcp/server/mcpserver/exceptions.py +++ b/src/mcp/server/mcpserver/exceptions.py @@ -13,6 +13,10 @@ class ResourceError(MCPServerError): """Error in resource operations.""" +class ResourceNotFoundError(ResourceError): + """Requested resource does not exist.""" + + class ToolError(MCPServerError): """Error in tool operations.""" diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index ec2365810e..78e21d9895 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -31,7 +31,7 @@ from mcp.server.lowlevel.server import LifespanResultT, Server from mcp.server.lowlevel.server import lifespan as default_lifespan from mcp.server.mcpserver.context import Context -from mcp.server.mcpserver.exceptions import ResourceError +from mcp.server.mcpserver.exceptions import ResourceError, ResourceNotFoundError from mcp.server.mcpserver.prompts import Prompt, PromptManager from mcp.server.mcpserver.resources import FunctionResource, Resource, ResourceManager from mcp.server.mcpserver.tools import Tool, ToolManager @@ -44,6 +44,7 @@ from mcp.server.transport_security import TransportSecuritySettings from mcp.shared.exceptions import MCPError from mcp.types import ( + RESOURCE_NOT_FOUND, Annotations, BlobResourceContents, CallToolRequestParams, @@ -341,7 +342,10 @@ async def _handle_read_resource( self, ctx: ServerRequestContext[LifespanResultT], params: ReadResourceRequestParams ) -> ReadResourceResult: context = Context(request_context=ctx, mcp_server=self) - results = await self.read_resource(params.uri, context) + try: + results = await self.read_resource(params.uri, context) + except ResourceNotFoundError as exc: + raise MCPError(RESOURCE_NOT_FOUND, str(exc)) from exc contents: list[TextResourceContents | BlobResourceContents] = [] for item in results: if isinstance(item.content, bytes): @@ -448,7 +452,7 @@ async def read_resource( try: resource = await self._resource_manager.get_resource(uri, context) except ValueError as exc: - raise ResourceError(f"Unknown resource: {uri}") from exc + raise ResourceNotFoundError(f"Unknown resource: {uri}") from exc try: content = await resource.read() diff --git a/src/mcp/types/__init__.py b/src/mcp/types/__init__.py index b2d537fb70..8176019cc8 100644 --- a/src/mcp/types/__init__.py +++ b/src/mcp/types/__init__.py @@ -153,6 +153,7 @@ METHOD_NOT_FOUND, PARSE_ERROR, REQUEST_TIMEOUT, + RESOURCE_NOT_FOUND, URL_ELICITATION_REQUIRED, ErrorData, JSONRPCError, @@ -320,6 +321,7 @@ "METHOD_NOT_FOUND", "PARSE_ERROR", "REQUEST_TIMEOUT", + "RESOURCE_NOT_FOUND", "URL_ELICITATION_REQUIRED", "ErrorData", "JSONRPCError", diff --git a/src/mcp/types/jsonrpc.py b/src/mcp/types/jsonrpc.py index 84304a37c1..41d2b0c3b3 100644 --- a/src/mcp/types/jsonrpc.py +++ b/src/mcp/types/jsonrpc.py @@ -37,6 +37,9 @@ class JSONRPCResponse(BaseModel): # MCP-specific error codes in the range [-32000, -32099] +RESOURCE_NOT_FOUND = -32002 +"""Error code indicating that a requested resource does not exist.""" + URL_ELICITATION_REQUIRED = -32042 """Error code indicating that a URL mode elicitation is required before the request can be processed.""" diff --git a/tests/interaction/_requirements.py b/tests/interaction/_requirements.py index caed8905d0..bf0bf23636 100644 --- a/tests/interaction/_requirements.py +++ b/tests/interaction/_requirements.py @@ -901,12 +901,6 @@ def __post_init__(self) -> None: "mcpserver:resource:unknown-uri": Requirement( source=f"{SPEC_BASE_URL}/server/resources#error-handling", behavior="resources/read for a URI matching no registered resource returns JSON-RPC error -32002.", - divergence=Divergence( - note=( - "The spec reserves -32002 for resource-not-found; MCPServer raises ResourceError, which " - "the low-level server converts to error code 0." - ), - ), ), # ═══════════════════════════════════════════════════════════════════════════ # Prompts diff --git a/tests/interaction/mcpserver/test_resources.py b/tests/interaction/mcpserver/test_resources.py index 57b0fdc86d..422d2a53b1 100644 --- a/tests/interaction/mcpserver/test_resources.py +++ b/tests/interaction/mcpserver/test_resources.py @@ -6,6 +6,7 @@ from mcp import MCPError from mcp.server.mcpserver import MCPServer from mcp.types import ( + RESOURCE_NOT_FOUND, ErrorData, ListResourcesResult, ListResourceTemplatesResult, @@ -114,7 +115,7 @@ def user_profile(user_id: str) -> str: async def test_read_unknown_uri_is_error(connect: Connect) -> None: """Reading a URI that matches no registered resource fails with a JSON-RPC error. - The spec reserves -32002 for resource-not-found; see the divergence note on the requirement. + The spec reserves -32002 for resource-not-found. """ mcp = MCPServer("library") @@ -127,7 +128,9 @@ def app_config() -> str: with pytest.raises(MCPError) as exc_info: await client.read_resource("config://missing") - assert exc_info.value.error == snapshot(ErrorData(code=0, message="Unknown resource: config://missing")) + assert exc_info.value.error == snapshot( + ErrorData(code=RESOURCE_NOT_FOUND, message="Unknown resource: config://missing") + ) @requirement("mcpserver:resource:read-throws-surfaced") diff --git a/tests/server/mcpserver/test_server.py b/tests/server/mcpserver/test_server.py index 21352b5f2f..c77eca31cc 100644 --- a/tests/server/mcpserver/test_server.py +++ b/tests/server/mcpserver/test_server.py @@ -19,6 +19,7 @@ from mcp.server.transport_security import TransportSecuritySettings from mcp.shared.exceptions import MCPError from mcp.types import ( + RESOURCE_NOT_FOUND, AudioContent, BlobResourceContents, Completion, @@ -730,9 +731,11 @@ async def test_read_unknown_resource(self): mcp = MCPServer() async with Client(mcp) as client: - with pytest.raises(MCPError, match="Unknown resource: unknown://missing"): + with pytest.raises(MCPError, match="Unknown resource: unknown://missing") as exc_info: await client.read_resource("unknown://missing") + assert exc_info.value.error.code == RESOURCE_NOT_FOUND + async def test_read_resource_error(self): """Test that resource read errors are properly wrapped in MCPError.""" mcp = MCPServer() @@ -742,9 +745,11 @@ def failing_resource(): raise ValueError("Resource read failed") async with Client(mcp) as client: - with pytest.raises(MCPError, match="Error reading resource resource://failing"): + with pytest.raises(MCPError, match="Error reading resource resource://failing") as exc_info: await client.read_resource("resource://failing") + assert exc_info.value.error.code == 0 + async def test_binary_resource(self): mcp = MCPServer()