Python: Replace create_mcp_http_client with direct httpx.AsyncClient in MCPStreamableHTTPTool#4849
Python: Replace create_mcp_http_client with direct httpx.AsyncClient in MCPStreamableHTTPTool#4849eavanvalkenburg wants to merge 5 commits intomicrosoft:mainfrom
Conversation
Add a header_provider callback parameter to MCPStreamableHTTPTool that
enables injecting dynamic per-request HTTP headers from runtime kwargs
(originating from FunctionInvocationContext.kwargs set in agent middleware).
The implementation uses contextvars and httpx event hooks to ensure headers
are task-local and safe for concurrent tool calls:
- header_provider receives the runtime kwargs dict and returns headers
- call_tool sets a ContextVar before delegating to MCPTool.call_tool
- An httpx request event hook reads from the ContextVar and injects headers
Example usage:
mcp_tool = MCPStreamableHTTPTool(
name="web-api",
url="https://api.example.com/mcp",
header_provider=lambda kwargs: {
"X-Auth-Token": kwargs.get("auth_token", ""),
},
)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t#4808) Replace import of create_mcp_http_client (not explicitly exported from mcp.client.streamable_http) with inline httpx.AsyncClient creation using the same MCP defaults (follow_redirects=True, 30s timeout, 300s read). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||
eavanvalkenburg
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 89%
✓ Correctness
The diff replaces
create_mcp_http_client()with an inlinehttpx.AsyncClient(...)using identical defaults (follow_redirects=True, timeout 30s/read 300s), which is functionally correct. However, the surrounding code has a pre-existing bug that this diff doesn't fix but is worth noting: every call toget_mcp_client()appends a new_inject_headerscallback tohttp_client.event_hooks["request"](line 1434) on the same cachedself._httpx_client. If the tool reconnects multiple times, duplicate hooks accumulate and headers are injected multiple times per request. This isn't introduced by the diff but is a correctness concern in the method this diff touches.
✓ Security Reliability
The diff replaces the MCP SDK's
create_mcp_http_client()helper with an equivalent inlinehttpx.AsyncClientconstruction. The hardcoded timeout values (30.0s general, 300.0s read) exactly match the SDK'sMCP_DEFAULT_TIMEOUTandMCP_DEFAULT_SSE_READ_TIMEOUTconstants, so the behavior is preserved. However, this introduces a maintenance coupling: if the MCP SDK updates its defaults in a future version, this code won't track those changes. More critically, the pre-existing event_hooks append at line 1434 (untouched by this diff) causes unbounded growth of_inject_headerscallbacks on everyget_mcp_client()call, which is a reliability/resource leak concern that this change should address since it's creating the client specifically for this code path.
✓ Test Coverage
The diff replaces
create_mcp_http_client()(from the MCP SDK) with an inlinehttpx.AsyncClient(follow_redirects=True, timeout=httpx.Timeout(30.0, read=300.0))for the auto-created client whenheader_provideris set. The existing testtest_mcp_streamable_http_tool_header_provider_with_httpx_event_hookexercises the changed code path and verifies that a client is created and the event hook is wired up. However, no test asserts the specific configuration properties (follow_redirects, timeout) of the auto-created client, meaning a regression in these values (or a drift from what the MCP SDK expects) would go undetected. This is a minor gap, not a blocking issue.
✗ Design Approach
The diff replaces a lazy import of
create_mcp_http_client()with an equivalent inlinehttpx.AsyncClientconstruction using identical default values (follow_redirects=True,timeout=30.0/read=300.0). The values are functionally correct for now, but two concerns exist: (1) hardcoding the timeout values decouples from MCP's own constants so future MCP default changes won't be inherited; (2) the pre-existinghttp_client.event_hooks["request"].append(_inject_headers)at line 1434 sits outside theif http_client is None:guard, so on every call toget_mcp_client()a new closure is appended to the same persistentAsyncClient, causing the hook list to grow unboundedly — headers get injected N times after N tool calls.
Flagged Issues
- Event hook accumulation:
http_client.event_hooks["request"].append(_inject_headers)(line 1434) executes on everyget_mcp_client()call, not just on first client creation. Becauseself._httpx_clientis reused across calls, the hook list grows by one closure per invocation, causing headers to be injected N times after N tool calls. The hook registration should be moved inside theif http_client is None:block, immediately after client creation.
Suggestions
- The timeout values (30.0 / 300.0) are hardcoded copies of MCP SDK constants (
MCP_DEFAULT_TIMEOUT,MCP_DEFAULT_SSE_READ_TIMEOUTfrommcp.shared._httpx_utils). Consider importing and using the SDK constants so the defaults stay in sync if the SDK updates them. - Consider adding assertions in
test_mcp_streamable_http_tool_header_provider_with_httpx_event_hookto verify the auto-created httpx client's configuration (follow_redirects=True,timeout.connect == 30.0,timeout.read == 300.0) since this diff explicitly replacescreate_mcp_http_client()with hardcoded values.
Automated review by eavanvalkenburg's agents
There was a problem hiding this comment.
Pull request overview
Updates the Python MCP integration so MCPStreamableHTTPTool no longer depends on an MCP-SDK internal HTTP client factory when header_provider is used, enabling reliable per-call header injection (e.g., forwarding AgentContext-derived values) via a standard httpx.AsyncClient.
Changes:
- Add
header_providersupport toMCPStreamableHTTPToolusing acontextvars.ContextVarand anhttpxrequest event hook to inject per-invocation headers. - Replace MCP SDK internal HTTP client construction with direct
httpx.AsyncClient(...)defaults when needed. - Add unit tests covering contextvar behavior and event-hook-based header injection.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| python/packages/core/agent_framework/_mcp.py | Introduces header_provider plumbing, ContextVar-based header scoping, and httpx event hook injection. |
| python/packages/core/tests/core/test_mcp.py | Adds tests for header provider behavior, contextvar reset, and request hook injection. |
…pass AgentContext to MCPStreamableHTTPTool
eavanvalkenburg
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 89%
✓ Correctness
The diff fixes a real bug where repeated
get_mcp_client()calls would accumulate duplicate httpx event hooks, and replaces hardcoded timeout values with MCP SDK constants. The hook deduplication logic usinghasattris correct, and the new tests properly cover the fix. The main concern is importing frommcp.shared._httpx_utils, a private (underscore-prefixed) module of the MCP SDK, which could break on SDK upgrades. All logic changes are correct and the test cleanup improvements are good practice.
✓ Security Reliability
The changes fix a real bug (duplicate event hooks on repeated
get_mcp_clientcalls) and align timeout defaults with the MCP SDK. The implementation is sound: usinghasattrto guard against duplicate hook registration,contextvarsfor async-safe per-call headers, and proper test cleanup withaclose(). One concern is the import frommcp.shared._httpx_utils, which is a private module — but the constants are in its__all__, reducing the risk. The user-provided httpx client test (test_mcp_streamable_http_tool_header_provider_with_user_httpx_client) was not updated with the sametry/finallycleanup pattern, leaving it inconsistent, but it does callaclose()at the end. No security, injection, or resource leak issues found in the changed code.
✓ Test Coverage
The diff fixes a hook duplication bug in
get_mcp_client()(usinghasattrguard), replaces hardcoded timeout values with MCP SDK constants, and adds a new test for the dedup fix plus improves an existing test with proper resource cleanup and timeout assertions. Test coverage for the changed behavior is adequate: the newtest_mcp_streamable_http_tool_hook_not_duplicated_on_repeated_get_mcp_clienttest directly validates the fix, and the updatedtest_mcp_streamable_http_tool_header_provider_with_httpx_event_hooktest verifies the timeout constants and adds properaclose()cleanup. Minor gaps exist around testing dedup with a user-provided httpx client and error-path behavior ofheader_provider, but these are not blocking.
✗ Design Approach
The PR introduces two changes: (1) replacing hardcoded httpx timeout literals with constants imported from
mcp.shared._httpx_utils, and (2) guarding the event-hook registration withhasattrto prevent duplicate hooks on repeatedget_mcp_client()calls. The hook-deduplication fix is correct and the test covers it well. However, importing from a private module (_httpx_utils) is a fragile design choice, and thehasattr-based guard has a structural smell that the two# type: ignore[attr-defined]comments confirm — the attribute is never declared in__init__.
Flagged Issues
- Importing from
mcp.shared._httpx_utils(underscore-prefixed = private module) couples the code to MCP SDK internals with no stability guarantees — the module can be renamed, moved, or removed between minor releases. The previous hardcoded literals (30.0 / 300.0) were explicit and stable. At minimum, add a try/except import with literal fallbacks so the code doesn't break on SDK upgrades.
Suggestions
- Consider adding a test that calls
get_mcp_client()multiple times when a user-providedhttp_clientis passed, to verify the hook dedup guard also works in that code path. - Consider adding a test that verifies behavior when
header_providerraises an exception — confirming the exception propagates cleanly without leaving the contextvar in a dirty state. - The
_inject_headers_hookattribute is never declared in__init__, which is whyhasattrand two# type: ignore[attr-defined]comments are needed. A cleaner design: declareself._inject_headers_hook: Callable | None = Nonein__init__, then checkif self._inject_headers_hook is Noneinget_mcp_client. This eliminates thehasattrsmell, removes the type ignores, and makes the class's full interface visible at construction time.
Automated review by eavanvalkenburg's agents
…ocationContext Addresses PR review comment: exercises the full pipeline from FunctionInvocationContext.kwargs through FunctionTool.invoke to MCPStreamableHTTPTool.call_tool and header_provider, rather than testing call_tool in isolation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Motivation and Context
When
header_providerwas set onMCPStreamableHTTPTool, the code usedcreate_mcp_http_client()from the MCP library to create an HTTP client. This function may not be available in all versions of the MCP SDK and introduces an unnecessary dependency on an internal MCP helper, causing failures when trying to passAgentContextheaders.Fixes #4808
Description
The root cause was that
MCPStreamableHTTPTool._get_client()relied onmcp.client.streamable_http.create_mcp_http_clientto construct the defaulthttpx.AsyncClientwhen aheader_providerwas configured but no explicithttpx_clientwas supplied. This was replaced with a directhttpx.AsyncClient(follow_redirects=True, timeout=httpx.Timeout(30.0, read=300.0))construction, which matches the equivalent defaults and eliminates the problematic import. This ensures header injection (used for passingAgentContext) works reliably without depending on the MCP SDK's internal client factory.Contribution Checklist