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 eecd92409..ba8679e3a 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 38dc5a916..00fdad42e 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 8519f15ce..a7d03f87c 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 da45923e2..d8338a683 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 b2ac67744..4b0bbbc1e 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 fa0524e6e..93705946f 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 06d404e31..c7c6dbabc 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 3033acd0e..c807e291c 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 42f5ce407..ecd058c63 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) diff --git a/tests/shared/test_session.py b/tests/shared/test_session.py index e609397e5..b355a4bf2 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: