Skip to content

Commit 64e70e5

Browse files
committed
fix: return resource-not-found code from MCPServer
1 parent ac96f88 commit 64e70e5

7 files changed

Lines changed: 28 additions & 13 deletions

File tree

src/mcp/server/mcpserver/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ class ResourceError(MCPServerError):
1313
"""Error in resource operations."""
1414

1515

16+
class ResourceNotFoundError(ResourceError):
17+
"""Requested resource does not exist."""
18+
19+
1620
class ToolError(MCPServerError):
1721
"""Error in tool operations."""
1822

src/mcp/server/mcpserver/server.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from mcp.server.lowlevel.server import LifespanResultT, Server
3232
from mcp.server.lowlevel.server import lifespan as default_lifespan
3333
from mcp.server.mcpserver.context import Context
34-
from mcp.server.mcpserver.exceptions import ResourceError
34+
from mcp.server.mcpserver.exceptions import ResourceError, ResourceNotFoundError
3535
from mcp.server.mcpserver.prompts import Prompt, PromptManager
3636
from mcp.server.mcpserver.resources import FunctionResource, Resource, ResourceManager
3737
from mcp.server.mcpserver.tools import Tool, ToolManager
@@ -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,
@@ -341,7 +342,10 @@ async def _handle_read_resource(
341342
self, ctx: ServerRequestContext[LifespanResultT], params: ReadResourceRequestParams
342343
) -> ReadResourceResult:
343344
context = Context(request_context=ctx, mcp_server=self)
344-
results = await self.read_resource(params.uri, context)
345+
try:
346+
results = await self.read_resource(params.uri, context)
347+
except ResourceNotFoundError as exc:
348+
raise MCPError(RESOURCE_NOT_FOUND, str(exc)) from exc
345349
contents: list[TextResourceContents | BlobResourceContents] = []
346350
for item in results:
347351
if isinstance(item.content, bytes):
@@ -448,7 +452,7 @@ async def read_resource(
448452
try:
449453
resource = await self._resource_manager.get_resource(uri, context)
450454
except ValueError as exc:
451-
raise ResourceError(f"Unknown resource: {uri}") from exc
455+
raise ResourceNotFoundError(f"Unknown 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 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: 5 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")

tests/server/mcpserver/test_server.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from mcp.server.transport_security import TransportSecuritySettings
2020
from mcp.shared.exceptions import MCPError
2121
from mcp.types import (
22+
RESOURCE_NOT_FOUND,
2223
AudioContent,
2324
BlobResourceContents,
2425
Completion,
@@ -730,9 +731,11 @@ async def test_read_unknown_resource(self):
730731
mcp = MCPServer()
731732

732733
async with Client(mcp) as client:
733-
with pytest.raises(MCPError, match="Unknown resource: unknown://missing"):
734+
with pytest.raises(MCPError, match="Unknown resource: unknown://missing") as exc_info:
734735
await client.read_resource("unknown://missing")
735736

737+
assert exc_info.value.error.code == RESOURCE_NOT_FOUND
738+
736739
async def test_read_resource_error(self):
737740
"""Test that resource read errors are properly wrapped in MCPError."""
738741
mcp = MCPServer()
@@ -742,9 +745,11 @@ def failing_resource():
742745
raise ValueError("Resource read failed")
743746

744747
async with Client(mcp) as client:
745-
with pytest.raises(MCPError, match="Error reading resource resource://failing"):
748+
with pytest.raises(MCPError, match="Error reading resource resource://failing") as exc_info:
746749
await client.read_resource("resource://failing")
747750

751+
assert exc_info.value.error.code == 0
752+
748753
async def test_binary_resource(self):
749754
mcp = MCPServer()
750755

0 commit comments

Comments
 (0)