Skip to content

Commit 5cf4cd2

Browse files
committed
fix(shared): preserve MCPError payload on pickle
1 parent 3d7b311 commit 5cf4cd2

File tree

2 files changed

+57
-0
lines changed

2 files changed

+57
-0
lines changed

src/mcp/shared/exceptions.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@
55
from mcp.types import URL_ELICITATION_REQUIRED, ElicitRequestURLParams, ErrorData, JSONRPCError
66

77

8+
def _restore_mcp_error(exc_type: type[MCPError], error: ErrorData) -> MCPError:
9+
"""Reconstruct a pickled MCPError or subclass from ErrorData."""
10+
if exc_type is UrlElicitationRequiredError:
11+
return exc_type.from_error(error)
12+
13+
if hasattr(exc_type, "from_error_data"):
14+
return exc_type.from_error_data(error)
15+
16+
restored = exc_type.__new__(exc_type)
17+
Exception.__init__(restored, error.code, error.message, error.data)
18+
restored.error = error
19+
return restored
20+
21+
822
class MCPError(Exception):
923
"""Exception type raised when an error arrives over an MCP connection."""
1024

@@ -40,6 +54,9 @@ def from_error_data(cls, error: ErrorData) -> MCPError:
4054
def __str__(self) -> str:
4155
return self.message
4256

57+
def __reduce__(self) -> tuple[Any, tuple[type[MCPError], ErrorData]]:
58+
return (_restore_mcp_error, (type(self), self.error))
59+
4360

4461
class StatelessModeNotSupported(RuntimeError):
4562
"""Raised when attempting to use a method that is not supported in stateless mode.

tests/shared/test_exceptions.py

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

3+
import pickle
4+
35
import pytest
46

57
from mcp.shared.exceptions import MCPError, UrlElicitationRequiredError
@@ -162,3 +164,41 @@ def test_url_elicitation_required_error_exception_message() -> None:
162164

163165
# The exception's string representation should match the message
164166
assert str(error) == "URL elicitation required"
167+
168+
169+
def test_mcp_error_pickle_roundtrip() -> None:
170+
"""Test that MCPError survives a normal pickle round-trip."""
171+
original = MCPError(
172+
code=-32600,
173+
message="Authentication Required",
174+
data={"scope": "files.read"},
175+
)
176+
177+
restored = pickle.loads(pickle.dumps(original))
178+
179+
assert isinstance(restored, MCPError)
180+
assert restored.code == -32600
181+
assert restored.message == "Authentication Required"
182+
assert restored.data == {"scope": "files.read"}
183+
assert str(restored) == "Authentication Required"
184+
185+
186+
def test_url_elicitation_required_error_pickle_roundtrip() -> None:
187+
"""Test that specialized MCPError subclasses survive pickle too."""
188+
original = UrlElicitationRequiredError(
189+
[
190+
ElicitRequestURLParams(
191+
mode="url",
192+
message="Auth required",
193+
url="https://example.com/auth",
194+
elicitation_id="test-123",
195+
)
196+
]
197+
)
198+
199+
restored = pickle.loads(pickle.dumps(original))
200+
201+
assert isinstance(restored, UrlElicitationRequiredError)
202+
assert restored.elicitations[0].elicitation_id == "test-123"
203+
assert restored.elicitations[0].url == "https://example.com/auth"
204+
assert restored.message == "URL elicitation required"

0 commit comments

Comments
 (0)