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
22 changes: 21 additions & 1 deletion src/mcp/server/fastmcp/exceptions.py
Original file line number Diff line number Diff line change
@@ -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."""
Expand All @@ -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):
Expand Down
7 changes: 4 additions & 3 deletions src/mcp/server/fastmcp/resources/resource_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down
19 changes: 16 additions & 3 deletions src/mcp/server/fastmcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions src/mcp/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ class JSONRPCResponse(BaseModel):
METHOD_NOT_FOUND = -32601
INVALID_PARAMS = -32602
INTERNAL_ERROR = -32603
RESOURCE_NOT_FOUND = -32002


class ErrorData(BaseModel):
Expand Down
5 changes: 3 additions & 2 deletions tests/issues/test_141_resource_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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


Expand Down
6 changes: 5 additions & 1 deletion tests/server/fastmcp/resources/test_resource_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down