Skip to content

mcp-proxy: drive per-connection login on a 401 (generic, all agents) - #557

Merged
sunishsheth2009 merged 10 commits into
databricks:mainfrom
sunishsheth2009:mcp-connection-login-proxy
Sep 23, 2026
Merged

sunishsheth2009 merged 10 commits into
databricks:mainfrom
sunishsheth2009:mcp-connection-login-proxy

Conversation

@sunishsheth2009

@sunishsheth2009 sunishsheth2009 commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

Dependency merged: databricks auth login --resource shipped in databricks/cli#6763, so the connection sign-in works with a current Databricks CLI. Rebased on main.

#654 (ug mcp login) is stacked on this and reuses the mcp_connection_login module introduced here (connection_from_url, run_connection_login). Merge this first.

What

Give every coding agent (codex, cursor, gemini, claude, …) a working login flow for connection-backed AI Gateway mcp-services endpoints (e.g. system.ai.github) — implemented in the ug mcp-proxy stdio bridge every agent already spawns. No new library (this is what a generic OAuth MCP bridge like mcp-remote does, done in ucode with the Databricks CLI), and no per-agent OAuth app.

How — login is driven lazily, on a 401 (not eagerly at startup)

A connection-backed mcp-services endpoint needs a per-user connection credential before its tools can be used; until then AI Gateway answers with an RFC 9728 401. The proxy handles that inside its httpx Auth hook:

  1. The bridge comes up immediately — tools/list needs no credential, so nothing blocks startup.
  2. On the first request that gets a 401 from a connection-backed service, the auth flow runs a one-time databricks auth login --resource <mcp-url> and retries. The RFC 8707 resource indicator makes a resource-aware /oidc route the browser through the connection's own SaaS login (/mcp-service-login) before minting the token. It uses the CLI's own default client + registered loopback redirect — no --client-id, no auth-side redirect change.
  3. The blocking login runs off the event loop (anyio.to_thread.run_sync in async_auth_flow), so the bridge's other pumps aren't starved while the user completes the sign-in. It fires at most once per session; an already-signed-in service never prompts; PAT profiles have no connection OAuth to drive and skip it entirely.

This is deliberately lazy: a browser opens only for a service whose tool you actually call, one at a time — not N browsers at startup for every configured server. The proxy never writes to stdout (the MCP JSON-RPC wire); the CLI's prompts and authorize URL go to stderr, so a headless/remote box still shows the URL to open manually.

