From 0a1c933f5c951e856932fbc6ab2d85dc12e33b81 Mon Sep 17 00:00:00 2001 From: Sarvesh Date: Mon, 8 Jun 2026 10:59:17 +0530 Subject: [PATCH 1/3] fix: return resource-not-found error code --- .../server/mcpserver/resources/__init__.py | 3 ++- .../mcpserver/resources/resource_manager.py | 6 ++++- src/mcp/server/mcpserver/server.py | 8 ++++-- src/mcp/types/__init__.py | 2 ++ src/mcp/types/jsonrpc.py | 3 +++ tests/interaction/_requirements.py | 6 ----- tests/interaction/mcpserver/test_resources.py | 25 +++++++++++++++++-- 7 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/mcp/server/mcpserver/resources/__init__.py b/src/mcp/server/mcpserver/resources/__init__.py index b5805fb348..3936a71e22 100644 --- a/src/mcp/server/mcpserver/resources/__init__.py +++ b/src/mcp/server/mcpserver/resources/__init__.py @@ -1,5 +1,5 @@ from .base import Resource -from .resource_manager import ResourceManager +from .resource_manager import ResourceManager, UnknownResourceError from .templates import ResourceTemplate from .types import ( BinaryResource, @@ -20,4 +20,5 @@ "DirectoryResource", "ResourceTemplate", "ResourceManager", + "UnknownResourceError", ] diff --git a/src/mcp/server/mcpserver/resources/resource_manager.py b/src/mcp/server/mcpserver/resources/resource_manager.py index 766cf51aea..b356158896 100644 --- a/src/mcp/server/mcpserver/resources/resource_manager.py +++ b/src/mcp/server/mcpserver/resources/resource_manager.py @@ -19,6 +19,10 @@ logger = get_logger(__name__) +class UnknownResourceError(ValueError): + """Raised when no registered resource or resource template matches a URI.""" + + class ResourceManager: """Manages MCPServer resources.""" @@ -95,7 +99,7 @@ async def get_resource(self, uri: AnyUrl | str, context: Context[LifespanContext except Exception as e: # pragma: no cover raise ValueError(f"Error creating resource from template: {e}") - raise ValueError(f"Unknown resource: {uri}") + raise UnknownResourceError(f"Unknown resource: {uri}") def list_resources(self) -> list[Resource]: """List all registered resources.""" diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index ec2365810e..30bb695970 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -33,7 +33,7 @@ from mcp.server.mcpserver.context import Context from mcp.server.mcpserver.exceptions import ResourceError from mcp.server.mcpserver.prompts import Prompt, PromptManager -from mcp.server.mcpserver.resources import FunctionResource, Resource, ResourceManager +from mcp.server.mcpserver.resources import FunctionResource, Resource, ResourceManager, UnknownResourceError from mcp.server.mcpserver.tools import Tool, ToolManager from mcp.server.mcpserver.utilities.context_injection import find_context_parameter from mcp.server.mcpserver.utilities.logging import configure_logging, get_logger @@ -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, @@ -447,8 +448,11 @@ async def read_resource( context = Context(mcp_server=self) try: resource = await self._resource_manager.get_resource(uri, context) + except UnknownResourceError as exc: + raise MCPError(RESOURCE_NOT_FOUND, f"Unknown resource: {uri}") from exc except ValueError as exc: - raise ResourceError(f"Unknown resource: {uri}") from exc + logger.exception(f"Error getting resource {uri}") + raise ResourceError(f"Error reading 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..7c2d6abe35 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 URI 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..7522521d22 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") @@ -151,6 +154,24 @@ def boom() -> str: assert exc_info.value.error == snapshot(ErrorData(code=0, message="Error reading resource res://boom")) +@requirement("mcpserver:resource:read-throws-surfaced") +async def test_templated_resource_function_that_raises_is_not_reported_as_missing(connect: Connect) -> None: + """A matching resource template that raises is a read failure, not a missing resource.""" + mcp = MCPServer("library") + + @mcp.resource("users://{user_id}/profile") + def user_profile(user_id: str) -> str: + raise RuntimeError(f"profile unavailable for {user_id}") + + async with connect(mcp) as client: + with pytest.raises(MCPError) as exc_info: + await client.read_resource("users://42/profile") + + assert exc_info.value.error == snapshot( + ErrorData(code=0, message="Error reading resource users://42/profile") + ) + + @requirement("mcpserver:resource:duplicate-name") async def test_registering_a_duplicate_resource_uri_warns_and_keeps_the_first(connect: Connect) -> None: """Registering a second static resource at an already-used URI keeps the first registration. From c6499ee42a72dc2d73863437cce3e773b1464a8a Mon Sep 17 00:00:00 2001 From: Sarvesh Date: Mon, 8 Jun 2026 19:15:24 +0530 Subject: [PATCH 2/3] fix: preserve direct resource read errors --- src/mcp/server/mcpserver/server.py | 9 +++++++-- tests/interaction/mcpserver/test_resources.py | 4 +--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index 30bb695970..264c9f6632 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -342,7 +342,12 @@ 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 ResourceError as exc: + if isinstance(exc.__cause__, UnknownResourceError): + raise MCPError(RESOURCE_NOT_FOUND, str(exc)) from exc + raise contents: list[TextResourceContents | BlobResourceContents] = [] for item in results: if isinstance(item.content, bytes): @@ -449,7 +454,7 @@ async def read_resource( try: resource = await self._resource_manager.get_resource(uri, context) except UnknownResourceError as exc: - raise MCPError(RESOURCE_NOT_FOUND, f"Unknown resource: {uri}") from exc + raise ResourceError(f"Unknown resource: {uri}") from exc except ValueError as exc: logger.exception(f"Error getting resource {uri}") raise ResourceError(f"Error reading resource {uri}") from exc diff --git a/tests/interaction/mcpserver/test_resources.py b/tests/interaction/mcpserver/test_resources.py index 7522521d22..543f0f3c72 100644 --- a/tests/interaction/mcpserver/test_resources.py +++ b/tests/interaction/mcpserver/test_resources.py @@ -167,9 +167,7 @@ def user_profile(user_id: str) -> str: with pytest.raises(MCPError) as exc_info: await client.read_resource("users://42/profile") - assert exc_info.value.error == snapshot( - ErrorData(code=0, message="Error reading resource users://42/profile") - ) + assert exc_info.value.error == snapshot(ErrorData(code=0, message="Error reading resource users://42/profile")) @requirement("mcpserver:resource:duplicate-name") From 087f0e770326d564ac6270b2cb0a1121b87b7616 Mon Sep 17 00:00:00 2001 From: Sarvesh Date: Mon, 8 Jun 2026 19:22:16 +0530 Subject: [PATCH 3/3] test: remove covered resource pragma --- src/mcp/server/mcpserver/resources/resource_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/server/mcpserver/resources/resource_manager.py b/src/mcp/server/mcpserver/resources/resource_manager.py index b356158896..7f415a4860 100644 --- a/src/mcp/server/mcpserver/resources/resource_manager.py +++ b/src/mcp/server/mcpserver/resources/resource_manager.py @@ -96,7 +96,7 @@ async def get_resource(self, uri: AnyUrl | str, context: Context[LifespanContext if params := template.matches(uri_str): try: return await template.create_resource(uri_str, params, context=context) - except Exception as e: # pragma: no cover + except Exception as e: raise ValueError(f"Error creating resource from template: {e}") raise UnknownResourceError(f"Unknown resource: {uri}")