Repository navigation
Chore/mcp sdk v2 - #6868
Chore/mcp sdk v2#6868
Conversation
33e2dd9 to
3d8dba5
Compare
|
The rerun exposed two independent issues:
I verified the patched fixture returns HTTP 200 and a session ID for the exact request that previously returned After the dev-tools fix is merged and released, this workflow will need its |
* chore(deps): drop fastmcp dev dependency and protocol-compliance make targets Signed-off-by: Jonathan Springer <jps@s390x.com> * test(live-gateway): rewrite MCP protocol E2E on the official mcp SDK Signed-off-by: Jonathan Springer <jps@s390x.com> * chore(tests): remove the compliance reference server and matrix tooling The fastmcp-based protocol-compliance harness was the only consumer of the compliance reference server and the compliance matrix generator, so both go with it. - delete mcp-servers/python/compliance_reference_server/ and scripts/compliance_matrix.py - drop the compliance-reference-server dev dependency and its [tool.uv.sources] editable entry; regenerate uv.lock (also sheds fastmcp, fastmcp-slim, authlib and their transitives) - drop the compliance-only pytest markers from pyproject.toml - refresh the pip-audit skip-list note and the PYTEST_IGNORE comment Signed-off-by: Jonathan Springer <jps@s390x.com> * docs: drop protocol-compliance references and name the mcp SDK client Follow-up to the harness removal: the PR-review validation gate, the live-gateway README/target list, and the test docs still pointed at `make test-protocol-compliance` and described the protocol E2E suites as FastMCP-driven. - AGENTS.md, tests/AGENTS.md, llms/NOTES.template.md, docs/docs/development/release-management.md, tests/live_gateway/README.md: retire the removed targets, name the official mcp SDK client - tests/live_gateway/helpers/mcp_test_helpers.py: correct the module docstring's consumer list and point new tests at the SDK ClientSession pattern Signed-off-by: Jonathan Springer <jps@s390x.com> * test(sso): move Entra ID integration test to tests/integration Ports the relocation from mcp-2-0-0-prerelease (d7c1f9a): the Entra ID suite is fully in-process (httpx.ASGITransport) and self-skips when the AZURE_* credentials are absent, so it belongs with the integration tests that run under the default 'make test' rather than the live-gateway tree. - tests/live_gateway/sso/test_entra_id_integration.py -> tests/integration/test_entra_id_integration.py (header updated) - tests/live_gateway/sso/__init__.py: docstring now covers only the Keycloak suite and points at the new Entra ID location - docs/docs/testing/entra-id-e2e.md: all pytest invocation paths updated - Makefile: test-e2e-sso targets Keycloak only, notes the Entra ID move (help comment aligned as well) Signed-off-by: Jonathan Springer <jps@s390x.com> * chore(deps): remove fastmcp and rebuild live-gateway E2E on the official mcp SDK Drops the fastmcp dev dependency and everything that depended on it, replacing the live-gateway protocol and RBAC E2E suites with a client built on the official mcp Python SDK. - drop fastmcp dev dependency and protocol-compliance make targets - remove the fastmcp-based protocol compliance harness and the fast-server go/rust parity test - rewrite the MCP protocol and RBAC transport E2E suites on the official mcp SDK ClientSession - remove the compliance reference server and compliance matrix tooling (the harness was their only consumer); regenerate uv.lock, which also sheds fastmcp-slim, authlib and their transitives - update AGENTS.md, test docs, and the live-gateway README to retire the removed targets and name the mcp SDK client Closes #5910 Signed-off-by: Jonathan Springer <jps@s390x.com> * chore(deps): drop fastmcp dev dependency and protocol-compliance make targets Signed-off-by: Jonathan Springer <jps@s390x.com> * chore(tests): remove the compliance reference server and matrix tooling The fastmcp-based protocol-compliance harness was the only consumer of the compliance reference server and the compliance matrix generator, so both go with it. - delete mcp-servers/python/compliance_reference_server/ and scripts/compliance_matrix.py - drop the compliance-reference-server dev dependency and its [tool.uv.sources] editable entry; regenerate uv.lock (also sheds fastmcp, fastmcp-slim, authlib and their transitives) - drop the compliance-only pytest markers from pyproject.toml - refresh the pip-audit skip-list note and the PYTEST_IGNORE comment Signed-off-by: Jonathan Springer <jps@s390x.com> * docs: drop protocol-compliance references and name the mcp SDK client Follow-up to the harness removal: the PR-review validation gate, the live-gateway README/target list, and the test docs still pointed at `make test-protocol-compliance` and described the protocol E2E suites as FastMCP-driven. - AGENTS.md, tests/AGENTS.md, llms/NOTES.template.md, docs/docs/development/release-management.md, tests/live_gateway/README.md: retire the removed targets, name the official mcp SDK client - tests/live_gateway/helpers/mcp_test_helpers.py: correct the module docstring's consumer list and point new tests at the SDK ClientSession pattern Signed-off-by: Jonathan Springer <jps@s390x.com> * feat: migrate to MCP SDK 2.0.0b2 with modern 2026-07-28 upstream negotiation Squashed arc: - MCP SDK migration 2.0.0a2 -> 2.0.0b1 -> 2.0.0b2 (Python 3.14 support added; floor stays >=3.11 — mcp 2.0.0b2 needs only >=3.10, cpex >=3.11, CI is py3.12, MCPError rename, transport/Client compatibility fixes) - Upstream federation path migrated from raw mcp.ClientSession to mcp.client.Client: mode=auto probes server/discover and adopts modern 2026-07-28, with transparent legacy fallback; MCP_CLIENT_CONNECT_MODE=legacy rollback flag - Pooled upstream session registry on the high-level Client with typed SessionLifecycle handle (replaces _cf_* attribute smuggling), cache=False to preserve ADR-052 notification wiring, dispatcher-aware transport health check - mcp_proxy_client gained transport= (streamablehttp/sse) and mode= options; all five ad-hoc SSE fallback sites routed through it - Compose test stack: single cfex-mcp-fast-time-server image (stale Go-era flags fixed), strict modern-only 2026-07-28 fast_time_2026 variant added, five register_* one-shots consolidated into a single 'register' container running scripts/register_mcp_servers.py 'stack' - Tests: SDK invariant spike suite, config validators, reworked registry/service suites, E2E negotiation suite (11 tests) proving auto/legacy modes against strict and legacy upstreams - Docs: MCP_CLIENT_CONNECT_MODE and legacy rollback documented - Fixes: four-parameter httpx2.Timeout in proxy-client default path, token-exchange integration test isolation from shared Redis, pre-existing get_httpx_client_factory dedent repair in gateway_service Packaging — the SDK pin lands in core dependencies, not in extras: - An earlier revision of this branch split the `mcp` pin into mutually exclusive `runtime` / `live-tests` extras backed by a `[tool.uv] conflicts` block, so the gateway could stay on `mcp` 2.x while the fastmcp-based protocol-compliance harness stayed on `mcp` < 2.0. That harness is gone, so the partition is removed: `mcp==2.0.0b2` and `mcp-types==2.0.0b2` sit in core `dependencies` alongside `cpex`, and `pip install mcp-contextforge-gateway` once again resolves a working install. - Every `--extra runtime` / `--extra live-tests` flag is dropped from the Make targets, the pytest/pytest-rust/wrapper workflows and both Containerfiles, restoring the install targets to their pre-extras form. The `granian` extra is dropped from the Containerfile install lists as well; no such extra has ever been defined. - All install documentation, the four affected ADRs and the agent-facing dependency-layout guidance revert to their extras-free wording. - Fixes a duplicate `cpex-secrets-detection` key in `[tool.uv.exclude-newer-package]` that left pyproject.toml invalid TOML. Also realigns two upstream session-factory tests that were left on the pre-migration SDK shape: they patched `streamablehttp_client` / `ClientSession` and reached for the retired `_cf_shutdown_event` / `_cf_owner_task` attributes, so they errored during setup instead of exercising the post-ready WARNING path and the timeout structured-logging fallback. Signed-off-by: Jonathan Springer <jps@s390x.com> * feat: upgrade MCP SDK from 2.0.0b2 to 2.0.0 final The 2.0.0 final release removes mcp.shared.session entirely (the b2 typing-only RequestResponder stub is gone) and changes Client(cache=) from a bool flag to CacheConfig | None, where None disables the response cache so message_handler passes through unwrapped. - pyproject/uv.lock: mcp/mcp-types >=2.0.0, cpex tracked from the 0.1.x-mcp-2.0.0 branch, drop stale exclude-newer pins - mcpgateway/utils/session_compat.py: new shim re-creating the RequestResponder typing stub so existing annotations and isinstance guards keep working (never instantiated, matching b2 semantics) - notification_service/upstream_session_registry: import from the shim; pass cache=None (was cache=False) in the pooled upstream session factory so the response cache does not wrap message_handler - tests: repoint RequestResponder imports to the shim, assert cache is None, update gateway refresh test to the mcp_proxy_client seam, refresh upstream session error category expectations, regenerate .secrets.baseline Signed-off-by: Jonathan Springer <jps@s390x.com> * fix(services): align MCP httpx client factories on httpx2 types Signed-off-by: Jonathan Springer <jps@s390x.com> * test(live-gateway): migrate MCP e2e suites to SDK 2.0 final client API Signed-off-by: Jonathan Springer <jps@s390x.com> * chore(compose): restore per-service REST auto-registration, drop strict 2026-07-28 probe Signed-off-by: Jonathan Springer <jps@s390x.com> * chore(testing): drop stale Fast Time 2026 strict-probe references The fast_time_2026_server service was removed from docker-compose.yml in 5e54707, but its advertising and test wiring lingered: - scripts/register_mcp_servers.py: delete (unused anywhere in the repo) - Makefile: drop the :8887 banner and auto-registration echo from testing-up - tests/e2e/test_upstream_connect_mode_e2e.py: strip the strict-upstream behavior matrix, wire-capture proofs, and strict registration tests; keep the legacy connect-mode proofs against fast_time_server (:8888) Signed-off-by: Jonathan Springer <jps@s390x.com> * Restore Containerfile and Makefile to main state Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com> * fix(mcp-2.0): address SDK 2.0 review findings and live-stack build issues Review findings (PR #5493): - transports/streamablehttp: ctx.meta is a TypedDict (RequestParamsMeta) in MCP 2.0 with no model_dump(); extract via dict() with a legacy-model fallback at the call_tool/get_prompt/read_resource sites - transports/streamablehttp: complete() now returns types.CompleteResult wrapping types.Completion so the wire shape carries the required 'completion' nesting; update test assertions to the corrected contract - utils/streamable_http_compat: build httpx2.AsyncClient (not v0 httpx) in the no-factory fallback, matching what the MCP 2.0 SDK transports expect; same-class fixes in resource_service factory and the upstream session registry (HttpxClientFactory alias + httpx/httpx2 exception matching) - services/http_client_service: add get_httpx2_timeout(); the v0 httpx.Timeout from get_http_timeout() is rejected by httpx2.AsyncClient, so point the gateway/tool/resource factory fallbacks at the new helper - transports/streamablehttp: capture server-emitted session IDs whenever stateful sessions are enabled, not only when multi-worker affinity is on, so the logical-owner claim fires on single-node deployments; unskip the affinity-disabled regression test Live stack: - Containerfile: add git to the builder stage (uv needs it to fetch the git-pinned cpex dependency) and split the monolithic venv RUN into discrete steps so build failures name the exact stage - docker-compose: repin cfex-mcp-fast-time-server to the current digest; the previous pin predates the 2026-07-28 caching directives and its tools/list responses failed ListToolsResult validation during federation Also rewrite test_invoke_tool_mcp_refuses_stored_env_filter to the v2 mcp_proxy_client mock pattern (pre-existing stale migration leftover). Signed-off-by: Jonathan Springer <jps@s390x.com> * fix: direct_proxy resource reads — SSE transport, final content shape, blob decoding Three defects left direct_proxy federated resource reads broken or silently corrupting content: 1. mcp_proxy_client was called without a transport argument, so SSE gateways were dialed with streamable-HTTP framing and the connect failed. Select the transport from the gateway's registered type. 2. The proxied read already returns final content, but execution fell through into the cache-mode pointer-resolution machinery, which assumes a DB-shaped object and crashed on content.id (AttributeError, masked to "" by the ingress). Build the final ResourceContent (with id and mime_type) in the branch and skip the machinery via a flag initialized at function scope alongside the other locals. 3. MCP transports carry blobs as base64 strings while ResourceContent.blob holds raw bytes; passing the string through meant the ingress re-encoded it, double-encoding every binary resource. Decode to raw bytes at the seam. Tests: TestReadResourceDirectProxy updated to assert the final content shape (ResourceContent) — the previously asserted intermediate shape is exactly what crashed the pointer-resolution machinery in production — and the blob test now uses valid base64 and asserts decoded raw bytes. Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com> * fix: let 2026-era requests through the session-less peeks so the ingress can serve modern sessions The streamable ingress wrapper peeks at session-less POSTs to give unknown-method probes a truthful -32601 (instead of the SDK's 'Missing session ID'). But 2026-era requests are self-describing and session-less BY DESIGN, and server/discover is not in the gateway's known-method list — so every modern client's discover probe was answered -32601 here, before the SDK's era routing ever ran, and the client silently fell back to the legacy handshake. The gateway could consume modern upstream sessions but never serve one. Mirror the SDK's own era-dispatch rule (MCP-Protocol-Version header present and not a handshake version => modern) and skip both session-less peeks for such requests: the SDK's modern classifier owns their validation and rejections. Also add server/discover to the known-method list. With this, a v2 client negotiates 2026-07-28 through the gateway (tools, prompts, and resources verified against both a 1.x-era SSE downstream and a 2026-era streamable downstream); legacy clients are unaffected (no header / handshake header keeps the old peek behavior, pinned by test). Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com> * refactor(tests): group era-aware peek-bypass tests under TestModernEraPeekBypass Groups the three behaviors pinning the modern-era peek bypass (modern header passes through, handshake header still peeked, server/discover is a known method) under one class with a shared docstring telling the era-boundary story — easier to locate and read as a unit in this large test module. No behavior change. Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com> * rename era peek-bypass group to TestDualEra Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com> * feat: standing listen streams for modern servers feeding debounced refresh Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com> * refactor: module-level listener imports, tighter notify_list_changed docstring Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com> * fix: prune stale catalog rows created via notification-driven refresh Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com> * fix: answer unknown tool/prompt/resource requests in proper error shapes for both eras Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com> * feat: relay upstream tool progress to downstream callers Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com> * fix: surface tool invocation failures as isError results for both eras Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com> * feat: pass through 2026 MRTR elicitation for modern clients on modern servers Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com> * port: move #5934 MCP handshake probe onto the SDK 2.0 client The Test Connection endpoint's initialize-path probe was written against the SDK 1.x client API. Port it to SDK 2.0: _SniPinningTransport is now built on httpx2 (the stack the v2 client transports run on) with identical pinning logic, the local pinned-client factory returns httpx2.AsyncClient, _probe_session reads the v2 result field names, and the probe connects via the v2 sse_client / streamable_http_client. The raw server/discover probe path is unchanged. Tests updated to the v2 shapes; every pinning assertion (sni_hostname, pinned_host, TLS verify context) still asserts. Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com> --------- Signed-off-by: Jonathan Springer <jps@s390x.com> Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com> Co-authored-by: Jonathan Springer <jps@s390x.com>
* ci: run PR checks on pull requests targeting chore/mcp-sdk-v2
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
* fix: relay plugin-blocked tool calls as isError results on the /mcp path
A PluginViolationError raised on the SDK-served /servers/{id}/mcp path fell
through to the generic handler and was wrapped as a bare code-0 JSON-RPC
error. Catch it in call_tool and return an isError CallToolResult, matching
the contract the plugin e2e suites assert.
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
* fix: port inherited unit tests to SDK v2 call shapes
The tests still targeted the SDK 1.x API: the streamablehttp_client patch
target and 3-tuple transport, camelCase result fields, session.call_tool
mocks where v2 calls client.session.call_tool, and call_tool assertions
without the v2 keyword arguments. Header assertions now inspect the
http_client passed to the SDK instead of a headers kwarg.
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
* fix: satisfy pylint and pre-commit on migration files
Disable no-member on the mcp_app.request_context reads (the property is
injected at runtime for the v1 handler adapters) and try-except-raise on
the listener's CancelledError guard; strip a trailing space flagged by
pre-commit.
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
* fix: satisfy pre-commit and diff coverage for the plugin-block change
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
* fix: remove unwanted commentsn from tests
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
---------
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
…release verification (#6716) * feat(compose): add 'ui' profile for contextforge-web-ui and document release verification Signed-off-by: Marek Dano <mk.dano@gmail.com> * fix: lint issue Signed-off-by: Marek Dano <mk.dano@gmail.com> * docs: fix forbid-source-placeholders hook failure in ui.md Replace '(source: ...)' pattern on line 130 with an inline link so the pre-commit forbid-source-placeholders hook no longer rejects the file. Also refresh .secrets.baseline line numbers after the one-line change. Closes #6716 Signed-off-by: Marek Dano <mk.dano@gmail.com> * chore: refresh .secrets.baseline line numbers after rebase onto chore/mcp-sdk-v2 Signed-off-by: Marek Dano <mk.dano@gmail.com> * fix(compose): pin web_ui image to a released version and clarify BFF UI docs Address PR review feedback: pin WEB_UI_IMAGE to a specific released tag instead of `latest`, add a release-checklist step to bump and verify that version, and clarify that the built-in UI and the BFF-style contextforge-web-ui are both supported side by side. Signed-off-by: Marek Dano <mk.dano@gmail.com> * fix(compose): pin web_ui image by digest and add ui profile config check Pin WEB_UI_IMAGE to tag@digest (docker-compose.yml, .env.example) so the supported ui profile's session/auth-handling BFF service can't be silently swapped by a retargeted registry tag. Document the resolve-and-bump-digest step in release-management.md #6.4, add `make compose-ui-config-check` (docker compose --profile ui config --quiet) plus a matching CI workflow to catch profile/schema regressions on PRs, and drop the duplicated WEB_UI_IMAGE version literal from AGENTS.md so there's one source of truth. Addresses review feedback from vishu-bh and gcgoncalves on #6716. Signed-off-by: Marek Dano <mk.dano@gmail.com> * fix: refresh .secrets.baseline line numbers for docker-compose.yml The baseline committed in ef347b9 was generated before the last edit to docker-compose.yml's web_ui image comment block, leaving it 5 lines stale and failing detect-secrets in CI (Pre-commit Checks, run 34468285949) even though it passed locally against the pre-edit state. Signed-off-by: Marek Dano <mk.dano@gmail.com> --------- Signed-off-by: Marek Dano <mk.dano@gmail.com>
…uest middleware Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
…tbound paths Add MCP_INBOUND_PROTOCOL_MODE config to gate inbound client protocol versions at the middleware layer. When set to 'legacy', the gateway rejects 2026-07-28 with 400 and supported handshake versions, steering dual-era clients to retry with initialize. Change MCP_CLIENT_CONNECT_MODE default from 'auto' to 'legacy' so outbound connections use initialize-only by default. Gate the server/discover probe in test_gateway_handshake() behind the connect mode, and add a startup guard that force-disables modern_listener_service when legacy mode is active. Closes #6767 Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com>
…ation reference Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com>
Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com>
Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com>
- Fix test_default_protocol_version_applied to pin mode to auto (legacy case already covered by dedicated test) - Fix doctest in protocol_version.py to compare against LATEST_HANDSHAKE_VERSION matching the new legacy default - Add TestGatewayHandshakeLegacyDiscoverSkip verifying discover probe is skipped in legacy mode and attempted in auto mode - Add test_lifespan_legacy_mode_skips_modern_listeners verifying init_modern_listener_service is not called in legacy connect mode Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com>
…dshake tests mcp_client_connect_mode defaults to "legacy", which skips the server/discover probe. Tests mocking ResilientHttpClient for discover-path behaviour were timing out (30 s health_check_timeout) while the SDK attempted initialize against a real host. - Add configure_handshake_env fixture (configure_allowlist + auto mode) so all discover-path handshake tests exercise the probe without repeating the pair - Revert 9 connectivity tests to configure_allowlist-only; mcp_client_connect_mode has no effect on check_mcp_server_connectivity - Add test_handshake_legacy_mode_skips_discover_uses_initialize: acceptance criterion for #6767 — legacy mode bypasses the probe and reports negotiation_path='initialize' with no ResilientHttpClient calls Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com>
Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com>
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
7043d20 to
e7968f1
Compare
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
msureshkumar88
left a comment
There was a problem hiding this comment.
Requesting changes. The SDK 2.0 migration is necessary, and the rollout defaults are safe: both protocol modes default to legacy, and the modern listener defaults to off. Era routing uses the same HANDSHAKE_PROTOCOL_VERSIONS rule as the SDK. DNS pinning moves cleanly to httpx2. Inline comments give the exact change for each finding.
Blocking
- OAuth
authorization_codehealth checks treat 401/403 as unhealthy. This is an SDK 2 regression (gateway_service.py:5062). - The tests mock exception shapes that SDK 2 never raises, so they hide item 1 (
test_oauth_authorization_code_health.py,test_upstream_session_error_categories.py).
Fix before merge, or document a waiver
3. APP_ROOT_PATH bypasses the MCP_INBOUND_PROTOCOL_MODE=legacy gate (protocol_version.py:121, streamablehttp_transport.py:5055).
4. SDK 2 session manager defaults: 30 min idle timeout and 10k session cap (streamablehttp_transport.py:338 → :4341).
5. Modern listener: it runs in every worker, loops hot after a graceful close, never matches ListenNotSupportedError, ignores custom TLS and query auth, and keeps stale credentials (main.py:1608, modern_listener_service.py).
6. MRTR fields go upstream without the mrtr_allowed gate (streamablehttp_transport.py:2042).
7. The streamable-HTTP resource fallback lost its retry and teardown tolerance (resource_service.py:2246).
8. The direct-proxy read timeout drops from 300 s to 30 s, and redirects are no longer followed (mcp_proxy_client.py).
9. CWE-209: raw exception text reaches clients from get_prompt and read_resource.
10. No black-box test covers a modern (2026-07-28) upstream.
11. Dependency ranges have no upper bound (mcp, mcp-types, cpex).
12. Unrelated changes: the Rust coverage gate goes from 93 to 90, the chore/mcp-sdk-v2 triggers in 23 workflows, and the web UI ui profile (which drops experimental).
13. Docs: a broken anchor, and new environment variables missing from .env.example and Helm.
Outside the diff
- CHANGELOG: add an entry for the SDK 1.x → 2.x bump. List the behavior changes: session defaults, the direct-proxy timeout and redirects, prompt and resource error responses, Test Connection skipping
server/discoverin legacy mode (requested in #6767), and the removedexperimentalcompose profile. - PR description: add a summary,
Refs #6218 #6356, and the list of breaking changes. Epic #6218 says that nothing merges tomainbefore the dual-version validation gate passes; link the evidence. - DCO: commit
eafa744dfhas a sign-off email that does not match its author. Tracked internally.
Resolved in discussion
- Sampling, roots and elicitation relay: not supported today, and elicitation is phase 3. Remove or clearly mark the dead path (see
session_compat.py:40). - Deny-path tests for the modern inbound path: deferred while the modern path stays disabled. Item 3 closes the gap that makes it reachable.
Verification
- The changed unit and integration tests pass on mcp 2.2.0. Diff coverage on changed statements is 91.7%.
- Item 1 is reproduced with a real SDK 2 transport and an
httpx2.MockTransportthat returns 401. The root exception isMCPError(-32603), with no status code.
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
msureshkumar88
left a comment
There was a problem hiding this comment.
Approving. Both blocking findings are fixed and verified (7345e3de8):
- OAuth
authorization_codehealth checks treated 401/403 as unhealthy —ErrorResponseHooknow surfaces the upstream status ashttpx2.HTTPStatusError. Verified against a real SDK 2 transport inlegacyandautomodes. - Tests mocked an exception shape SDK 2 never raises —
test_oauth_authorization_code_health.pyandtest_upstream_session_error_categories.pynow drive the real proxy client and SDK against an in-process ASGI upstream. Confirmed they fail on the pre-fix source and pass on the fix.
Status of the remaining, non-blocking findings, per the discussion with @madhu-mohan-jaishankar:
Fixed
chore/mcp-sdk-v2CI trigger removed from all 23 workflow files (3e59329d1).
Acknowledged, deferred to a follow-up
- Dead sampling/roots/elicitation relay code (
session_compat.py:40) — not supported today, elicitation is phase 3. GATEWAY_MODERN_LISTENERS_ENABLEDmissing from docs/Helm (config.py:2937).- Broken doc anchor (
configuration.md:1093). - Web UI
uiprofile /experimentalremoval (docker-compose.yml:1200) — agreed unrelated, to land in a separate PR tomain. - Rust coverage gate 93→90 (
pytest-rust.yml:120) — kept intentionally, to match the Python gate.
Disputed, evidence posted, still open
APP_ROOT_PATHbypassesMCP_INBOUND_PROTOCOL_MODE(protocol_version.py:121) — marked Invalid, but I reproduced the bypass against the real middleware chain: a direct/mcp/call is correctly rejected with 400, the same call underAPP_ROOT_PATH=/appreturns 200 for a modern header or a garbage one. Not blocking this PR (auth/RBAC still enforce ahead of it), but the policy gate itself is skipped in that supported topology. Repro posted on the thread.
Not yet triaged
- Session idle timeout / max_sessions defaults (
streamablehttp_transport.py:338) - Modern listener: primary-worker gating, hot-reconnect loop,
ListenNotSupportedErrorunreachable, ignored TLS/query auth, stale credentials (main.py:1608,modern_listener_service.py) - MRTR fields forwarded upstream without the
mrtr_allowedgate (transport.py:2043) - Resource fallback dropped retry/teardown tolerance (
resource_service.py:2246) - Direct-proxy timeout/redirect regression (
mcp_proxy_client.py:116) - Raw exception text reaching clients (CWE-209) on
get_prompt/read_resource - No e2e coverage of a modern (2026-07-28) upstream
- Unbounded
mcp/mcp-types/cpexversion ranges
None of these are blocking; carrying them into a follow-up per the discussion above.
gandhipratik203
left a comment
There was a problem hiding this comment.
LGTM! This has the necessary changes for the merge to main. Passed the key validation checks!
Tests patched the removed tool_service.sse_client / tool_service.streamablehttp_client / tool_service.ClientSession symbols and asserted the camelCase structuredContent/isError attributes. Main's MCP SDK v2 migration (#6868) replaced those with a single mcp_proxy_client context manager and renamed CallToolResult's fields to structured_content/is_error. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Tests patched the removed tool_service.sse_client / tool_service.streamablehttp_client / tool_service.ClientSession symbols and asserted the camelCase structuredContent/isError attributes. Main's MCP SDK v2 migration (#6868) replaced those with a single mcp_proxy_client context manager and renamed CallToolResult's fields to structured_content/is_error. Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
…ailures (IBM#6181) * fix: return MCP-compliant error responses for tool execution failures MCP protocol requires all tool results to include a 'content' field with an 'isError' flag for failures. Previously, runtime errors (server crashes, timeouts, network failures) raised exceptions that were converted to JSON-RPC errors, causing clients to fail with 'content: Field required' validation errors. Changes: - Modified invoke_tool_direct to return CallToolResult with isError=true instead of raising ToolInvocationError for connection/execution failures - Updated SSE transport timeout handler to return structured error response - Updated SSE transport exception handler to return structured error response - Updated StreamableHTTP transport timeout handler to return structured error - Updated StreamableHTTP transport exception handler to return structured error - Updated affected tests to expect structured error responses instead of exceptions All MCP execution paths now return properly structured CallToolResult objects even when the upstream server fails, ensuring protocol compliance and better error handling for MCP clients like langchain-mcp-adapters. Pre-execution errors (auth failures, validation errors, configuration issues) continue to raise exceptions as appropriate since they prevent execution from starting. Signed-off-by: Bogdan-Marius-Catanus <bogdan-marius.catanus@ibm.com> * fix(mcp): address PR review feedback for protocol compliance - Update invoke_tool_direct docstring to reflect runtime error handling - Clarify inline comment about previous exception behavior - Simplify test assertions to use result.isError directly Addresses review feedback on PR fixing MCP protocol violation (IBM#6275) Signed-off-by: Bogdan-Marius-Catanus <bogdan-marius.catanus@ibm.com> * fix: address review findings for MCP tool error response structure - Fix #1 (blocking/CWE-209): route invoke_tool_direct exception text through sanitize_exception_message before returning in CallToolResult; snapshot gateway.auth_query_params inside the ORM session so secrets embedded in gateway URLs are redacted before reaching the client. - Fix #2 (blocking/retry_on_status): attach structured_content with status_code to CallToolResult in the SSE and StreamableHTTP inner BaseException handlers so the retry plugin can honour retry_on_status (e.g. 429, 503) without reaching the outer handler. - Fix #3 (functionally-impacting): re-raise SystemExit, GeneratorExit, and KeyboardInterrupt before the BaseException catch-all in both SSE and StreamableHTTP handlers; mirrors the existing CancelledError guard (PR IBM#3202 review B7). - Should-fix: update connect_to_sse_server and connect_to_streamablehttp_server docstrings to reflect that these functions now return instead of raising for runtime failures. - Should-fix: extract _make_mcp_tool_error() static helper to eliminate five duplicate CallToolResult(isError=True) constructions; structured_ content is now threaded through a single site. - Tests: add test_invoke_tool_direct_error_is_sanitized to cover finding #1; add test_sse/streamablehttp_http_status_error_sets_structured_ content_status_code to cover finding #2. Signed-off-by: Bogdan-Marius-Catanus <bogdan-marius.catanus@ibm.com> * fix: restore ToolTimeoutError raise on SSE/StreamableHTTP timeout paths and fix structuredContent kwarg - SSE and StreamableHTTP timeout handlers were returning self._make_mcp_tool_error() instead of raising ToolTimeoutError. The return value fell through into the post-process block, firing TOOL_POST_INVOKE a second time after _run_timeout_post_invoke() had already fired it once. Restore raise ToolTimeoutError() so the existing outer handler (line 7174) is the sole post-invoke callsite for the timeout path. - _make_mcp_tool_error() passed structured_content= (snake_case) to types.CallToolResult, whose field is structuredContent (camelCase) with no alias and extra='allow'. The value was stored as an unrecognised extra attribute, so _coerce_to_tool_result() dropped it and structured_content was always None on the direct-proxy path. Fix: use structuredContent= kwarg. - Update _run_timeout_post_invoke docstring: 'control returns to the caller which raises' was stale; the caller now raises itself. - Add tests: TestMcpSseTimeoutPostInvokeFiresOnce: asserts invoke_hook fires exactly once on an unretried SSE timeout. TestMcpStreamableHttpTimeoutPostInvokeFiresOnce: same for StreamableHTTP. TestMakeToolErrorStructuredContentSurvivesCoercion: asserts structuredContent from _make_mcp_tool_error survives _coerce_to_tool_result (direct-proxy regression guard). - Update two existing SSE/StreamableHTTP timeout tests that asserted the old broken return-instead-of-raise behaviour; they now expect ToolTimeoutError to be raised. Signed-off-by: Bogdan-Marius-Catanus <bogdan-marius.catanus@ibm.com> * tests: improve coverage Signed-off-by: Bogdan-Marius-Catanus <bogdan-marius.catanus@ibm.com> * fix: update tool_service tests for MCP SDK v2 client API Tests patched the removed tool_service.sse_client / tool_service.streamablehttp_client / tool_service.ClientSession symbols and asserted the camelCase structuredContent/isError attributes. Main's MCP SDK v2 migration (IBM#6868) replaced those with a single mcp_proxy_client context manager and renamed CallToolResult's fields to structured_content/is_error. Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com> --------- Signed-off-by: Bogdan-Marius-Catanus <bogdan-marius.catanus@ibm.com> Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com> Co-authored-by: Bogdan-Marius-Catanus <bogdan-marius.catanus@ibm.com> Co-authored-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
MCP SDK 2.0