feat: mcp v2 compatible changes under a flag - #3708
Conversation
|
@strandly-the-agent review the PR and tell me what's missing or to be improved. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
Issue (Important): The whole point of this layer is 2.x compatibility, but nothing in CI verifies it. With the pin at Suggestion: Add a lightweight CI job / nox session that installs |
|
Assessment: Comment (approve-worthy with minor follow-ups) Clean, well-scoped groundwork PR. The single internal Review Categories
Nicely documented change — the PR description alone made the review straightforward. |
strandly-the-agent
left a comment
There was a problem hiding this comment.
Reviewed 8aed1e85 end to end. Short version: the code that ships is correct — but CI is red because of this PR, and the description claims more than the code delivers.
The useful thing I could do here that a normal review can't: mcp==2.0.0 is a real release and installable, so I built venvs on both lines and actually ran the 2.x branch instead of reasoning about it.
| Check | Result |
|---|---|
pytest tests/strands/tools/mcp/ on mcp 1.23.0 (pin floor) |
✅ 217 passed |
| Same suite on mcp 2.0.0 | 🔴 64 failed / 153 passed |
Python / Lint + CI Gate on this head |
🔴 failure (main is green) |
ruff check locally |
✅ All checks passed |
mypy ./src locally, mcp 1.23.0 |
🔴 5 errors, all in _compat.py — reproduces the CI failure |
| HTTPX leak on the 2.x branch | 🔴 confirmed by execution (aclose() never called) |
MCP_V2 probe, MCPError alias, transport names vs real 2.0.0 |
✅ all correct |
| OTel 1.x-only gating | ✅ real (connect wrapped on 1.23.0, untouched on 2.0.0) |
The one blocker is mundane: mypy runs inside hatch fmt --linter --check (pyproject.toml:139-149 → lint-check = ["ruff check", "mypy ./src"]), which hatch run prepare and the gating lint job both call. It's failing on _compat.py. That also means the checklist's "I ran hatch run prepare" can't be accurate for this commit.
The framing question is the one that matters (in Questions below): is this PR meant to make 2.x work, or only import so the pin can move later? The body says "imports and runs cleanly on both major lines" and "the pin can later widen to <3 with no code changes" — I measured both and neither holds today. If the answer is "import-only groundwork," the code is basically right and only the wording needs fixing. I've filed nothing; the 2.x functional gaps are pre-existing and listed below for you to decide on.
3 inline comments. Nothing here re-litigates the resolved threads on #3611.
Questions (the framing one is blocking)
Blocking
-
Is the goal "2.x works" or "2.x imports"? This decides how everything else is tiered. What ships makes
strands.tools.mcpimportable on 2.x and consolidates the version probe — both real and both correct. ButMCPClientis not functional on 2.x:start()raises unconditionally, ~15 camelCase field accesses break, andtasks_config=raisesImportErrorat construction. If that's intentional staging, could the description say so? As written the next reader will believe the pin can move. -
Merge order — this is stacked on #3611, which is still open, and includes its commit. GitHub's Files-changed tab merges both into one diff, so anyone approving from that tab is implicitly approving #3611's content without it clearing its own gate. Is bottom-up (land #3611 first, let this shrink to its own commit) the intent?
Non-blocking, but worth settling before more code branches on the pattern
-
One boolean is standing in for at least six independently-probeable axes (property-vs-method, tasks surface, transport takes-a-client, transport yields-session-id, field casing, task types importable) — I verified each discriminates on its own.
_compat.pyalready uses per-nametry/exceptforMCPError/GetSessionIdCallbackbut the global flag for the transport. Since the body says lifecycle/tasks/native-OTel will all branch on this same flag, would per-capability constants age better than one flag a future 3.x can't name correctly? (hasattr(ClientSession, "discover")reads as "≥2" whileMCP_V2reads as "==2".) -
MCPTransportis public (mcp_types.py:47, exported in__all__) and quietly gets a second meaning here — on 2.xGetSessionIdCallbackdegrades to a structuralCallable[[], str | None]and the 3-tuple arm is simply dead. Worth deciding whatMCPTransportshould mean on 2.x before more code depends on the current shape? Relatedly, the PR carries noapi/needs-review/api/review-completelabel —check-api-review-labelis green only because it skips when no label is present. -
Should HTTPX ownership be a written rule for future adapters here — the adapter owns what it creates, never what it's given — rather than a per-call-site decision?
Appendix — non-blocking (6)
- ⚪
_compat.py:17-19— the comment callsClientSession.discover"the 2.x replacement for the removed initialize handshake," buthasattr(ClientSession, "initialize")isTrueon both installed lines. The spec removed the handshake; the Python client hasn't. Wording only. - ⚪
_compat.py:28-35catches onlyImportErrorand itsexceptbranch doesn't re-import frommcp, so an unrelated import failure inmcp.client.streamable_httpgets swallowed and you get a fake alias plus a worse stack trace later. TheMCPErrorsibling at:22-26doesn't have this gap because itsexceptre-imports from the same module. No caller triggers it today. - ⚪ Anything that adds a
discoverattribute toClientSessionon a real 1.x (subclass, backport, test double) flipsMCP_V2and sends the transport down the 2.x path. Contrived — same root cause as Question 3. - ⚪
test_mcp_instrumentation.py:426assertsregister_post_import_hookis called, which the new gating correctly skips on 2.x, so the suite can't be green there. Inherited from #3611's commit, not this one. - ⚪ No CI leg installs
mcp>=2, so every 2.x branch is mock-only. Smallest thing that would work today and passes right now: a@pytest.mark.skipif(not MCP_V2, …)import-smoke test plus one single-Python job doingpip install "mcp>=2.0.0,<3.0.0" --no-depsand running justtest__compat.py. That alone would have caught the vacuous-mock problem in comment 3. - ⚪ Codecov reports 3 uncovered lines in
_compat.py— exactly the 2.x/except ImportErrorbranches that need a realmcp>=2install to execute.
Pre-existing — needs an issue, not a fix here (4)
I verified with git diff 898cc0ef 8aed1e85 that this PR touches none of these lines. They're unchanged from main and only become reachable if the pin widens — so they are not regressions here, and I'm not treating them as blockers. Flagging because they're what stands between this PR and its stated goal. Happy to file them if you want.
MCPClient.start()raises on 2.x —mcp_client.py:1063callssession.get_server_capabilities(); 2.x made it aserver_capabilitiesproperty. Unconditional bootstrap, every transport. Repro against a real in-process server:AttributeError: 'ClientSession' object has no attribute 'get_server_capabilities'→MCPClientInitializationError.- ~15 camelCase→snake_case field sites — worst is
mcp_client.py:1007.isError, where theAttributeErroris swallowed into an errorToolResult, so every successful 2.x tool call reports as failed with the rawAttributeErroras the tool's output to the model. Wrong-answer shaped, not crash shaped. Alsomcp_agent_tool.py:77,82,83andmcp_client.py:622,1015,1149,1179-1199. tasks_config=raises a bareImportErrorat construction on 2.x (mcp_client.py:299) and isn't aliasable —ClientSession.experimentaldoesn't exist on 2.0.0, so:1259,1541,1584,1625have no 2.x surface. Needs a design call (clear "unsupported on 2.x" error vs. port to the extension), and #1659 already defers tasks pending the tasks-extension GA.tests_integ/mcp/*and the separatestrands-mcppackage are 2.x-broken (FastMCP,mcp.server.experimental.task_context;strands-mcphas its own<2.0.0pin). Out of scope, worth tracking so it isn't rediscovered piecemeal.
Automated review by strandly-the-agent — ran on both mcp lines with real installs. Solid work for a human to approve, not a gate; push back where I'm wrong.
|
Assessment update: Request Changes (correcting my earlier "Comment / approve-worthy" summary — that was wrong; CI is red because of this PR.) I independently reproduced the mypy failure @strandly-the-agent flagged, and it's a hard merge blocker regardless of the
The detailed dual-line review already on this PR covers everything else well (HTTPX leak, framing question, pre-existing 2.x gaps) — I won't re-litigate it. My one net-new emphasis: green CI is the gate here, and it's currently red on the supported floor. |
576da59 to
884973a
Compare
|
Re-review of the force-pushed head The rebase cleaned up the commit history and description, but the mypy blocker is unchanged — I re-ran it against the current head: (3 on Nothing else changed materially in |
|
Assessment update: Approve (was Request Changes) Re-reviewed the new head Verification
Clean, well-scoped compat layer with tests that pin the exact contracts (lifecycle ordering, per-line name resolution) rather than incidental details — nice work. |
|
Re-reviewed Verification of the new tests
Thanks for following through on the coverage — the branch-directional tests and the reload/restore pattern keep it honest without pretending to run both lines at once. |
b00237b to
b121d27
Compare
|
@poshinchen — reviewed the current changes ( I re-derived the true scope by diffing the four branch commits alone ( What I verified this pass:
One optional, non-blocking note:
Assessment: Approve. Clean, well-scoped, well-documented compatibility layer with both major lines verified end-to-end in CI. |
Resolve every mcp name that was renamed or relocated in mcp 2.0 through a single _compat module so the package imports cleanly on both major lines: - MCPError (spelled McpError in 1.x) - streamable_http_transport adapter (2.x streamable_http_client takes a pre-configured HTTPX client instead of loose header kwargs) - GetSessionIdCallback (removed in 2.x along with protocol sessions) - ProgressFnT now imported from mcp.client.session (available on both lines) The MCP_V2 flag is feature-probed via ClientSession.discover rather than version-parsed, and replaces mcp_instrumentation's _is_mcp_v1 probe so the whole codebase branches on one source of truth. Related to strands-agents#1659
… mcp lines Address review findings on the compat layer: - mcp 2.x's streamable_http_client only closes an HTTPX client it created itself, so the adapter now binds the client's lifetime to the transport's via a wrapping context manager. The 1.x branch is unchanged. - mypy sees only the installed mcp line, so each try/except branch carries its own ignore and a per-module warn_unused_ignores override silences the branch the installed line doesn't take. - Branch tests no longer patch with create=True (which invents missing attributes and passes against any spelling); each branch test patches real attributes and skips on the other line, and a hasattr test checks the installed module directly. - Test error construction goes through a make_mcp_error helper because the two lines have different constructors: 2.x MCPError(code, message, data), 1.x McpError(ErrorData). The .error accessor the production code reads is identical on both. Related to strands-agents#1659
Late 1.x mcp releases backport the 2.x transport names, so the client lifecycle test now gates on name availability instead of the installed line and forces the MCP_V2 flag. A reload test covers the GetSessionIdCallback fallback. This brings _compat.py to full line and branch coverage under the mcp<2 pin CI runs with.
The unit-test matrix only ever exercises mcp 1.x under the <2 pin, so nothing in CI catches a module-level import of a 1.x-only name breaking import strands on 2.x. This job force-installs mcp 2.0.* over the pin, asserts the package imports with MCP_V2 set, and runs the _compat tests so their 2.x-gated paths execute against the real package instead of mocks. It runs on the PR gate, push to main, and the release gate via python-test-lint.yml, and uploads no coverage.
b121d27 to
53aa5a1
Compare
|
@strandly-the-agent review the PR along with the comments again. |
|
@poshinchen — re-reviewed the current state (
Assessment: Approve stands. The only outstanding item is the reviewer thread, which I believe your explanation resolves. |
strandly-the-agent
left a comment
There was a problem hiding this comment.
Re-reviewed at 53aa5a1, including the comment threads. Still good to approve — one new 🟡 in the delta, with a one-line verified fix inline.
The real delta since the head I last verified (b00237b) is threading main's new OAuth support through the compat layer: streamable_http_transport gained auth: httpx.Auth | None, passed to 1.x as a kwarg and on 2.x into create_mcp_http_client. I verified it end to end: floor-safe (1.23.0's streamablehttp_client accepts auth — checked its real signature), both call-shape tests updated, and mutation-proven (dropping auth from either branch fails the suite). The rest is rebase.
The one new finding (inline at _compat.py:80): the auth threading regressed the "type-checks on both mcp lines" property this PR's own fix commit established — mypy on a 2.x env now reports one arg-type error there, and it points at a real runtime fact I proved on mcp==2.0.1: httpx2.AsyncClient rejects a v1 httpx.Auth object at construction (TypeError: Invalid "auth" argument). The built-in auth={client_id, client_secret} path is fine on both lines — mcp's own ClientCredentialsOAuthProvider follows the installed line (it's an httpx2.Auth subclass on 2.x, verified). Only a custom v1 httpx.Auth via auth_provider= breaks, only on the 2.x branch, and only once the pin widens — so 🟡, not a blocker; CI can't see it because its mypy runs on the 1.x env.
On Unshure's open thread (capability probe vs version parsing): the author's answer is correct, and I have measured data backing it — name matrix from real installs in the collapsed section. A major-version parse would have needed a special case for the 1.29.x backports; the per-name probe can't mispredict by construction.
My standing description ask from both prior reviews: addressed. "Runs cleanly" and "pin can widen with no code changes" are gone; the new text scopes the claim honestly and documents the CI job. Thanks for that.
Evidence ledger (all measured on this head)
| Check | Result |
|---|---|
test__compat.py on mcp 1.23.0 (floor) / 2.0.1 |
4 passed, 1 skipped — each line, mirror-image skips |
Full tests/strands/tools/mcp/ on 1.23.0 |
256 passed, 1 skipped |
| Same on 2.0.1 | 63 failed / 193 passed — the known pre-existing 2.x gaps (start(), camelCase fields, tasks), unchanged, latent behind the pin |
mypy _compat.py on 1.23.0 |
✅ Success |
mypy _compat.py on 2.0.1 |
🟡 1 error: :80 arg-type (httpx._auth.Auth | None vs expected httpx2._auth.Auth | None) |
| Runtime probe on 2.0.1 | create_mcp_http_client(auth=<v1 httpx.Auth>) → TypeError at construction; auth=<ClientCredentialsOAuthProvider> → OK |
Mutation: drop auth from 2.x branch / from 1.x branch |
lifecycle test fails / v1 call-shape test fails — both mutants killed |
Prior findings (leak guard, no-create=True tests, make_mcp_error) |
intact at this head |
import httpx at _compat.py:20 |
safe on any resolve — httpx>=0.28.1,<1.0.0 is a direct strands dep (pyproject.toml:35), not just transitive via mcp 1.x |
CI on 53aa5a1 |
Python / Lint, CI Gate, Python / MCP 2.x Compat, all unit matrices ✅ |
Name matrix for the capability-probe thread (real installs):
streamablehttp_client |
streamable_http_client |
create_mcp_http_client |
ClientSession.discover |
|
|---|---|---|---|---|
| mcp 1.23.0 (floor) | ✅ | ❌ | ❌ | ❌ |
| mcp 1.29.x | ✅ | ✅ (backport) | ✅ (backport) | ❌ |
| mcp 2.0.x | ❌ | ✅ | ✅ | ✅ |
Two smaller notes
- Integ CI:
Run integration tests - Python 3.10 - allis red on this head with an async-teardown signature (Event loop is closed×7, no MCP-specific annotation), andmain's own recent integ runs (Aug 18–19) also show failures. Likely unrelated to this PR — but I couldn't read the job log to fully attribute it, so worth confirming it's the known flake before merge; integ isn't part ofCI Gate. - Self-correction: in my Aug 11 review I called
motoin themcp-v2-compatjob's pip install unnecessary. It's now load-bearing — a parent conftest imports it, and the compat tests fail to collect without it. Good call not taking that nit.
Follow-up re-review by strandly-the-agent — delta-scoped, run against real installs of mcp 1.23.0 / 1.29.x / 2.0.1 on this exact head.
Description
mcp2.0 renamed and relocated several public names, sostrandswill fail whenmcp>=2is installed. This PR adds an internal compatibility module,strands.tools.mcp._compat, so the package imports and runs on both major lines. The dependency pin stays<2and nothing changes for current installs. This is groundwork for adopting the MCP 2026-07-28 spec (#1659) under an internal feature flag.All version-dependent names now resolve through
_compat:MCP_V2tells the rest of the codebase which version of MCP is installed. It probes forClientSession.discoverinstead of parsing the version string, so pre-releases and backports resolve by capability. It replaces the version probe inmcp_instrumentation.py, and follow-up work (lifecycle, tasks, native OTel) will branch on this same flag.MCPErroraliases the exception class, which 1.x spellsMcpError.streamable_http_transport(url, headers)keeps the 1.x call shape on both lines. 2.x dropped theheaderskwarg in favor of a pre-configured HTTPX client, so on 2.x the adapter creates that client itself and closes it when the transport closes.GetSessionIdCallbackfalls back to a plain callable alias on 2.x, which removed protocol sessions. It is only used for typing.ProgressFnTneeds no compat entry; its import location exists on both lines down to the 1.23 pin floor.A new
mcp-v2-compatCI job force-installsmcp==2.0.*over the pin, verifiesimport strandssucceeds, and runs the compat tests against the real 2.x package. The rest of CI keeps testing 1.x only. Without this job, nothing would catch a new 1.x-only import breaking 2.x again.Related Issues
#1659
Documentation PR
No documentation changes needed; the compat module is internal.
Type of Change
New feature
Testing
How have you tested the change? Verify that the changes do not break functionality or introduce new warnings.
hatch run preparemcp1.x.mcp==2.0.0, both locally and in the new CI job.Checklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.