Skip to content

mcp-proxy: support both mcp 1.x and 2.x (drop the <2 cap) (#307) - #338

Merged
sunishsheth2009 merged 3 commits into
databricks:mainfrom
sunishsheth2009:sunish-sheth_data/mcp-proxy-compat-guard
Aug 14, 2026
Merged

mcp-proxy: support both mcp 1.x and 2.x (drop the <2 cap) (#307)#338
sunishsheth2009 merged 3 commits into
databricks:mainfrom
sunishsheth2009:sunish-sheth_data/mcp-proxy-compat-guard

Conversation

@sunishsheth2009

@sunishsheth2009 sunishsheth2009 commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #307. A fresh ucode install resolved mcp 2.0.0 off mcp>=1.28.0, and ucode mcp-proxy crashed on import — 2.x renamed streamablehttp_clientstreamable_http_client and moved from httpx to httpx2. Every MCP server through the proxy then failed to connect.

PR #311 capped mcp<2 to stop the bad resolution. This PR instead makes the proxy work against both mcp 1.x and 2.x and removes the cap, so a clean install on either major just works.

How one code path covers both majors

The key realization: both mcp 1.28+ and 2.x export the 2.x-native call shape streamable_http_client(url, http_client=<AsyncClient>) (1.x's streamablehttp_client is a deprecated shim over it). Only two things differ across the majors, and both are handled:

mcp 1.x mcp 2.x handling
HTTP lib httpx httpx2 _httpx() resolves whichever the SDK uses
client yield (read, write, get_session_id) (read, write) _run takes streams[0], streams[1]
auth httpx.Auth httpx2.Auth _build_token_auth() subclasses the resolved flavor
  • _httpx() imports httpx2 (mcp 2.x) else httpx (mcp 1.x), via importlib so the type checker / CI — pinned to mcp 1.x, no httpx2 present — don't choke on a static import httpx2.
  • _build_token_auth() builds the per-request bearer Auth from that flavor; auth_flow's generator contract is identical in httpx and httpx2.
  • pyproject: mcp>=1.28.0,<2mcp>=1.28.0. httpx stays a direct dep (the 1.x path); httpx2 arrives transitively with mcp 2.x, so no direct httpx2 dep is declared.

The fail-fast auth behavior from #201's follow-up is preserved unchanged.

How do you know it works?

Verified in a real venv against both majors (not just the pinned one):

  • mcp 1.28.1 (pinned): 21 proxy tests pass; ty check src/ clean; full suite 1788 passed.
  • mcp 2.0.0 + httpx2 2.9.1 (installed into the venv, --no-sync): the proxy imports (the [Bug] Fresh ucode install pulls mcp 2.0 and mcp-proxy crashes on a removed API #307 crash point is gone), _httpx() selects httpx2, and httpx2.AsyncClient(auth=…) + streamable_http_client(http_client=…) compose without error; 21 proxy tests pass; ty check src/ clean.
  • Confirmed the yield-arity difference empirically (1.28.1 yields a 3-tuple, 2.0.0 a 2-tuple) and that streams[0], streams[1] handles both.
  • New/updated tests in tests/test_mcp_proxy.py: mcp dep is uncapped; _httpx() matches the installed SDK; the shared streamable_http_client symbol imports; the token Auth is an instance of the selected flavor's Auth.
  • The only 2 suite failures are the pre-existing test_e2e_user_agent live-gateway network tests (fail identically on main). ruff format + ruff check clean.

Not done / caveats

  • No live-gateway handshake under mcp 2.x: all my OAuth tokens expired this session (no browser), so I proved transport construction + import + preflight under 2.x, not a real end-to-end tool call. A reviewer with a live workspace and mcp 2.x installed can confirm the round trip.
  • The internal pypi mirror currently resolves mcp 1.28.1 (2.x may not be mirrored yet); the uncapped constraint takes whatever is newest available.

This supersedes the earlier "keep the cap + actionable error" revision of this branch.

This pull request and its description were written by Isaac.

@sunishsheth2009 sunishsheth2009 changed the title mcp-proxy: actionable error on incompatible mcp SDK + compat tests (#307) mcp-proxy: support both mcp 1.x and 2.x (drop the <2 cap) (#307) Aug 14, 2026
@sunishsheth2009

Copy link
Copy Markdown
Collaborator Author

Reworked per maintainer direction: instead of keeping the mcp<2 cap + surfacing an error, the proxy now supports both mcp 1.x and 2.x and the cap is removed — a clean install on either major works.

One code path handles both because mcp 1.28+ and 2.x both export streamable_http_client(url, http_client=…). The only differences — httpx vs httpx2, and a 3- vs 2-tuple yield — are resolved at runtime (_httpx() picks the flavor the installed SDK uses; _run unpacks positionally).

Verified against both mcp 1.28.1 and mcp 2.0.0 in a real venv: proxy imports, transport composes, 21 proxy tests + ty check clean on each. Details in the updated description.

Comment thread src/ucode/mcp_proxy.py Outdated
@sunishsheth2009
sunishsheth2009 force-pushed the sunish-sheth_data/mcp-proxy-compat-guard branch from 0e8cabe to cbd037c Compare August 14, 2026 21:09
…atabricks#307)

A fresh `ucode` install used to resolve `mcp 2.0.0` off an unconstrained
`mcp>=1.28.0`, and `ucode mcp-proxy` then crashed on import because 2.x renamed
`streamablehttp_client` (and swapped httpx for httpx2). Every MCP server
configured through the proxy failed to connect, surfaced to the coding agent as
a generic connection failure with no hint at the real cause.

The dependency cap (`mcp>=1.28.0,<2`) already landed in databricks#311. This adds the
remaining two acceptance criteria from the issue:

- **Actionable compatibility error.** `ucode mcp-proxy` catches the ImportError
  at the proxy import site and prints a reinstall instruction naming the
  installed version and the supported range, then exits non-zero — instead of an
  opaque traceback. The message is built in cli.py (not mcp_proxy) because
  mcp_proxy is exactly the module that fails to import.
- **Compat tests.** Assert the pin stays `<2`; assert the *resolved* mcp SDK
  satisfies it and that `mcp_proxy` imports against it (the clean-install guard,
  at test time); and cover the cli guard end-to-end (exit 1 + guidance).

Verified: simulated an mcp-2.x-style import failure and confirmed the CLI exits
1 with the reinstall hint and no traceback. Full suite: 1788 passed (2
pre-existing live-gateway e2e failures unrelated); ruff + ty clean.

Fixes databricks#307

Co-authored-by: Isaac
)

A fresh `ucode` install resolved mcp 2.0.0 off `mcp>=1.28.0` and `ucode
mcp-proxy` crashed on import, because 2.x renamed `streamablehttp_client` to
`streamable_http_client` and moved from httpx to httpx2. databricks#311 had capped
`mcp<2` to stop the bad resolution; this instead makes the proxy work against
both majors so the cap can be removed and a clean install on mcp 2.x just works.

Key realization: both mcp 1.28+ and 2.x export the 2.x-native
`streamable_http_client(url, http_client=<AsyncClient>)` (1.x's
`streamablehttp_client` is a deprecated shim over it). So a single code path
works if we (a) build the AsyncClient/Auth from the httpx flavor the installed
SDK uses, and (b) tolerate the yield arity change.

- `_httpx()` resolves httpx2 (mcp 2.x) else httpx (mcp 1.x), via importlib so the
  type checker/CI (pinned to 1.x, no httpx2) don't choke on a static import.
- `_build_token_auth()` subclasses that flavor's `Auth` — its `auth_flow`
  generator contract is identical in httpx and httpx2.
- `_run` unpacks `streams[0], streams[1]`: mcp 1.x yields
  (read, write, get_session_id), mcp 2.x yields (read, write); we don't use the
  trailing callback.
- pyproject: `mcp>=1.28.0,<2` -> `mcp>=1.28.0`; httpx stays a direct dep for the
  1.x path, httpx2 comes in transitively with mcp 2.x (no direct dep needed).

Verified against BOTH majors in a real venv: proxy tests (21) pass and `ty
check src/` is clean under mcp 1.28.1 AND mcp 2.0.0; under 2.x the proxy imports
(the databricks#307 crash point), selects httpx2, composes
httpx2.AsyncClient(auth=...) + streamable_http_client(http_client=...), and
reaches the auth preflight. Full suite: 1788 passed (2 pre-existing live-gateway
e2e failures, unrelated). ruff clean.

Fixes databricks#307

Co-authored-by: Isaac
`ucode mcp-proxy --use-pat` didn't actually work: `databricks auth token` only
reads OAuth caches, so a static-PAT profile is never resolved by the per-request
token mint on its own — the PAT has to be exported as DATABRICKS_BEARER first
(ensure_pat_bearer). Nothing in the proxy did that, so PAT — a previously
supported auth path — silently failed in the mcp-2 rework.

`serve` now calls `ensure_pat_bearer(profile)` when `use_pat` is set (before the
token preflight), exporting the profile's PAT so every per-request mint takes
the DATABRICKS_BEARER short-circuit; it fails fast with an actionable message if
no PAT is found. This lives in `serve` (which already owns the proxy's auth
preflight + fail-fast), not the CLI, so the command stays a thin forwarder:

    serve(url, workspace, profile, use_pat=use_pat or bool(state.get("use_pat")))

`_run` takes no use_pat — once the PAT is in the env, the transport path is
identical to OAuth.

Tests: test_mcp_proxy covers serve's PAT handling (exports the bearer before
serving; missing PAT exits before the bridge opens; OAuth path never consults
ensure_pat_bearer); test_cli asserts the flag/saved-state is forwarded to serve.
Full suite 1794 passed (2 pre-existing live-gateway e2e failures). ruff + ty clean.

Co-authored-by: Isaac
@sunishsheth2009
sunishsheth2009 force-pushed the sunish-sheth_data/mcp-proxy-compat-guard branch from cbd037c to 39d6482 Compare August 14, 2026 22:11
@sunishsheth2009
sunishsheth2009 enabled auto-merge (squash) August 14, 2026 22:21
@sunishsheth2009
sunishsheth2009 merged commit 94271a7 into databricks:main Aug 14, 2026
2 checks passed
@sunishsheth2009
sunishsheth2009 deleted the sunish-sheth_data/mcp-proxy-compat-guard branch August 14, 2026 22:22
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.

[Bug] Fresh ucode install pulls mcp 2.0 and mcp-proxy crashes on a removed API

2 participants