ADR 0283: Remote MCP Server OAuth 2.1 Authentication
- Status: Accepted
- Date: 2026-09-18
- Related: ADR 0038 · ADR 0098 · ADR 0142 · 03-runtime/01-ipc-protocol · 03-runtime/06-host-rpc-protocol
Context
Remote HTTP MCP servers (such as Notion, Linear, or custom enterprise servers) often protect endpoints with OAuth 2.1 authorization rather than static tokens. Model Context Protocol specifies authorization discovery via RFC 9728 (OAuth Protected Resource Metadata) and RFC 8414 (Authorization Server Metadata), dynamic registration via RFC 7591, and resource indicators via RFC 8707.
Previous MCP implementations in PI-Desktop supported only static HTTP headers. Users had to manually obtain Bearer tokens or were unable to connect to OAuth-protected servers.
Decision
- Authentication Architecture:
- OAuth login runs in the Electron main process via
McpOAuthManager, mirroring theVendorOAuthnon-blocking pattern (ADR 0098):mcp/oauth/startreturns{ ok: true, loginId }immediately and streams progress, authorization URL, completion, or failure events viapi-desktop/mcp/oauth/event. - IPC calls are never blocked during the user's browser interaction. A user or UI cancellation triggers
pi-desktop/mcp/oauth/cancel, which closes the loopback listener and aborts the login.
- OAuth login runs in the Electron main process via
- Loopback & Security Boundaries:
- The loopback callback server binds strictly to IPv4 loopback
127.0.0.1on an ephemeral port (port 0), and the redirect URI is formatted ashttp://127.0.0.1:<port>/callbackper RFC 8252 (preventing IPv6 loopback resolution mismatches). - Inbound query parameters (
error,error_description,code,state) rendered on the loopback HTML completion page are strictly HTML-entity escaped to eliminate reflected XSS. - Per ADR 0142, discovery and token requests use
redirect: "manual"to prevent silent cross-host redirect attacks.
- The loopback callback server binds strictly to IPv4 loopback
- MCP Protocol Compliance:
- RFC 8707
resourceindicators are passed on both the authorization endpoint and token exchange/refresh requests, pointing to the protected resource URI discovered via RFC 9728. - Dynamic Client Registration (RFC 7591) credentials (
clientId,clientSecret) are cached and reused for subsequent logins against the same registration endpoint. - Token lifetimes (
expires_in) are parsed as numbers or strings (e.g."3600"), computing an absolute expiry time.
- RFC 8707
- Token Storage & Isolation:
- OAuth tokens never reach the renderer. They are stored in host-core encrypted secrets under
secret:mcp:<serverId>:oauth. - Token refresh operations are serialized per server to prevent race conditions with rotating refresh tokens.
- Moving or renaming an MCP server (
mcp.transfer) migrates the correspondingsecret:mcp:<serverId>:oauthto the new ID.
- OAuth tokens never reach the renderer. They are stored in host-core encrypted secrets under
- Runtime Integration:
UserMcpRuntimeinjects the valid OAuth access token asAuthorization: Bearer <token>into live HTTP MCP connections.- Token refreshes or re-authorizations invalidate stale connection entries so live clients immediately adopt updated credentials.
- If a tool invocation returns a 401 Unauthorized status, the server is marked with
authRequired: trueandstate: "failed".
Consequences
- Remote HTTP MCP servers requiring OAuth 2.1 PKCE can be authorized securely from the Settings UI.
- Long-running browser interactions do not block IPC channels or leave orphan HTTP listeners upon window reload or quit.
- Token material is kept out of the renderer process and ordinary logs.
Amendment (2026-09-18) — landing hardening
- Authorization-server endpoints (
authorization_endpoint,token_endpoint,registration_endpoint, discoveredauthorization_servers, andresource_metadata) must be HTTPS. Loopbackhttp://127.0.0.1/localhost/::1remains allowed so local mock and LAN-loopback AS still work. The MCP resource URL itself may still behttp://(ADR 0142). - Dynamic client registration prefers RFC 8252 portless
http://127.0.0.1/callbackplus the current exact URI. Stored clients are reused only when that portless URI is on file, or the exact current redirect matches. Otherwise a new client is registered. Login also tries to rebind the previously used loopback port so strict AS that require an exact URI keep working. - Loopback
/callbackvalidatesstatebefore looking aterrororcode. A mismatchedstateis a stray request and does not abort the login. A matching callback is consumed once (replay returns 409). invalid_granton refresh deletes the stored secret; transient 5xx keeps the existing access token.mcp/oauth/startpasses the listedMcpServerRecordthrough toonAuthorizedso a project-level server that is not in the current workspace runtime can still handshake after login.- Scope selection remains an MVP heuristic (
defaultif advertised, elsescopes_supported[0]). There is no per-server scope picker, device-code flow, or DPoP.