Skip to content

Commit f76804f

Browse files
committed
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
1 parent 0dedbd9 commit f76804f

File tree

9 files changed

+36
-46
lines changed

9 files changed

+36
-46
lines changed

examples/clients/conformance-auth-client/mcp_conformance_auth_client/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import logging
3030
import os
3131
import sys
32-
from datetime import timedelta
3332
from urllib.parse import ParseResult, parse_qs, urlparse
3433

3534
import httpx
@@ -263,8 +262,8 @@ async def _run_session(server_url: str, oauth_auth: OAuthClientProvider) -> None
263262
async with streamablehttp_client(
264263
url=server_url,
265264
auth=oauth_auth,
266-
timeout=timedelta(seconds=30),
267-
sse_read_timeout=timedelta(seconds=60),
265+
timeout=30.0,
266+
sse_read_timeout=60.0,
268267
) as (read_stream, write_stream, _):
269268
async with ClientSession(read_stream, write_stream) as session:
270269
# Initialize the session

examples/clients/simple-auth-client/mcp_simple_auth_client/main.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import threading
1212
import time
1313
import webbrowser
14-
from datetime import timedelta
1514
from http.server import BaseHTTPRequestHandler, HTTPServer
1615
from typing import Any
1716
from urllib.parse import parse_qs, urlparse
@@ -215,7 +214,7 @@ async def _default_redirect_handler(authorization_url: str) -> None:
215214
async with streamablehttp_client(
216215
url=self.server_url,
217216
auth=oauth_auth,
218-
timeout=timedelta(seconds=60),
217+
timeout=60.0,
219218
) as (read_stream, write_stream, get_session_id):
220219
await self._run_session(read_stream, write_stream, get_session_id)
221220

src/mcp/client/session.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
from datetime import timedelta
32
from typing import Any, Protocol, overload
43

