Skip to content

Commit 389fe64

Browse files
committed
Update MCP resource methods to raise upstream server exceptions rather than try to cast to None | []
1 parent 9d31dd0 commit 389fe64

File tree

2 files changed

+11
-47
lines changed

2 files changed

+11
-47
lines changed

pydantic_ai_slim/pydantic_ai/mcp.py

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ def capabilities(self) -> ServerCapabilities:
347347
f'The `{self.__class__.__name__}.capabilities` is only instantiated after initialization.'
348348
)
349349
return self._server_capabilities
350-
350+
351351
@property
352352
def instructions(self) -> str | None:
353353
"""Access the instructions sent by the MCP server during initialization."""
@@ -504,18 +504,16 @@ async def list_resource_templates(self) -> list[ResourceTemplate]:
504504
return [_mcp.map_from_mcp_resource_template(t) for t in result.resourceTemplates]
505505

506506
@overload
507-
async def read_resource(
508-
self, uri: str
509-
) -> str | messages.BinaryContent | list[str | messages.BinaryContent] | None: ...
507+
async def read_resource(self, uri: str) -> str | messages.BinaryContent | list[str | messages.BinaryContent]: ...
510508

511509
@overload
512510
async def read_resource(
513511
self, uri: Resource
514-
) -> str | messages.BinaryContent | list[str | messages.BinaryContent] | None: ...
512+
) -> str | messages.BinaryContent | list[str | messages.BinaryContent]: ...
515513

516514
async def read_resource(
517515
self, uri: str | Resource
518-
) -> str | messages.BinaryContent | list[str | messages.BinaryContent] | None:
516+
) -> str | messages.BinaryContent | list[str | messages.BinaryContent]:
519517
"""Read the contents of a specific resource by URI.
520518
521519
Args:
@@ -524,26 +522,17 @@ async def read_resource(
524522
Returns:
525523
The resource contents. If the resource has a single content item, returns that item directly.
526524
If the resource has multiple content items, returns a list of items.
527-
Returns `None` if the server does not support resources or the resource is not found.
528525
529526
Raises:
530-
MCPError: If the server returns an error other than resource not found.
527+
MCPError: If the server returns an error.
531528
"""
532529
resource_uri = uri if isinstance(uri, str) else uri.uri
533530
async with self: # Ensure server is running
534-
if not self.capabilities.resources:
535-
return None
536531
try:
537532
result = await self._client.read_resource(AnyUrl(resource_uri))
538533
except mcp_exceptions.McpError as e:
539-
# As per https://modelcontextprotocol.io/specification/2025-06-18/server/resources#error-handling
540-
if e.error.code == -32002:
541-
return None
542534
raise MCPError.from_mcp_sdk_error(e) from e
543535

544-
if not result.contents:
545-
return None
546-
547536
return (
548537
self._get_content(result.contents[0])
549538
if len(result.contents) == 1
@@ -646,12 +635,8 @@ async def _map_tool_result_part(
646635
resource = part.resource
647636
return self._get_content(resource)
648637
elif isinstance(part, mcp_types.ResourceLink):
649-
result = await self.read_resource(str(part.uri))
650-
# Rather than hide an invalid resource link, we raise an error so it's consistent with any
651-
# other error that could happen during resource reading.
652-
if result is None:
653-
raise MCPError(message=f'Invalid ResourceLink {part.uri} returned by tool', code=-32002)
654-
return result
638+
# read_resource will raise MCPError if the resource is not found or has any other error
639+
return await self.read_resource(str(part.uri))
655640
else:
656641
assert_never(part)
657642

tests/test_mcp.py

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,25 +1607,6 @@ async def test_read_resource_template(run_context: RunContext[int]):
16071607
assert content == snapshot('Hello, Alice!')
16081608

16091609

1610-
async def test_read_resource_not_found(mcp_server: MCPServerStdio) -> None:
1611-
"""Test that read_resource returns None for MCP spec error code -32002 (resource not found).
1612-
1613-
As per https://modelcontextprotocol.io/specification/2025-06-18/server/resources#error-handling
1614-
1615-
Note: We mock this because FastMCP uses error code 0 instead of -32002, which is non-standard.
1616-
"""
1617-
mcp_error = McpError(error=ErrorData(code=-32002, message='Resource not found'))
1618-
1619-
async with mcp_server:
1620-
with patch.object(
1621-
mcp_server._client, # pyright: ignore[reportPrivateUsage]
1622-
'read_resource',
1623-
new=AsyncMock(side_effect=mcp_error),
1624-
):
1625-
result = await mcp_server.read_resource('resource://missing')
1626-
assert result is None
1627-
1628-
16291610
async def test_read_resource_error(mcp_server: MCPServerStdio) -> None:
16301611
"""Test that read_resource converts McpError to MCPError for generic errors."""
16311612
mcp_error = McpError(
@@ -1648,7 +1629,7 @@ async def test_read_resource_error(mcp_server: MCPServerStdio) -> None:
16481629

16491630

16501631
async def test_read_resource_empty_contents(mcp_server: MCPServerStdio) -> None:
1651-
"""Test that read_resource returns None when server returns empty contents."""
1632+
"""Test that read_resource returns empty list when server returns empty contents."""
16521633
from mcp.types import ReadResourceResult
16531634

16541635
# Mock a result with empty contents
@@ -1661,7 +1642,7 @@ async def test_read_resource_empty_contents(mcp_server: MCPServerStdio) -> None:
16611642
new=AsyncMock(return_value=empty_result),
16621643
):
16631644
result = await mcp_server.read_resource('resource://empty')
1664-
assert result is None
1645+
assert result == []
16651646

16661647

16671648
async def test_list_resources_error(mcp_server: MCPServerStdio) -> None:
@@ -1961,7 +1942,7 @@ async def test_capabilities(mcp_server: MCPServerStdio) -> None:
19611942

19621943

19631944
async def test_resource_methods_without_capability(mcp_server: MCPServerStdio) -> None:
1964-
"""Test that resource methods return empty values when resources capability is not available."""
1945+
"""Test that resource list methods return empty values when resources capability is not available."""
19651946
async with mcp_server:
19661947
# Mock the capabilities to not support resources
19671948
mock_capabilities = ServerCapabilities(resources=False)
@@ -1974,9 +1955,7 @@ async def test_resource_methods_without_capability(mcp_server: MCPServerStdio) -
19741955
result = await mcp_server.list_resource_templates()
19751956
assert result == []
19761957

1977-
# read_resource should return None
1978-
result = await mcp_server.read_resource('resource://test')
1979-
assert result is None
1958+
19801959
async def test_instructions(mcp_server: MCPServerStdio) -> None:
19811960
with pytest.raises(
19821961
AttributeError, match='The `MCPServerStdio.instructions` is only available after initialization.'

0 commit comments

Comments
 (0)