Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/mcp/server/mcpserver/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
10 changes: 7 additions & 3 deletions src/mcp/server/mcpserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions src/mcp/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@
METHOD_NOT_FOUND,
PARSE_ERROR,
REQUEST_TIMEOUT,
RESOURCE_NOT_FOUND,
URL_ELICITATION_REQUIRED,
ErrorData,
JSONRPCError,
Expand Down Expand Up @@ -320,6 +321,7 @@
"METHOD_NOT_FOUND",
"PARSE_ERROR",
"REQUEST_TIMEOUT",
"RESOURCE_NOT_FOUND",
"URL_ELICITATION_REQUIRED",
"ErrorData",
"JSONRPCError",
Expand Down
3 changes: 3 additions & 0 deletions src/mcp/types/jsonrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
6 changes: 0 additions & 6 deletions tests/interaction/_requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions tests/interaction/mcpserver/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from mcp import MCPError
from mcp.server.mcpserver import MCPServer
from mcp.types import (
RESOURCE_NOT_FOUND,
ErrorData,
ListResourcesResult,
ListResourceTemplatesResult,
Expand Down Expand Up @@ -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")

Expand All @@ -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")
Expand Down
9 changes: 7 additions & 2 deletions tests/server/mcpserver/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand All @@ -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()

Expand Down
Loading