You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Summary
Implement the
oauth(manual / user) preset end-to-end so users can authorizeagainst 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/registerreturns 403); the spec calls this case outexplicitly 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/registerendpoint returns 403 to anonymous callers, andthe metadata even lists the
registration_endpointbut rejects anonymousregistrations. The spec explicitly tells the client to fall back to a
user-driven flow in this case.
Existing state
client/src/lib/integration-modes.tsalready defines theoauthpresetwith
acquisition: "oauth"and the right fields (clientId,authUrl,tokenUrl,scopes, optionalclientSecret). It'shidden: true.server/lib/integration-auth.tsreturnsreauth_neededforacquisition: "oauth"with a "needs to be connected" message but no actual flow.logic in
server/lib/oauth-dynamic.tscan be reused as-is — the onlything that's different from the dynamic flow is that
clientIdandclientSecretcome from the user instead of from a DCR response.Acceptance criteria
mode that supports it (any HTTP-family transport, including MCP).
Client ID,Authorization URL,Token URL,Scopes, and the optionalClient secretfields, and a workingConnect button.
flow against the user-supplied
authUrl/tokenUrl(no DCR step).loopback callback at
127.0.0.1:<ephemeral>/callback. The callbackURL must be one the user has registered with the AS (Figma's
configuration explicitly checks the redirect URI against its
allowlist).
access_tokenandrefresh_tokenat thetokenUrl, and both are stored in theat-rest secret store keyed by scheme id.
token. On refresh failure the scheme's
acquired.statusflips toexpiredand the UI shows a Reconnect button.Figma's metadata advertises
token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"](notnone), so thetoken-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
authUrltohttps://www.figma.com/oauth/mcpandregisters
http://127.0.0.1/callback(or another loopback URL) asa redirect URI on their Figma OAuth app.
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.tsmodule that reuses the loopbacklistener, PKCE helpers, and token-refresh logic from
server/lib/oauth-dynamic.ts. The new module'sstartInteractiveOauthis parameterized by the user-suppliedclientId/clientSecret/authUrl/tokenUrl/scopesandskips 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.tsso both modulesuse 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
Figma (e.g. some enterprise-only SaaS MCPs gate the registration
endpoint behind their own allowlist). Implementing the manual
preset unblocks those too.
oauthpreset'sclientSecretfield isoptional: trueinintegration-modes.ts. Most ASes that require a secretfor the token-exchange step reject PKCE-public registrations; the
form should mark the secret as required when the metadata
advertises
token_endpoint_auth_methods_supportedother thannone(similar to the dynamic flow's DCR fix). This is a smallpolish and can be a separate small commit if it's awkward to land
with the main work.
oauth(manual) preset is distinct fromoauth_client_credentials. The latter is non-interactivemachine-to-machine (no browser, no user consent) and is already
implemented.