Skip to content

Commit 0a1c933

Browse files
committed
fix: return resource-not-found error code
1 parent ac96f88 commit 0a1c933

7 files changed

Lines changed: 41 additions & 12 deletions

File tree

src/mcp/server/mcpserver/resources/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .base import Resource
2-
from .resource_manager import ResourceManager
2+
from .resource_manager import ResourceManager, UnknownResourceError
33
from .templates import ResourceTemplate
44
from .types import (
55
BinaryResource,
@@ -20,4 +20,5 @@
2020
"DirectoryResource",
2121
"ResourceTemplate",
2222
"ResourceManager",
23+
"UnknownResourceError",
2324
]

src/mcp/server/mcpserver/resources/resource_manager.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
logger = get_logger(__name__)
2020

2121

22+
class UnknownResourceError(ValueError):
23+
"""Raised when no registered resource or resource template matches a URI."""
24+
25+
2226
class ResourceManager:
2327
"""Manages MCPServer resources."""
2428

@@ -95,7 +99,7 @@ async def get_resource(self, uri: AnyUrl | str, context: Context[LifespanContext
9599
except Exception as e: # pragma: no cover
96100
raise ValueError(f"Error creating resource from template: {e}")
97101

98-
raise ValueError(f"Unknown resource: {uri}")
102+
raise UnknownResourceError(f"Unknown resource: {uri}")
99103

100104
def list_resources(self) -> list[Resource]:
101105
"""List all registered resources."""

src/mcp/server/mcpserver/server.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from mcp.server.mcpserver.context import Context
3434
from mcp.server.mcpserver.exceptions import ResourceError
3535
from mcp.server.mcpserver.prompts import Prompt, PromptManager
36-
from mcp.server.mcpserver.resources import FunctionResource, Resource, ResourceManager
36+
from mcp.server.mcpserver.resources import FunctionResource, Resource, ResourceManager, UnknownResourceError
3737
from mcp.server.mcpserver.tools import Tool, ToolManager
3838
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter
3939
from mcp.server.mcpserver.utilities.logging import configure_logging, get_logger
@@ -44,6 +44,7 @@
4444
from mcp.server.transport_security import TransportSecuritySettings
4545
from mcp.shared.exceptions import MCPError
4646
from mcp.types import (
47+
RESOURCE_NOT_FOUND,
4748
Annotations,
4849
BlobResourceContents,
4950
CallToolRequestParams,
@@ -447,8 +448,11 @@ async def read_resource(
447448
context = Context(mcp_server=self)
448449
try:
449450
resource = await self._resource_manager.get_resource(uri, context)
451+
except UnknownResourceError as exc:
452+
raise MCPError(RESOURCE_NOT_FOUND, f"Unknown resource: {uri}") from exc
450453
except ValueError as exc:
451-
raise ResourceError(f"Unknown resource: {uri}") from exc
454+
logger.exception(f"Error getting resource {uri}")
455+
raise ResourceError(f"Error reading resource {uri}") from exc
452456

453457
try:
454458
content = await resource.read()

src/mcp/types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@
153153
METHOD_NOT_FOUND,
154154
PARSE_ERROR,
155155
REQUEST_TIMEOUT,
156+
RESOURCE_NOT_FOUND,
156157
URL_ELICITATION_REQUIRED,
157158
ErrorData,
158159
JSONRPCError,
@@ -320,6 +321,7 @@
320321
"METHOD_NOT_FOUND",
321322
"PARSE_ERROR",
322323
"REQUEST_TIMEOUT",
324+
"RESOURCE_NOT_FOUND",
323325
"URL_ELICITATION_REQUIRED",
324326
"ErrorData",
325327
"JSONRPCError",

src/mcp/types/jsonrpc.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ class JSONRPCResponse(BaseModel):
3737

3838

3939
# MCP-specific error codes in the range [-32000, -32099]
40+
RESOURCE_NOT_FOUND = -32002
41+
"""Error code indicating that a requested resource URI does not exist."""
42+
4043
URL_ELICITATION_REQUIRED = -32042
4144
"""Error code indicating that a URL mode elicitation is required before the request can be processed."""
4245

tests/interaction/_requirements.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -901,12 +901,6 @@ def __post_init__(self) -> None:
901901
"mcpserver:resource:unknown-uri": Requirement(
902902
source=f"{SPEC_BASE_URL}/server/resources#error-handling",
903903
behavior="resources/read for a URI matching no registered resource returns JSON-RPC error -32002.",
904-
divergence=Divergence(
905-
note=(
906-
"The spec reserves -32002 for resource-not-found; MCPServer raises ResourceError, which "
907-
"the low-level server converts to error code 0."
908-
),
909-
),
910904
),
911905
# ═══════════════════════════════════════════════════════════════════════════
912906
# Prompts

tests/interaction/mcpserver/test_resources.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from mcp import MCPError
77
from mcp.server.mcpserver import MCPServer
88
from mcp.types import (
9+
RESOURCE_NOT_FOUND,
910
ErrorData,
1011
ListResourcesResult,
1112
ListResourceTemplatesResult,
@@ -114,7 +115,7 @@ def user_profile(user_id: str) -> str:
114115
async def test_read_unknown_uri_is_error(connect: Connect) -> None:
115116
"""Reading a URI that matches no registered resource fails with a JSON-RPC error.
116117
117-
The spec reserves -32002 for resource-not-found; see the divergence note on the requirement.
118+
The spec reserves -32002 for resource-not-found.
118119
"""
119120
mcp = MCPServer("library")
120121

@@ -127,7 +128,9 @@ def app_config() -> str:
127128
with pytest.raises(MCPError) as exc_info:
128129
await client.read_resource("config://missing")
129130

130-
assert exc_info.value.error == snapshot(ErrorData(code=0, message="Unknown resource: config://missing"))
131+
assert exc_info.value.error == snapshot(
132+
ErrorData(code=RESOURCE_NOT_FOUND, message="Unknown resource: config://missing")
133+
)
131134

132135

133136
@requirement("mcpserver:resource:read-throws-surfaced")
@@ -151,6 +154,24 @@ def boom() -> str:
151154
assert exc_info.value.error == snapshot(ErrorData(code=0, message="Error reading resource res://boom"))
152155

153156

157+
@requirement("mcpserver:resource:read-throws-surfaced")
158+
async def test_templated_resource_function_that_raises_is_not_reported_as_missing(connect: Connect) -> None:
159+
"""A matching resource template that raises is a read failure, not a missing resource."""
160+
mcp = MCPServer("library")
161+
162+
@mcp.resource("users://{user_id}/profile")
163+
def user_profile(user_id: str) -> str:
164+
raise RuntimeError(f"profile unavailable for {user_id}")
165+
166+
async with connect(mcp) as client:
167+
with pytest.raises(MCPError) as exc_info:
168+
await client.read_resource("users://42/profile")
169+
170+
assert exc_info.value.error == snapshot(
171+
ErrorData(code=0, message="Error reading resource users://42/profile")
172+
)
173+
174+
154175
@requirement("mcpserver:resource:duplicate-name")
155176
async def test_registering_a_duplicate_resource_uri_warns_and_keeps_the_first(connect: Connect) -> None:
156177
"""Registering a second static resource at an already-used URI keeps the first registration.

0 commit comments

Comments
 (0)