Skip to content

Commit bcbb7ac

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 3007354 commit bcbb7ac

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
@@ -90,12 +91,9 @@ async def get_resource(self, uri: AnyUrl | str, context: Context[LifespanContext
9091
# Then check templates
9192
for template in self._templates.values():
9293
if params := template.matches(uri_str):
93-
try:
94-
return await template.create_resource(uri_str, params, context=context)
95-
except Exception as e: # pragma: no cover
96-
raise ValueError(f"Error creating resource from template: {e}")
94+
return await template.create_resource(uri_str, params, context=context)
9795

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

10098
def list_resources(self) -> list[Resource]:
10199
"""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
@@ -11,6 +11,7 @@
1111
import anyio.to_thread
1212
from pydantic import BaseModel, Field, validate_call
1313

14+
from mcp.server.mcpserver.exceptions import ResourceError
1415
from mcp.server.mcpserver.resources.types import FunctionResource, Resource
1516
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter, inject_context
1617
from mcp.server.mcpserver.utilities.func_metadata import func_metadata
@@ -106,7 +107,7 @@ async def create_resource(
106107
"""Create a resource from the template with the given parameters.
107108
108109
Raises:
109-
ValueError: If creating the resource fails.
110+
ResourceError: If creating the resource fails.
110111
"""
111112
try:
112113
# Add context to params if needed
@@ -130,4 +131,4 @@ async def create_resource(
130131
fn=lambda: result, # Capture result in closure
131132
)
132133
except Exception as e:
133-
raise ValueError(f"Error creating resource from template: {e}")
134+
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,
@@ -346,6 +347,8 @@ async def _handle_read_resource(
346347
results = await self.read_resource(params.uri, context)
347348
except ResourceNotFoundError as err:
348349
raise MCPError(code=INVALID_PARAMS, message=str(err), data={"uri": str(params.uri)})
350+
except ResourceError as err:
351+
raise MCPError(code=INTERNAL_ERROR, message=str(err), data={"uri": str(params.uri)})
349352
contents: list[TextResourceContents | BlobResourceContents] = []
350353
for item in results:
351354
if isinstance(item.content, bytes):
@@ -449,10 +452,7 @@ async def read_resource(
449452
"""Read a resource by URI."""
450453
if context is None:
451454
context = Context(mcp_server=self)
452-
try:
453-
resource = await self._resource_manager.get_resource(uri, context)
454-
except ValueError:
455-
raise ResourceNotFoundError(f"Unknown resource: {uri}")
455+
resource = await self._resource_manager.get_resource(uri, context)
456456

457457
try:
458458
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

@@ -101,7 +102,7 @@ def greet(name: str) -> str:
101102
async def test_get_unknown_resource():
102103
"""Test getting a non-existent resource."""
103104
manager = ResourceManager()
104-
with pytest.raises(ValueError, match="Unknown resource"):
105+
with pytest.raises(ResourceNotFoundError, match="Unknown resource"):
105106
await manager.get_resource(AnyUrl("unknown://test"), Context())
106107

107108

tests/server/mcpserver/resources/test_resource_template.py

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

88
from mcp.server.mcpserver import Context, MCPServer
9+
from mcp.server.mcpserver.exceptions import ResourceError
910
from mcp.server.mcpserver.resources import FunctionResource, ResourceTemplate
1011
from mcp.types import Annotations
1112

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

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

9394
@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,
@@ -750,6 +751,21 @@ def failing_resource():
750751
with pytest.raises(MCPError, match="Error reading resource resource://failing"):
751752
await client.read_resource("resource://failing")
752753

754+
async def test_read_resource_template_error(self):
755+
"""Template-creation failure must surface as INTERNAL_ERROR, not INVALID_PARAMS (not-found)."""
756+
mcp = MCPServer()
757+
758+
@mcp.resource("resource://item/{item_id}")
759+
def get_item(item_id: str) -> str:
760+
raise RuntimeError("backend unavailable")
761+
762+
async with Client(mcp) as client:
763+
with pytest.raises(MCPError, match="Error creating resource from template") as exc_info:
764+
await client.read_resource("resource://item/42")
765+
766+
assert exc_info.value.error.code == INTERNAL_ERROR
767+
assert exc_info.value.error.code != INVALID_PARAMS
768+
753769
async def test_binary_resource(self):
754770
mcp = MCPServer()
755771

0 commit comments

Comments
 (0)