From f76804f01001c660f9a4188eeda9c3a3e459d2df Mon Sep 17 00:00:00 2001 From: Yugan Date: Wed, 10 Dec 2025 21:52:20 +0530 Subject: [PATCH 1/2] feat: standardize timeout values to floats in seconds - Convert all timeout parameters from timedelta to float type - Update StreamableHttpParameters, SseServerParameters timeouts to float - Change read_timeout_seconds from timedelta | None to float | None - Remove timedelta imports from modified files - Update examples and tests to use float values (e.g., 30.0 instead of timedelta(seconds=30)) - Add '(in seconds)' clarification to all timeout parameter docstrings - Align with Python ecosystem conventions (httpx, starlette, stdlib) BREAKING CHANGE: All timeout parameters now accept float (seconds) instead of timedelta objects Fixes #1747 --- .../mcp_conformance_auth_client/__init__.py | 5 ++-- .../mcp_simple_auth_client/main.py | 3 +-- src/mcp/client/session.py | 5 ++-- src/mcp/client/session_group.py | 25 +++++++++---------- src/mcp/client/sse.py | 8 +++--- src/mcp/client/streamable_http.py | 19 ++++++-------- src/mcp/shared/memory.py | 3 +-- src/mcp/shared/session.py | 9 +++---- tests/issues/test_88_random_error.py | 5 ++-- 9 files changed, 36 insertions(+), 46 deletions(-) diff --git a/examples/clients/conformance-auth-client/mcp_conformance_auth_client/__init__.py b/examples/clients/conformance-auth-client/mcp_conformance_auth_client/__init__.py index eecd92409a..ba8679e3ac 100644 --- a/examples/clients/conformance-auth-client/mcp_conformance_auth_client/__init__.py +++ b/examples/clients/conformance-auth-client/mcp_conformance_auth_client/__init__.py @@ -29,7 +29,6 @@ import logging import os import sys -from datetime import timedelta from urllib.parse import ParseResult, parse_qs, urlparse import httpx @@ -263,8 +262,8 @@ async def _run_session(server_url: str, oauth_auth: OAuthClientProvider) -> None async with streamablehttp_client( url=server_url, auth=oauth_auth, - timeout=timedelta(seconds=30), - sse_read_timeout=timedelta(seconds=60), + timeout=30.0, + sse_read_timeout=60.0, ) as (read_stream, write_stream, _): async with ClientSession(read_stream, write_stream) as session: # Initialize the session diff --git a/examples/clients/simple-auth-client/mcp_simple_auth_client/main.py b/examples/clients/simple-auth-client/mcp_simple_auth_client/main.py index 38dc5a9167..00fdad42eb 100644 --- a/examples/clients/simple-auth-client/mcp_simple_auth_client/main.py +++ b/examples/clients/simple-auth-client/mcp_simple_auth_client/main.py @@ -11,7 +11,6 @@ import threading import time import webbrowser -from datetime import timedelta from http.server import BaseHTTPRequestHandler, HTTPServer from typing import Any from urllib.parse import parse_qs, urlparse @@ -215,7 +214,7 @@ async def _default_redirect_handler(authorization_url: str) -> None: async with streamablehttp_client( url=self.server_url, auth=oauth_auth, - timeout=timedelta(seconds=60), + timeout=60.0, ) as (read_stream, write_stream, get_session_id): await self._run_session(read_stream, write_stream, get_session_id) diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index 8519f15cec..a7d03f87cd 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -1,5 +1,4 @@ import logging -from datetime import timedelta from typing import Any, Protocol, overload import anyio.lowlevel @@ -113,7 +112,7 @@ def __init__( self, read_stream: MemoryObjectReceiveStream[SessionMessage | Exception], write_stream: MemoryObjectSendStream[SessionMessage], - read_timeout_seconds: timedelta | None = None, + read_timeout_seconds: float | None = None, sampling_callback: SamplingFnT | None = None, elicitation_callback: ElicitationFnT | None = None, list_roots_callback: ListRootsFnT | None = None, @@ -369,7 +368,7 @@ async def call_tool( self, name: str, arguments: dict[str, Any] | None = None, - read_timeout_seconds: timedelta | None = None, + read_timeout_seconds: float | None = None, progress_callback: ProgressFnT | None = None, *, meta: dict[str, Any] | None = None, diff --git a/src/mcp/client/session_group.py b/src/mcp/client/session_group.py index da45923e2a..d8338a6839 100644 --- a/src/mcp/client/session_group.py +++ b/src/mcp/client/session_group.py @@ -12,7 +12,6 @@ import logging from collections.abc import Callable from dataclasses import dataclass -from datetime import timedelta from types import TracebackType from typing import Any, TypeAlias, overload @@ -39,11 +38,11 @@ class SseServerParameters(BaseModel): # Optional headers to include in requests. headers: dict[str, Any] | None = None - # HTTP timeout for regular operations. - timeout: float = 5 + # HTTP timeout for regular operations (in seconds). + timeout: float = 5.0 - # Timeout for SSE read operations. - sse_read_timeout: float = 60 * 5 + # Timeout for SSE read operations (in seconds). + sse_read_timeout: float = 300.0 class StreamableHttpParameters(BaseModel): @@ -55,11 +54,11 @@ class StreamableHttpParameters(BaseModel): # Optional headers to include in requests. headers: dict[str, Any] | None = None - # HTTP timeout for regular operations. - timeout: timedelta = timedelta(seconds=30) + # HTTP timeout for regular operations (in seconds). + timeout: float = 30.0 - # Timeout for SSE read operations. - sse_read_timeout: timedelta = timedelta(seconds=60 * 5) + # Timeout for SSE read operations (in seconds). + sse_read_timeout: float = 300.0 # Close the client session when the transport closes. terminate_on_close: bool = True @@ -74,7 +73,7 @@ class StreamableHttpParameters(BaseModel): class ClientSessionParameters: """Parameters for establishing a client session to an MCP server.""" - read_timeout_seconds: timedelta | None = None + read_timeout_seconds: float | None = None sampling_callback: SamplingFnT | None = None elicitation_callback: ElicitationFnT | None = None list_roots_callback: ListRootsFnT | None = None @@ -195,7 +194,7 @@ async def call_tool( self, name: str, arguments: dict[str, Any], - read_timeout_seconds: timedelta | None = None, + read_timeout_seconds: float | None = None, progress_callback: ProgressFnT | None = None, *, meta: dict[str, Any] | None = None, @@ -208,7 +207,7 @@ async def call_tool( name: str, *, args: dict[str, Any], - read_timeout_seconds: timedelta | None = None, + read_timeout_seconds: float | None = None, progress_callback: ProgressFnT | None = None, meta: dict[str, Any] | None = None, ) -> types.CallToolResult: ... @@ -217,7 +216,7 @@ async def call_tool( self, name: str, arguments: dict[str, Any] | None = None, - read_timeout_seconds: timedelta | None = None, + read_timeout_seconds: float | None = None, progress_callback: ProgressFnT | None = None, *, meta: dict[str, Any] | None = None, diff --git a/src/mcp/client/sse.py b/src/mcp/client/sse.py index b2ac67744e..4b0bbbc1e7 100644 --- a/src/mcp/client/sse.py +++ b/src/mcp/client/sse.py @@ -31,8 +31,8 @@ def _extract_session_id_from_endpoint(endpoint_url: str) -> str | None: async def sse_client( url: str, headers: dict[str, Any] | None = None, - timeout: float = 5, - sse_read_timeout: float = 60 * 5, + timeout: float = 5.0, + sse_read_timeout: float = 300.0, httpx_client_factory: McpHttpClientFactory = create_mcp_http_client, auth: httpx.Auth | None = None, on_session_created: Callable[[str], None] | None = None, @@ -46,8 +46,8 @@ async def sse_client( Args: url: The SSE endpoint URL. headers: Optional headers to include in requests. - timeout: HTTP timeout for regular operations. - sse_read_timeout: Timeout for SSE read operations. + timeout: HTTP timeout for regular operations (in seconds). + sse_read_timeout: Timeout for SSE read operations (in seconds). auth: Optional HTTPX authentication handler. on_session_created: Optional callback invoked with the session ID when received. """ diff --git a/src/mcp/client/streamable_http.py b/src/mcp/client/streamable_http.py index fa0524e6ef..93705946f7 100644 --- a/src/mcp/client/streamable_http.py +++ b/src/mcp/client/streamable_http.py @@ -10,7 +10,6 @@ from collections.abc import AsyncGenerator, Awaitable, Callable from contextlib import asynccontextmanager from dataclasses import dataclass -from datetime import timedelta import anyio import httpx @@ -82,8 +81,8 @@ def __init__( self, url: str, headers: dict[str, str] | None = None, - timeout: float | timedelta = 30, - sse_read_timeout: float | timedelta = 60 * 5, + timeout: float = 30.0, + sse_read_timeout: float = 300.0, auth: httpx.Auth | None = None, ) -> None: """Initialize the StreamableHTTP transport. @@ -91,16 +90,14 @@ def __init__( Args: url: The endpoint URL. headers: Optional headers to include in requests. - timeout: HTTP timeout for regular operations. - sse_read_timeout: Timeout for SSE read operations. + timeout: HTTP timeout for regular operations (in seconds). + sse_read_timeout: Timeout for SSE read operations (in seconds). auth: Optional HTTPX authentication handler. """ self.url = url self.headers = headers or {} - self.timeout = timeout.total_seconds() if isinstance(timeout, timedelta) else timeout - self.sse_read_timeout = ( - sse_read_timeout.total_seconds() if isinstance(sse_read_timeout, timedelta) else sse_read_timeout - ) + self.timeout = timeout + self.sse_read_timeout = sse_read_timeout self.auth = auth self.session_id = None self.protocol_version = None @@ -563,8 +560,8 @@ def get_session_id(self) -> str | None: async def streamablehttp_client( url: str, headers: dict[str, str] | None = None, - timeout: float | timedelta = 30, - sse_read_timeout: float | timedelta = 60 * 5, + timeout: float = 30.0, + sse_read_timeout: float = 300.0, terminate_on_close: bool = True, httpx_client_factory: McpHttpClientFactory = create_mcp_http_client, auth: httpx.Auth | None = None, diff --git a/src/mcp/shared/memory.py b/src/mcp/shared/memory.py index 06d404e311..c7c6dbabc2 100644 --- a/src/mcp/shared/memory.py +++ b/src/mcp/shared/memory.py @@ -6,7 +6,6 @@ from collections.abc import AsyncGenerator from contextlib import asynccontextmanager -from datetime import timedelta from typing import Any import anyio @@ -49,7 +48,7 @@ async def create_client_server_memory_streams() -> AsyncGenerator[tuple[MessageS @asynccontextmanager async def create_connected_server_and_client_session( server: Server[Any] | FastMCP, - read_timeout_seconds: timedelta | None = None, + read_timeout_seconds: float | None = None, sampling_callback: SamplingFnT | None = None, list_roots_callback: ListRootsFnT | None = None, logging_callback: LoggingFnT | None = None, diff --git a/src/mcp/shared/session.py b/src/mcp/shared/session.py index cceefccce6..ca058bc9a9 100644 --- a/src/mcp/shared/session.py +++ b/src/mcp/shared/session.py @@ -1,7 +1,6 @@ import logging from collections.abc import Callable from contextlib import AsyncExitStack -from datetime import timedelta from types import TracebackType from typing import Any, Generic, Protocol, TypeVar @@ -189,7 +188,7 @@ def __init__( receive_request_type: type[ReceiveRequestT], receive_notification_type: type[ReceiveNotificationT], # If none, reading will never time out - read_timeout_seconds: timedelta | None = None, + read_timeout_seconds: float | None = None, ) -> None: self._read_stream = read_stream self._write_stream = write_stream @@ -241,7 +240,7 @@ async def send_request( self, request: SendRequestT, result_type: type[ReceiveResultT], - request_read_timeout_seconds: timedelta | None = None, + request_read_timeout_seconds: float | None = None, metadata: MessageMetadata = None, progress_callback: ProgressFnT | None = None, ) -> ReceiveResultT: @@ -283,9 +282,9 @@ async def send_request( # request read timeout takes precedence over session read timeout timeout = None if request_read_timeout_seconds is not None: # pragma: no cover - timeout = request_read_timeout_seconds.total_seconds() + timeout = request_read_timeout_seconds elif self._session_read_timeout_seconds is not None: # pragma: no cover - timeout = self._session_read_timeout_seconds.total_seconds() + timeout = self._session_read_timeout_seconds try: with anyio.fail_after(timeout): diff --git a/tests/issues/test_88_random_error.py b/tests/issues/test_88_random_error.py index 42f5ce407f..ecd058c637 100644 --- a/tests/issues/test_88_random_error.py +++ b/tests/issues/test_88_random_error.py @@ -1,7 +1,6 @@ """Test to reproduce issue #88: Random error thrown on response.""" from collections.abc import Sequence -from datetime import timedelta from pathlib import Path from typing import Any @@ -93,10 +92,10 @@ async def client( assert not slow_request_lock.is_set() # Second call should timeout (slow operation with minimal timeout) - # Use 10ms timeout to trigger quickly without waiting + # Use very small timeout to trigger quickly without waiting with pytest.raises(McpError) as exc_info: await session.call_tool( - "slow", read_timeout_seconds=timedelta(microseconds=1) + "slow", read_timeout_seconds=0.000001 ) # artificial timeout that always fails assert "Timed out while waiting" in str(exc_info.value) From faff0bb5f2352d70f8c5901a63ab2eb83569b372 Mon Sep 17 00:00:00 2001 From: Yugan Date: Wed, 10 Dec 2025 22:08:03 +0530 Subject: [PATCH 2/2] fix: update merged test to use float timeout value - Fix test_response_id_non_numeric_string_no_match to use float (0.5) instead of timedelta(seconds=0.5) - Resolves TypeError: unsupported operand type(s) for +: 'float' and 'datetime.timedelta' --- tests/shared/test_session.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/shared/test_session.py b/tests/shared/test_session.py index e609397e5e..b355a4bf2d 100644 --- a/tests/shared/test_session.py +++ b/tests/shared/test_session.py @@ -270,12 +270,10 @@ async def mock_server(): async def make_request(client_session: ClientSession): try: # Use a short timeout since we expect this to fail - from datetime import timedelta - await client_session.send_request( ClientRequest(types.PingRequest()), types.EmptyResult, - request_read_timeout_seconds=timedelta(seconds=0.5), + request_read_timeout_seconds=0.5, ) pytest.fail("Expected timeout") # pragma: no cover except McpError as e: