diff --git a/src/mcp/server/fastmcp/exceptions.py b/src/mcp/server/fastmcp/exceptions.py index fb5bda106..30f3aac67 100644 --- a/src/mcp/server/fastmcp/exceptions.py +++ b/src/mcp/server/fastmcp/exceptions.py @@ -1,5 +1,9 @@ """Custom exceptions for FastMCP.""" +from typing import Any + +from mcp.types import INTERNAL_ERROR, ErrorData + class FastMCPError(Exception): """Base error for FastMCP.""" @@ -10,7 +14,23 @@ class ValidationError(FastMCPError): class ResourceError(FastMCPError): - """Error in resource operations.""" + """Error in resource operations. + + Defaults to INTERNAL_ERROR (-32603), but can be set to RESOURCE_NOT_FOUND (-32002) + for resource not found errors per MCP spec. + """ + + error: ErrorData + + def __init__(self, message: str, code: int = INTERNAL_ERROR, data: Any | None = None): + """Initialize ResourceError with error code and message. + + Args: + message: Error message + code: Error code (defaults to INTERNAL_ERROR -32603, use RESOURCE_NOT_FOUND -32002 for not found) + """ + super().__init__(message) + self.error = ErrorData(code=code, message=message, data=data) class ToolError(FastMCPError): diff --git a/src/mcp/server/fastmcp/resources/resource_manager.py b/src/mcp/server/fastmcp/resources/resource_manager.py index b1efac3ec..8adb56d51 100644 --- a/src/mcp/server/fastmcp/resources/resource_manager.py +++ b/src/mcp/server/fastmcp/resources/resource_manager.py @@ -7,10 +7,11 @@ from pydantic import AnyUrl +from mcp.server.fastmcp.exceptions import ResourceError from mcp.server.fastmcp.resources.base import Resource from mcp.server.fastmcp.resources.templates import ResourceTemplate from mcp.server.fastmcp.utilities.logging import get_logger -from mcp.types import Annotations, Icon +from mcp.types import RESOURCE_NOT_FOUND, Annotations, Icon if TYPE_CHECKING: from mcp.server.fastmcp.server import Context @@ -98,9 +99,9 @@ async def get_resource( try: return await template.create_resource(uri_str, params, context=context) except Exception as e: - raise ValueError(f"Error creating resource from template: {e}") + raise ResourceError(f"Error creating resource from template: {e}") - raise ValueError(f"Unknown resource: {uri}") + raise ResourceError(f"Unknown resource: {uri}", code=RESOURCE_NOT_FOUND) def list_resources(self) -> list[Resource]: """List all registered resources.""" diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 719595916..f3f3c02f7 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -61,7 +61,17 @@ from mcp.server.streamable_http_manager import StreamableHTTPSessionManager from mcp.server.transport_security import TransportSecuritySettings from mcp.shared.context import LifespanContextT, RequestContext, RequestT -from mcp.types import Annotations, AnyFunction, ContentBlock, GetPromptResult, Icon, ToolAnnotations +from mcp.shared.exceptions import McpError +from mcp.types import ( + RESOURCE_NOT_FOUND, + Annotations, + AnyFunction, + ContentBlock, + ErrorData, + GetPromptResult, + Icon, + ToolAnnotations, +) from mcp.types import Prompt as MCPPrompt from mcp.types import PromptArgument as MCPPromptArgument from mcp.types import Resource as MCPResource @@ -367,9 +377,12 @@ async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContent """Read a resource by URI.""" context = self.get_context() - resource = await self._resource_manager.get_resource(uri, context=context) + try: + resource = await self._resource_manager.get_resource(uri, context=context) + except ResourceError as e: + raise McpError(error=e.error) if not resource: - raise ResourceError(f"Unknown resource: {uri}") + raise McpError(error=ErrorData(code=RESOURCE_NOT_FOUND, message=f"Unknown resource: {uri}")) try: content = await resource.read() diff --git a/src/mcp/types.py b/src/mcp/types.py index 871322740..0b702af40 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -156,6 +156,7 @@ class JSONRPCResponse(BaseModel): METHOD_NOT_FOUND = -32601 INVALID_PARAMS = -32602 INTERNAL_ERROR = -32603 +RESOURCE_NOT_FOUND = -32002 class ErrorData(BaseModel): diff --git a/tests/issues/test_141_resource_templates.py b/tests/issues/test_141_resource_templates.py index 3145f65e8..141d5f164 100644 --- a/tests/issues/test_141_resource_templates.py +++ b/tests/issues/test_141_resource_templates.py @@ -2,6 +2,7 @@ from pydantic import AnyUrl from mcp.server.fastmcp import FastMCP +from mcp.shared.exceptions import McpError from mcp.shared.memory import ( create_connected_server_and_client_session as client_session, ) @@ -57,10 +58,10 @@ def get_user_profile_missing(user_id: str) -> str: assert result_list[0].mime_type == "text/plain" # Verify invalid parameters raise error - with pytest.raises(ValueError, match="Unknown resource"): + with pytest.raises(McpError, match="Unknown resource"): await mcp.read_resource("resource://users/123/posts") # Missing post_id - with pytest.raises(ValueError, match="Unknown resource"): + with pytest.raises(McpError, match="Unknown resource"): await mcp.read_resource("resource://users/123/posts/456/extra") # Extra path component diff --git a/tests/server/fastmcp/resources/test_resource_manager.py b/tests/server/fastmcp/resources/test_resource_manager.py index bab0e9ad8..815f124be 100644 --- a/tests/server/fastmcp/resources/test_resource_manager.py +++ b/tests/server/fastmcp/resources/test_resource_manager.py @@ -4,7 +4,9 @@ import pytest from pydantic import AnyUrl, FileUrl +from mcp.server.fastmcp.exceptions import ResourceError from mcp.server.fastmcp.resources import FileResource, FunctionResource, ResourceManager, ResourceTemplate +from mcp.types import RESOURCE_NOT_FOUND @pytest.fixture @@ -113,8 +115,10 @@ def greet(name: str) -> str: async def test_get_unknown_resource(self): """Test getting a non-existent resource.""" manager = ResourceManager() - with pytest.raises(ValueError, match="Unknown resource"): + with pytest.raises(ResourceError) as exc_info: await manager.get_resource(AnyUrl("unknown://test")) + assert exc_info.value.error.code == RESOURCE_NOT_FOUND + assert "Unknown resource" in str(exc_info.value) def test_list_resources(self, temp_file: Path): """Test listing all resources."""