Why this shape (supersedes the earlier bridges in this PR's history)

Earlier iterations tried a proxy that faked a sign_in tool / rewrote tools/list, and an eager connect-time login that ran a blocking databricks auth login before opening the bridge. The eager form popped a browser at every launch and, with N configured connection-backed servers, opened N browsers at once and stalled the agent's MCP startup until each timed out. Driving the login lazily on a real 401, off the event loop, once per session, is the minimal generic realization and matches how mcp-remote behaves.

Dependencies

Scope

  • Connection-backed mcp-services endpoints get the lazy login-on-401. PAT profiles skip it (no connection OAuth to drive).
  • Plain Databricks MCP services keep the existing transparent per-request token-injection (no extra login).

Tests

test_mcp_connection_login — connection-FQN parsing, and the CLI login runner with the subprocess mocked (sends --resource/--host and no --client-id; routes output to stderr; reports nonzero-exit, timeout, and missing-binary; old-CLI clear error). test_mcp_proxy — the lazy auth flow (no login when the response isn't a connection 401; login-then-retry on a 401; login fires at most once; non-connection URLs and --use-pat never log in). uv run pytest tests/test_mcp*.py green; ruff + ty clean.

This pull request and its description were written by Isaac.

Test evidence

Local run on c28a77b (the pushed head, rebased on latest main): lint, type-check, and the full proxy + connection-login suites all green. CI on this PR is also green (Unit tests, Agent launch tests for all six agents, Gateway API, Integration/Installation).

### ruff check       → All checks passed!
### ty typecheck     → All checks passed!

### pytest tests/test_mcp_connection_login.py tests/test_mcp_proxy.py
============================= test session starts ==============================
platform linux -- Python 3.12.13, pytest-9.1.1
collected 46 items

tests/test_mcp_connection_login.py ..........                            [ 21%]
tests/test_mcp_proxy.py ....................................             [100%]
============================== 46 passed in 1.31s ==============================
All 46 tests (click to expand)
TestConnectionFromUrl:
  test_plain_endpoint
  test_with_trailing_path_and_query
  test_non_aigw_url_is_none
  test_missing_service_is_none
TestRunConnectionLogin:
  test_success_sends_resource_and_host_without_client_id
  test_nonzero_exit_reports_failure
  test_output_is_routed_to_stderr_not_stdout
  test_timeout_is_reported
  test_binary_missing_is_reported
  test_old_cli_without_resource_flag_reports_clearly
test_mcp_proxy (SDK/httpx wiring):
  test_httpx_is_a_direct_runtime_dependency
  test_mcp_dependency_is_uncapped
  test_selected_httpx_matches_the_installed_mcp_sdk
  test_proxy_imports_the_streamable_http_client_shared_by_both_majors
TestDatabricksTokenAuth (the lazy login-on-401 core):
  test_injects_bearer_from_minted_token
  test_auth_is_an_instance_of_the_selected_httpx_auth
  test_calls_get_token_with_workspace_and_profile
  test_mints_a_fresh_token_per_request
  test_auth_flow_yields_the_same_request
  test_dead_auth_becomes_a_terminal_proxy_auth_error
  test_on_401_from_connection_service_runs_login_then_retries
  test_non_connection_401_is_not_retried
  test_success_response_never_logs_in
  test_login_runs_at_most_once_per_session
  test_use_pat_never_drives_connection_login
  test_login_failure_becomes_a_proxy_auth_error
  test_async_auth_flow_drives_login_on_401
TestPump / TestServe / TestPreflightToken:
  test_forwards_all_messages_in_order
  test_closes_destination_when_source_exhausts
  test_client_errors_are_forwarded
  test_upstream_errors_are_raised
  test_upstream_eof_is_an_error
  test_run_uses_mcp_http_defaults
  test_runs_the_bridge_with_parsed_args
  test_defaults_profile_none
  test_use_pat_exports_the_bearer_before_serving
  test_use_pat_without_a_resolvable_pat_exits_before_serving
  test_oauth_path_never_touches_pat
  test_preflights_auth_before_opening_the_bridge
  test_dead_auth_exits_fast_without_starting_the_bridge
  test_auth_expiring_mid_session_exits_with_the_actionable_message
  test_transport_failure_exits_with_a_one_line_message
  test_non_auth_failures_still_propagate
  test_passes_through_when_a_token_is_available
  test_surfaces_the_cli_error_message
  test_checks_the_same_workspace_and_profile_the_bridge_will_use

@sunishsheth2009
sunishsheth2009 force-pushed the mcp-connection-login-proxy branch from 42235ff to 79ee16c Compare September 10, 2026 23:44
@sunishsheth2009 sunishsheth2009 changed the title mcp-proxy: generic per-connection sign-in for AI Gateway MCP services mcp-proxy: drive per-connection login on a 401 (generic, all agents) Sep 10, 2026
@sunishsheth2009
sunishsheth2009 force-pushed the mcp-connection-login-proxy branch 2 times, most recently from 9dec3fa to 35c1a1d Compare September 11, 2026 04:01
@sunishsheth2009
sunishsheth2009 marked this pull request as ready for review September 11, 2026 04:05
sunishsheth2009 added a commit that referenced this pull request Sep 14, 2026
… generic proxy (#560)

## What

Give **Claude Code and Cursor** their *native* connection-login
experience for connection-backed AI Gateway `mcp-services` endpoints
(e.g. `system.ai.github`): the agent itself is the OAuth client and
drives the `/oidc` → `/mcp-service-login` login, so `/mcp` shows a real
"Authenticate" (Claude) / login (Cursor) prompt instead of a token
silently injected by the proxy. This is a much better UX than the
generic stdio-proxy path for the agents that can do it.

It is **probe-driven with a safe fallback**: only used when the agent's
OAuth app is actually published on the workspace; otherwise (and for
every other agent) it falls back to the generic `ug mcp-proxy` stdio
path (#557).

## How

`/oidc` has **no** dynamic client registration, so the agent must
present a **pre-registered public client** whose loopback `/callback`
redirect is registered:
- **Claude Code** → `claude mcp add --transport http --client-id
claude-code` (Claude drives MCP OAuth with the RFC 8707 `resource`
indicator).
- **Cursor** → a `url` + `auth: {CLIENT_ID: cursor-desktop}` entry in
`~/.cursor/mcp.json` (Cursor supports a pre-registered client; its fixed
`http://localhost:8787/callback` redirect matches by path, RFC 8252
§8.4).

`mcp_oauth.oauth_client_available(workspace, client_id)`
back-channel-probes `/oidc/v1/token` (401 `invalid_client` = absent,
else present) and caches the answer per workspace (weekly TTL).
`configure_client_mcp_server` registers direct-HTTP only when: the URL
is a connection-backed `mcp-services` endpoint, the agent has a mapped
OAuth client (`AGENT_OAUTH_CLIENT`), that client is published, and it
isn't a `--use-pat` / skills-registry registration. Everything else
keeps the stdio proxy.

## Relationship to the generic proxy (#557)

Complementary. #557 gives **every** agent a working login via the proxy
(connect-time `databricks auth login --resource`). This PR is a UX
upgrade layered on top: where an agent *can* be the OAuth client and its
app exists, we let it drive the login natively. Same server-side flow
underneath (`/oidc` → `/mcp-service-login`), same §2/§3 dependency.

## Dependencies

Same server side as #557: AI Gateway §1 (`WWW-Authenticate` 401, already
on staging) + login §2 (`/oidc` resource-indicator redirect) + webapp §3
(`/mcp-service-login` `return_to`). No CLI dependency for the native
path (the agent is the OAuth client, not the CLI).

## Scope / fallback

- Connection-backed `mcp-services` + Claude/Cursor + published app →
native direct-HTTP.
- No published app, other agents (codex/gemini/copilot/opencode), PAT,
non-connection MCPs, skills registry → generic stdio proxy (unchanged).

## Tests

`test_mcp_oauth` (probe: present/absent/network-fail, cache TTL).
`test_mcp.TestConfigureClientMcpServer` (Claude + Cursor each:
direct-HTTP when the app is available, proxy fallback when not, proxy
under `--use-pat`, proxy for non-connection URLs; an OAuth-less agent
proxies and never probes). `test_agent_cursor` (the `url`+`auth` entry
shape and merge). `uv run pytest` green (pre-existing unrelated failures
in `test_codex_smart_routing_v2` / `test_e2e_user_agent` confirmed on
`main`); ruff + ty clean.

_This pull request and its description were written by Isaac._

---------

Co-authored-by: Isaac <no-reply@databricks.com>
@sunishsheth2009
sunishsheth2009 force-pushed the mcp-connection-login-proxy branch 3 times, most recently from 3242638 to 608e8d7 Compare September 20, 2026 19:48
@sunishsheth2009
sunishsheth2009 force-pushed the mcp-connection-login-proxy branch from 06e3966 to 720cb96 Compare September 23, 2026 04:40
Comment thread src/ucode/mcp_connection_login.py
Comment thread src/ucode/mcp_proxy.py
@xsh310

xsh310 commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Might be helpful to add a video recording of this

Comment thread src/ucode/mcp_proxy.py Outdated
sunishsheth2009 and others added 9 commits September 23, 2026 20:49
Give every coding agent a working login flow for connection-backed AI Gateway
mcp-services endpoints, done in the stdio proxy every agent already spawns — no
new library (cf. mcp-remote) and no per-agent OAuth app.

When AI Gateway has no per-user connection credential it answers with HTTP 401
(RFC 9728). The proxy's httpx auth hook already sees every response, so on a 401
for a connection-backed URL it runs the Databricks CLI U2M login with an RFC 8707
resource indicator (`databricks auth login --resource <mcp-url>`, using the CLI's
own registered redirect — no --client-id), then retries with a fresh token. A
resource-aware /oidc drives the connection's SaaS login before minting the token,
so the retry succeeds — transparently to the agent, which just sees the request
authenticate rather than a failed tools/list. A later credential revoke re-triggers
the login on the next 401.

New module mcp_connection_login holds connection_from_url + run_connection_login;
mcp_proxy._build_token_auth gains the login-on-401 retry. Unit-tested. Depends on
the CLI --resource flag (databricks/cli#6621) and /oidc resource handling (login).

Co-authored-by: Isaac <no-reply@databricks.com>
Fixes the two defects that made the proxy hang "connecting…" on a 401 instead
of behaving like a generic OAuth MCP bridge (mcp-remote):

1. Non-blocking: the browser login ran via a synchronous subprocess inside the
   sync httpx auth_flow, which the async client executes on the event-loop
   thread — freezing the transport (stdio pumps included) for the whole login.
   Add async_auth_flow that offloads run_connection_login to a worker thread
   (anyio.to_thread.run_sync), so the loop stays responsive and cancellable
   while the user completes the browser flow. sync auth_flow kept for parity;
   both share the decision + login logic.

2. Visible URL: run_connection_login captured the CLI's output, hiding the
   authorize URL. Route the CLI's stdout+stderr to the proxy's stderr (fd 2,
   the agent's MCP log) — never fd 1 (the JSON-RPC wire) — and let the CLI open
   the browser, so the login is discoverable exactly like mcp-remote's.

Co-authored-by: Isaac <no-reply@databricks.com>
Makes the generic proxy behave like a generic OAuth MCP bridge (mcp-remote): the
connection login happens while the agent shows "connecting…", and the browser
opens on its own — instead of racing the agent's tools/list timeout or burying
the URL.

- Connect-time login: before opening the bridge, serve() probes the connection
  (_connection_login_required: a lightweight initialize + tools/list to AI
  Gateway); on a 401 it drives run_connection_login *then*, so the agent's
  session comes up already authenticated. The on-401 retry in the auth hook
  stays as a mid-session fallback (credential revoked while connected). PAT
  profiles skip it (no connection OAuth).
- Browser auto-open: the login inherits the environment (incl. $BROWSER), so
  databricks-cli opens the browser on the user's machine; the authorize URL is
  the printed fallback.

Co-authored-by: Isaac <no-reply@databricks.com>
… the probe

Simplify to what a generic OAuth MCP bridge (mcp-remote) does: authenticate at
connect, then serve. Before opening the bridge, serve() runs a blocking
`databricks auth login --resource <mcp-url>` for connection-backed services —
the databricks-cli equivalent of mcp-remote's in-process OAuth, where --resource
also routes /oidc through the connection sign-in (/mcp-service-login). The agent
blocks on "connecting…" while it runs (browser opens via $BROWSER, or the URL is
printed), then the session comes up authenticated, so AI Gateway is never asked
to elicit a login. Idempotent: once signed in it returns immediately.

Removes the redundant startup probe (Claude already fires initialize+tools/list;
the proxy shouldn't duplicate that) and the on-401 retry inside the auth hook
(the connect-time login makes it unnecessary). _build_token_auth is back to a
plain per-request bearer read of the session that login established.

Co-authored-by: Isaac <no-reply@databricks.com>
The connect-time connection login runs `databricks auth login --resource`; an
older CLI without databricks/cli#6621 rejects the flag and the login exits with
a cryptic parse error (seen as a bare "connection closed" in the agent). Add a
one-time `auth login --help` pre-check and, when `--resource` is absent, return a
clear "upgrade your Databricks CLI" message instead of attempting the doomed
login. Fail-open if --help can't run.

Co-authored-by: Isaac <no-reply@databricks.com>
…lly missing

The connect-time login ran `databricks auth login --resource` unconditionally on
every proxy startup for every connection-backed server. But `databricks auth
login` always re-runs the browser OAuth (it does not short-circuit on a cached
token), so N configured servers meant N browser windows on every session — and
concurrent `auth login` runs also race on the shared token cache.

Gate the login on the actual per-user credential state (new
`connection_credential_state`, using the same Unity Catalog REST APIs as `ug mcp
login`): only run the login when the credential is confirmed MISSING. When it is
already PRESENT — or the state can't be determined (UNKNOWN) or the connection
isn't OAuth-U2M (NO_LOGIN) — skip the browser entirely. `tools/list` still works
without the credential, so UNKNOWN fails safe rather than risking a spurious
browser. This makes repeat sessions open zero browsers and first sessions prompt
only for the servers genuinely needing a sign-in.

Co-authored-by: Isaac <no-reply@databricks.com>
… startup

The connect-time login blocked the bridge from opening until it finished. With N
configured connection-backed servers all missing their credential, an agent
spawns N proxies at once, each launches a blocking `databricks auth login`, and
they stall on browser flows / racing loopback ports until every one hits the
agent's MCP startup timeout — all N servers fail to start (observed with 5 on
Codex, all timing out at 120s).

Move the login into the httpx auth flow, triggered by a 401: the bridge opens
immediately (tools/list needs no credential and returns the full list), and
`databricks auth login --resource` runs only when a request actually gets a 401
— i.e. when a tool you invoke needs the per-user connection credential. So a
browser opens only for a service you use, one at a time; an already-signed-in
service never prompts; and startup never blocks. The login runs at most once per
session, off the event loop (async_auth_flow + anyio.to_thread) so the bridge's
pumps aren't starved. PAT profiles have no connection OAuth, so they skip it.

This drops the eager credential-state gate added in the previous commit (the
lazy 401 signal supersedes it) along with its UC-REST probe.

Co-authored-by: Isaac <no-reply@databricks.com>
…gin`

Drop the proxy-specific `ucode mcp-proxy:` prefix from the connect-time
sign-in message so `run_connection_login` / `connection_from_url` can be reused
verbatim by `ug mcp login` (PR databricks#654), which stacks on this one. Behavior is
unchanged for the proxy.

Co-authored-by: Isaac <no-reply@databricks.com>
… parse

Consolidate the AI Gateway mcp-services URL knowledge that had drifted into three
copies: the path-segment literal lived in databricks.py (URL building),
mcp.py (AIGW_MCP_SERVICES_PATH, URL-shape classification), and
mcp_connection_login.py (AIGW_MCP_SERVICES_SEGMENT, connection-backed detection).

Define AIGW_MCP_SERVICES_SEGMENT once in databricks.py (where the URL is built) and
import it into mcp.py and mcp_connection_login.py. Also reuse connection_from_url in
mcp._mcp_server_location instead of a parallel url.split(), so the URL -> FQN parse
lives in one place too. No behavior change (the parser additionally strips a trailing
slash/query, which is strictly more correct for the label).

Co-authored-by: Isaac <no-reply@databricks.com>
… nit)

Clearer name for the closure that drives the per-connection sign-in on a 401.

Co-authored-by: Isaac <no-reply@databricks.com>
@sunishsheth2009
sunishsheth2009 force-pushed the mcp-connection-login-proxy branch from d99ae4b to c28a77b Compare September 23, 2026 20:52
@sunishsheth2009
sunishsheth2009 enabled auto-merge (squash) September 23, 2026 22:13
@sunishsheth2009
sunishsheth2009 merged commit 2870686 into databricks:main Sep 23, 2026
18 checks passed
@sunishsheth2009
sunishsheth2009 deleted the mcp-connection-login-proxy branch September 23, 2026 22:13
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