fix: reconnect MCP sessions on transient stream failures
maintainer edit: treat transient MCP stream failures as dead sessions so the existing reconnect handler can refresh the session before retrying. Also cover retry failure as a structured tool error.
This commit is contained in:
@@ -129,6 +129,8 @@ def _is_transient(exc: BaseException) -> bool:
|
||||
|
||||
def _is_session_terminated(exc: BaseException) -> bool:
|
||||
"""Return True when the MCP SDK reports a dead client session."""
|
||||
if _is_transient(exc):
|
||||
return True
|
||||
messages = [str(exc)]
|
||||
error = getattr(exc, "error", None)
|
||||
if error is not None:
|
||||
|
||||
@@ -478,6 +478,61 @@ async def test_execute_handles_generic_exception() -> None:
|
||||
assert is_tool_error_result(wrapper.name, result)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_reconnects_on_transient_failure() -> None:
|
||||
class ClosedResourceError(Exception):
|
||||
pass
|
||||
|
||||
reconnects = 0
|
||||
|
||||
async def stale_call_tool(_name: str, arguments: dict) -> object:
|
||||
raise ClosedResourceError("stream closed")
|
||||
|
||||
async def fresh_call_tool(_name: str, arguments: dict) -> object:
|
||||
return SimpleNamespace(content=[_FakeTextContent("ok")])
|
||||
|
||||
wrapper = _make_wrapper(SimpleNamespace(call_tool=stale_call_tool))
|
||||
|
||||
async def reconnect(_server_name: str, _tool_name: str, _stale_tool: object) -> object:
|
||||
nonlocal reconnects
|
||||
reconnects += 1
|
||||
return SimpleNamespace(_session=SimpleNamespace(call_tool=fresh_call_tool))
|
||||
|
||||
wrapper.set_reconnect_handler(reconnect)
|
||||
|
||||
result = await wrapper.execute()
|
||||
|
||||
assert result == "ok"
|
||||
assert reconnects == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_marks_transient_retry_failure_as_tool_error(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
class ClosedResourceError(Exception):
|
||||
pass
|
||||
|
||||
calls = 0
|
||||
|
||||
async def call_tool(_name: str, arguments: dict) -> object:
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
raise ClosedResourceError("closed")
|
||||
|
||||
async def fast_sleep(_delay: float) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(mcp_mod.asyncio, "sleep", fast_sleep)
|
||||
wrapper = _make_wrapper(SimpleNamespace(call_tool=call_tool))
|
||||
|
||||
result = await wrapper.execute()
|
||||
|
||||
assert calls == 2
|
||||
assert result == "(MCP tool call failed after retry: ClosedResourceError)"
|
||||
assert is_tool_error_result(wrapper.name, result)
|
||||
|
||||
|
||||
def _make_tool_def(name: str) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
name=name,
|
||||
|
||||
Reference in New Issue
Block a user