|
1 | 1 | import base64 |
2 | 2 | import logging |
| 3 | +import pickle |
3 | 4 | from collections.abc import AsyncIterator |
4 | 5 | from contextlib import asynccontextmanager |
| 6 | +from copy import copy, deepcopy |
5 | 7 | from dataclasses import dataclass |
6 | 8 | from pathlib import Path |
7 | 9 | from types import SimpleNamespace |
@@ -3340,6 +3342,23 @@ async def test_missing_tool_direct_call_remains_a_tool_error() -> None: |
3340 | 3342 | assert str(exc_info.value) == snapshot("Unknown tool: missing") |
3341 | 3343 |
|
3342 | 3344 |
|
| 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 | + |
3343 | 3362 | @pytest.mark.parametrize("mode", ["legacy", "2026-07-28"]) |
3344 | 3363 | async def test_nested_missing_tool_returns_an_execution_error(mode: Literal["legacy", "2026-07-28"]) -> None: |
3345 | 3364 | """A missing tool called while executing an existing tool remains an execution failure.""" |
|
0 commit comments