Skip to content

fix(mcp-builder): support mcp>=2 streamable_http_client import and custom headers - #1742

Open
Kuldeeep18 wants to merge 4 commits into
anthropics:mainfrom
Kuldeeep18:fix/mcp-builder-streamable-http-client-import
Open

Kuldeeep18 wants to merge 4 commits into
anthropics:mainfrom
Kuldeeep18:fix/mcp-builder-streamable-http-client-import

Conversation

@Kuldeeep18

Copy link
Copy Markdown

Fixes #1668

Problem

In mcp>=2.0.0, streamablehttp_client was renamed to streamable_http_client, and custom HTTP headers are configured via create_mcp_http_client / http_client rather than as a direct kwarg to streamable_http_client.

Because skills/mcp-builder/scripts/connections.py unconditionally imported streamablehttp_client at module scope, importing connections or running evaluation.py fails on startup when installing requirements.txt (which specifies mcp>=1.1.0 without an upper bound):

Traceback (most recent call last):
  File ".../scripts/evaluation.py", line 19, in <module>
    from connections import create_connection
  File ".../scripts/connections.py", line 10, in <module>
    from mcp.client.streamable_http import streamablehttp_client
ImportError: cannot import name 'streamablehttp_client' from 'mcp.client.streamable_http'

Solution

  1. Compatible import: In skills/mcp-builder/scripts/connections.py, import streamable_http_client and create_mcp_http_client with fallback to streamablehttp_client as streamable_http_client for mcp<2.0.0.
  2. Context configuration: In MCPConnectionHTTP._create_context():
    • When create_mcp_http_client is available, create http_client with custom headers and pass to streamable_http_client(url=self.url, http_client=http_client).
    • On mcp<2.0.0, pass headers=self.headers directly.
  3. Regression tests: Added skills/mcp-builder/scripts/test_connections.py testing import resolution, stdio/sse/http transport factories, context manager creation, and validation error cases.

Verification

  • Ran pytest skills/mcp-builder/scripts/test_connections.py -v: all 5 tests passed.
  • Verified python skills/mcp-builder/scripts/evaluation.py --help under mcp 2.1.1 and Python 3.14: cleanly displays full usage and exits 0.
  • Verified compilation with python -m py_compile.

@yao23 yao23 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.

Static review of 8a2d825: the custom-header path creates an HTTP client but does not enter or close it. The SDK only manages clients it creates internally; caller-supplied clients remain caller-owned.

The current HTTP test uses no headers and never enters the context, so it misses this path. Could we register the custom client with the connection’s AsyncExitStack and add async regressions covering both normal exit and initialization failure?

AI-assisted static review; I haven’t run a reproduction yet.

@yao23

yao23 commented Sep 10, 2026

Copy link
Copy Markdown

Following up with an independent local reproduction at 8a2d825fa2d5fbaf2d375554985bd80503e4eda0, using Python 3.11.15 with both mcp==1.29.1 and mcp==2.1.1.

Both SDK versions produced the same results:

  • Unmodified PR: 5 passed, 2 failed. Normal exit and an explicitly asserted session.initialize() failure both left the caller-owned client open. The failures were at client.is_closed is True.
  • With the minimal fix: 7 passed. The original five tests also passed separately.

The fix registers cleanup immediately after creating the custom client:

if http_client is not None:
    self._stack.push_async_callback(http_client.aclose)

This lets the transport exit before closing the caller-owned client and also covers initialization failure.

The regressions retain the real SDK transport lifecycle and the actual client created by MCPConnectionHTTP, stubbing only network I/O and ClientSession responses. They do not mock is_closed or close the client from a stub. This is an isolated lifecycle regression, not a live-network end-to-end test.

Implementation and validation were AI-assisted.

@yao23

yao23 commented Sep 10, 2026

Copy link
Copy Markdown

I independently reproduced the custom-header HTTP client lifecycle issue at
8a2d825fa2d5fbaf2d375554985bd80503e4eda0 on CPython 3.11.15 with both
mcp==1.29.1 and mcp==2.1.1. Both isolated environments used
anthropic==1.4.0, pytest==9.1.1, and pytest-asyncio==1.4.0; imports and
evaluation.py --help passed in both.

The tests retain the real client created by MCPConnectionHTTP and the real SDK
streamable_http_client lifecycle. Only the network writer and ClientSession
response are stubbed. The stubs do not close the client, mock is_closed, or
fabricate closure. The initialization-error case first confirms the specific
expected exception, then checks the real client's state. This is therefore a
transport-isolated regression, not a live-network end-to-end test.

On the unmodified head, each SDK run produced 5 passed, 2 failed; both failures
were only at the final client.is_closed is True assertion. Registering
http_client.aclose on the connection's AsyncExitStack immediately after
creation made both runs report 7 passed. The five existing connection tests
also pass independently under both versions (5 passed, 2 deselected).

Re-run from a clean checkout, with the two patch files in the current directory:

