Skip to content

Cap mcp below 2.0 so ucode mcp-proxy imports - #311

Merged
xsh310 merged 1 commit into
databricks:mainfrom
xsh310:cap-mcp-below-2
Aug 11, 2026
Merged

Cap mcp below 2.0 so ucode mcp-proxy imports#311
xsh310 merged 1 commit into
databricks:mainfrom
xsh310:cap-mcp-below-2

Conversation

@xsh310

@xsh310 xsh310 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Problem

ucode mcp-proxy — the stdio bridge every Databricks MCP server ucode configures points at — crashes on startup:

File ".../ucode/mcp_proxy.py", line 33, in <module>
    from mcp.client.streamable_http import streamablehttp_client
ImportError: cannot import name 'streamablehttp_client' from 'mcp.client.streamable_http'

The dependency was pinned mcp>=1.28.0 with no upper bound, so once mcp 2.0.0 published, fresh uv tool installs resolved to it. mcp 2.x renamed streamablehttp_client -> streamable_http_client and swapped httpx for httpx2, so ucode.mcp_proxy fails at import, before the MCP handshake. Because the crash is at module import, every Databricks MCP server routed through the proxy fails identically; clients surface it as the generic Failed to reconnect ... -32000.

Fix

Cap the constraint at mcp>=1.28.0,<2. The resolved version stays 1.28.1, so this is a pure constraint change with no code or transitive-resolution change (only the specifier line in uv.lock).

Porting the proxy to mcp 2.x is deliberately not in scope: 2.0 dropped the auth= kwarg the proxy relies on, requiring a rewrite of the per-request token-refresh path onto httpx2.AsyncClient, plus a new httpx2 dependency. The proxy is transport-only and gains nothing functional from 2.0, so that port should be a separate, deliberately-tested change (and should land as >=2,<3, keeping an upper bound).

Testing

  • Reproduced the crash against mcp==2.0.0; confirmed mcp==1.28.1 imports ucode.mcp_proxy cleanly.

This pull request and its description were written by Isaac.

`uv tool install` resolves deps fresh and ignores uv.lock, so the
unbounded `mcp>=1.28.0` picked up mcp 2.0.0 once it published. That
breaks `ucode.mcp_proxy` at import: mcp 2.x moved from httpx to httpx2
and renamed `streamablehttp_client` to `streamable_http_client`, so the
proxy died with `ModuleNotFoundError: No module named 'httpx'` and every
MCP client saw `-32000: Connection closed`.

Lockfile-based runs stayed on 1.28.1, which is why CI never caught it.
Only the uv.lock `requires-dist` specifier changes -- the resolved pin is
already 1.28.1 and satisfies the cap, so no dependency versions move.

Unblocks bugbash. The real follow-up is porting the proxy to mcp 2.x.

Co-authored-by: Isaac
@xsh310
xsh310 requested a review from AarushiShah-db August 11, 2026 05:23
@xsh310
xsh310 merged commit cd28116 into databricks:main Aug 11, 2026
2 checks passed
sunishsheth2009 added a commit to sunishsheth2009/ucode that referenced this pull request Aug 14, 2026
)

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
sunishsheth2009 added a commit to sunishsheth2009/ucode that referenced this pull request Aug 14, 2026
…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
sunishsheth2009 added a commit to sunishsheth2009/ucode that referenced this pull request Aug 14, 2026
)

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
sunishsheth2009 added a commit that referenced this pull request Aug 14, 2026
* mcp-proxy: actionable error on incompatible mcp SDK + compat tests (#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 #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 #307

Co-authored-by: Isaac

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

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. #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 #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 #307

Co-authored-by: Isaac

* mcp-proxy: resolve static PAT auth in serve() (--use-pat)

`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
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.

2 participants