From 0a221a0d829be472413ca7dcd02502cf71f7c7a7 Mon Sep 17 00:00:00 2001 From: Vincent <0426vincent@gmail.com> Date: Wed, 5 Nov 2025 16:46:09 -0800 Subject: [PATCH 1/3] fix: Raise resource not found error --- src/mcp/server/fastmcp/exceptions.py | 22 ++++++++++++++++++- .../fastmcp/resources/resource_manager.py | 7 +++--- src/mcp/server/fastmcp/server.py | 19 +++++++++++++--- src/mcp/types.py | 1 + 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/mcp/server/fastmcp/exceptions.py b/src/mcp/server/fastmcp/exceptions.py index fb5bda106b..30f3aac677 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 b1efac3ece..8adb56d51f 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 719595916a..f3f3c02f7d 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 8713227404..0b702af40c 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): From 7e95a99c07d917533768febfb10aaccf458ab23e Mon Sep 17 00:00:00 2001 From: Vincent <0426vincent@gmail.com> Date: Wed, 5 Nov 2025 16:54:33 -0800 Subject: [PATCH 2/3] fix: Fix test get unknown resource --- tests/server/fastmcp/resources/test_resource_manager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/server/fastmcp/resources/test_resource_manager.py b/tests/server/fastmcp/resources/test_resource_manager.py index bab0e9ad8b..815f124bea 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.""" From ea2f89e37bf1e72f4c061a9ae751963d0562ede3 Mon Sep 17 00:00:00 2001 From: Vincent <0426vincent@gmail.com> Date: Wed, 5 Nov 2025 17:09:26 -0800 Subject: [PATCH 3/3] fix: Fix resource test --- tests/issues/test_141_resource_templates.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/issues/test_141_resource_templates.py b/tests/issues/test_141_resource_templates.py index 3145f65e8c..141d5f1644 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