OAuth Authentication
MCPProxy supports OAuth 2.1 authentication with PKCE for secure authentication to remote MCP servers.
Overview
OAuth authentication is used when connecting to MCP servers that require authorization, such as:
- GitHub MCP servers
- Enterprise API gateways
- Third-party service integrations
Configuration
Basic OAuth Setup
{
"mcpServers": [
{
"name": "github-server",
"url": "https://api.github.com/mcp",
"protocol": "http",
"oauth": {
"client_id": "your-client-id",
"scopes": ["repo", "user"]
},
"enabled": true
}
]
}
OAuth Configuration Options
Note: OAuth authorization and token endpoints are automatically discovered from the server's OAuth metadata (RFC 8414 .well-known/oauth-authorization-server), not configured manually.
Advanced Configuration
For OAuth providers that require additional parameters (like RFC 8707 resource indicators):
{
"oauth": {
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"scopes": ["read", "write"],
"extra_params": {
"audience": "https://api.example.com",
"resource": "https://api.example.com"
}
}
}
Authentication Flow
Initial Authentication
- MCPProxy detects OAuth requirement on first connection
- Opens browser to authorization URL
- User completes authentication
- MCPProxy receives callback with authorization code
- Exchanges code for access token
- Stores tokens securely in system keyring
Reused client registrations
When a server has no static oauth.client_id, MCPProxy registers a client with
Dynamic Client Registration on the first sign-in and reuses that client_id
on later sign-ins. Some providers delete these registrations after a while
(Cloudflare's MCP servers do). The provider then answers the authorization URL
with HTTP 400 {"error":"invalid_request","error_description":"Invalid client_id"}
and never redirects back to MCPProxy.
Before handing out an authorization URL that uses a reused registration,
MCPProxy sends one request to that URL (no redirects followed, 5 s timeout). If
the response says the client is unknown (invalid_client, or
invalid_request with a description such as "Invalid client_id" or "Client
not found"), MCPProxy:
- clears the stored registration, but only if it still holds that same
client_id(if another sign-in has already replaced it, this sign-in stops and asks you to sign in again rather than registering yet another client); - registers a new client once and stores it, unless another sign-in stored
one in the meantime (then this sign-in stops and asks you to sign in
again). A rejected registration that had a
client_secretis removed and you are asked to sign in again, so the new client is registered without the old secret; - builds the authorization URL again with the new
client_id.
If the provider also rejects the new client, sign-in stops with an error
instead of registering again; configure a static oauth.client_id in that
case. A static oauth.client_id, a client registered during the same sign-in,
and a client_id set through oauth.extra_params are never checked or
cleared. If the check itself fails (network error, timeout, 5xx, a redirect, a
login page or an unrecognised error), sign-in continues as before.
Token Refresh
MCPProxy automatically refreshes tokens before expiration:
- Checks token expiration before each request
- Uses refresh token to get new access token
- Falls back to browser re-authentication if refresh fails
Refreshes are serialized per server. Whether a refresh is triggered by a request that finds the token about to expire or by the background refresh schedule, at most one refresh request is sent to the authorization server per expiry; every caller waiting at that moment receives the same new token (or the same error). This matters for providers that rotate refresh tokens and revoke the whole grant when an old refresh token is reused. A scheduled refresh that fires within 5 seconds of a token being saved (by another refresh or a sign-in) uses that token instead of rotating it again.
Refresh failures are classified from the RFC 6749 §5.2 error code and the
HTTP status of the token endpoint response:
| Response | Behaviour |
|---|---|
invalid_grant | Stops retrying; sign in again. |
invalid_client, dynamically registered client | The stored client registration is cleared; sign in again to register a new client. If the provider rejects the new registration too, configure a static oauth.client_id. |
invalid_client, static oauth.client_id | Stops retrying; fix oauth.client_id / oauth.client_secret. |
unauthorized_client, unsupported_grant_type, invalid_scope | Stops retrying; the code is shown in the error. |
server_error, temporarily_unavailable, HTTP 5xx or 429, network errors | Retried with exponential backoff (10 s doubling to 5 min). |
| Anything else | Retried with backoff, then marked failed after the retry limit. |
The mcpproxy_oauth_refresh_total metric labels these as failed_invalid_grant,
failed_invalid_client, failed_server_error, failed_network,
failed_server_gone and failed_other.
Signing in from the Web UI
When an OAuth-protected server has no usable token, the Web UI does not render it as a generic red "Server Error". Instead it surfaces a calm, actionable Sign-in state:
- The server's status chip reads an amber "Sign-in required" (rather than a red "Disconnected"/"Unhealthy"), on both the Servers list and the server detail page.
- The server detail page shows a calm amber "🔑 Sign in to {server}" panel
with a primary Log in button (it triggers the same browser OAuth flow as
mcpproxy auth login) and a link to this page. No "file a bug" prompt is shown for OAuth states — signing in is the fix. - If a prior session expired or was revoked, the panel keeps an error tone and leads with a Re-login button.
- A server can be both quarantined and login-required: you may sign in while it is quarantined, but its tools stay blocked until you Approve the server. The panel makes both gates explicit.
Switching accounts and retrying sign-in
To switch accounts or recover an OAuth error, use Sign in again in the server card’s More actions menu or the detail page’s Actions menu. For an OAuth-capable upstream this remains available during reconnect attempts and generic timeout errors, even when the health panel does not suggest login. After logout it is labelled Sign in. Select the intended account/workspace at the provider; if the provider reuses its own browser session, switch that account before starting sign-in again.
CLI Commands
Start Authentication
mcpproxy auth login --server=github-server
Check Status
mcpproxy auth status
Debug Authentication
mcpproxy auth login --server=github-server --log-level=debug
RFC 8707 Resource Parameter
MCPProxy supports automatic RFC 8707 resource parameter detection for OAuth providers that require it:
{
"oauth": {
"client_id": "your-client-id",
"extra_params": {
"resource": "https://api.example.com"
}
}
}
For providers like Runlayer, MCPProxy can auto-detect the resource parameter from the server's OAuth metadata.
Token Storage
Tokens are stored securely using the system keyring:
| Platform | Storage |
|---|---|
| macOS | Keychain |
| Windows | Credential Manager |
| Linux | Secret Service (libsecret) |
Error Handling
MCPProxy provides structured error responses for OAuth failures, making it easier to diagnose and fix authentication issues.
Error Types
CLI Error Output
When OAuth fails, the CLI displays rich error information:
❌ OAuth Error: dcr_failed
Server: github-server
Message: Dynamic Client Registration failed: 403 Forbidden
Suggestion: Check if the OAuth server requires pre-registered clients
🔍 Debug:
Server logs: mcpproxy upstream logs github-server
Activity log: mcpproxy activity list --request-id req-xyz-123
Request ID: req-xyz-123
Correlation ID: a1b2c3d4e5f6789012345678
API Error Response
The REST API returns structured OAuthFlowError responses:
{
"success": false,
"error_type": "dcr_failed",
"server_name": "github-server",
"message": "Dynamic Client Registration failed: 403 Forbidden",
"suggestion": "Check if the OAuth server requires pre-registered clients",
"correlation_id": "a1b2c3d4e5f6789012345678",
"request_id": "req-xyz-123",
"details": {
"metadata": {
"status": "ok",
"protected_resource_url": "https://api.example.com/.well-known/oauth-protected-resource"
},
"dcr": {
"attempted": true,
"status": "failed",
"error": "403 Forbidden"
}
}
}
Log Correlation
Use the request_id from error responses to find related logs:
# Find activity records for a specific request
mcpproxy activity list --request-id req-xyz-123
# View server-specific logs
mcpproxy upstream logs github-server --tail 50
Troubleshooting
Signing in through a remote Web UI
When MCPProxy runs on a headless host, the Web UI opens the authorization URL in the browser where you clicked Log in. A blocked popup leaves a clickable sign-in link; the proxy's API key is never placed in that URL.
OAuth's default loopback callback reaches the browser's machine, which may be
different from the proxy host. To finish sign-in without an SSH port forward,
serve MCPProxy's HTTP listener through an HTTPS reverse proxy (including
Tailscale Serve), and set the upstream's oauth.callback_base_url to that
external origin:
{
"name": "linear",
"url": "https://mcp.linear.app/mcp",
"protocol": "http",
"oauth": {
"callback_base_url": "https://proxy.example.ts.net:8447"
}
}
The provider receives https://proxy.example.ts.net:8447/oauth/callback/linear
as its redirect URI. Forward that path to the main MCPProxy HTTP listener, just
like /ui/ and /api/. Static OAuth clients must register this exact callback
URL with their provider; dynamically registered clients are re-registered when
their saved redirect URI changes. The provider must permit HTTPS callbacks.
callback_base_url accepts an HTTPS origin with no credentials, path, query or
fragment. It is explicit configuration, never inferred from request headers.
Only upstreams opting into it accept remote callbacks, validated against their
existing single-use state and PKCE flow. The callback needs no API key and does
not start an OAuth flow; missing, expired, reused and cross-server states fail.
Callback responses disable caching and referrer disclosure.
Leave HEADLESS=true on a remote host: the Web UI opens the user's browser;
turning off headless mode only attempts to open a browser on the proxy host.
Without callback_base_url, loopback callbacks behave as before. A configured
redirect_uri continues to pin the private loopback listener; the external
callback takes precedence for provider registration and token exchange.
Browser Doesn't Open
# Run in headless mode with manual URL copy
HEADLESS=true mcpproxy auth login --server=github-server
Token Refresh Fails
- Check token expiration in
auth status - Verify refresh token is stored
- Re-authenticate if needed
Invalid Scope Errors
Verify the requested scopes are valid for the OAuth provider:
mcpproxy auth login --server=github-server --log-level=debug
Connection Errors
Ensure the OAuth server URLs are accessible:
curl -I https://auth.example.com/.well-known/oauth-authorization-server
Security Best Practices
- Use PKCE: MCPProxy uses PKCE by default for public clients
- Minimal Scopes: Request only necessary permissions
- Token Storage: Tokens are encrypted in system keyring
- Callback Security: Uses dynamic port allocation to prevent port conflicts
