Skip to content

Chore/mcp sdk v2 - #6868

Merged
ja8zyjits merged 17 commits into
mainfrom
chore/mcp-sdk-v2
Sep 24, 2026
Merged

ja8zyjits merged 17 commits into
mainfrom
chore/mcp-sdk-v2

Conversation

@madhu-mohan-jaishankar

Copy link
Copy Markdown
Collaborator

MCP SDK 2.0

@lucarlig

Copy link
Copy Markdown
Collaborator

The rerun exposed two independent issues:

  1. The image build now fails because pyproject.toml references the deleted CPEX branch 0.1.x-mcp-2.0.0. Draft fix(deps): pin CPEX MCP 2 revision #6882 pins the exact commit already recorded in uv.lock, preserving dependency contents.
  2. The original gateway-registration failure is caused by the pinned conformance fixture misclassifying a valid session-era initialize containing params._meta. The MCP schema allows that field and Python SDK 2.x emits it. The root fix is fix(server): accept metadata on stateful initialize modelcontextprotocol/conformance#507; the immediately usable pinned-fixture fix is fix: accept metadata on legacy conformance initialize contextforge-org/contextforge-dev-tools#20.

I verified the patched fixture returns HTTP 200 and a session ID for the exact request that previously returned -32601. Both upstream conformance CI and the dev-tools multi-architecture image smoke suite pass.

After the dev-tools fix is merged and released, this workflow will need its cf-integration pin bumped to that release before the original conformance job can pass.

* 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>
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>

@msureshkumar88 msureshkumar88 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

  1. OAuth authorization_code health checks treat 401/403 as unhealthy. This is an SDK 2 regression (gateway_service.py:5062).
  2. 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/discover in legacy mode (requested in #6767), and the removed experimental compose profile.
  • PR description: add a summary, Refs #6218 #6356, and the list of breaking changes. Epic #6218 says that nothing merges to main before the dual-version validation gate passes; link the evidence.
  • DCO: commit eafa744df has 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.MockTransport that returns 401. The root exception is MCPError(-32603), with no status code.

Comment thread mcpgateway/services/gateway_service.py
Comment thread tests/unit/mcpgateway/services/test_oauth_authorization_code_health.py Outdated
Comment thread tests/unit/mcpgateway/services/test_upstream_session_error_categories.py Outdated
Comment thread mcpgateway/middleware/protocol_version.py
Comment thread mcpgateway/transports/streamablehttp_transport.py
Comment thread .github/workflows/pytest.yml Outdated
Comment thread docker-compose.yml
Comment thread docs/docs/manage/configuration.md
Comment thread mcpgateway/config.py
Comment thread mcpgateway/utils/session_compat.py
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 msureshkumar88 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. Both blocking findings are fixed and verified (7345e3de8):

  • OAuth authorization_code health checks treated 401/403 as unhealthy — ErrorResponseHook now surfaces the upstream status as httpx2.HTTPStatusError. Verified against a real SDK 2 transport in legacy and auto modes.
  • Tests mocked an exception shape SDK 2 never raises — test_oauth_authorization_code_health.py and test_upstream_session_error_categories.py now 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-v2 CI 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_ENABLED missing from docs/Helm (config.py:2937).
  • Broken doc anchor (configuration.md:1093).
  • Web UI ui profile / experimental removal (docker-compose.yml:1200) — agreed unrelated, to land in a separate PR to main.
  • Rust coverage gate 93→90 (pytest-rust.yml:120) — kept intentionally, to match the Python gate.

Disputed, evidence posted, still open

  • APP_ROOT_PATH bypasses MCP_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 under APP_ROOT_PATH=/app returns 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, ListenNotSupportedError unreachable, ignored TLS/query auth, stale credentials (main.py:1608, modern_listener_service.py)
  • MRTR fields forwarded upstream without the mrtr_allowed gate (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/cpex version ranges

None of these are blocking; carrying them into a follow-up per the discussion above.

@cafalchio cafalchio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@gandhipratik203 gandhipratik203 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM! This has the necessary changes for the merge to main. Passed the key validation checks!

@ja8zyjits
ja8zyjits added this pull request to the merge queue Sep 24, 2026
Merged via the queue into main with commit 7e3526b Sep 24, 2026
72 of 73 checks passed
msureshkumar88 pushed a commit that referenced this pull request Sep 24, 2026
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>
msureshkumar88 pushed a commit that referenced this pull request Sep 24, 2026
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>
pull Bot pushed a commit to sjoerd2025/mcp-context-forge that referenced this pull request Sep 24, 2026
…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>
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.

8 participants