git checkout --detach 8a2d825fa2d5fbaf2d375554985bd80503e4eda0
python3.11 -m venv .venv-mcp-1.29.1
python3.11 -m venv .venv-mcp-2.1.1
.venv-mcp-1.29.1/bin/python -m pip install 'mcp==1.29.1' 'anthropic==1.4.0' 'pytest==9.1.1' 'pytest-asyncio==1.4.0'
.venv-mcp-2.1.1/bin/python -m pip install 'mcp==2.1.1' 'anthropic==1.4.0' 'pytest==9.1.1' 'pytest-asyncio==1.4.0'
.venv-mcp-1.29.1/bin/python skills/mcp-builder/scripts/evaluation.py --help
.venv-mcp-2.1.1/bin/python skills/mcp-builder/scripts/evaluation.py --help
git apply regression-tests.patch
.venv-mcp-1.29.1/bin/python -m pytest -q skills/mcp-builder/scripts/test_connections.py
.venv-mcp-2.1.1/bin/python -m pytest -q skills/mcp-builder/scripts/test_connections.py
git apply minimal-fix.patch
.venv-mcp-1.29.1/bin/python -m pytest -q skills/mcp-builder/scripts/test_connections.py
.venv-mcp-2.1.1/bin/python -m pytest -q skills/mcp-builder/scripts/test_connections.py
Regression test patch
diff --git a/skills/mcp-builder/scripts/test_connections.py b/skills/mcp-builder/scripts/test_connections.py
index f914eba..3078e1b 100644
--- a/skills/mcp-builder/scripts/test_connections.py
+++ b/skills/mcp-builder/scripts/test_connections.py
@@ -6,10 +6,15 @@ Verifies that:
 - argument validation operates as expected for missing parameters or unsupported transports
 """
 
+from contextlib import asynccontextmanager
+
+import anyio
 import pytest
 import sys
 from pathlib import Path
 
+import mcp.client.streamable_http as streamable_http_module
+
 # Add scripts directory to sys.path
 SCRIPT_DIR = Path(__file__).resolve().parent
 if str(SCRIPT_DIR) not in sys.path:
@@ -24,6 +29,58 @@ from connections import (
 )
 
 
+class ExpectedInitializationError(RuntimeError):
+    """Sentinel error used to verify initialization-failure cleanup."""
+
+
+class StubClientSession:
+    """Minimal session stub; transport and HTTP-client lifecycles stay real."""
+
+    initialize_error = None
+
+    def __init__(self, read, write):
+        self.read = read
+        self.write = write
+
+    async def __aenter__(self):
+        return self
+
+    async def __aexit__(self, exc_type, exc_val, exc_tb):
+        return False
+
+    async def initialize(self):
+        if self.initialize_error is not None:
+            raise self.initialize_error
+
+
+@pytest.fixture
+def captured_production_http_client(monkeypatch):
+    """Capture the real client passed into the real SDK transport context."""
+    captured = {}
+    real_streamable_http_client = streamable_http_client
+
+    @asynccontextmanager

@Kuldeeep18

Copy link
Copy Markdown
Author

Thanks for the thorough review and independent reproduction, @yao23!

You're spot on—caller-supplied HTTP clients remain caller-owned in the MCP SDK, so failing to register cleanup left the custom client open.

In commit 0ba9e31, I have updated the PR to:

  1. Register http_client.aclose via self._stack.push_async_callback(http_client.aclose) when custom headers create an HTTP client.
  2. Add isolated lifecycle regression tests covering both normal context exit and initialization failure without requiring live network calls.

All 7 tests are passing. Thanks again for catching this!

@yao23

yao23 commented Sep 11, 2026

Copy link
Copy Markdown

Kuldeeep18

Thanks for incorporating this so quickly!

One small test-hardening suggestion: could the lifecycle tests assert that the custom client was actually captured before checking its state?

client = captured_production_http_client.get("client")
assert client is not None
assert client.is_closed is True

With the current conditional checks, a regression that stops creating or passing the custom client could make the tests pass vacuously. This should preserve the network-isolated setup while ensuring the intended custom-header path was exercised.

AI-assisted follow-up review.

…stom headers

Fixes anthropics#1668

In mcp>=2.0.0, streamablehttp_client was renamed to streamable_http_client,
and custom headers are configured via create_mcp_http_client / http_client rather
than as a direct keyword argument. Because connections.py unconditionally
imported streamablehttp_client at module scope, importing connections or running
evaluation.py failed with:

    ImportError: cannot import name 'streamablehttp_client' from 'mcp.client.streamable_http'

Changes:
- Add backwards- and forwards-compatible import fallback for streamable_http_client
  to support both mcp>=2 and mcp<2.
- Configure custom headers using create_mcp_http_client when available in mcp>=2,
  with fallback to headers parameter on mcp<2.
- Add test_connections.py regression tests covering all connection transports
  (stdio, sse, http), context creation, and validation error cases.
@Kuldeeep18
Kuldeeep18 force-pushed the fix/mcp-builder-streamable-http-client-import branch from 0ba9e31 to b26eba8 Compare September 11, 2026 13:20
@Kuldeeep18

Copy link
Copy Markdown
Author

Good catch, @yao23! Updated in b26eba8:

  1. Added explicit \�ssert client is not None\ in both lifecycle tests (\ est_http_custom_headers_client_closed_on_exit\ and \ est_http_custom_headers_client_closed_on_init_failure) to ensure the custom client was actually captured and that the tests cannot pass vacuously.
  2. Added @pytest.mark.skipif(connections.create_mcp_http_client is None, ...)\ so that environments running legacy \mcp < 2\ (where \streamable_http_client\ doesn't use caller-owned custom HTTP clients) cleanly skip these lifecycle tests rather than failing.
  3. Rebased on latest \upstream/main.

All 7 tests pass cleanly. Thanks again for the follow-up review!

@98zc5g5jyw-arch

Copy link
Copy Markdown

Independent verification: checked out the PR branch and ran the new suite on a clean venv (Python 3.11, latest mcp 2.x, pytest 9.1): 7/7 passed, including the caller-owned HTTP client being closed on both normal exit and initialization failure. The mcp<2 fallback path is guarded by skipif(create_mcp_http_client is None) and degrades gracefully. Import shim is correct for both API generations. Minor observation (non-blocking): in the mcp>=2 branch, if self._stack were None the created http_client would not be registered for cleanup — unlikely to occur in practice, but a comment would help future readers. LGTM for merge.

@yao23 yao23 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.

Reviewed the current head b26eba8b7fab86fa74de9bc1d76a3ca0a4f53bf9. The caller-owned HTTP client is now registered with the connection’s AsyncExitStack, and the lifecycle regressions cover both normal context exit and initialization failure. The explicit client is not None assertions ensure the custom-header path cannot pass vacuously, while the legacy SDK path is guarded separately.

I previously reproduced the lifecycle defect at 8a2d825fa2d5fbaf2d375554985bd80503e4eda0 and verified the minimal correction under mcp==1.29.1 and mcp==2.1.1. I have not rerun the rebased current head in this review, but I found no remaining blocking issue in the current diff.

Approved. AI-assisted review.

@Kuldeeep18

Copy link
Copy Markdown
Author

Hi maintainers, just a gentle check-in on this PR. It has been independently verified and approved by community reviewers with all regression tests passing, and the branch is clean and up to date with \main. Whenever you have a moment, could someone from the team take a look for merge? Thank you!

@Kuldeeep18

Copy link
Copy Markdown
Author

Hi @cj-ant, @rlancemartin — checking in on this PR.

The branch is clean, conflict-free, and synced with latest \main. The fix and lifecycle regressions have been independently verified and approved by community reviewers (@yao23\ and @98zc5g5jyw-arch) with 7/7 tests passing under both \mcp==1.x\ and \mcp==2.x.

Whenever you have a moment, could someone from the Anthropic team please give it a final review for merge? Thank you!

@98zc5g5jyw-arch

Copy link
Copy Markdown

Follow-up re-review at head 7724488883.

Since our previous review at b26eba8b7fab (comment above), the only change is a merge of main (3337550, claude-api docs): connections.py and test_connections.py are byte-identical to the previously reviewed revision (blobs 3e906284, 23c35419). Re-verified at the new head (Python 3.11, mcp 2.2.0, pytest 9.1.1):

Verified

  • Full suite re-run at the new head: 7/7 passed (skills/mcp-builder/scripts/test_connections.py).
  • Two-state, to show the lifecycle tests are not vacuous: head test file against the pre-fix connections.py (2951788) → 2 failed / 5 passed, both failures on client.is_closed is True; against the new head → 7 passed.
  • Import shim both arms: real mcp 2.2.0 resolves streamable_http_client + create_mcp_http_client; a simulated mcp<2 package (offline) falls back to streamablehttp_client, leaves create_mcp_http_client is None (so the skipif guard trips cleanly), and the header fallback path executes.
  • My earlier non-blocking observation is addressed: cleanup registration is now guarded (if http_client is not None and self._stack is not None).
  • py_compile clean; no network/subprocess patterns in either touched file; PR still MERGEABLE (BLOCKED = maintainer review pending, not a code issue).

No blockers from my side. Still LGTM for merge.

@Kuldeeep18

Copy link
Copy Markdown
Author

Thank you for the thorough follow-up re-review and independent verification, @98zc5g5jyw-arch! Appreciate you taking the time to test both the dual-arm compatibility and the lifecycle edge cases.

Hi @rlancemartin @maheshmurag @cj-ant — this PR is fully green, conflict-free, and has now been independently verified and approved by community reviewers (@yao23 and @98zc5g5jyw-arch) with all 7/7 lifecycle regression tests passing under both mcp==1.x and mcp>=2.x.

Whenever you have a moment, could someone from the Anthropic team please give it a final review for merge? Thank you!

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.

mcp-builder: requirements.txt allows mcp>=2, where connections.py fails at import (streamablehttp_client renamed)

3 participants