Initial Checks
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__:
classMcpError(Exception):
error: ErrorDatadef__init__(self, error: ErrorData):
super().__init__(error.message)
self.error=error
Expected 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
error payload, 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:
which crashes for str.
Suggested fix
McpError likely needs to be pickle-safe by design. Any of these would probably fix it:
- Make
__init__ accept both ErrorData and str, normalizing str into an ErrorData. - Implement
__reduce__ so pickle reconstructs using the full ErrorData. - 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
fromimportlib.metadataimportversionimportcloudpicklefrommcp.shared.exceptionsimportMcpErrorfrommcp.typesimportErrorDataprint("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
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