Skip to content

Commit ee4ed70

Browse files
committed
Preserve tool lookup errors during copying and pickling
1 parent bdf3cef commit ee4ed70

2 files changed

Lines changed: 22 additions & 0 deletions

File tree

‎src/mcp/server/mcpserver/exceptions.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ def __init__(self, name: str) -> None:
7171
self.name = name
7272
super().__init__(f"Unknown tool: {name}")
7373

74+
def __reduce__(self) -> tuple[type["ToolNotFoundError"], tuple[str], dict[str, object]]:
75+
return type(self), (self.name,), self.__dict__
76+
7477

7578
class UnexpectedToolError(ToolError):
7679
"""A tool call failed with something other than `ToolError`, `ResourceError`, or `MCPError`.

‎tests/server/mcpserver/test_server.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import base64
22
import logging
3+
import pickle
34
from collections.abc import AsyncIterator
45
from contextlib import asynccontextmanager
6+
from copy import copy, deepcopy
57
from dataclasses import dataclass
68
from pathlib import Path
79
from types import SimpleNamespace
@@ -3340,6 +3342,23 @@ async def test_missing_tool_direct_call_remains_a_tool_error() -> None:
33403342
assert str(exc_info.value) == snapshot("Unknown tool: missing")
33413343

33423344

3345+
async def test_missing_tool_error_survives_copying_and_pickling() -> None:
3346+
"""SDK-defined: reconstructing a lookup error preserves its name, message, and exception notes."""
3347+
server = MCPServer()
3348+
with pytest.raises(ToolNotFoundError) as exc_info:
3349+
await server.call_tool("missing", {})
3350+
original = exc_info.value
3351+
original.add_note("Requested by a background worker")
3352+
3353+
for cloned in (copy(original), deepcopy(original), pickle.loads(pickle.dumps(original))):
3354+
assert isinstance(cloned, ToolNotFoundError)
3355+
assert cloned is not original
3356+
assert cloned.name == original.name
3357+
assert cloned.args == original.args
3358+
assert str(cloned) == str(original)
3359+
assert cloned.__notes__ == original.__notes__
3360+
3361+
33433362
@pytest.mark.parametrize("mode", ["legacy", "2026-07-28"])
33443363
async def test_nested_missing_tool_returns_an_execution_error(mode: Literal["legacy", "2026-07-28"]) -> None:
33453364
"""A missing tool called while executing an existing tool remains an execution failure."""

0 commit comments

Comments
 (0)