Skip to content

feat(mcp): OAuth 2.1 authorization for /api/v1/mcp (self-serve connect from claude.ai / Claude Code / Desktop) #2698

Description

@os-zhuang

Context

The open edition's AI surface is MCP-only (BYO-AI via @objectstack/mcp, see content/docs/ai/agents.mdx). Today the HTTP MCP endpoint (/api/v1/mcp) authenticates only with API keys (x-api-key / Authorization: ApiKey, hashed at rest, scoped, fail-closed — packages/core/src/security/api-key.ts). That works for headless agents and developers, but it blocks the mainstream human-connected clients:

  • claude.ai custom connectors and the Anthropic connector directory require the MCP-spec authorization flow (OAuth 2.1).
  • Claude Desktop / Claude Code offer a much smoother connect experience over OAuth (browser login as yourself) than hand-copying header config.
  • Every user currently needs an admin-minted API key before they can connect anything.

The MCP authorization spec is deliberately decentralized: the client discovers the authorization server via .well-known metadata on the MCP host itself, self-registers via Dynamic Client Registration (RFC 7591), and runs an authorization-code + PKCE flow. This means self-hosted / private deployments work out of the box — each deployment is its own authorization server; nothing is pre-registered with Anthropic or with any central ObjectStack service.

Goal

Add spec-compliant OAuth 2.1 authorization to the MCP endpoint so any MCP client can connect self-serve, while keeping API keys as the headless track.

Design decisions (rationale lives here + changeset; no separate ADR per repo convention)

  1. Each deployment is its own authorization server. Back the flow with the embedded better-auth instance (it already owns login/sessions). No central authority, no dependency on cloud — the community/self-hosted edition is the primary target, not an afterthought.
  2. Discovery endpoints: serve .well-known/oauth-protected-resource (resource metadata pointing at the AS) and .well-known/oauth-authorization-server (AS metadata) from the same origin as /api/v1/mcp.
  3. Dynamic Client Registration is mandatory. Since every deployment is a distinct AS, clients cannot ship pre-registered client IDs. DCR is the enabler for "one generic client config works against any deployment".
  4. PKCE required; TLS required (localhost exempt per OAuth 2.1). Intranet deployments with an internal CA work for local clients (Claude Code/Desktop); claude.ai web connectors additionally need the endpoint publicly reachable — a network decision, out of our hands, but must be documented.
  5. Token → ExecutionContext mapping: a valid access token resolves to the same principal-bound ExecutionContext used today (via resolveAuthzContext); RLS and permissions apply unchanged. OAuth adds a second provenance for the principal, not a second authz model.
  6. Dual-track, additive: API keys stay exactly as they are (CI, headless agents, environments without TLS). Nothing about the existing header auth changes.
  7. Fail-closed parity with API keys: unknown/expired/revoked tokens, missing scopes, or ambiguous state → 401/403, never fallback to anonymous.
  8. Scopes: map coarse scopes to the exposed tool families (data read, data write, actions). Keep the initial scope set minimal; finer grades can follow.

Scope

  • .well-known discovery endpoints (protected-resource + authorization-server metadata)
  • better-auth wired as the OAuth 2.1 AS: DCR, authorization-code + PKCE, token issuance/refresh, revocation
  • Bearer-token authentication on /api/v1/mcp, resolving to the caller's ExecutionContext (single-sourced through resolveAuthzContext, per the authz-resolver lint gate)
  • 401 responses advertise the discovery metadata (WWW-Authenticate per MCP spec) so clients can bootstrap the flow
  • Scope enforcement at the tool-dispatch layer, fail-closed
  • Tests: metadata endpoints, DCR round-trip, PKCE flow, token→principal mapping (RLS respected), expired/revoked/scope-miss rejections, API-key path regression
  • End-to-end verification: connect a real MCP client (Claude Code) to a dev instance via the OAuth flow

Docs to update on completion (acceptance criteria)

  • packages/mcp/README.md — connect section: OAuth as the human-client path, API key as the headless path
  • packages/mcp/src/skill.ts — the generated SKILL.md "Connect" section currently documents only x-api-key; describe both tracks
  • content/docs/ai/agents.mdx — BYO-AI connect instructions per client (claude.ai / Claude Desktop / Claude Code), incl. the private-deployment reachability note (local clients reach intranet; claude.ai web needs public HTTPS)
  • content/docs/getting-started/build-with-claude-code.mdx — update the connect snippet
  • Regenerate reference docs if any spec schema changes

Out of scope (follow-ups, tracked separately)

  • Official Claude Code plugin bundling the generic skill + ${OBJECTSTACK_MCP_URL} MCP config (executes ADR-0036 Amendment C; one plugin serves every deployment — URL is the only per-customer input)
  • "Connect an agent" page in Setup (objectui): mint key / show per-client snippets / download SKILL.md
  • Anthropic connector directory submission (depends on this issue)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions