Skip to content

Commit 3000511

Browse files
committed
Add MCPServerError and use it appropriately upon upstream resource errors.
1 parent 72d66ac commit 3000511

File tree

3 files changed

+72
-4
lines changed

3 files changed

+72
-4
lines changed

pydantic_ai_slim/pydantic_ai/exceptions.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
'ModelHTTPError',
2626
'MCPError',
2727
'ServerCapabilitiesError',
28+
'MCPServerError',
2829
'FallbackExceptionGroup',
2930
)
3031

@@ -178,6 +179,44 @@ class ServerCapabilitiesError(MCPError):
178179
"""Raised when attempting to access server capabilities that aren't present."""
179180

180181

182+
class MCPServerError(MCPError):
183+
"""Raised when an MCP server returns an error response.
184+
185+
This exception wraps error responses from MCP servers, following the ErrorData schema
186+
from the MCP specification.
187+
"""
188+
189+
code: int
190+
"""The error code returned by the server."""
191+
192+
data: Any | None
193+
"""Additional information about the error, if provided by the server."""
194+
195+
def __init__(self, message: str, code: int, data: Any | None = None):
196+
super().__init__(message)
197+
self.code = code
198+
self.data = data
199+
200+
@classmethod
201+
def from_mcp_sdk_error(cls, error: Any) -> MCPServerError:
202+
"""Create an MCPServerError from an MCP SDK McpError.
203+
204+
Args:
205+
error: An McpError from the MCP SDK.
206+
207+
Returns:
208+
A new MCPServerError instance with the error data.
209+
"""
210+
# Extract error data from the McpError.error attribute
211+
error_data = error.error
212+
return cls(message=error_data.message, code=error_data.code, data=error_data.data)
213+
214+
def __str__(self) -> str:
215+
if self.data:
216+
return f'{self.message} (code: {self.code}, data: {self.data})'
217+
return f'{self.message} (code: {self.code})'
218+
219+
181220
class FallbackExceptionGroup(ExceptionGroup):
182221
"""A group of exceptions that can be raised when all fallback models fail."""
183222

pydantic_ai_slim/pydantic_ai/mcp.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -322,23 +322,31 @@ async def list_resources(self) -> list[_mcp.Resource]:
322322
323323
Raises:
324324
ServerCapabilitiesError: If the server does not support resources.
325+
MCPServerError: If the server returns an error.
325326
"""
326327
async with self: # Ensure server is running
327328
if not self.capabilities.resources:
328329
raise exceptions.ServerCapabilitiesError('Server does not support resources capability')
329-
result = await self._client.list_resources()
330+
try:
331+
result = await self._client.list_resources()
332+
except McpError as e:
333+
raise exceptions.MCPServerError.from_mcp_sdk_error(e) from e
330334
return [_mcp.map_from_mcp_resource(r) for r in result.resources]
331335

332336
async def list_resource_templates(self) -> list[_mcp.ResourceTemplate]:
333337
"""Retrieve resource templates that are currently present on the server.
334338
335339
Raises:
336340
ServerCapabilitiesError: If the server does not support resources.
341+
MCPServerError: If the server returns an error.
337342
"""
338343
async with self: # Ensure server is running
339344
if not self.capabilities.resources:
340345
raise exceptions.ServerCapabilitiesError('Server does not support resources capability')
341-
result = await self._client.list_resource_templates()
346+
try:
347+
result = await self._client.list_resource_templates()
348+
except McpError as e:
349+
raise exceptions.MCPServerError.from_mcp_sdk_error(e) from e
342350
return [_mcp.map_from_mcp_resource_template(t) for t in result.resourceTemplates]
343351

344352
@overload
@@ -363,12 +371,16 @@ async def read_resource(
363371
364372
Raises:
365373
ServerCapabilitiesError: If the server does not support resources.
374+
MCPServerError: If the server returns an error (e.g., resource not found).
366375
"""
367376
resource_uri = uri if isinstance(uri, str) else uri.uri
368377
async with self: # Ensure server is running
369378
if not self.capabilities.resources:
370379
raise exceptions.ServerCapabilitiesError('Server does not support resources capability')
371-
result = await self._client.read_resource(AnyUrl(resource_uri))
380+
try:
381+
result = await self._client.read_resource(AnyUrl(resource_uri))
382+
except McpError as e:
383+
raise exceptions.MCPServerError.from_mcp_sdk_error(e) from e
372384
return (
373385
self._get_content(result.contents[0])
374386
if len(result.contents) == 1

tests/test_mcp.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@
2525
)
2626
from pydantic_ai._mcp import Resource, ServerCapabilities
2727
from pydantic_ai.agent import Agent
28-
from pydantic_ai.exceptions import ModelRetry, ServerCapabilitiesError, UnexpectedModelBehavior, UserError
28+
from pydantic_ai.exceptions import (
29+
MCPServerError,
30+
ModelRetry,
31+
ServerCapabilitiesError,
32+
UnexpectedModelBehavior,
33+
UserError,
34+
)
2935
from pydantic_ai.mcp import MCPServerStreamableHTTP, load_mcp_servers
3036
from pydantic_ai.models import Model
3137
from pydantic_ai.models.test import TestModel
@@ -1539,6 +1545,17 @@ async def test_read_resource_template(run_context: RunContext[int]):
15391545
assert content == snapshot('Hello, Alice!')
15401546

15411547

1548+
async def test_read_resource_not_found(mcp_server: MCPServerStdio) -> None:
1549+
"""Test that read_resource raises MCPServerError for non-existent resources."""
1550+
async with mcp_server:
1551+
with pytest.raises(MCPServerError, match='Unknown resource: resource://does_not_exist') as exc_info:
1552+
await mcp_server.read_resource('resource://does_not_exist')
1553+
1554+
# Verify the exception has the expected attributes
1555+
assert exc_info.value.code == 0
1556+
assert exc_info.value.message == 'Unknown resource: resource://does_not_exist'
1557+
1558+
15421559
def test_load_mcp_servers(tmp_path: Path):
15431560
config = tmp_path / 'mcp.json'
15441561

0 commit comments

Comments
 (0)