Repository navigation
McpError is not pickle-safe and fails to unpickle #2431
Description
Initial Checks
- I confirm that I'm using the latest version of MCP Python SDK
- I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
Description
Summary
mcp.shared.exceptions.McpError does not survive a normal cloudpickle.dumps() / cloudpickle.loads() round-trip.
The failure appears to come from McpError.__init__ expecting an ErrorData object, while exception unpickling reconstructs it with a plain string from Exception.args.
This is surfacing for us through background task execution, but the bug reproduces without Docket/FastMCP task machinery.
Actual behavior
Unpickling fails with:
AttributeError: 'str' object has no attribute 'message'
Traceback points at McpError.__init__:
class McpError(Exception):
error: ErrorData
def __init__(self, error: ErrorData):
super().__init__(error.message)
self.error = errorExpected behavior
McpError(ErrorData(...)) should round-trip through pickle/cloudpickle without crashing.
At minimum, this should work:
- serialize
McpError - deserialize
McpError - preserve the message
- preserve the
errorpayload, or at least degrade safely without raising during unpickle
Suspected root cause
McpError stores error.message in Exception.args via super().__init__(error.message).
On unpickle, exception reconstruction uses args, so McpError is effectively reconstructed as:
McpError("Authentication Required")But McpError.__init__ assumes error is always an ErrorData, so it does:
error.messagewhich crashes for str.
Suggested fix
McpError likely needs to be pickle-safe by design. Any of these would probably fix it:
- Make
__init__accept bothErrorDataandstr, normalizingstrinto anErrorData. - Implement
__reduce__so pickle reconstructs using the fullErrorData. - Ensure constructor args and exception state are aligned with standard exception pickling behavior.
A robust version would probably do both __reduce__ and tolerant initialization.
Notes
This bug is easy to misattribute to cloudpickle or task runners, but the reproducer above shows it is local to McpError itself.
Example Code
from importlib.metadata import version
import cloudpickle
from mcp.shared.exceptions import McpError
from mcp.types import ErrorData
print("Versions:")
print(f" mcp={version('mcp')}")
print(f" cloudpickle={version('cloudpickle')}")
original = McpError(ErrorData(code=-32600, message="Authentication Required"))
print("\nOriginal exception:")
print(f" type={type(original).__name__}")
print(f" str={str(original)!r}")
print(f" error_type={type(original.error).__name__}")
print(f" error_message={original.error.message!r}")
payload = cloudpickle.dumps(original)
print("\nUnpickling:")
restored = cloudpickle.loads(payload)
print(f" restored_type={type(restored).__name__}")
print(f" restored_args={restored.args!r}")
print(f" restored_error={getattr(restored, 'error', None)!r}")Python & MCP Python SDK
- `mcp==1.26.0`
- `fastmcp==3.2.3`
- `cloudpickle==3.1.2`
- Python 3.13
Activity
faridun-m commentedon Apr 13, 2026
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 net
I 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?
fix: make UrlElicitationRequiredError pickle-safe
Christian-Sidak commentedon Apr 19, 2026
Opened a fix in #2471 -- adds a
__reduce__method toUrlElicitationRequiredErrorso it round-trips correctly throughpickle, and adds pickle round-trip tests for bothMCPErrorandUrlElicitationRequiredError.Naveenreddie-think commentedon Jul 23, 2026
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.
IgorGanapolsky commentedon Jul 24, 2026
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:
McpErrorin the worker and re-raise a plain exception withcode,message, anddataas strings/dicts only.pickle.loads(pickle.dumps(err))in CI for every public error type the SDK exports.Free diagnostic framing only — no product pitch.
guiyangyuan commentedon Aug 19, 2026
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.
mikemikimike commentedon Aug 27, 2026
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.
jstar0 commentedon Aug 28, 2026
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.
jstar0 commentedon Aug 28, 2026
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.
Kludex commentedon Oct 14, 2026
Fixed on main (2.x):
MCPErroris pickle-safe. v1.x only takes critical and security fixes, so closing.