Repository navigation
Description
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 noWWW-Authenticateheader, 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:
401on/mcpcarriesWWW-Authenticate: Bearer resource_metadata="https://<admin>/.well-known/oauth-protected-resource"(RFC 9728)./.well-known/oauth-protected-resourcenames the authorization server and the scopes (site:read,site:write,site:publish,site:rollback, …) the policy engine already knows.- 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.
- A token's subject is a member of the installation; its scopes are capped by the member's role, and the resulting
Principalgoes through the same policy engine, audit log and approval flow as an agent key (no second authorization path). - Consent screen in Studio (Kumo, i18n), a "Connected agents" list with revocation, audit entries for grant/revoke.
- 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-Authenticateon 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
fix(api): harden the MCP OAuth server after review (#144)
fix(api): bind refresh families and codes to the grant (#144)
fix(api): mirror the provider's authorize parameters; check codes (#144)
fix(api): grant ids by rejection sampling, no modulo bias (#144)
Merge pull request #176 from cloudhal-dev/feat/mcp-oauth-authorizatio…
Madraka commentedon Nov 9, 2026
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:
createInstance. Access tokens are JWTs that live 1 h and carry acloudhal_grantclaim. 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).0031_oauthwith the Better Auth tables andagent_grants, plus an index for refresh-family lookup.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.agentCredential()).cloudhal studiotakes its issuer from--base-url. The admin Worker uses its public origin. The playground stays out.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.McpClientSetuptab,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_embargofrom an OAuth connection saves the gate but files no approval request. It should file one, as the key flow does.fix(studio): show an OAuth client's identity safely on the consent sc…
fix(api): notify an on-approval release for as long as its request wa…
fix(api): bound CIMD clients, accept native redirect schemes, check t…
fix(api): notify an on-approval release only while its request is pen…
fix(api): close the CIMD budget and cap lockouts; consent identity ro…
fix(api): evict CIMD clients by their real host; prune and look-alike…
Merge pull request #178 from cloudhal-dev/feat/mcp-oauth-client-regis…
fix(studio): stack Connected agents on phones, warn hosted clients of…
Merge pull request #179 from cloudhal-dev/feat/mcp-oauth-connected-ag…
Madraka commentedon Nov 10, 2026
Phase 3 is merged. RFC-0044 is fully implemented on
main, and the pending changesets ship it in 0.6.6.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_embargofrom a connection now files an approval request.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 againstcloudhal 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:
CLOUDHAL_BEHIND_PROXY=1), one party can rate-limit another's stale CIMD refetch for a minute at a time. The self-host guide documents this.I'm closing this issue when 0.6.6 is released.