feat: standardize timeout values to floats in seconds (#1766)

This commit is contained in:
Yugan 2025-12-19 17:52:56 +05:30 • committed by GitHub
commit 2aa1ad2a69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 38 additions and 49 deletions

View file

@ -29,7 +29,6 @@ import json
import logging import logging
import os import os
import sys import sys
from datetime import timedelta
from urllib.parse import ParseResult, parse_qs, urlparse from urllib.parse import ParseResult, parse_qs, urlparse
import httpx import httpx
@ -263,8 +262,8 @@ async def _run_session(server_url: str, oauth_auth: OAuthClientProvider) -> None
async with streamablehttp_client( async with streamablehttp_client(
url=server_url, url=server_url,
auth=oauth_auth, auth=oauth_auth,
timeout=timedelta(seconds=30), timeout=30.0,
sse_read_timeout=timedelta(seconds=60), sse_read_timeout=60.0,
) as (read_stream, write_stream, _): ) as (read_stream, write_stream, _):
async with ClientSession(read_stream, write_stream) as session: async with ClientSession(read_stream, write_stream) as session:
# Initialize the session # Initialize the session

View file

@ -207,7 +207,7 @@ class SimpleAuthClient:
async with sse_client( async with sse_client(
url=self.server_url, url=self.server_url,
auth=oauth_auth, auth=oauth_auth,
timeout=60, timeout=60.0,
) as (read_stream, write_stream): ) as (read_stream, write_stream):
await self._run_session(read_stream, write_stream, None) await self._run_session(read_stream, write_stream, None)
else: else:

View file

@ -1,5 +1,4 @@
import logging import logging
from datetime import timedelta
from typing import Any, Protocol, overload from typing import Any, Protocol, overload
import anyio.lowlevel import anyio.lowlevel
@ -113,7 +112,7 @@ class ClientSession(
self, self,
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception], read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
write_stream: MemoryObjectSendStream[SessionMessage], write_stream: MemoryObjectSendStream[SessionMessage],
read_timeout_seconds: timedelta | None = None, read_timeout_seconds: float | None = None,
sampling_callback: SamplingFnT | None = None, sampling_callback: SamplingFnT | None = None,
elicitation_callback: ElicitationFnT | None = None, elicitation_callback: ElicitationFnT | None = None,
list_roots_callback: ListRootsFnT | None = None, list_roots_callback: ListRootsFnT | None = None,
@ -369,7 +368,7 @@ class ClientSession(
self, self,
name: str, name: str,
arguments: dict[str, Any] | None = None, arguments: dict[str, Any] | None = None,
read_timeout_seconds: timedelta | None = None, read_timeout_seconds: float | None = None,
progress_callback: ProgressFnT | None = None, progress_callback: ProgressFnT | None = None,
*, *,
meta: dict[str, Any] | None = None, meta: dict[str, Any] | None = None,

View file

@ -12,7 +12,6 @@ import contextlib
import logging import logging
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta
from types import TracebackType from types import TracebackType
from typing import Any, TypeAlias, overload from typing import Any, TypeAlias, overload
@ -41,11 +40,11 @@ class SseServerParameters(BaseModel):
# Optional headers to include in requests. # Optional headers to include in requests.
headers: dict[str, Any] | None = None headers: dict[str, Any] | None = None
# HTTP timeout for regular operations. # HTTP timeout for regular operations (in seconds).
timeout: float = 5 timeout: float = 5.0
# Timeout for SSE read operations. # Timeout for SSE read operations (in seconds).
sse_read_timeout: float = 60 * 5 sse_read_timeout: float = 300.0
class StreamableHttpParameters(BaseModel): class StreamableHttpParameters(BaseModel):
@ -57,11 +56,11 @@ class StreamableHttpParameters(BaseModel):
# Optional headers to include in requests. # Optional headers to include in requests.
headers: dict[str, Any] | None = None headers: dict[str, Any] | None = None
# HTTP timeout for regular operations. # HTTP timeout for regular operations (in seconds).
timeout: timedelta = timedelta(seconds=30) timeout: float = 30.0
# Timeout for SSE read operations. # Timeout for SSE read operations (in seconds).
sse_read_timeout: timedelta = timedelta(seconds=60 * 5) sse_read_timeout: float = 300.0
# Close the client session when the transport closes. # Close the client session when the transport closes.
terminate_on_close: bool = True terminate_on_close: bool = True
@ -76,7 +75,7 @@ ServerParameters: TypeAlias = StdioServerParameters | SseServerParameters | Stre
class ClientSessionParameters: class ClientSessionParameters:
"""Parameters for establishing a client session to an MCP server.""" """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 sampling_callback: SamplingFnT | None = None
elicitation_callback: ElicitationFnT | None = None elicitation_callback: ElicitationFnT | None = None
list_roots_callback: ListRootsFnT | None = None list_roots_callback: ListRootsFnT | None = None
@ -197,7 +196,7 @@ class ClientSessionGroup:
self, self,
name: str, name: str,
arguments: dict[str, Any], arguments: dict[str, Any],
read_timeout_seconds: timedelta | None = None, read_timeout_seconds: float | None = None,
progress_callback: ProgressFnT | None = None, progress_callback: ProgressFnT | None = None,
*, *,
meta: dict[str, Any] | None = None, meta: dict[str, Any] | None = None,
@ -210,7 +209,7 @@ class ClientSessionGroup:
name: str, name: str,
*, *,
args: dict[str, Any], args: dict[str, Any],
read_timeout_seconds: timedelta | None = None, read_timeout_seconds: float | None = None,
progress_callback: ProgressFnT | None = None, progress_callback: ProgressFnT | None = None,
meta: dict[str, Any] | None = None, meta: dict[str, Any] | None = None,
) -> types.CallToolResult: ... ) -> types.CallToolResult: ...
@ -219,7 +218,7 @@ class ClientSessionGroup:
self, self,
name: str, name: str,
arguments: dict[str, Any] | None = None, arguments: dict[str, Any] | None = None,
read_timeout_seconds: timedelta | None = None, read_timeout_seconds: float | None = None,
progress_callback: ProgressFnT | None = None, progress_callback: ProgressFnT | None = None,
*, *,
meta: dict[str, Any] | None = None, meta: dict[str, Any] | None = None,
@ -314,8 +313,8 @@ class ClientSessionGroup:
httpx_client = create_mcp_http_client( httpx_client = create_mcp_http_client(
headers=server_params.headers, headers=server_params.headers,
timeout=httpx.Timeout( timeout=httpx.Timeout(
server_params.timeout.total_seconds(), server_params.timeout,
read=server_params.sse_read_timeout.total_seconds(), read=server_params.sse_read_timeout,
), ),
) )
await session_stack.enter_async_context(httpx_client) await session_stack.enter_async_context(httpx_client)

View file

@ -31,8 +31,8 @@ def _extract_session_id_from_endpoint(endpoint_url: str) -> str | None:
async def sse_client( async def sse_client(
url: str, url: str,
headers: dict[str, Any] | None = None, headers: dict[str, Any] | None = None,
timeout: float = 5, timeout: float = 5.0,
sse_read_timeout: float = 60 * 5, sse_read_timeout: float = 300.0,
httpx_client_factory: McpHttpClientFactory = create_mcp_http_client, httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,
auth: httpx.Auth | None = None, auth: httpx.Auth | None = None,
on_session_created: Callable[[str], None] | None = None, on_session_created: Callable[[str], None] | None = None,
@ -46,8 +46,8 @@ async def sse_client(
Args: Args:
url: The SSE endpoint URL. url: The SSE endpoint URL.
headers: Optional headers to include in requests. headers: Optional headers to include in requests.
timeout: HTTP timeout for regular operations. timeout: HTTP timeout for regular operations (in seconds).
sse_read_timeout: Timeout for SSE read operations. sse_read_timeout: Timeout for SSE read operations (in seconds).
auth: Optional HTTPX authentication handler. auth: Optional HTTPX authentication handler.
on_session_created: Optional callback invoked with the session ID when received. on_session_created: Optional callback invoked with the session ID when received.
""" """

View file

@ -100,8 +100,8 @@ class StreamableHTTPTransport:
self, self,
url: str, url: str,
headers: dict[str, str] | None = None, headers: dict[str, str] | None = None,
timeout: float | timedelta = 30, timeout: float = 30.0,
sse_read_timeout: float | timedelta = 60 * 5, sse_read_timeout: float = 300.0,
auth: httpx.Auth | None = None, auth: httpx.Auth | None = None,
) -> None: ... ) -> None: ...
@ -118,8 +118,8 @@ class StreamableHTTPTransport:
Args: Args:
url: The endpoint URL. url: The endpoint URL.
headers: Optional headers to include in requests. headers: Optional headers to include in requests.
timeout: HTTP timeout for regular operations. timeout: HTTP timeout for regular operations (in seconds).
sse_read_timeout: Timeout for SSE read operations. sse_read_timeout: Timeout for SSE read operations (in seconds).
auth: Optional HTTPX authentication handler. auth: Optional HTTPX authentication handler.
""" """
# Check for deprecated parameters and issue runtime warning # Check for deprecated parameters and issue runtime warning

View file

@ -6,7 +6,6 @@ from __future__ import annotations
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import timedelta
from typing import Any from typing import Any
import anyio import anyio
@ -49,7 +48,7 @@ async def create_client_server_memory_streams() -> AsyncGenerator[tuple[MessageS
@asynccontextmanager @asynccontextmanager
async def create_connected_server_and_client_session( async def create_connected_server_and_client_session(
server: Server[Any] | FastMCP, server: Server[Any] | FastMCP,
read_timeout_seconds: timedelta | None = None, read_timeout_seconds: float | None = None,
sampling_callback: SamplingFnT | None = None, sampling_callback: SamplingFnT | None = None,
list_roots_callback: ListRootsFnT | None = None, list_roots_callback: ListRootsFnT | None = None,
logging_callback: LoggingFnT | None = None, logging_callback: LoggingFnT | None = None,

View file

@ -1,7 +1,6 @@
import logging import logging
from collections.abc import Callable from collections.abc import Callable
from contextlib import AsyncExitStack from contextlib import AsyncExitStack
from datetime import timedelta
from types import TracebackType from types import TracebackType
from typing import Any, Generic, Protocol, TypeVar from typing import Any, Generic, Protocol, TypeVar
@ -189,7 +188,7 @@ class BaseSession(
receive_request_type: type[ReceiveRequestT], receive_request_type: type[ReceiveRequestT],
receive_notification_type: type[ReceiveNotificationT], receive_notification_type: type[ReceiveNotificationT],
# If none, reading will never time out # If none, reading will never time out
read_timeout_seconds: timedelta | None = None, read_timeout_seconds: float | None = None,
) -> None: ) -> None:
self._read_stream = read_stream self._read_stream = read_stream
self._write_stream = write_stream self._write_stream = write_stream
@ -241,7 +240,7 @@ class BaseSession(
self, self,
request: SendRequestT, request: SendRequestT,
result_type: type[ReceiveResultT], result_type: type[ReceiveResultT],
request_read_timeout_seconds: timedelta | None = None, request_read_timeout_seconds: float | None = None,
metadata: MessageMetadata = None, metadata: MessageMetadata = None,
progress_callback: ProgressFnT | None = None, progress_callback: ProgressFnT | None = None,
) -> ReceiveResultT: ) -> ReceiveResultT:
@ -283,9 +282,9 @@ class BaseSession(
# request read timeout takes precedence over session read timeout # request read timeout takes precedence over session read timeout
timeout = None timeout = None
if request_read_timeout_seconds is not None: # pragma: no cover 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 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: try:
with anyio.fail_after(timeout): with anyio.fail_after(timeout):

View file

@ -273,7 +273,7 @@ class TestClientSessionGroup:
"mcp.client.session_group.mcp.stdio_client", "mcp.client.session_group.mcp.stdio_client",
), ),
( (
SseServerParameters(url="http://test.com/sse", timeout=10), SseServerParameters(url="http://test.com/sse", timeout=10.0),
"sse", "sse",
"mcp.client.session_group.sse_client", "mcp.client.session_group.sse_client",
), # url, headers, timeout, sse_read_timeout ), # url, headers, timeout, sse_read_timeout

View file

@ -1,7 +1,6 @@
"""Test to reproduce issue #88: Random error thrown on response.""" """Test to reproduce issue #88: Random error thrown on response."""
from collections.abc import Sequence from collections.abc import Sequence
from datetime import timedelta
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -93,11 +92,9 @@ async def test_notification_validation_error(tmp_path: Path):
assert not slow_request_lock.is_set() assert not slow_request_lock.is_set()
# Second call should timeout (slow operation with minimal timeout) # 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: with pytest.raises(McpError) as exc_info:
await session.call_tool( await session.call_tool("slow", read_timeout_seconds=0.000001) # artificial timeout that always fails
"slow", read_timeout_seconds=timedelta(microseconds=1)
) # artificial timeout that always fails
assert "Timed out while waiting" in str(exc_info.value) assert "Timed out while waiting" in str(exc_info.value)
# release the slow request not to have hanging process # release the slow request not to have hanging process

View file

@ -270,12 +270,10 @@ async def test_response_id_non_numeric_string_no_match():
async def make_request(client_session: ClientSession): async def make_request(client_session: ClientSession):
try: try:
# Use a short timeout since we expect this to fail # Use a short timeout since we expect this to fail
from datetime import timedelta
await client_session.send_request( await client_session.send_request(
ClientRequest(types.PingRequest()), ClientRequest(types.PingRequest()),
types.EmptyResult, types.EmptyResult,
request_read_timeout_seconds=timedelta(seconds=0.5), request_read_timeout_seconds=0.5,
) )
pytest.fail("Expected timeout") # pragma: no cover pytest.fail("Expected timeout") # pragma: no cover
except McpError as e: except McpError as e:

View file

@ -9,7 +9,6 @@ import multiprocessing
import socket import socket
import time import time
from collections.abc import Generator from collections.abc import Generator
from datetime import timedelta
from typing import Any from typing import Any
from unittest.mock import MagicMock from unittest.mock import MagicMock
@ -2370,8 +2369,8 @@ async def test_streamable_http_transport_deprecated_params_ignored(basic_server:
transport = StreamableHTTPTransport( # pyright: ignore[reportDeprecated] transport = StreamableHTTPTransport( # pyright: ignore[reportDeprecated]
url=f"{basic_server_url}/mcp", url=f"{basic_server_url}/mcp",
headers={"X-Should-Be-Ignored": "ignored"}, headers={"X-Should-Be-Ignored": "ignored"},
timeout=999, timeout=999.0,
sse_read_timeout=timedelta(seconds=999), sse_read_timeout=999.0,
auth=None, auth=None,
) )