Repository navigation
McpError is not pickle-safe and fails to unpickle #2431
Description
Activity
Hi! I'd like to work on this.
I've reproduced the bug on mcp==1.27.0 with both pickle and cloudpickle. As you noted, the root cause is super().init(error.message) storing a plain string in Exception.args, while McpError.init expects ErrorData.
I agree that combining approaches 2 + 1 is the most robust fix:reduce to preserve the full ErrorData through pickle round-trips
Tolerant init that normalizes str → ErrorData as a safety netI also noticed UrlElicitationRequiredError has the same issue on v1.x — its init expects (elicitations, message) but inherits args that don't match. I'll fix both in the same PR.
Targeting v1.x per the contributing guide. Can I be assigned?- added a commit that references this issue
on Apr 14, 2026 bug is partially fixed on main (
MCPErrornow pickle-safe), butUrlElicitationRequiredErrorstill fails. bothMcpErrorandUrlElicitationRequiredErrorfail on v1.x.root cause: pickle reconstructs exceptions by calling
cls(*self.args). on main,UrlElicitationRequiredErrorstoresargs=(code_int, message_str, data_dict)viaMCPError.__init__, but its own__init__expects(elicitations: list[ElicitRequestURLParams], message)— the types don't match. on v1.x,McpErrorstoresargs=(message_str,)but its__init__expects anErrorDataobject.workaround: implement
__reduce__to control pickle reconstruction, or add a__new__-compatible signature.repro script (repro.py, runs on main)
import pickle import traceback from mcp.shared.exceptions import MCPError, UrlElicitationRequiredError from mcp.types import ElicitRequestURLParams, ErrorData print("=== MCPError pickle round-trip ===") try: original = MCPError(code=-32600, message="Authentication Required") payload = pickle.dumps(original) restored = pickle.loads(payload) print(f"restored message: {restored.message!r}") print("MCPError: PASS") except Exception as e: print(f"MCPError: FAIL — {type(e).__name__}: {e}") traceback.print_exc() print() print("=== UrlElicitationRequiredError pickle round-trip ===") try: elicitations = [ ElicitRequestURLParams( message="Authorization required", url="https://example.com/oauth/authorize", elicitation_id="auth-001", ) ] original = UrlElicitationRequiredError(elicitations) print(f"original: args={original.args!r}") payload = pickle.dumps(original) restored = pickle.loads(payload) print(f"restored elicitations: {restored.elicitations!r}") print("UrlElicitationRequiredError: PASS") except Exception as e: print(f"UrlElicitationRequiredError: FAIL — {type(e).__name__}: {e}") traceback.print_exc()
command + output (main, commit 3d7b311)
$ uv run python repro.py === MCPError pickle round-trip === original: args=(-32600, 'Authentication Required', None), error=ErrorData(...) restored message: 'Authentication Required' MCPError: PASS === UrlElicitationRequiredError pickle round-trip === original: args=(-32042, 'URL elicitation required', {'elicitations': [...]}) UrlElicitationRequiredError: FAIL — TypeError: UrlElicitationRequiredError.__init__() takes from 2 to 3 positional arguments but 4 were givenv1.x (commit 73d458b)
both fail:
McpError:AttributeError: 'str' object has no attribute 'message'UrlElicitationRequiredError:AttributeError: 'str' object has no attribute 'model_dump'
status: fixed on main for
MCPError, not fixed forUrlElicitationRequiredError. v1.x needs both fixed. fix is straightforward — add__reduce__toUrlElicitationRequiredErroron main; add__reduce__to bothMcpErrorandUrlElicitationRequiredErroron v1.x.suggested fix
# src/mcp/shared/exceptions.py elicitations = [ElicitRequestURLParams.model_validate(e) for e in raw_elicitations] return cls(elicitations, error.message) + + def __reduce__(self) -> tuple: + return (self.from_error, (self.error,))
test to verify: pickle round-trip of
UrlElicitationRequiredErrorrestores elicitations list and message intact (seetest_url_elicitation_required_error_pickle_roundtripintests/shared/test_exceptions.py).- addedbugSomething isn't workingSomething isn't workingready for workEnough information for someone to start working onEnough information for someone to start working onP2Moderate issues affecting some users, edge cases, potentially valuable featureModerate issues affecting some users, edge cases, potentially valuable featurefix proposedBot has a verified fix diff in the commentBot has a verified fix diff in the comment
on Apr 17, 2026 Opened a fix in #2471 -- adds a
__reduce__method toUrlElicitationRequiredErrorso it round-trips correctly throughpickle, and adds pickle round-trip tests for bothMCPErrorandUrlElicitationRequiredError.I'd like to pick this up , noticed PR #2439 had a working approach that stalled from inactivity. Happy to open a fresh PR building on that fix if that works.
Reliability note when MCP errors cross process boundaries:
If
McpErroris not pickle-safe, any fan-out that uses multiprocessing / job queues / Ray-style workers will convert a classified tool failure into an unpickling crash. Operators then see “worker died” instead of the original MCP error code/message — classic expected-vs-actual loss.Fail-closed patterns while fixed:
- Catch
McpErrorin the worker and re-raise a plain exception withcode,message, anddataas strings/dicts only. - Prefer JSON serialization for cross-process tool results; do not rely on pickle for control-plane errors.
- Test:
pickle.loads(pickle.dumps(err))in CI for every public error type the SDK exports.
Free diagnostic framing only — no product pitch.
- Catch
I reproduced this against current main: MCPError round-trips successfully, while UrlElicitationRequiredError fails during unpickling because its inherited exception args do not match its constructor signature.
I have a minimal v2-scoped fix that defines reduce to reconstruct through the existing from_error method, together with a focused pickle round-trip test preserving the complete ErrorData and elicitation payload.
Local validation: the regression test fails before the fix and passes afterward; the full suite reports 5712 passed with 100% coverage, and Ruff and Linux Pyright are clean.
I would be happy to submit the PR. Please assign #2431 to @guiyangyuan if this approach is welcome.
I was able to reproduce this issue with the current MCP Python SDK.
The failure appears to come from the mismatch between
McpError.__init__, which expects anErrorDatainstance, and the standard exception pickling path, which reconstructs the exception fromException.args.If this issue is suitable for assignment, I’d be happy to work on it. My initial approach would be to add a focused pickle round-trip regression test, then make
McpErrorreconstruct safely while preserving the originalErrorDatapayload. I’ll also verify compatibility with the existing constructor and error-handling paths, including the message and optional data fields.Please let me know if this direction fits the project’s expectations.
I would like to prepare the remaining v1.x backport for this issue. Current main already handles the MCPError case, while v1.x still needs pickle-safe reconstruction for McpError and UrlElicitationRequiredError; I will keep the change limited to the exception model and focused round-trip tests.
I have reviewed the repository guidance and will disclose AI assistance in the PR while personally reviewing the implementation and verification. Please assign this issue to me if this scope is still welcome.
Independent v1.x confirmation: on the current maintenance branch, standard-library
pickle.loads(pickle.dumps(...))still fails for bothMcpError(AttributeError: str has no attribute message) andUrlElicitationRequiredError(AttributeError: str has no attribute model_dump). The two focused regression tests in PR #3410 pass with explicit reconstruction from the originalErrorData, while the full v1.x suite passes with the proxy environment disabled.PR #3410 was auto-closed by the repository readiness workflow because this issue is not assigned to the contributor; this is an assignment gate, not an implementation failure. I will leave the branch available and wait for maintainer assignment rather than opening another PR.
Initial Checks
Description
Summary
mcp.shared.exceptions.McpErrordoes not survive a normalcloudpickle.dumps()/cloudpickle.loads()round-trip.The failure appears to come from
McpError.__init__expecting anErrorDataobject, while exception unpickling reconstructs it with a plain string fromException.args.This is surfacing for us through background task execution, but the bug reproduces without Docket/FastMCP task machinery.
Actual behavior
Unpickling fails with:
Traceback points at
McpError.__init__:Expected behavior
McpError(ErrorData(...))should round-trip through pickle/cloudpickle without crashing.At minimum, this should work:
McpErrorMcpErrorerrorpayload, or at least degrade safely without raising during unpickleSuspected root cause
McpErrorstoreserror.messageinException.argsviasuper().__init__(error.message).On unpickle, exception reconstruction uses
args, soMcpErroris effectively reconstructed as:But
McpError.__init__assumeserroris always anErrorData, so it does:which crashes for
str.Suggested fix
McpErrorlikely needs to be pickle-safe by design. Any of these would probably fix it:__init__accept bothErrorDataandstr, normalizingstrinto anErrorData.__reduce__so pickle reconstructs using the fullErrorData.A robust version would probably do both
__reduce__and tolerant initialization.Notes
This bug is easy to misattribute to
cloudpickleor task runners, but the reproducer above shows it is local toMcpErroritself.Example Code
Python & MCP Python SDK