Skip to content

Add OAuth (user / manual) preset for closed-DCR MCP servers #286

Description

@germanescobar

Summary

Implement the oauth (manual / user) preset end-to-end so users can authorize
against MCP / REST servers that don't support public RFC 7591 dynamic client
registration. The dynamic preset in #280 doesn't help on those servers
(Figma's /v1/oauth/mcp/register returns 403); the spec calls this case out
explicitly and says the client must "present a UI to users that allows them
to enter these details, after registering an OAuth client themselves." The
data model, preset definition, and form fields are already in place; the
acquisition flow and the auth resolution on the server are not.

Background

#280 added an "OAuth (dynamic / MCP)" preset that runs the full RFC 8414 +
RFC 7591 + PKCE flow — perfect for MCP servers whose authorization server
supports public dynamic client registration (Linear, Sentry remote, Notion
remote, etc.). Figma's MCP server is the canonical counterexample: its
/v1/oauth/mcp/register endpoint returns 403 to anonymous callers, and
the metadata even lists the registration_endpoint but rejects anonymous
registrations. The spec explicitly tells the client to fall back to a
user-driven flow in this case.

Existing state

  • client/src/lib/integration-modes.ts already defines the oauth preset
    with acquisition: "oauth" and the right fields (clientId, authUrl,
    tokenUrl, scopes, optional clientSecret). It's hidden: true.
  • server/lib/integration-auth.ts returns reauth_needed for acquisition: "oauth" with a "needs to be connected" message but no actual flow.
  • The loopback listener, PKCE generation, token exchange, and refresh
    logic in server/lib/oauth-dynamic.ts can be reused as-is — the only
    thing that's different from the dynamic flow is that clientId and
    clientSecret come from the user instead of from a DCR response.

Acceptance criteria

  • "OAuth (user)" appears in the Add-scheme picker for any connection
    mode that supports it (any HTTP-family transport, including MCP).
  • Selecting it exposes Client ID, Authorization URL, Token URL,
    Scopes, and the optional Client secret fields, and a working
    Connect button.
  • Clicking Connect runs the standard authorization-code + PKCE
    flow against the user-supplied authUrl / tokenUrl (no DCR step).
  • The user is sent to the auth URL in the system browser with a
    loopback callback at 127.0.0.1:<ephemeral>/callback. The callback
    URL must be one the user has registered with the AS (Figma's
    configuration explicitly checks the redirect URI against its
    allowlist).
  • After consent, the auth code is exchanged for access_token and
    refresh_token at the tokenUrl, and both are stored in the
    at-rest secret store keyed by scheme id.
  • Subsequent agent runs attach the access token as a bearer.
  • The scheme proactively refreshes on expiry using the refresh
    token. On refresh failure the scheme's acquired.status flips to
    expired and the UI shows a Reconnect button.
  • The dynamic preset (Add OAuth (dynamic / MCP) authentication to the integrations form #280) remains unchanged. Figma-specific note:
    Figma's metadata advertises token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"] (not none), so the
    token-exchange step must use the AS-advertised method — same shape
    as the dynamic flow's Add OAuth (dynamic / MCP) authentication to the integrations form #280 fix. For an MCP-mode Figma connection
    the user sets authUrl to https://www.figma.com/oauth/mcp and
    registers http://127.0.0.1/callback (or another loopback URL) as
    a redirect URI on their Figma OAuth app.
  • Unit tests cover the PKCE happy path, the loopback callback
    mismatch (state check), refresh-on-expiry, refresh-failure →
    expired, and the per-server auth-method selection.

Suggested implementation shape

A new server/lib/oauth-manual.ts module that reuses the loopback
listener, PKCE helpers, and token-refresh logic from
server/lib/oauth-dynamic.ts. The new module's
startInteractiveOauth is parameterized by the user-supplied
clientId / clientSecret / authUrl / tokenUrl / scopes and
skips the DCR + metadata-discovery steps. The dynamic module's
internal loopback helpers (PKCE, listener, exchange, refresh) are
extracted into a shared server/lib/oauth-flow.ts so both modules
use the same primitives. The route handlers and UI affordance mirror
#280's shape with one new route
(POST/GET/DELETE /api/integrations/:id/schemes/:schemeId/acquire)
per scheme kind.

Notes

  • The DCR endpoint is closed for many third-party MCP servers beyond
    Figma (e.g. some enterprise-only SaaS MCPs gate the registration
    endpoint behind their own allowlist). Implementing the manual
    preset unblocks those too.
  • The existing oauth preset's clientSecret field is optional: true in integration-modes.ts. Most ASes that require a secret
    for the token-exchange step reject PKCE-public registrations; the
    form should mark the secret as required when the metadata
    advertises token_endpoint_auth_methods_supported other than
    none (similar to the dynamic flow's DCR fix). This is a small
    polish and can be a separate small commit if it's awkward to land
    with the main work.
  • The oauth (manual) preset is distinct from
    oauth_client_credentials. The latter is non-interactive
    machine-to-machine (no browser, no user consent) and is already
    implemented.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions