Skip to content

Commit dfb9119

Browse files
committed
fix(shared): make MCPError subclasses pickle-safe
Signed-off-by: ulofiai <monsterking@tutamail.com>
1 parent fe31e62 commit dfb9119

2 files changed

Lines changed: 26 additions & 5 deletions

File tree

src/mcp/shared/exceptions.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ def from_error_data(cls, error: ErrorData) -> MCPError:
5151
def __str__(self) -> str:
5252
return self.message
5353

54+
def __reduce__(self) -> tuple[Any, tuple[type[MCPError], tuple[Any, ...]], dict[str, Any]]:
55+
return (_restore_mcp_error, (type(self), self.args), self.__dict__)
56+
57+
58+
def _restore_mcp_error(error_type: type[MCPError], args: tuple[Any, ...]) -> MCPError:
59+
"""Reconstruct an MCPError without invoking a subclass constructor."""
60+
restored = MCPError.__new__(error_type)
61+
Exception.__init__(restored, *args)
62+
return restored
63+
5464

5565
class NoBackChannelError(MCPError):
5666
"""Raised when a server-initiated request has no channel that can deliver it.
@@ -107,10 +117,6 @@ def elicitations(self) -> list[ElicitRequestURLParams]:
107117
"""The list of URL elicitations required before the request can proceed."""
108118
return self._elicitations
109119

110-
def __reduce__(self) -> tuple[type, tuple[list[ElicitRequestURLParams], str]]:
111-
"""Support pickling by reconstructing with the original constructor signature."""
112-
return (self.__class__, (self._elicitations, self.message))
113-
114120
@classmethod
115121
def from_error(cls, error: ErrorData) -> UrlElicitationRequiredError:
116122
"""Reconstruct from an ErrorData received over the wire."""

tests/shared/test_exceptions.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66
from mcp_types import URL_ELICITATION_REQUIRED, ElicitRequestURLParams, ErrorData, JSONRPCError
77

8-
from mcp.shared.exceptions import MCPError, UrlElicitationRequiredError
8+
from mcp.shared.exceptions import MCPError, NoBackChannelError, UrlElicitationRequiredError
99

1010

1111
def test_url_elicitation_required_error_create_with_single_elicitation() -> None:
@@ -184,7 +184,21 @@ def test_mcp_error_pickle_roundtrip() -> None:
184184
restored = pickle.loads(pickle.dumps(original))
185185

186186
assert type(restored) is MCPError
187+
assert restored.args == original.args
187188
assert restored.error == original.error
189+
assert str(restored) == str(original)
190+
191+
192+
def test_no_back_channel_error_pickle_roundtrip_preserves_method() -> None:
193+
"""NoBackChannelError preserves its method and structured payload when pickled."""
194+
original = NoBackChannelError("sampling/createMessage")
195+
196+
restored = pickle.loads(pickle.dumps(original))
197+
198+
assert isinstance(restored, NoBackChannelError)
199+
assert restored.args == original.args
200+
assert restored.error == original.error
201+
assert restored.method == original.method
188202

189203

190204
def test_url_elicitation_required_error_pickle_roundtrip() -> None:
@@ -208,5 +222,6 @@ def test_url_elicitation_required_error_pickle_roundtrip() -> None:
208222
restored = pickle.loads(pickle.dumps(original))
209223

210224
assert type(restored) is UrlElicitationRequiredError
225+
assert restored.args == original.args
211226
assert restored.error == original.error
212227
assert restored.elicitations == original.elicitations

0 commit comments

Comments
 (0)