Skip to content

ug mcp add: native OAuth (direct HTTP) for Claude Code + Cursor, else generic proxy - #560

Merged
sunishsheth2009 merged 5 commits into
databricks:mainfrom
sunishsheth2009:mcp-claude-code-http
Sep 14, 2026
Merged

sunishsheth2009 merged 5 commits into
databricks:mainfrom
sunishsheth2009:mcp-claude-code-http

Conversation

@sunishsheth2009

Copy link
Copy Markdown
Collaborator

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

Comment thread src/ucode/mcp_oauth.py Outdated
Comment thread src/ucode/mcp_oauth.py Outdated
Comment thread src/ucode/mcp_oauth.py
Comment thread src/ucode/mcp.py
Comment thread src/ucode/mcp_oauth.py Outdated

@xsh310 xsh310 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.

Overall looks good, if someone with more auth context can sanity check that'd be best

sunishsheth2009 and others added 4 commits September 14, 2026 22:41
…r OAuth-client-capable agents

Connection-backed AI Gateway MCP services (…/ai-gateway/mcp-services/…) need a
per-user connection login before their tools can be called. Registered as the
stdio proxy, an agent only ever sees "connected" (the proxy injects a workspace
token) and the login can't be triggered from /mcp. Registered as a direct-HTTP
server, the agent does the OAuth itself: /mcp shows "needs authentication" →
Authenticate → /oidc → /mcp-service-login → login → connected.

/oidc has no dynamic client registration, so the agent must present a
pre-registered public client. Only Claude Code's `mcp add` supports pinning one
(--client-id); its published claude-code app has the loopback /callback redirect
Claude uses. Other agents' `mcp add` accept only a static bearer, not an OAuth
client — codex (--bearer-token-env-var), gemini (--header), cursor (config) — so
despite their published apps (codex-cli, cursor-desktop, …) they can't drive
this flow and stay on the stdio proxy.

ug mcp add now registers these services as direct HTTP with the agent's OAuth
client (AGENT_OAUTH_CLIENT — today just claude → claude-code) when that client is
registered on the workspace, probed via a back-channel token-endpoint check
(401 invalid_client = absent, cached per workspace+client). Everything else keeps
the stdio proxy: non-connection MCPs, the skills registry, PAT auth, agents with
no mapped OAuth client, and workspaces where the client isn't published.

Co-authored-by: Isaac <no-reply@databricks.com>
Cursor's mcp.json accepts a pre-registered OAuth client via an `auth.CLIENT_ID`
object (no dynamic client registration, which /oidc lacks), and drives the login
to its fixed loopback `/callback` redirect — the same precondition claude-code
satisfies. So for a connection-backed mcp-services endpoint, when the workspace
has the `cursor-desktop` OAuth app (probed + cached like claude-code), register a
url+auth server in ~/.cursor/mcp.json instead of the stdio proxy, giving Cursor
its native connection login. Everything else (no published app, other agents, PAT,
non-connection MCPs) still falls back to the generic proxy.

Co-authored-by: Isaac <no-reply@databricks.com>
always_load is set only for the skills registry, whose URL is /ai-gateway/skills/
and never matches the AIGW_MCP_SERVICES_PATH (/ai-gateway/mcp-services/) check that
already gates the direct-HTTP path — so the guard was dead. Removing it leaves the
mcp-services URL check as the single, precise discriminator. Behavior-identical.

Co-authored-by: Isaac <no-reply@databricks.com>
- _probe_oauth_client now returns True/False/None: 400 => registered, 401 =>
  absent, and anything else (429/5xx, 404/redirect, network error, unexpected
  2xx) => None (inconclusive). oauth_client_available treats None as "fall back
  to the stdio proxy" and does NOT cache it, so a transient status/incident can't
  flip a workspace to the direct-HTTP path or get stuck for the week-long TTL.
- Cache I/O goes through config_io.read_json_safe / write_json_file, which
  ensures APP_DIR exists before writing (consistent with the other APP_DIR
  writers), fixing the write on a fresh ~/.ucode.

Co-authored-by: Isaac <no-reply@databricks.com>
Collapse the one-line lambda flagged by `ruff format --check` (test_lint gate).

Co-authored-by: Isaac <no-reply@databricks.com>
@sunishsheth2009
sunishsheth2009 merged commit b972125 into databricks:main Sep 14, 2026
2 checks passed
@sunishsheth2009
sunishsheth2009 deleted the mcp-claude-code-http branch September 14, 2026 22:59
sunishsheth2009 added a commit that referenced this pull request Sep 15, 2026
…623)

## What
Cleanup follow-up to #560. Two independent tidy-ups:

1. **`agents/cursor.py` dedup.** `write_mcp_server_config` (stdio proxy)
and `write_http_mcp_server_config` (OAuth HTTP) were byte-for-byte
identical except the entry they store, so the read → merge → write logic
is factored into a single `_upsert_mcp_server(name, entry)` helper and
both writers are thin wrappers.

2. **Move the Claude MCP-registration helpers into `agents/claude.py`.**
`add_claude_mcp_server`, `add_claude_http_mcp_server`, and
`remove_claude_mcp_server` lived in `ucode.mcp` even though every other
agent's registration lives in its own `agents/<agent>.py`. They now sit
next to the Claude agent, so `mcp.py` dispatches uniformly (`claude.X`,
like `cursor.X` / `opencode.X`).
- Shared scope constants (`MCP_USER_SCOPE`, `MCP_CLEANUP_SCOPES`) move
to the leaf `ucode.constants` so both `mcp` and `agents.claude` import
them with **no import cycle** (verified both import orders load clean).
- `_is_missing_mcp_server_output` stays in `mcp.py` (shared by the
codex/gemini removers); `claude.remove_claude_mcp_server` imports it
lazily to avoid the `mcp -> agents.claude` load cycle — the same
lazy-import pattern `claude.py` already used for these helpers.
- `claude.py`'s web_search register/unregister now call the local
helpers directly.
   - The corresponding unit tests move to `tests/test_agent_claude.py`.

## Why
Removes duplicated JSON-merge logic and puts each agent's
MCP-registration code in that agent's module, so `mcp.py` is
orchestration-only.

## Testing
`uv run pytest tests/test_mcp.py tests/test_agent_claude.py
tests/test_agent_cursor.py tests/test_mcp_oauth.py` (330 passed); full
suite green except 7 pre-existing failures unrelated to this change
(smart-routing v2 / e2e-user-agent, fail on master too). ruff
format/check + ty clean. Behavior-identical.

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

---------

Co-authored-by: Isaac <no-reply@databricks.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.

2 participants