Skip to content

MCP: remote clients cannot connect with OAuth — implement OAuth 2.1 authorization (RFC) #144

Open

Description

@Madraka

Problem

Remote MCP clients that implement the MCP Authorization spec (Claude.ai / Claude Desktop custom connectors, ChatGPT, Cursor, VS Code, the Inspector's OAuth flow) cannot connect to a Cloudhal deployment's /mcp with OAuth. The connection attempt fails at discovery.

What the server does today (packages/api/src/app.ts, app.all("/mcp"), ADR-0004 "No OAuth provider", ADR-0011 §5–6):

  • Accepts only a Studio session cookie or an agent key (x-api-key / Authorization: Bearer <key>).
  • An unauthenticated request gets 401 {"error":"unauthorized","message":"sign in required"} with no WWW-Authenticate header, so a client cannot find the protected-resource metadata.
  • There is no /.well-known/oauth-protected-resource, no authorization server (/.well-known/oauth-authorization-server), no /authorize, /token, /register (DCR) and no client ID metadata document (CIMD) support.

Agent keys keep working for clients that accept a static header (Claude Code claude mcp add --transport http --header, scripts, CI), so this is a gap for the hosted / "add connector by URL" case, not a regression.

Expected

A Cloudhal deployment (Cloudflare admin Worker and self-hosted cloudhal studio on Node + SQLite) is a spec-compliant MCP resource server:

  1. 401 on /mcp carries WWW-Authenticate: Bearer resource_metadata="https://<admin>/.well-known/oauth-protected-resource" (RFC 9728).
  2. /.well-known/oauth-protected-resource names the authorization server and the scopes (site:read, site:write, site:publish, site:rollback, …) the policy engine already knows.
  3. The authorization server (RFC 8414 metadata, OAuth 2.1 authorization code + PKCE, refresh tokens, resource indicators RFC 8707) is Better Auth's provider plugin on the same database — the ADR-0010 decision ("hand-rolling an OAuth AS is the riskiest code a small team can write"), so it runs on D1 and SQLite alike. Client registration: CIMD first, dynamic client registration (RFC 7591) as the fallback, as ADR-0004 planned.
  4. A token's subject is a member of the installation; its scopes are capped by the member's role, and the resulting Principal goes through the same policy engine, audit log and approval flow as an agent key (no second authorization path).
  5. Consent screen in Studio (Kumo, i18n), a "Connected agents" list with revocation, audit entries for grant/revoke.
  6. Tested on sqlite and Miniflare D1 with the shared suites; an end-to-end test with @modelcontextprotocol/client's OAuth provider against the Node server.

Plan

This changes a public interface, so it starts as an RFC (docs/rfcs/, .agents/skills/write-rfc): spec versions, Better Auth @better-auth/oauth-provider / @better-auth/mcp 1.7.x evaluation (we are on 1.7.12), token format (opaque vs JWT with JWKS), where the AS lives for multi-site installations (RFC-0034), migration for existing agent keys, threat model (DNS rebinding check already in place, token audience, PKCE only, no implicit flow), the self-host base URL (depends on #143's --base-url).

Implementation phases after the RFC is accepted:

  • Phase 1 — discovery + resource metadata + WWW-Authenticate on 401 (small, unblocks diagnosis).
  • Phase 2 — authorization server via Better Auth on sqlite + D1, consent screen, token verification on /mcp.
  • Phase 3 — CIMD + DCR, Studio "Connected agents", docs (docs/guides/), ADR-0004/0011 updates.

References

  • ADR-0004 (MCP server), ADR-0010 (Better Auth, OAuth sources), ADR-0011 (agent layer; OAuth 2.1 deferred to "Sprint 6")
  • .10x/status.md "Sprint 6 candidates": OAuth 2.1 authorization server for remote agents
  • MCP specification, Authorization section (2025-06-18 and later revisions)

Activity

  1. added
    enhancementNew feature or request
    rfcA design proposal in docs/rfcs
    on Nov 7, 2026
  2. Madraka commented on Nov 9, 2026

    @Madraka
    ContributorAuthor

    Phase 2 is merged in #176 (RFC-0044 §9: the authorization server). It is not released yet; the pending changeset ships with the next release.

    What landed:

    • Better Auth 1.7.18 family, with the OAuth provider and JWT plugins in createInstance. Access tokens are JWTs that live 1 h and carry a cloudhal_grant claim. Refresh tokens rotate, with a 30 s reuse window and a 90-day cap measured from the start of the family. PKCE S256 is required. DPoP is refused rather than ignored (ADR-0045).
    • Migration 0031_oauth with the Better Auth tables and agent_grants, plus an index for refresh-family lookup.
    • Token → Principal. Every request reads the live grant. Old tokens, refresh families and authorization codes are bound to the grant generation, so revoking a connection and reconnecting does not bring back credentials issued before the revoke. Member removal ends the connections in the same batch as the membership delete.
    • Studio consent screen. Requested scopes are narrowed by the installation role. Mode is chosen at consent. Step-up shows only the new scopes as unticked.
    • Agent self-approval is closed: OAuth token principals are treated as agents wherever agent keys were (agentCredential()).
    • cloudhal studio takes its issuer from --base-url. The admin Worker uses its public origin. The playground stays out.
    • ADR-0045 is new. ADR-0004, ADR-0010 and ADR-0011 are updated.

    Review: an independent 40-agent security review, then three fix rounds, each one verified. CodeQL is clean.

    Still open for Phase 3 (keep this issue open):

    • cimd() client metadata documents, and DCR with limits and pruning. Until then only pre-registered clients can connect.
    • Studio "Connected agents", the McpClientSetup tab, docs/guides/mcp-oauth.md, the docs site's clients page, the hardening guide, and the e2e test with MCP Inspector / the TS SDK.
    • set_embargo from an OAuth connection saves the gate but files no approval request. It should file one, as the key flow does.
    • Studio's 18 consent-screen translations are machine drafts awaiting review (i18n: review machine-drafted Studio strings (fields, listings, collections) #112).
  3. Madraka commented on Nov 10, 2026

    @Madraka
    ContributorAuthor

    Phase 3 is merged. RFC-0044 is fully implemented on main, and the pending changesets ship it in 0.6.6.

    • feat(api): MCP OAuth client registration (CIMD and DCR), RFC-0044 phase 3a (#144) #178, client registration: Client ID Metadata Documents with a fetch guard and budgets, stored-client freshness, a capped store with eviction by real host, and pruning. Dynamic client registration for public clients only, with per-address limits, a cap, pruning and CLOUDHAL_MCP_DCR=off. Private-use redirect schemes for native clients. A client host allow-list checked at every authorization. Consent identity: host first, DCR shown as "self-registered, not verified", and a look-alike warning. set_embargo from a connection now files an approval request.
    • feat(studio): Connected agents and MCP client setup, RFC-0044 phase 3b (#144) #179, Studio and docs: Connected agents, for your own connections and an administrator view of everyone's, with Disconnect. "Connect an MCP client" per-client instructions, with a public-URL warning for hosted clients on a local Studio. docs/guides/mcp-oauth.md, the docs site's client page and the mcp-server README. An e2e test runs in CI on Node 24 and 26 against cloudhal studio: discovery, registration, PKCE, consent, token, tools/call.

    Every PR went through an independent multi-agent review and verified fix rounds. CodeQL is clean.

    Follow-ups, not blocking:

    I'm closing this issue when 0.6.6 is released.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestrfcA design proposal in docs/rfcs

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions