diff --git a/cecli/mcp/server.py b/cecli/mcp/server.py index c3758e76a55..bb3cf4fc279 100644 --- a/cecli/mcp/server.py +++ b/cecli/mcp/server.py @@ -337,7 +337,9 @@ async def _enter_client_session(self, read, write): ClientSession( read, write, - read_timeout_seconds=timedelta(seconds=self._request_timeout_seconds()), + read_timeout_seconds=_client_session_read_timeout( + self._request_timeout_seconds() + ), ) ) await session.initialize() @@ -732,3 +734,19 @@ def _unpack_transport(transport): """ read, write = transport[0], transport[1] return read, write + + +def _client_session_read_timeout(seconds: float) -> timedelta | float: + """Return a read timeout in the form the installed mcp SDK expects. + + mcp SDK 1.x types ``ClientSession.read_timeout_seconds`` as a + ``timedelta``; SDK 2.x types it as a float number of seconds and adds it to + other floats internally, so a ``timedelta`` raises ``TypeError: + unsupported operand type(s) for +: 'float' and 'datetime.timedelta'`` at + connect time. Return whichever type the installed SDK wants, so both majors + keep the same per-request timeout instead of failing to connect. + """ + if _get_mcp_major_version() >= 2: + return seconds + + return timedelta(seconds=seconds) diff --git a/tests/mcp/test_server_sdk_compat.py b/tests/mcp/test_server_sdk_compat.py index abe7dcafe7f..1f274c99174 100644 --- a/tests/mcp/test_server_sdk_compat.py +++ b/tests/mcp/test_server_sdk_compat.py @@ -6,12 +6,14 @@ static headers are configured). """ +from datetime import timedelta from unittest.mock import AsyncMock, MagicMock, patch import pytest from cecli.mcp.server import ( HttpStreamingServer, + _client_session_read_timeout, _get_http_client_module, _get_mcp_major_version, _get_oauth_callback_handler, @@ -77,6 +79,23 @@ def test_unpack_transport_mcp2_two_tuple(monkeypatch): assert (read, write) == ("r", "w") +def test_client_session_read_timeout_timedelta_for_mcp1(monkeypatch): + """mcp 1.x types the session read timeout as a timedelta.""" + monkeypatch.setattr("cecli.mcp.server._get_mcp_major_version", lambda: 1) + + assert _client_session_read_timeout(120.0) == timedelta(seconds=120) + + +def test_client_session_read_timeout_float_for_mcp2(monkeypatch): + """mcp 2.x types the session read timeout as a float number of seconds.""" + monkeypatch.setattr("cecli.mcp.server._get_mcp_major_version", lambda: 2) + + timeout = _client_session_read_timeout(120.0) + + assert timeout == 120.0 + assert not isinstance(timeout, timedelta) + + @pytest.mark.asyncio async def test_oauth_callback_handler_mcp1_passthrough(monkeypatch): """mcp 1.x uses the raw callback returning an (auth_code, state) tuple."""