Skip to content

Commit 9804f18

Browse files
committed
Cleanup + relocate MCP errors.
1 parent 28cf761 commit 9804f18

File tree

3 files changed

+65
-70
lines changed

3 files changed

+65
-70
lines changed

pydantic_ai_slim/pydantic_ai/exceptions.py

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@
2323
'UnexpectedModelBehavior',
2424
'UsageLimitExceeded',
2525
'ModelHTTPError',
26-
'MCPError',
27-
'MCPServerError',
2826
'IncompleteToolCall',
2927
'FallbackExceptionGroup',
3028
)
@@ -161,58 +159,6 @@ def __init__(self, status_code: int, model_name: str, body: object | None = None
161159
super().__init__(message)
162160

163161

164-
class MCPError(RuntimeError):
165-
"""Base class for errors occurring during interaction with an MCP server."""
166-
167-
message: str
168-
"""The error message."""
169-
170-
def __init__(self, message: str):
171-
self.message = message
172-
super().__init__(message)
173-
174-
def __str__(self) -> str:
175-
return self.message
176-
177-
178-
class MCPServerError(MCPError):
179-
"""Raised when an MCP server returns an error response.
180-
181-
This exception wraps error responses from MCP servers, following the ErrorData schema
182-
from the MCP specification.
183-
"""
184-
185-
code: int
186-
"""The error code returned by the server."""
187-
188-
data: Any | None
189-
"""Additional information about the error, if provided by the server."""
190-
191-
def __init__(self, message: str, code: int, data: Any | None = None):
192-
super().__init__(message)
193-
self.code = code
194-
self.data = data
195-
196-
@classmethod
197-
def from_mcp_sdk_error(cls, error: Any) -> MCPServerError:
198-
"""Create an MCPServerError from an MCP SDK McpError.
199-
200-
Args:
201-
error: An McpError from the MCP SDK.
202-
203-
Returns:
204-
A new MCPServerError instance with the error data.
205-
"""
206-
# Extract error data from the McpError.error attribute
207-
error_data = error.error
208-
return cls(message=error_data.message, code=error_data.code, data=error_data.data)
209-
210-
def __str__(self) -> str:
211-
if self.data:
212-
return f'{self.message} (code: {self.code}, data: {self.data})'
213-
return f'{self.message} (code: {self.code})'
214-
215-
216162
class FallbackExceptionGroup(ExceptionGroup[Any]):
217163
"""A group of exceptions that can be raised when all fallback models fail."""
218164

pydantic_ai_slim/pydantic_ai/mcp.py

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,59 @@
4242

4343
# after mcp imports so any import error maps to this file, not _mcp.py
4444
from . import _mcp, _utils, exceptions, messages, models
45-
from .exceptions import MCPServerError
4645

47-
__all__ = 'MCPServer', 'MCPServerStdio', 'MCPServerHTTP', 'MCPServerSSE', 'MCPServerStreamableHTTP', 'load_mcp_servers'
46+
__all__ = (
47+
'MCPServer',
48+
'MCPServerStdio',
49+
'MCPServerHTTP',
50+
'MCPServerSSE',
51+
'MCPServerStreamableHTTP',
52+
'load_mcp_servers',
53+
'MCPError',
54+
)
55+
56+
57+
class MCPError(RuntimeError):
58+
"""Raised when an MCP server returns an error response.
59+
60+
This exception wraps error responses from MCP servers, following the ErrorData schema
61+
from the MCP specification.
62+
"""
63+
64+
message: str
65+
"""The error message."""
66+
67+
code: int
68+
"""The error code returned by the server."""
69+
70+
data: Any | None
71+
"""Additional information about the error, if provided by the server."""
72+
73+
def __init__(self, message: str, code: int, data: Any | None = None):
74+
self.message = message
75+
self.code = code
76+
self.data = data
77+
super().__init__(message)
78+
79+
@classmethod
80+
def from_mcp_sdk_error(cls, error: Any) -> MCPError:
81+
"""Create an MCPError from an MCP SDK McpError.
82+
83+
Args:
84+
error: An McpError from the MCP SDK.
85+
86+
Returns:
87+
A new MCPError instance with the error data.
88+
"""
89+
# Extract error data from the McpError.error attribute
90+
error_data = error.error
91+
return cls(message=error_data.message, code=error_data.code, data=error_data.data)
92+
93+
def __str__(self) -> str:
94+
if self.data:
95+
return f'{self.message} (code: {self.code}, data: {self.data})'
96+
return f'{self.message} (code: {self.code})'
97+
4898

4999
TOOL_SCHEMA_VALIDATOR = pydantic_core.SchemaValidator(
50100
schema=pydantic_core.core_schema.dict_schema(
@@ -322,30 +372,30 @@ async def list_resources(self) -> list[_mcp.Resource]:
322372
- We also don't subscribe to resource changes to avoid complexity.
323373
324374
Raises:
325-
MCPServerError: If the server returns an error.
375+
MCPError: If the server returns an error.
326376
"""
327377
async with self: # Ensure server is running
328378
if not self.capabilities.resources:
329379
return []
330380
try:
331381
result = await self._client.list_resources()
332382
except mcp_exceptions.McpError as e:
333-
raise MCPServerError.from_mcp_sdk_error(e) from e
383+
raise MCPError.from_mcp_sdk_error(e) from e
334384
return [_mcp.map_from_mcp_resource(r) for r in result.resources]
335385

336386
async def list_resource_templates(self) -> list[_mcp.ResourceTemplate]:
337387
"""Retrieve resource templates that are currently present on the server.
338388
339389
Raises:
340-
MCPServerError: If the server returns an error.
390+
MCPError: If the server returns an error.
341391
"""
342392
async with self: # Ensure server is running
343393
if not self.capabilities.resources:
344394
return []
345395
try:
346396
result = await self._client.list_resource_templates()
347397
except mcp_exceptions.McpError as e:
348-
raise MCPServerError.from_mcp_sdk_error(e) from e
398+
raise MCPError.from_mcp_sdk_error(e) from e
349399
return [_mcp.map_from_mcp_resource_template(t) for t in result.resourceTemplates]
350400

351401
@overload
@@ -372,7 +422,7 @@ async def read_resource(
372422
Returns `None` if the server does not support resources or the resource is not found.
373423
374424
Raises:
375-
MCPServerError: If the server returns an error other than resource not found.
425+
MCPError: If the server returns an error other than resource not found.
376426
"""
377427
resource_uri = uri if isinstance(uri, str) else uri.uri
378428
async with self: # Ensure server is running
@@ -384,7 +434,7 @@ async def read_resource(
384434
# As per https://modelcontextprotocol.io/specification/2025-06-18/server/resources#error-handling
385435
if e.error.code == -32002:
386436
return None
387-
raise MCPServerError.from_mcp_sdk_error(e) from e
437+
raise MCPError.from_mcp_sdk_error(e) from e
388438

389439
if not result.contents:
390440
return None

tests/test_mcp.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,11 @@
2626
from pydantic_ai._mcp import Resource, ServerCapabilities
2727
from pydantic_ai.agent import Agent
2828
from pydantic_ai.exceptions import (
29-
MCPServerError,
3029
ModelRetry,
3130
UnexpectedModelBehavior,
3231
UserError,
3332
)
34-
from pydantic_ai.mcp import MCPServerStreamableHTTP, load_mcp_servers
33+
from pydantic_ai.mcp import MCPError, MCPServerStreamableHTTP, load_mcp_servers
3534
from pydantic_ai.models import Model
3635
from pydantic_ai.models.test import TestModel
3736
from pydantic_ai.tools import RunContext
@@ -1545,10 +1544,10 @@ async def test_read_resource_template(run_context: RunContext[int]):
15451544

15461545

15471546
async def test_read_resource_not_found(mcp_server: MCPServerStdio) -> None:
1548-
"""Test that read_resource raises MCPServerError for non-existent resources with non-standard error codes."""
1547+
"""Test that read_resource raises MCPError for non-existent resources with non-standard error codes."""
15491548
async with mcp_server:
15501549
# FastMCP uses error code 0 instead of -32002, so it should raise
1551-
with pytest.raises(MCPServerError, match='Unknown resource: resource://does_not_exist') as exc_info:
1550+
with pytest.raises(MCPError, match='Unknown resource: resource://does_not_exist') as exc_info:
15521551
await mcp_server.read_resource('resource://does_not_exist')
15531552

15541553
# Verify the exception has the expected attributes
@@ -1589,7 +1588,7 @@ async def test_read_resource_empty_contents(mcp_server: MCPServerStdio) -> None:
15891588

15901589

15911590
async def test_list_resources_error(mcp_server: MCPServerStdio) -> None:
1592-
"""Test that list_resources converts McpError to MCPServerError."""
1591+
"""Test that list_resources converts McpError to MCPError."""
15931592
mcp_error = McpError(
15941593
error=ErrorData(code=-32603, message='Failed to list resources', data={'details': 'server overloaded'})
15951594
)
@@ -1600,7 +1599,7 @@ async def test_list_resources_error(mcp_server: MCPServerStdio) -> None:
16001599
'list_resources',
16011600
new=AsyncMock(side_effect=mcp_error),
16021601
):
1603-
with pytest.raises(MCPServerError, match='Failed to list resources') as exc_info:
1602+
with pytest.raises(MCPError, match='Failed to list resources') as exc_info:
16041603
await mcp_server.list_resources()
16051604

16061605
# Verify the exception has the expected attributes
@@ -1613,7 +1612,7 @@ async def test_list_resources_error(mcp_server: MCPServerStdio) -> None:
16131612

16141613

16151614
async def test_list_resource_templates_error(mcp_server: MCPServerStdio) -> None:
1616-
"""Test that list_resource_templates converts McpError to MCPServerError."""
1615+
"""Test that list_resource_templates converts McpError to MCPError."""
16171616
mcp_error = McpError(error=ErrorData(code=-32001, message='Service unavailable'))
16181617

16191618
async with mcp_server:
@@ -1622,7 +1621,7 @@ async def test_list_resource_templates_error(mcp_server: MCPServerStdio) -> None
16221621
'list_resource_templates',
16231622
new=AsyncMock(side_effect=mcp_error),
16241623
):
1625-
with pytest.raises(MCPServerError, match='Service unavailable') as exc_info:
1624+
with pytest.raises(MCPError, match='Service unavailable') as exc_info:
16261625
await mcp_server.list_resource_templates()
16271626

16281627
# Verify the exception has the expected attributes

0 commit comments

Comments
 (0)