mcp-proxy: drive per-connection login on a 401 (generic, all agents) - #557
Merged
sunishsheth2009 merged 10 commits intoSep 23, 2026
Merged
sunishsheth2009 merged 10 commits into
sunishsheth2009 merged 10 commits into
Conversation
This was referenced Sep 10, 2026
sunishsheth2009
force-pushed
the
mcp-connection-login-proxy
branch
from
September 10, 2026 23:44
42235ff to
79ee16c
Compare
sunishsheth2009
force-pushed
the
mcp-connection-login-proxy
branch
2 times, most recently
from
September 11, 2026 04:01
9dec3fa to
35c1a1d
Compare
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
force-pushed
the
mcp-connection-login-proxy
branch
3 times, most recently
from
September 20, 2026 19:48
3242638 to
608e8d7
Compare
sunishsheth2009
force-pushed
the
mcp-connection-login-proxy
branch
from
September 23, 2026 04:40
06e3966 to
720cb96
Compare
xsh310
reviewed
Sep 23, 2026
xsh310
reviewed
Sep 23, 2026
xsh310
approved these changes
Sep 23, 2026
Collaborator
|
Might be helpful to add a video recording of this |
xsh310
reviewed
Sep 23, 2026
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
force-pushed
the
mcp-connection-login-proxy
branch
from
September 23, 2026 20:52
d99ae4b to
c28a77b
Compare
sunishsheth2009
enabled auto-merge (squash)
September 23, 2026 22:13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Give every coding agent (codex, cursor, gemini, claude, …) a working login flow for connection-backed AI Gateway
mcp-servicesendpoints (e.g.system.ai.github) — implemented in theug mcp-proxystdio bridge every agent already spawns. No new library (this is what a generic OAuth MCP bridge likemcp-remotedoes, 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-servicesendpoint needs a per-user connection credential before its tools can be used; until then AI Gateway answers with an RFC 9728401. The proxy handles that inside its httpxAuthhook:tools/listneeds no credential, so nothing blocks startup.401from a connection-backed service, the auth flow runs a one-timedatabricks auth login --resource <mcp-url>and retries. The RFC 8707resourceindicator makes a resource-aware/oidcroute 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.anyio.to_thread.run_syncinasync_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_intool / rewrotetools/list, and an eager connect-time login that ran a blockingdatabricks auth loginbefore 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 howmcp-remotebehaves.Dependencies
--resource: Allow OAuth U2M logins to send RFC 8707 resource indicators cli#6763 (merged). An older CLI without the flag fails fast with a clear "upgrade your CLI" message./oidcresource-indicator handling (login §2) +/mcp-service-loginreturn_to (§3) — the server side of this flow. AI Gateway §1 (the 401) is already on staging.Scope
Tests
test_mcp_connection_login— connection-FQN parsing, and the CLI login runner with the subprocess mocked (sends--resource/--hostand 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-patnever log in).uv run pytest tests/test_mcp*.pygreen; 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).All 46 tests (click to expand)