* fix(settings): give the MCP copy button success feedback
The MCP server URL Copy button copied to the clipboard but showed no
feedback. It is a DS::Button (single icon), but clipboard_controller's
showSuccess() unconditionally toggled iconDefault/iconSuccess targets —
which that markup does not have — so it threw right after the copy and
the user saw nothing.
Guard the icon-swap path (still used by invite codes, MFA and profiles)
and add a fallback that briefly flips the button's own label to Copied!
via a new copiedText value. Wire it up on the MCP page.
* fix(settings): capture copy button before async clipboard resolve
event.currentTarget is null by the time the writeText().then() callback
runs (it's only valid during event dispatch), so showSuccess received
null and the label never flipped. Capture the button synchronously in
copy() and pass it through. Verified in-browser: Copy -> Copied! -> Copy.
* refactor(clipboard): unify feedback reset delay, harden label lookup
Extract a shared RESET_DELAY_MS so the icon-swap and label-flash paths last the
same duration when both copy buttons render on one page. Scope the label lookup
to span.truncate (the DS::Button text node) so it ignores any future icon span.
Per review feedback.
---------
Co-authored-by: Guillem Arias <guillem.arias@col.vueling.com>
* feat(mcp): add OAuth well-known discovery endpoints (RFC 8414 + RFC 9728)
Serves /.well-known/oauth-protected-resource (RFC 9728) and
/.well-known/oauth-authorization-server (RFC 8414) so MCP clients
can auto-discover the authorization server. Both endpoints are
unauthenticated and respect APP_URL for reverse-proxy deployments.
* feat(mcp): add dynamic client registration endpoint (RFC 7591)
POST /register creates a public Doorkeeper::Application on demand so
MCP clients (e.g. Claude.ai) can self-register without manual setup.
Validates redirect_uris (including blank entries), falls back to
"MCP Client" name, returns no client_secret (public client, PKCE only).
Rate-limited to 10 registrations/min/IP via Rack::Attack.
* feat(mcp): authenticate via Doorkeeper OAuth2, keep MCP_API_TOKEN as fallback
MCP endpoint now accepts OAuth2 Bearer tokens issued by Doorkeeper.
Falls back to the existing MCP_API_TOKEN env-var flow so self-hosted
deployments are not broken. Requires MCP_OAUTH_ENABLED or MCP_API_TOKEN
to be set — the endpoint returns 503 otherwise.
- OauthBase concern provides APP_URL-aware configured_base_url (trailing
slash stripped to prevent double-slash URLs)
- Bearer scheme parsed case-insensitively (RFC 7235)
- Only read_write scope accepted — read scope would allow mutating tools
(CreateGoal, ImportBankStatement), so read-only tokens are rejected
- Deactivated users rejected even with a valid Doorkeeper token
- WWW-Authenticate header on 401 points to RFC 9728 resource metadata
- SHA-256 digest used for constant-time env-var comparison
- Rack::Attack throttle added for POST /register
- Routes wired: /.well-known/*, /register, use_doorkeeper
* fix(mcp): disable Turbo on OAuth consent form for external redirect URIs
Turbo was intercepting the authorization form POST and XHR-fetching
the redirect_uri (e.g. https://claude.ai/api/mcp/auth_callback),
which CORS blocks. Extend the existing turbo_disabled guard to cover
any redirect_uri that doesn't originate from the app itself.
* feat(mcp): add Settings::McpController with connected clients view
- Settings > MCP page (under Advanced) shows the MCP server URL with
copy button and step-by-step instructions for connecting Claude.ai
- Lists active non-mobile OAuth tokens with app name and revoke action;
mobile device tokens are excluded to prevent accidental disconnection
- Removes the MCP_OAUTH_ENABLED env-var gate — OAuth auth is always
available since Doorkeeper handles consent; MCP_API_TOKEN remains
as a self-hosted fallback
* fix(mcp): remove client_credentials from grant_types_supported metadata
Only authorization_code is supported by the registration endpoint.
Advertising client_credentials was misleading — a client that reads
the metadata and attempts that flow would get an application with the
wrong grant type.