Return -32602 for resource not found (SEP-2164) (#2920)

This commit is contained in:
Marcelo Trylesinski 2026-06-20 15:55:23 +01:00 • committed by GitHub
commit f253682393
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 113 additions and 43 deletions

View file

@ -113,6 +113,10 @@ class Context(BaseModel, Generic[LifespanContextT, RequestT]):
Returns:
The resource content as either text or bytes
Raises:
ResourceNotFoundError: If no resource or template matches the URI.
ResourceError: If template creation or resource reading fails.
"""
assert self._mcp_server is not None, "Context is not available outside of a request"
return await self._mcp_server.read_resource(uri, self)

View file

@ -13,6 +13,15 @@ class ResourceError(MCPServerError):
"""Error in resource operations."""
class ResourceNotFoundError(ResourceError):
"""Resource does not exist.
Raise this from a resource template handler to signal that the requested instance does not exist;
clients receive `-32602` (invalid params) per
[SEP-2164](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2164).
"""
class ToolError(MCPServerError):
"""Error in tool operations."""

View file

@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any
from pydantic import AnyUrl
from mcp.server.mcpserver.exceptions import ResourceNotFoundError
from mcp.server.mcpserver.resources.base import Resource
from mcp.server.mcpserver.resources.templates import ResourceTemplate
from mcp.server.mcpserver.utilities.logging import get_logger
@ -79,7 +80,12 @@ class ResourceManager:
return template
async def get_resource(self, uri: AnyUrl | str, context: Context[LifespanContextT, RequestT]) -> Resource:
"""Get resource by URI, checking concrete resources first, then templates."""
"""Get resource by URI, checking concrete resources first, then templates.
Raises:
ResourceNotFoundError: If no resource or template matches the URI.
ResourceError: If a matching template fails to create the resource.
"""
uri_str = str(uri)
logger.debug("Getting resource", extra={"uri": uri_str})
@ -90,12 +96,9 @@ class ResourceManager:
# Then check templates
for template in self._templates.values():
if params := template.matches(uri_str):
try:
return await template.create_resource(uri_str, params, context=context)
except Exception as e: # pragma: no cover
raise ValueError(f"Error creating resource from template: {e}")
return await template.create_resource(uri_str, params, context=context)
raise ValueError(f"Unknown resource: {uri}")
raise ResourceNotFoundError(f"Unknown resource: {uri}")
def list_resources(self) -> list[Resource]:
"""List all registered resources."""

View file

@ -11,12 +11,16 @@ from urllib.parse import unquote
import anyio.to_thread
from pydantic import BaseModel, Field, validate_call
from mcp.server.mcpserver.exceptions import ResourceError
from mcp.server.mcpserver.resources.types import FunctionResource, Resource
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter, inject_context
from mcp.server.mcpserver.utilities.func_metadata import func_metadata
from mcp.server.mcpserver.utilities.logging import get_logger
from mcp.shared._callable_inspection import is_async_callable
from mcp.types import Annotations, Icon
logger = get_logger(__name__)
if TYPE_CHECKING:
from mcp.server.context import LifespanContextT, RequestT
from mcp.server.mcpserver.context import Context
@ -106,7 +110,7 @@ class ResourceTemplate(BaseModel):
"""Create a resource from the template with the given parameters.
Raises:
ValueError: If creating the resource fails.
ResourceError: If creating the resource fails.
"""
try:
# Add context to params if needed
@ -129,5 +133,8 @@ class ResourceTemplate(BaseModel):
meta=self.meta,
fn=lambda: result, # Capture result in closure
)
except Exception as e:
raise ValueError(f"Error creating resource from template: {e}")
except ResourceError:
raise
except Exception as exc:
logger.exception(f"Error creating resource from template {uri}")
raise ResourceError(f"Error creating resource from template {uri}") from exc

View file

@ -31,7 +31,7 @@ from mcp.server.lowlevel.helper_types import ReadResourceContents
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,8 @@ from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from mcp.server.transport_security import TransportSecuritySettings
from mcp.shared.exceptions import MCPError
from mcp.types import (
INTERNAL_ERROR,
INVALID_PARAMS,
Annotations,
BlobResourceContents,
CallToolRequestParams,
@ -341,7 +343,12 @@ class MCPServer(Generic[LifespanResultT]):
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 err:
raise MCPError(code=INVALID_PARAMS, message=str(err), data={"uri": str(params.uri)})
except ResourceError as err:
raise MCPError(code=INTERNAL_ERROR, message=str(err), data={"uri": str(params.uri)})
contents: list[TextResourceContents | BlobResourceContents] = []
for item in results:
if isinstance(item.content, bytes):
@ -442,13 +449,15 @@ class MCPServer(Generic[LifespanResultT]):
async def read_resource(
self, uri: AnyUrl | str, context: Context[LifespanResultT, Any] | None = None
) -> Iterable[ReadResourceContents]:
"""Read a resource by URI."""
"""Read a resource by URI.
Raises:
ResourceNotFoundError: If no resource or template matches the URI.
ResourceError: If template creation or resource reading fails.
"""
if context is None:
context = Context(mcp_server=self)
try:
resource = await self._resource_manager.get_resource(uri, context)
except ValueError as exc:
raise ResourceError(f"Unknown resource: {uri}") from exc
resource = await self._resource_manager.get_resource(uri, context)
try:
content = await resource.read()