Skip to content

Commit 79a30f4

Browse files
LUbaoshuaiLUbaoshuai
authored andcommitted
fix: return float timeouts on MCP SDK 2.x in session_context
_read_timeout() documented that MCP SDK 1.x wants a timedelta while 2.x wants the plain float, but the body always returned timedelta. On mcp 2.x that value reaches anyio.fail_after(), which adds it to current_time(), raising TypeError for every session creation (fixes #6938). Detect the installed SDK major version once (falling back to the current 1.x behavior when detection fails) and pass through float seconds on 2.x. Regression tests cover both branches and the detection-failure fallback, independent of the SDK version installed in the test environment.
1 parent 6d14518 commit 79a30f4

2 files changed

Lines changed: 60 additions & 8 deletions

File tree

src/google/adk/tools/mcp_tool/session_context.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
from contextlib import AbstractAsyncContextManager
1919
from contextlib import AsyncExitStack
2020
from datetime import timedelta
21+
from functools import lru_cache
22+
from importlib.metadata import PackageNotFoundError
23+
from importlib.metadata import version
2124
import logging
2225
from types import TracebackType
2326
from typing import Any
@@ -37,7 +40,21 @@
3740
_T = TypeVar('_T')
3841

3942

40-
def _read_timeout(seconds: Optional[float]) -> Optional[timedelta]:
43+
@lru_cache(maxsize=1)
44+
def _mcp_wants_float_timeouts() -> bool:
45+
"""Whether the installed MCP SDK expects plain seconds as timeouts.
46+
47+
MCP SDK 1.x requires a ``datetime.timedelta`` for ``read_timeout_seconds``,
48+
while 2.x takes the number of seconds as a float. Detection failures fall
49+
back to the 1.x behavior, which is what ADK pins today.
50+
"""
51+
try:
52+
return int(version('mcp').split('.')[0]) >= 2
53+
except (PackageNotFoundError, ValueError, IndexError):
54+
return False
55+
56+
57+
def _read_timeout(seconds: Optional[float]) -> Optional[float | timedelta]:
4158
"""Converts a timeout in seconds to the type ``ClientSession`` expects.
4259
4360
ADK carries every timeout as float seconds. MCP SDK 1.x wants a
@@ -52,6 +69,8 @@ def _read_timeout(seconds: Optional[float]) -> Optional[timedelta]:
5269
"""
5370
if seconds is None:
5471
return None
72+
if _mcp_wants_float_timeouts():
73+
return float(seconds)
5574
return timedelta(seconds=seconds)
5675

5776

tests/unittests/tools/mcp_tool/test_session_context.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import asyncio
1818
from contextlib import AsyncExitStack
1919
from datetime import timedelta
20+
from importlib.metadata import PackageNotFoundError
2021
import time
2122
from unittest.mock import AsyncMock
2223
from unittest.mock import Mock
@@ -25,6 +26,7 @@
2526
from google.adk.features import FeatureName
2627
from google.adk.features._feature_registry import temporary_feature_override
2728
from google.adk.tools.mcp_tool.session_context import _format_exception
29+
from google.adk.tools.mcp_tool.session_context import _mcp_wants_float_timeouts
2830
from google.adk.tools.mcp_tool.session_context import _read_timeout
2931
from google.adk.tools.mcp_tool.session_context import SessionContext
3032
import httpx
@@ -1013,13 +1015,44 @@ class TestReadTimeout:
10131015
"""ADK carries timeouts as float seconds and converts at the SDK boundary."""
10141016

10151017
def test_none_stays_none(self):
1016-
assert _read_timeout(None) is None
1018+
with patch(
1019+
'google.adk.tools.mcp_tool.session_context._mcp_wants_float_timeouts',
1020+
return_value=False,
1021+
):
1022+
assert _read_timeout(None) is None
1023+
with patch(
1024+
'google.adk.tools.mcp_tool.session_context._mcp_wants_float_timeouts',
1025+
return_value=True,
1026+
):
1027+
assert _read_timeout(None) is None
10171028

1018-
def test_seconds_become_the_type_the_sdk_wants(self):
1019-
assert _read_timeout(30) == timedelta(seconds=30)
1029+
def test_mcp_1x_seconds_become_a_timedelta(self):
1030+
with patch(
1031+
'google.adk.tools.mcp_tool.session_context._mcp_wants_float_timeouts',
1032+
return_value=False,
1033+
):
1034+
assert _read_timeout(30) == timedelta(seconds=30)
1035+
assert _read_timeout(0) == timedelta(seconds=0)
1036+
assert _read_timeout(0.5) == timedelta(seconds=0.5)
10201037

1021-
def test_zero_is_a_real_timeout_not_a_missing_one(self):
1022-
assert _read_timeout(0) == timedelta(seconds=0)
1038+
def test_mcp_2x_seconds_stay_seconds(self):
1039+
with patch(
1040+
'google.adk.tools.mcp_tool.session_context._mcp_wants_float_timeouts',
1041+
return_value=True,
1042+
):
1043+
assert _read_timeout(30) == 30
1044+
assert _read_timeout(0) == 0
1045+
assert _read_timeout(0.5) == 0.5
1046+
assert isinstance(_read_timeout(30), float)
10231047

1024-
def test_fractional_seconds_survive(self):
1025-
assert _read_timeout(0.5) == timedelta(seconds=0.5)
1048+
def test_detection_failure_falls_back_to_timedelta(self):
1049+
with patch(
1050+
'google.adk.tools.mcp_tool.session_context.version',
1051+
side_effect=PackageNotFoundError('mcp'),
1052+
):
1053+
_mcp_wants_float_timeouts.cache_clear()
1054+
try:
1055+
assert _mcp_wants_float_timeouts() is False
1056+
assert _read_timeout(30) == timedelta(seconds=30)
1057+
finally:
1058+
_mcp_wants_float_timeouts.cache_clear()

0 commit comments

Comments
 (0)