Skip to content

Commit d4d62fb

Browse files
committed
fix(server): raise distinct errors for not-found vs template failure
ResourceManager.get_resource() and ResourceTemplate.create_resource() now raise ResourceNotFoundError and ResourceError respectively instead of generic ValueError. This lets read_resource() drop its broad except-ValueError translation, so a template whose user function throws is no longer reported to clients as -32602 (resource not found). It now propagates as ResourceError -> -32603 INTERNAL_ERROR, matching static-resource read failures.
1 parent d065e9b commit d4d62fb

6 files changed

Lines changed: 30 additions & 13 deletions

File tree

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from pydantic import AnyUrl
99

10+
from mcp.server.mcpserver.exceptions import ResourceNotFoundError
1011
from mcp.server.mcpserver.resources.base import Resource
1112
from mcp.server.mcpserver.resources.templates import ResourceTemplate
1213
from mcp.server.mcpserver.utilities.logging import get_logger
@@ -92,12 +93,9 @@ async def get_resource(self, uri: AnyUrl | str, context: Context[LifespanContext
9293
# Then check templates
9394
for template in self._templates.values():
9495
if params := template.matches(uri_str):
95-
try:
96-
return await template.create_resource(uri_str, params, context=context)
97-
except Exception as e: # pragma: no cover
98-
raise ValueError(f"Error creating resource from template: {e}")
96+
return await template.create_resource(uri_str, params, context=context)
9997

100-
raise ValueError(f"Unknown resource: {uri}")
98+
raise ResourceNotFoundError(f"Unknown resource: {uri}")
10199

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

src/mcp/server/mcpserver/resources/templates.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from pydantic import BaseModel, Field, validate_call
1212

13+
from mcp.server.mcpserver.exceptions import ResourceError
1314
from mcp.server.mcpserver.resources.types import FunctionResource, Resource
1415
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter, inject_context
1516
from mcp.server.mcpserver.utilities.func_metadata import func_metadata
@@ -104,7 +105,7 @@ async def create_resource(
104105
"""Create a resource from the template with the given parameters.
105106
106107
Raises:
107-
ValueError: If creating the resource fails.
108+
ResourceError: If creating the resource fails.
108109
"""
109110
try:
110111
# Add context to params if needed
@@ -127,4 +128,4 @@ async def create_resource(
127128
fn=lambda: result, # Capture result in closure
128129
)
129130
except Exception as e:
130-
raise ValueError(f"Error creating resource from template: {e}")
131+
raise ResourceError(f"Error creating resource from template: {e}")

src/mcp/server/mcpserver/server.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from mcp.server.transport_security import TransportSecuritySettings
4545
from mcp.shared.exceptions import MCPError
4646
from mcp.types import (
47+
INTERNAL_ERROR,
4748
INVALID_PARAMS,
4849
Annotations,
4950
BlobResourceContents,
@@ -337,6 +338,8 @@ async def _handle_read_resource(
337338
results = await self.read_resource(params.uri, context)
338339
except ResourceNotFoundError as err:
339340
raise MCPError(code=INVALID_PARAMS, message=str(err), data={"uri": str(params.uri)})
341+
except ResourceError as err:
342+
raise MCPError(code=INTERNAL_ERROR, message=str(err), data={"uri": str(params.uri)})
340343
contents: list[TextResourceContents | BlobResourceContents] = []
341344
for item in results:
342345
if isinstance(item.content, bytes):
@@ -440,10 +443,7 @@ async def read_resource(
440443
"""Read a resource by URI."""
441444
if context is None:
442445
context = Context(mcp_server=self)
443-
try:
444-
resource = await self._resource_manager.get_resource(uri, context)
445-
except ValueError:
446-
raise ResourceNotFoundError(f"Unknown resource: {uri}")
446+
resource = await self._resource_manager.get_resource(uri, context)
447447

448448
try:
449449
content = await resource.read()

tests/server/mcpserver/resources/test_resource_manager.py

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

77
from mcp.server.mcpserver import Context
8+
from mcp.server.mcpserver.exceptions import ResourceNotFoundError
89
from mcp.server.mcpserver.resources import FileResource, FunctionResource, ResourceManager, ResourceTemplate
910

1011

@@ -114,7 +115,7 @@ def greet(name: str) -> str:
114115
async def test_get_unknown_resource(self):
115116
"""Test getting a non-existent resource."""
116117
manager = ResourceManager()
117-
with pytest.raises(ValueError, match="Unknown resource"):
118+
with pytest.raises(ResourceNotFoundError, match="Unknown resource"):
118119
await manager.get_resource(AnyUrl("unknown://test"), Context())
119120

120121
def test_list_resources(self, temp_file: Path):

tests/server/mcpserver/resources/test_resource_template.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pydantic import BaseModel
66

77
from mcp.server.mcpserver import Context, MCPServer
8+
from mcp.server.mcpserver.exceptions import ResourceError
89
from mcp.server.mcpserver.resources import FunctionResource, ResourceTemplate
910
from mcp.types import Annotations
1011

@@ -86,7 +87,7 @@ def failing_func(x: str) -> str:
8687
name="fail",
8788
)
8889

89-
with pytest.raises(ValueError, match="Error creating resource from template"):
90+
with pytest.raises(ResourceError, match="Error creating resource from template"):
9091
await template.create_resource("fail://test", {"x": "test"}, Context())
9192

9293
@pytest.mark.anyio

tests/server/mcpserver/test_server.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from mcp.server.transport_security import TransportSecuritySettings
2121
from mcp.shared.exceptions import MCPError
2222
from mcp.types import (
23+
INTERNAL_ERROR,
2324
INVALID_PARAMS,
2425
AudioContent,
2526
BlobResourceContents,
@@ -715,6 +716,21 @@ def failing_resource():
715716
with pytest.raises(MCPError, match="Error reading resource resource://failing"):
716717
await client.read_resource("resource://failing")
717718

719+
async def test_read_resource_template_error(self):
720+
"""Template-creation failure must surface as INTERNAL_ERROR, not INVALID_PARAMS (not-found)."""
721+
mcp = MCPServer()
722+
723+
@mcp.resource("resource://item/{item_id}")
724+
def get_item(item_id: str) -> str:
725+
raise RuntimeError("backend unavailable")
726+
727+
async with Client(mcp) as client:
728+
with pytest.raises(MCPError, match="Error creating resource from template") as exc_info:
729+
await client.read_resource("resource://item/42")
730+
731+
assert exc_info.value.error.code == INTERNAL_ERROR
732+
assert exc_info.value.error.code != INVALID_PARAMS
733+
718734
async def test_binary_resource(self):
719735
mcp = MCPServer()
720736

0 commit comments

Comments
 (0)