feat: standardize timeout values to floats in seconds (#1766)
This commit is contained in:
parent
4807eb5a80
commit
2aa1ad2a69
12 changed files with 38 additions and 49 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue