Skip to content

Commit 1f1e2ad

Browse files
committed
Fix unknown tool/prompt/resource error handling
1 parent 5301298 commit 1f1e2ad

File tree

13 files changed

+77
-31
lines changed

13 files changed

+77
-31
lines changed

src/mcp/server/fastmcp/prompts/manager.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from mcp.server.fastmcp.prompts.base import Message, Prompt
88
from mcp.server.fastmcp.utilities.logging import get_logger
9+
from mcp.shared.exceptions import McpError
10+
from mcp.types import INVALID_PARAMS, ErrorData
911

1012
if TYPE_CHECKING:
1113
from mcp.server.fastmcp.server import Context
@@ -55,6 +57,7 @@ async def render_prompt(
5557
"""Render a prompt by name with arguments."""
5658
prompt = self.get_prompt(name)
5759
if not prompt:
58-
raise ValueError(f"Unknown prompt: {name}")
60+
# Unknown prompt is a protocol error per MCP spec
61+
raise McpError(ErrorData(code=INVALID_PARAMS, message=f"Unknown prompt: {name}"))
5962

6063
return await prompt.render(arguments, context=context)

src/mcp/server/fastmcp/resources/resource_manager.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from mcp.server.fastmcp.resources.base import Resource
1111
from mcp.server.fastmcp.resources.templates import ResourceTemplate
1212
from mcp.server.fastmcp.utilities.logging import get_logger
13-
from mcp.types import Annotations, Icon
13+
from mcp.shared.exceptions import McpError
14+
from mcp.types import RESOURCE_NOT_FOUND, Annotations, ErrorData, Icon
1415

1516
if TYPE_CHECKING:
1617
from mcp.server.fastmcp.server import Context
@@ -85,8 +86,12 @@ async def get_resource(
8586
self,
8687
uri: AnyUrl | str,
8788
context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,
88-
) -> Resource | None:
89-
"""Get resource by URI, checking concrete resources first, then templates."""
89+
) -> Resource:
90+
"""Get resource by URI, checking concrete resources first, then templates.
91+
92+
Raises:
93+
McpError: If the resource is not found (RESOURCE_NOT_FOUND error code).
94+
"""
9095
uri_str = str(uri)
9196
logger.debug("Getting resource", extra={"uri": uri_str})
9297

