Skip to content

Commit d33fbc1

Browse files
[v1.x] fix: make MCP errors pickle-safe
Co-authored-by: openhands <openhands@all-hands.dev> Signed-off-by: King Star <mcxin.y@gmail.com>
1 parent 5ebdfed commit d33fbc1

2 files changed

Lines changed: 42 additions & 0 deletions

File tree

src/mcp/shared/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ def __init__(self, error: ErrorData):
1717
super().__init__(error.message)
1818
self.error = error
1919

20+
def __reduce__(self) -> tuple[type[McpError], tuple[ErrorData]]:
21+
"""Reconstruct the exception from its complete wire error payload."""
22+
return type(self), (self.error,)
23+
2024

2125
class UrlElicitationRequiredError(McpError):
2226
"""
@@ -69,3 +73,7 @@ def from_error(cls, error: ErrorData) -> UrlElicitationRequiredError:
6973
raw_elicitations = cast(list[dict[str, Any]], data.get("elicitations", []))
7074
elicitations = [ElicitRequestURLParams.model_validate(e) for e in raw_elicitations]
7175
return cls(elicitations, error.message)
76+
77+
def __reduce__(self) -> tuple[Any, tuple[ErrorData]]:
78+
"""Reconstruct the specialized exception from its wire error payload."""
79+
return self.from_error, (self.error,)

tests/shared/test_exceptions.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
"""Tests for MCP exception classes."""
22

3+
import pickle
4+
35
import pytest
46

57
from mcp.shared.exceptions import McpError, UrlElicitationRequiredError
68
from mcp.types import URL_ELICITATION_REQUIRED, ElicitRequestURLParams, ErrorData
79

810

11+
def test_mcp_error_pickle_roundtrip() -> None:
12+
"""Test that McpError preserves its wire payload through pickle."""
13+
original = McpError(ErrorData(code=-32600, message="Authentication Required", data={"retry": True}))
14+
15+
reconstructed = pickle.loads(pickle.dumps(original))
16+
17+
assert isinstance(reconstructed, McpError)
18+
assert reconstructed.error == original.error
19+
assert str(reconstructed) == str(original)
20+
21+
922
class TestUrlElicitationRequiredError:
1023
"""Tests for UrlElicitationRequiredError exception class."""
1124

@@ -157,3 +170,24 @@ def test_exception_message(self) -> None:
157170

158171
# The exception's string representation should match the message
159172
assert str(error) == "URL elicitation required"
173+
174+
def test_pickle_roundtrip(self) -> None:
175+
"""Test that URL elicitation errors preserve their payload through pickle."""
176+
original = UrlElicitationRequiredError(
177+
[
178+
ElicitRequestURLParams(
179+
mode="url",
180+
message="Auth required",
181+
url="https://example.com/auth",
182+
elicitationId="test-123",
183+
)
184+
],
185+
message="Custom message",
186+
)
187+
188+
reconstructed = pickle.loads(pickle.dumps(original))
189+
190+
assert isinstance(reconstructed, UrlElicitationRequiredError)
191+
assert reconstructed.error == original.error
192+
assert reconstructed.elicitations == original.elicitations
193+
assert str(reconstructed) == "Custom message"

0 commit comments

Comments
 (0)