Skip to content

Commit ebf6e5a

Browse files
authored
Document the timeout a hand-built httpx2 client needs (#3618)
1 parent c8fcfc8 commit ebf6e5a

9 files changed

Lines changed: 51 additions & 8 deletions

File tree

‎docs/client/transports.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ Two things to notice:
3939
* You own the `httpx2.AsyncClient`, so **you** enter and exit it. The SDK never closes a client it didn't create.
4040
* `streamable_http_client(url, http_client=...)` returns a transport, and `Client(transport)` accepts it like anything else.
4141

42+
Keep the `timeout=`. It is the one the SDK's own client uses (30 seconds, 300 for reads); an `httpx2.AsyncClient` built without one gets `httpx2`'s 5-second default, and a tool call that runs longer than that fails with a read timeout.
43+
4244
One TLS note: `httpx2` verifies certificates against the operating system trust store (via
4345
[`truststore`](https://pypi.org/project/truststore/)), not a bundled CA list. In an environment with
4446
no usable system CA store (some minimal containers), set the standard `SSL_CERT_FILE`/`SSL_CERT_DIR`

‎docs_src/identity_assertion/tutorial001.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ async def fetch_id_jag(audience: str, resource: str) -> str:
6262

6363

6464
async def main() -> None:
65-
async with httpx2.AsyncClient(auth=oauth) as http_client:
65+
async with httpx2.AsyncClient(auth=oauth, timeout=httpx2.Timeout(30.0, read=300.0)) as http_client:
6666
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
6767
async with Client(transport) as client:
6868
result = await client.list_tools()

‎docs_src/oauth_clients/tutorial001.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ async def wait_for_callback() -> AuthorizationCodeResult:
5555

5656

5757
async def main() -> None:
58-
async with httpx2.AsyncClient(auth=oauth) as http_client:
58+
async with httpx2.AsyncClient(auth=oauth, timeout=httpx2.Timeout(30.0, read=300.0)) as http_client:
5959
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
6060
async with Client(transport) as client:
6161
result = await client.list_tools()

‎docs_src/oauth_clients/tutorial002.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ async def set_client_info(self, client_info: OAuthClientInformationFull) -> None
3535

3636

3737
async def main() -> None:
38-
async with httpx2.AsyncClient(auth=oauth) as http_client:
38+
async with httpx2.AsyncClient(auth=oauth, timeout=httpx2.Timeout(30.0, read=300.0)) as http_client:
3939
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
4040
async with Client(transport) as client:
4141
result = await client.list_tools()

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,9 @@ async def _default_redirect_handler(authorization_url: str) -> None:
233233
await self._run_session(read_stream, write_stream)
234234
else:
235235
print("📡 Opening StreamableHTTP transport connection with auth...")
236-
async with httpx2.AsyncClient(auth=oauth_auth) as custom_client:
236+
async with httpx2.AsyncClient(
237+
auth=oauth_auth, timeout=httpx2.Timeout(30.0, read=300.0)
238+
) as custom_client:
237239
async with streamable_http_client(url=self.server_url, http_client=custom_client) as (
238240
read_stream,
239241
write_stream,

‎examples/snippets/clients/identity_assertion_client.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ async def main() -> None:
6666
scope="user",
6767
)
6868

69-
async with httpx2.AsyncClient(auth=oauth_auth) as http_client:
69+
async with httpx2.AsyncClient(auth=oauth_auth, timeout=httpx2.Timeout(30.0, read=300.0)) as http_client:
7070
async with streamable_http_client("http://localhost:8001/mcp", http_client=http_client) as (read, write):
7171
async with ClientSession(read, write) as session:
7272
await session.initialize()

‎examples/snippets/clients/oauth_client.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ async def main():
7272
callback_handler=handle_callback,
7373
)
7474

75-
async with httpx2.AsyncClient(auth=oauth_auth) as custom_client:
75+
async with httpx2.AsyncClient(auth=oauth_auth, timeout=httpx2.Timeout(30.0, read=300.0)) as custom_client:
7676
async with streamable_http_client("http://localhost:8001/mcp", http_client=custom_client) as (read, write):
7777
async with ClientSession(read, write) as session:
7878
await session.initialize()

‎examples/stories/_harness.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,9 @@ async def _run() -> None:
182182
# server origin and relative paths like "/mcp" resolve.
183183
parts = urlsplit(url)
184184
base = f"{parts.scheme}://{parts.netloc}"
185-
http = await stack.enter_async_context(httpx2.AsyncClient(base_url=base))
185+
http = await stack.enter_async_context(
186+
httpx2.AsyncClient(base_url=base, timeout=httpx2.Timeout(30.0, read=300.0))
187+
)
186188
make = targets
187189
if build_auth is not None:
188190
http.auth = build_auth(http)

‎tests/docs_src/test_client_transports.py‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
"""`docs/client/transports.md`: every claim the page makes, proved against the real SDK."""
22

33
import inspect
4+
from typing import Any
45

6+
import httpx2
57
import pytest
68

7-
from docs_src.client_transports import tutorial001, tutorial004
9+
from docs_src.client_transports import tutorial001, tutorial002, tutorial003, tutorial004
810
from mcp import Client
911
from mcp.client.stdio import get_default_environment
1012
from mcp.client.streamable_http import streamable_http_client
13+
from mcp.server import MCPServer
1114

1215
# See test_index.py for why this is a per-module mark and not a conftest hook.
1316
pytestmark = [pytest.mark.anyio, pytest.mark.filterwarnings("error::mcp.MCPDeprecationWarning")]
@@ -46,6 +49,40 @@ async def test_streamable_http_configuration_lives_on_the_httpx_client() -> None
4649
]
4750

4851

52+
async def test_the_timeout_on_the_page_is_the_sdk_clients_and_a_client_without_one_has_five_seconds(
53+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
54+
) -> None:
55+
"""tutorial002 and tutorial003: the `timeout=` tutorial003 passes is what `Client(url)` builds for itself
56+
(30 seconds, 300 for reads); an `httpx2.AsyncClient` built without one has httpx2's 5-second default."""
57+
async with httpx2.AsyncClient() as bare:
58+
assert bare.timeout == httpx2.Timeout(5.0)
59+
60+
mcp = MCPServer("Bookshop")
61+
62+
@mcp.tool()
63+
def search_books(query: str) -> str:
64+
"""Search the catalog."""
65+
raise NotImplementedError
66+
67+
app = mcp.streamable_http_app()
68+
built: list[httpx2.Timeout] = []
69+
70+
class InProcessClient(httpx2.AsyncClient):
71+
"""Every `httpx2.AsyncClient` the two programs build, routed to the server above."""
72+
73+
def __init__(self, **kwargs: Any) -> None:
74+
super().__init__(transport=httpx2.ASGITransport(app=app), **kwargs)
75+
built.append(self.timeout)
76+
77+
monkeypatch.setattr(httpx2, "AsyncClient", InProcessClient)
78+
async with mcp.session_manager.run():
79+
await tutorial002.main()
80+
await tutorial003.main()
81+
82+
assert capsys.readouterr().out == "['search_books']\n['search_books']\n"
83+
assert built == [httpx2.Timeout(30.0, read=300.0), httpx2.Timeout(30.0, read=300.0)]
84+
85+
4986
async def test_stdio_parameters_go_straight_to_client() -> None:
5087
"""tutorial004: `Client` takes the `StdioServerParameters` directly, and nothing is spawned until you enter it."""
5188
client = Client(tutorial004.server)

0 commit comments

Comments
 (0)