auth: oauth() on an upstream MCP server and Fentaris performs the OAuth 2.1 client side for you: metadata discovery, dynamic client registration, PKCE, token exchange, refresh, and per-user token storage.
Quick Start
Four Ways to Declare It
oauth()
Zero configuration. Fentaris discovers the protected resource and authorization server, registers a client dynamically (RFC 7591), uses PKCE with S256, and stores one authorization per Fentaris user.
oauth({ clientId, clientSecret, scopes })
Pre-registered client. Fentaris skips dynamic registration and authenticates to the token endpoint with the strongest method the authorization server supports. clientSecret accepts a literal string or credential("..."); a credential reference must be declared under defaults.credentials, because the client secret belongs to the application rather than to a caller.
oauth.clientCredentials({ clientId, clientSecret, scopes })
Machine-to-machine access. No redirect and no human: Fentaris fetches a token directly and every caller shares it.
oauth({ provider })
Escape hatch. The factory receives { server, user, session, store, redirectUrl } and returns an MCP SDK OAuthClientProvider that Fentaris binds to that user’s transport.
Options
tokens
"per-user" (default for the authorization-code grant) isolates one authorization per Fentaris user. "shared" reuses the first completed authorization for every caller. Unauthenticated callers always collapse to the shared session, so a personal single-user proxy works with no identity configuration.
registration
Inferred from your declaration: "dynamic" with no clientId, "preregistered" with one, "metadata-url" when clientMetadataUrl is set. Set it explicitly to force one mode.
oauth.publicUrl
Externally reachable base URL of the proxy. Required behind a reverse proxy or any non-loopback deployment, because the authorization server redirects the browser to <publicUrl><callbackPath>. When unset, Fentaris derives the callback from the bound listener.
oauth.callbackPath
Path of the hosted redirect route. Defaults to /_fentaris/oauth/callback. Validation rejects a path that overlaps the MCP path or the Edge control-plane base path.
oauth.store
Any object implementing get, set, delete, and list. Defaults to the encrypted local store when a key is available, otherwise an in-memory store.
oauth.consentTimeoutMs
How long a tool call waits for the human to finish consent. Defaults to 90000, below the usual MCP client request timeout. On timeout the call returns a “pending, retry” result instead of hanging.
oauth.agentTools
Set to false to skip registering the built-in fentaris__auth_status and fentaris__auth_login tools.
How Consent Reaches the Human
- A tool call needs an authorization that does not exist yet.
- If the connected MCP client supports URL-mode elicitation, Fentaris sends it the authorization URL and waits.
- The human signs in; the authorization server redirects to the Fentaris callback; Fentaris exchanges the code and notifies the client.
- Fentaris retries the original tool call once and returns its real result.
tools/list never contacts an authorization server. An upstream that still needs a login contributes no tools instead of failing the whole listing, and the built-in fentaris__auth_* tools tell the agent why.Token Store
Tokens, client registrations, and cached discovery state live in<authDir>/oauth-tokens.enc.json, encrypted with the same key as credentials.enc.json, written atomically with owner-only permissions. The file is separate from the credentials store, so fentaris secrets set and the secrets manifest are never touched.
The proxy and the CLI share the file without IPC: a CLI login is visible to a running proxy on its next read, with no restart.
CLI
fentaris auth login <mcp>
Runs the whole flow in-process against a loopback redirect, opens the browser, waits for the callback, and writes the tokens.
--print-url and --non-interactive never spawn a browser. The command waits for the redirect callback for --timeout seconds (300 by default) and then exits non-zero, so an unattended run cannot block a pipeline forever.
fentaris auth status [mcp]
Lists stored authorizations with their session and expiry.
fentaris auth logout <mcp>
Deletes the stored authorization for one server and session.
Network Guardrails
Every authorization-server request — discovery, registration, token, revocation — passes the same guardrails as the upstream MCP URL. A private, loopback, or link-local authorization server is refused unless the transport allows it:Troubleshooting
The browser lands on a “not found” page after signing in
The authorization server redirected to a callback the proxy does not host. Setoauth.publicUrl to the externally reachable base URL and confirm oauth.callbackPath.
The tool call returns “Authorization is still pending”
Consent did not complete withinconsentTimeoutMs. Sign in, then call the tool again; the stored tokens are used immediately.
FENTARIS_CONFIG_OAUTH_TRANSPORT_UNSUPPORTED
oauth() only works on the native streamableHttp() and sse() upstream transports. A stdio server cannot carry an OAuth authorization.
FENTARIS_CONFIG_OAUTH_CLIENT_SECRET_UNRESOLVED
The credential(...) reference used for clientSecret is not declared in app defaults, groups, or users. Declare it and store the value with fentaris secrets set.
The upstream keeps asking for a new login
The refresh token was rejected or the authorization server invalidated the registered client. Runfentaris auth logout <mcp> and sign in again.