Skip to content

Document the timeout a hand-built httpx2 client needs - #3618

Merged
maxisbey merged 1 commit into
mainfrom
3238-docs-http-client-timeout
Oct 2, 2026
Merged

maxisbey merged 1 commit into
mainfrom
3238-docs-http-client-timeout

Conversation

@maxisbey

@maxisbey maxisbey commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #3238.

This documents the supported way to customise the HTTP client rather than exporting the helper the issue asked for. create_mcp_http_client is httpx2.AsyncClient(...) with a timeout filled in, so building the client yourself needs nothing private once that timeout is spelled out. The docs showed it but never said why it matters, and several of our own examples left it off.

What changed

  • docs/client/transports.md
    • Under "Bring your own httpx2.AsyncClient": keep the timeout=. A client built without one gets httpx2's 5-second default, and a tool call that runs longer than that fails with a read timeout. The value on the page (30 seconds, 300 for reads) is the one the SDK's own client uses.
  • Clients that were built without a timeout now pass timeout=httpx2.Timeout(30.0, read=300.0):
    • docs_src/oauth_clients/tutorial001.py and tutorial002.py
    • docs_src/identity_assertion/tutorial001.py
    • examples/snippets/clients/oauth_client.py and identity_assertion_client.py
    • examples/clients/simple-auth-client
    • examples/stories/_harness.py
  • tests/docs_src/test_client_transports.py
    • One test for the new claim: it runs the page's two HTTP programs in process and checks that the tutorial's client and the one Client(url) builds have the same timeouts, and that a client built without timeout= has 5 seconds.

Nothing changes under src/, and the translated pages aren't regenerated here.

AI Disclaimer

An `httpx2.AsyncClient` built without `timeout=` gets httpx2's 5-second
default, so a tool call that runs longer than that fails with a read timeout
once the client is passed to `streamable_http_client`. The client transports
page now says to keep the `timeout=` its example passes, which is the one the
SDK's own client uses (30 seconds, 300 for reads).

The OAuth and identity assertion tutorials, their example counterparts, the
simple-auth client and the stories harness built their clients without a
timeout; they now pass the same one.

Fixes #3238
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

📚 Documentation preview

Preview https://pr-3618.mcp-python-docs.pages.dev
Deployment https://77ae7ec5.mcp-python-docs.pages.dev
Commit 54c5b58
Triggered by @maxisbey
Updated 2026-10-02 10:50:03 UTC

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="docs/client/transports.md">

<violation number="1" location="docs/client/transports.md:42">
P2: This overstates `httpx2`’s read timeout as a maximum tool-call duration. A call can run longer than five seconds when response data or keepalives arrive within the read interval, while a shorter call can time out if an HTTP read is idle; document the idle-read condition instead.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread docs/client/transports.md
* You own the `httpx2.AsyncClient`, so **you** enter and exit it. The SDK never closes a client it didn't create.
* `streamable_http_client(url, http_client=...)` returns a transport, and `Client(transport)` accepts it like anything else.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This overstates httpx2’s read timeout as a maximum tool-call duration. A call can run longer than five seconds when response data or keepalives arrive within the read interval, while a shorter call can time out if an HTTP read is idle; document the idle-read condition instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/client/transports.md, line 42:

<comment>This overstates `httpx2`’s read timeout as a maximum tool-call duration. A call can run longer than five seconds when response data or keepalives arrive within the read interval, while a shorter call can time out if an HTTP read is idle; document the idle-read condition instead.</comment>

<file context>
@@ -39,6 +39,8 @@ Two things to notice:
 * You own the `httpx2.AsyncClient`, so **you** enter and exit it. The SDK never closes a client it didn't create.
 * `streamable_http_client(url, http_client=...)` returns a transport, and `Client(transport)` accepts it like anything else.
 
+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.
+
 One TLS note: `httpx2` verifies certificates against the operating system trust store (via
</file context>
Suggested change
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.
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, so a call can fail if an HTTP read receives no data for 5 seconds.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

LGTM — a docs/examples-only change that spells out the timeout the SDK already uses, with a test pinning the claim; the one inline note is a nit about bounding the in-process round trips.

Extended reasoning...

The diff touches only docs/client/transports.md, seven docs_src/examples clients that gain timeout=httpx2.Timeout(30.0, read=300.0), and one new test in tests/docs_src/test_client_transports.py; nothing under src/ changes and no auth, crypto or data-exposure surface is altered (the examples' OAuth providers are passed through unchanged). The page's numbers match MCP_DEFAULT_TIMEOUT / MCP_DEFAULT_SSE_READ_TIMEOUT in src/mcp/shared/_httpx_utils.py, and the test's monkeypatch reaches Client(url) because create_mcp_http_client constructs via the httpx2.AsyncClient module attribute; the InProcessClient only receives timeout=/headers= from the two programs, so there is no transport= collision. The changed docs_src files are not embedded in README.md, so no snippet regeneration was needed, and the repository has no CODEOWNERS file. The change is small, mechanical and self-contained, and the sole finding is a test-hardening nit, which decided approve.

Comment on lines +79 to +81
await tutorial002.main()
await tutorial003.main()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit (optional): a maintainer whose change breaks the in-process handshake gets a hung CI job here instead of a failed test. The awaits of tutorial002.main() and tutorial003.main() at tests/docs_src/test_client_transports.py:79-81 are full client round trips over an ASGI transport with nothing bounding them, and httpx2.ASGITransport does not apply the client's timeout. Fix: wrap the two program runs (and the session_manager.run() block around them) in anyio.fail_after(5) so a stalled list_tools fails the test instead of stalling the run.

Why this was flagged

The new test enters mcp.session_manager.run() and awaits tutorial002.main() then tutorial003.main() at tests/docs_src/test_client_transports.py:79-81. Each main() does Client.list_tools() over streamable_http_client routed through httpx2.ASGITransport (tests/docs_src/test_client_transports.py:74). If the server never answers a POST or the session manager stalls, that await never returns: ClientSession has no default read timeout and the in-process ASGI transport does not enforce the Timeout(30.0, read=300.0) the client carries. AGENTS.md asks that indefinite waits be wrapped in anyio.fail_after(5) to prevent hangs; the test adds none, so a regression shows up as a hung job rather than a test failure. On the base branch this test does not exist, so no new unbounded wait was present.

Verification: nit. Trigger: any future regression that stalls the streamable-HTTP handshake or a tools/list POST turns this test into a hang rather than a failure. The test at tests/docs_src/test_client_transports.py:78-80 runs await tutorial002.main(); await tutorial003.main() with no fail_after. Client defaults read_timeout_seconds=None (src/mcp/client/session.py:408), so the RPC wait is unbounded.

@maxisbey
maxisbey merged commit ebf6e5a into main Oct 2, 2026
42 checks passed
@maxisbey
maxisbey deleted the 3238-docs-http-client-timeout branch October 2, 2026 11:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose create_mcp_http_client and McpHttpClientFactory as public API (2.0 made them private-only)

1 participant