54
import anyio.lowlevel
@@ -113,7 +112,7 @@ def __init__(
113112
self,
114113
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
115114
write_stream: MemoryObjectSendStream[SessionMessage],
116-
read_timeout_seconds: timedelta | None = None,
115+
read_timeout_seconds: float | None = None,
117116
sampling_callback: SamplingFnT | None = None,
118117
elicitation_callback: ElicitationFnT | None = None,
119118
list_roots_callback: ListRootsFnT | None = None,
@@ -369,7 +368,7 @@ async def call_tool(
369368
self,
370369
name: str,
371370
arguments: dict[str, Any] | None = None,
372-
read_timeout_seconds: timedelta | None = None,
371+
read_timeout_seconds: float | None = None,
373372
progress_callback: ProgressFnT | None = None,
374373
*,
375374
meta: dict[str, Any] | None = None,

src/mcp/client/session_group.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import logging
1313
from collections.abc import Callable
1414
from dataclasses import dataclass
15-
from datetime import timedelta
1615
from types import TracebackType
1716
from typing import Any, TypeAlias, overload
1817

@@ -39,11 +38,11 @@ class SseServerParameters(BaseModel):
3938
# Optional headers to include in requests.
4039
headers: dict[str, Any] | None = None
4140

42-
# HTTP timeout for regular operations.
43-
timeout: float = 5
41+
# HTTP timeout for regular operations (in seconds).
42+
timeout: float = 5.0
4443

45-
# Timeout for SSE read operations.
46-
sse_read_timeout: float = 60 * 5
44+
# Timeout for SSE read operations (in seconds).
45+
sse_read_timeout: float = 300.0
4746

4847

4948
class StreamableHttpParameters(BaseModel):
@@ -55,11 +54,11 @@ class StreamableHttpParameters(BaseModel):
5554
# Optional headers to include in requests.
5655
headers: dict[str, Any] | None = None
5756

58-
# HTTP timeout for regular operations.
59-
timeout: timedelta = timedelta(seconds=30)
57+
# HTTP timeout for regular operations (in seconds).
58+
timeout: float = 30.0
6059

61-
# Timeout for SSE read operations.
62-
sse_read_timeout: timedelta = timedelta(seconds=60 * 5)
60+
# Timeout for SSE read operations (in seconds).
61+
sse_read_timeout: float = 300.0
6362

6463
# Close the client session when the transport closes.
6564
terminate_on_close: bool = True
@@ -74,7 +73,7 @@ class StreamableHttpParameters(BaseModel):
7473
class ClientSessionParameters:
7574
"""Parameters for establishing a client session to an MCP server."""
7675

77-
read_timeout_seconds: timedelta | None = None
76+
read_timeout_seconds: float | None = None
7877
sampling_callback: SamplingFnT | None = None
7978
elicitation_callback: ElicitationFnT | None = None
8079
list_roots_callback: ListRootsFnT | None = None
@@ -195,7 +194,7 @@ async def call_tool(
195194
self,
196195
name: str,
197196
arguments: dict[str, Any],
198-
read_timeout_seconds: timedelta | None = None,
197+
read_timeout_seconds: float | None = None,
199198
progress_callback: ProgressFnT | None = None,
200199
*,
201200
meta: dict[str, Any] | None = None,
@@ -208,7 +207,7 @@ async def call_tool(
208207
name: str,
209208
*,
210209
args: dict[str, Any],
211-
read_timeout_seconds: timedelta | None = None,
210+
read_timeout_seconds: float | None = None,
212211
progress_callback: ProgressFnT | None = None,
213212
meta: dict[str, Any] | None = None,
214213
) -> types.CallToolResult: ...
@@ -217,7 +216,7 @@ async def call_tool(
217216
self,
218217
name: str,
219218
arguments: dict[str, Any] | None = None,
220-
read_timeout_seconds: timedelta | None = None,
219+
read_timeout_seconds: float | None = None,
221220
progress_callback: ProgressFnT | None = None,
222221
*,
223222
meta: dict[str, Any] | None = None,

src/mcp/client/sse.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ def _extract_session_id_from_endpoint(endpoint_url: str) -> str | None:
3131
async def sse_client(
3232
url: str,
3333
headers: dict[str, Any] | None = None,
34-
timeout: float = 5,
35-
sse_read_timeout: float = 60 * 5,
34+
timeout: float = 5.0,
35+
sse_read_timeout: float = 300.0,
3636
httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,
3737
auth: httpx.Auth | None = None,
3838
on_session_created: Callable[[str], None] | None = None,
@@ -46,8 +46,8 @@ async def sse_client(
4646
Args:
4747
url: The SSE endpoint URL.
4848
headers: Optional headers to include in requests.
49-
timeout: HTTP timeout for regular operations.
50-
sse_read_timeout: Timeout for SSE read operations.
49+
timeout: HTTP timeout for regular operations (in seconds).
50+
sse_read_timeout: Timeout for SSE read operations (in seconds).
5151
auth: Optional HTTPX authentication handler.
5252
on_session_created: Optional callback invoked with the session ID when received.
5353
"""

src/mcp/client/streamable_http.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from collections.abc import AsyncGenerator, Awaitable, Callable
1111
from contextlib import asynccontextmanager
1212
from dataclasses import dataclass
13-
from datetime import timedelta
1413

1514
import anyio
1615
import httpx
@@ -82,25 +81,23 @@ def __init__(
8281
self,
8382
url: str,
8483
headers: dict[str, str] | None = None,
85-
timeout: float | timedelta = 30,
86-
sse_read_timeout: float | timedelta = 60 * 5,
84+
timeout: float = 30.0,
85+
sse_read_timeout: float = 300.0,
8786
auth: httpx.Auth | None = None,
8887
) -> None:
8988
"""Initialize the StreamableHTTP transport.
9089
9190
Args:
9291
url: The endpoint URL.
9392
headers: Optional headers to include in requests.
94-
timeout: HTTP timeout for regular operations.
95-
sse_read_timeout: Timeout for SSE read operations.
93+
timeout: HTTP timeout for regular operations (in seconds).
94+
sse_read_timeout: Timeout for SSE read operations (in seconds).
9695
auth: Optional HTTPX authentication handler.
9796
"""
9897
self.url = url
9998
self.headers = headers or {}
100-
self.timeout = timeout.total_seconds() if isinstance(timeout, timedelta) else timeout
101-
self.sse_read_timeout = (
102-
sse_read_timeout.total_seconds() if isinstance(sse_read_timeout, timedelta) else sse_read_timeout
103-
)
99+
self.timeout = timeout
100+
self.sse_read_timeout = sse_read_timeout
104101
self.auth = auth
105102
self.session_id = None
106103
self.protocol_version = None
@@ -563,8 +560,8 @@ def get_session_id(self) -> str | None:
563560
async def streamablehttp_client(
564561
url: str,
565562
headers: dict[str, str] | None = None,
566-
timeout: float | timedelta = 30,
567-
sse_read_timeout: float | timedelta = 60 * 5,
563+
timeout: float = 30.0,
564+
sse_read_timeout: float = 300.0,
568565
terminate_on_close: bool = True,
569566
httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,
570567
auth: httpx.Auth | None = None,

src/mcp/shared/memory.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from collections.abc import AsyncGenerator
88
from contextlib import asynccontextmanager
9-
from datetime import timedelta
109
from typing import Any
1110

1211
import anyio
@@ -49,7 +48,7 @@ async def create_client_server_memory_streams() -> AsyncGenerator[tuple[MessageS
4948
@asynccontextmanager
5049
async def create_connected_server_and_client_session(
5150
server: Server[Any] | FastMCP,
52-
read_timeout_seconds: timedelta | None = None,
51+
read_timeout_seconds: float | None = None,
5352
sampling_callback: SamplingFnT | None = None,
5453
list_roots_callback: ListRootsFnT | None = None,
5554
logging_callback: LoggingFnT | None = None,

src/mcp/shared/session.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import logging
22
from collections.abc import Callable
33
from contextlib import AsyncExitStack
4-
from datetime import timedelta
54
from types import TracebackType
65
from typing import Any, Generic, Protocol, TypeVar
76

@@ -189,7 +188,7 @@ def __init__(
189188
receive_request_type: type[ReceiveRequestT],
190189
receive_notification_type: type[ReceiveNotificationT],
191190
# If none, reading will never time out
192-
read_timeout_seconds: timedelta | None = None,
191+
read_timeout_seconds: float | None = None,
193192
) -> None:
194193
self._read_stream = read_stream
195194
self._write_stream = write_stream
@@ -241,7 +240,7 @@ async def send_request(
241240
self,
242241
request: SendRequestT,
243242
result_type: type[ReceiveResultT],
244-
request_read_timeout_seconds: timedelta | None = None,
243+
request_read_timeout_seconds: float | None = None,
245244
metadata: MessageMetadata = None,
246245
progress_callback: ProgressFnT | None = None,
247246
) -> ReceiveResultT:
@@ -283,9 +282,9 @@ async def send_request(
283282
# request read timeout takes precedence over session read timeout
284283
timeout = None
285284
if request_read_timeout_seconds is not None: # pragma: no cover
286-
timeout = request_read_timeout_seconds.total_seconds()
285+
timeout = request_read_timeout_seconds
287286
elif self._session_read_timeout_seconds is not None: # pragma: no cover
288-
timeout = self._session_read_timeout_seconds.total_seconds()
287+
timeout = self._session_read_timeout_seconds
289288

290289
try:
291290
with anyio.fail_after(timeout):

tests/issues/test_88_random_error.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Test to reproduce issue #88: Random error thrown on response."""
22

33
from collections.abc import Sequence
4-
from datetime import timedelta
54
from pathlib import Path
65
from typing import Any
76

@@ -93,10 +92,10 @@ async def client(
9392
assert not slow_request_lock.is_set()
9493

9594
# Second call should timeout (slow operation with minimal timeout)
96-
# Use 10ms timeout to trigger quickly without waiting
95+
# Use very small timeout to trigger quickly without waiting
9796
with pytest.raises(McpError) as exc_info:
9897
await session.call_tool(
99-
"slow", read_timeout_seconds=timedelta(microseconds=1)
98+
"slow", read_timeout_seconds=0.000001
10099
) # artificial timeout that always fails
101100
assert "Timed out while waiting" in str(exc_info.value)
102101

0 commit comments

Comments
 (0)