@@ -102,7 +107,8 @@ async def get_resource(
102107
except Exception as e: # pragma: no cover
103108
raise ValueError(f"Error creating resource from template: {e}")
104109

105-
raise ValueError(f"Unknown resource: {uri}")
110+
# Resource not found is a protocol error per MCP spec
111+
raise McpError(ErrorData(code=RESOURCE_NOT_FOUND, message=f"Unknown resource: {uri}"))
106112

107113
def list_resources(self) -> list[Resource]:
108114
"""List all registered resources."""

src/mcp/server/fastmcp/server.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,16 @@
4444
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
4545
from mcp.server.transport_security import TransportSecuritySettings
4646
from mcp.shared.context import LifespanContextT, RequestContext, RequestT
47-
from mcp.types import Annotations, ContentBlock, GetPromptResult, Icon, ToolAnnotations
47+
from mcp.shared.exceptions import McpError
48+
from mcp.types import (
49+
INVALID_PARAMS,
50+
Annotations,
51+
ContentBlock,
52+
ErrorData,
53+
GetPromptResult,
54+
Icon,
55+
ToolAnnotations,
56+
)
4857
from mcp.types import Prompt as MCPPrompt
4958
from mcp.types import PromptArgument as MCPPromptArgument
5059
from mcp.types import Resource as MCPResource
@@ -964,14 +973,16 @@ async def get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -
964973
try:
965974
prompt = self._prompt_manager.get_prompt(name)
966975
if not prompt:
967-
raise ValueError(f"Unknown prompt: {name}")
976+
raise McpError(ErrorData(code=INVALID_PARAMS, message=f"Unknown prompt: {name}"))
968977

969978
messages = await prompt.render(arguments, context=self.get_context())
970979

971980
return GetPromptResult(
972981
description=prompt.description,
973982
messages=pydantic_core.to_jsonable_python(messages),
974983
)
984+
except McpError:
985+
raise
975986
except Exception as e:
976987
logger.exception(f"Error getting prompt {name}")
977988
raise ValueError(str(e))

src/mcp/server/fastmcp/tools/tool_manager.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from mcp.server.fastmcp.tools.base import Tool
88
from mcp.server.fastmcp.utilities.logging import get_logger
99
from mcp.shared.context import LifespanContextT, RequestT
10-
from mcp.types import Icon, ToolAnnotations
10+
from mcp.shared.exceptions import McpError
11+
from mcp.types import INVALID_PARAMS, ErrorData, Icon, ToolAnnotations
1112

1213
if TYPE_CHECKING:
1314
from mcp.server.fastmcp.server import Context
@@ -88,6 +89,7 @@ async def call_tool(
8889
"""Call a tool by name with arguments."""
8990
tool = self.get_tool(name)
9091
if not tool:
91-
raise ToolError(f"Unknown tool: {name}")
92+
# Unknown tool is a protocol error per MCP spec
93+
raise McpError(ErrorData(code=INVALID_PARAMS, message=f"Unknown tool: {name}"))
9294

9395
return await tool.run(arguments, context=context, convert_result=convert_result)

src/mcp/server/lowlevel/server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,10 @@ async def handler(req: types.CallToolRequest):
600600
# Re-raise UrlElicitationRequiredError so it can be properly handled
601601
# by _handle_request, which converts it to an error response with code -32042
602602
raise
603+
except McpError:
604+
# Re-raise McpError as protocol error
605+
# (e.g., unknown tool returns INVALID_PARAMS per MCP spec)
606+
raise
603607
except Exception as e:
604608
return self._make_error_result(str(e))
605609

src/mcp/types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@
192192
METHOD_NOT_FOUND,
193193
PARSE_ERROR,
194194
REQUEST_TIMEOUT,
195+
RESOURCE_NOT_FOUND,
195196
URL_ELICITATION_REQUIRED,
196197
ErrorData,
197198
JSONRPCError,
@@ -404,6 +405,7 @@
404405
"METHOD_NOT_FOUND",
405406
"PARSE_ERROR",
406407
"REQUEST_TIMEOUT",
408+
"RESOURCE_NOT_FOUND",
407409
"URL_ELICITATION_REQUIRED",
408410
"ErrorData",
409411
"JSONRPCError",

src/mcp/types/jsonrpc.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ class JSONRPCResponse(BaseModel):
4343
# SDK error codes
4444
CONNECTION_CLOSED = -32000
4545
REQUEST_TIMEOUT = -32001
46+
RESOURCE_NOT_FOUND = -32002
47+
"""Error code indicating that a requested resource was not found."""
4648

4749
# Standard JSON-RPC error codes
4850
PARSE_ERROR = -32700

tests/issues/test_141_resource_templates.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
from mcp import Client
44
from mcp.server.fastmcp import FastMCP
5+
from mcp.shared.exceptions import McpError
56
from mcp.types import (
7+
RESOURCE_NOT_FOUND,
68
ListResourceTemplatesResult,
79
TextResourceContents,
810
)
@@ -53,12 +55,14 @@ def get_user_profile_missing(user_id: str) -> str: # pragma: no cover
5355
assert result_list[0].content == "Post 456 by user 123"
5456
assert result_list[0].mime_type == "text/plain"
5557

56-
# Verify invalid parameters raise error
57-
with pytest.raises(ValueError, match="Unknown resource"):
58+
# Verify invalid parameters raise protocol error
59+
with pytest.raises(McpError, match="Unknown resource") as exc_info:
5860
await mcp.read_resource("resource://users/123/posts") # Missing post_id
61+
assert exc_info.value.error.code == RESOURCE_NOT_FOUND
5962

60-
with pytest.raises(ValueError, match="Unknown resource"):
63+
with pytest.raises(McpError, match="Unknown resource") as exc_info:
6164
await mcp.read_resource("resource://users/123/posts/456/extra") # Extra path component
65+
assert exc_info.value.error.code == RESOURCE_NOT_FOUND
6266

6367

6468
@pytest.mark.anyio

tests/server/fastmcp/prompts/test_manager.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from mcp.server.fastmcp.prompts.base import Prompt, TextContent, UserMessage
44
from mcp.server.fastmcp.prompts.manager import PromptManager
5+
from mcp.shared.exceptions import McpError
6+
from mcp.types import INVALID_PARAMS
57

68

79
class TestPromptManager:
@@ -89,10 +91,11 @@ def fn(name: str) -> str:
8991

9092
@pytest.mark.anyio
9193
async def test_render_unknown_prompt(self):
92-
"""Test rendering a non-existent prompt."""
94+
"""Test rendering a non-existent prompt raises protocol error."""
9395
manager = PromptManager()
94-
with pytest.raises(ValueError, match="Unknown prompt: unknown"):
96+
with pytest.raises(McpError, match="Unknown prompt: unknown") as exc_info:
9597
await manager.render_prompt("unknown")
98+
assert exc_info.value.error.code == INVALID_PARAMS
9699

97100
@pytest.mark.anyio
98101
async def test_render_prompt_with_missing_args(self):

tests/server/fastmcp/resources/test_resource_manager.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from pydantic import AnyUrl
66

77
from mcp.server.fastmcp.resources import FileResource, FunctionResource, ResourceManager, ResourceTemplate
8+
from mcp.shared.exceptions import McpError
9+
from mcp.types import RESOURCE_NOT_FOUND
810

911

1012
@pytest.fixture
@@ -111,10 +113,11 @@ def greet(name: str) -> str:
111113

112114
@pytest.mark.anyio
113115
async def test_get_unknown_resource(self):
114-
"""Test getting a non-existent resource."""
116+
"""Test getting a non-existent resource raises protocol error."""
115117
manager = ResourceManager()
116-
with pytest.raises(ValueError, match="Unknown resource"):
118+
with pytest.raises(McpError, match="Unknown resource") as exc_info:
117119
await manager.get_resource(AnyUrl("unknown://test"))
120+
assert exc_info.value.error.code == RESOURCE_NOT_FOUND
118121

119122
def test_list_resources(self, temp_file: Path):
120123
"""Test listing all resources."""

0 commit comments

Comments
 (0)