can1357/oh-my-pi · error · Error
OAuth authentication failed: ${errorMsg}
Error message
OAuth authentication failed: ${errorMsg} What it means
This is the catch-all branch of the MCP OAuth login flow in MCPCommandController. When the underlying MCPOAuthFlow (dynamic client registration, authorization-code exchange, or token fetch) fails with an error that does not match the known timeout/403/invalid_grant/network patterns, the controller rethrows it wrapped as "OAuth authentication failed: <original message>" so the user still sees the root cause. It signals that the OAuth handshake failed for a reason the controller does not specifically classify.
Source
Thrown at packages/coding-agent/src/modes/controllers/mcp-command-controller.ts:1040
// cancellations. The timeout path also aborts the controller but does
// not set this flag, so it remains a surfaced error.
if (cancellationRequested) {
throw new MCPOAuthCancelledError();
}
const errorMsg = error instanceof Error ? error.message : String(error);
// Provide helpful error messages based on failure type
if (errorMsg.includes("timeout") || errorMsg.includes("timed out")) {
throw new Error("OAuth flow timed out. Please try again.");
} else if (errorMsg.includes("403") || errorMsg.includes("unauthorized")) {
throw new Error("OAuth authorization failed. Please check your client credentials.");
} else if (errorMsg.includes("invalid_grant")) {
throw new Error("OAuth authorization code is invalid or expired. Please try again.");
} else if (errorMsg.includes("ECONNREFUSED") || errorMsg.includes("fetch failed")) {
throw new Error("Could not connect to OAuth server. Please check the URLs and your network connection.");
} else {
throw new Error(`OAuth authentication failed: ${errorMsg}`);
}
} finally {
this.ctx.editor.onEscape = originalOnEscape;
externalSignal?.removeEventListener("abort", onExternalAbort);
manualInputClaim?.clear("Manual MCP OAuth input cleared");
flowClaim.release();
}
}
/**
* Fold a completed OAuth flow back into a server config. Owns the
* persistence policy in one place: the auth block records the credential
* pointer plus refresh material, the oauth block echoes the client id for
* pre-auth reuse, and only a user-supplied client secret is ever written —
* DCR-issued secrets stay embedded in the stored credential so they cannot
* leak into (possibly shared/committed) config files.
*/
#persistOAuthResult(View on GitHub (pinned to 9690622007)
Solutions
- Read the embedded original message after the colon — it names the actual OAuth failure (e.g. invalid_client, invalid_scope) and fix the client credentials/scopes in the server config accordingly.
- Verify the server's OAuth discovery metadata is reachable: curl <server-url>/.well-known/oauth-authorization-server and .well-known/oauth-protected-resource.
- Re-run the auth flow — transient token-endpoint errors (5xx) resolve on retry.
- If the provider requires a pre-registered client, add the correct clientId/clientSecret to the MCP server's auth block instead of relying on dynamic client registration.
Example fix
// before: relying on DCR against a provider that rejects it
{ "type": "http", "url": "https://mcp.example.com/mcp" }
// after: supply pre-registered OAuth client credentials
{
"type": "http",
"url": "https://mcp.example.com/mcp",
"oauth": { "clientId": "my-registered-client", "clientSecret": "..." }
} Defensive patterns
Strategy: try-catch
Validate before calling
const meta = await fetch(`${serverUrl.replace(/\/$/, '')}/.well-known/oauth-authorization-server`);
if (!meta.ok) throw new Error(`No OAuth metadata at ${serverUrl} (HTTP ${meta.status})`); Type guard
function isOAuthError(err: unknown): err is Error & { oauthFailure: true } {
return err instanceof Error && err.message.startsWith("OAuth authentication failed:");
} Try / catch
try {
await runMcpOAuthLogin(serverName);
} catch (err) {
if (err instanceof Error && err.message.startsWith("OAuth authentication failed:")) {
const rootCause = err.message.slice("OAuth authentication failed: ".length);
logger.warn("MCP OAuth failed", { serverName, rootCause }); // inspect rootCause for invalid_client/scope
} else throw err;
} Prevention
- Pre-register an OAuth client (clientId/clientSecret in the server's oauth config block) with providers that reject dynamic client registration.
- Curl the server's .well-known/oauth-authorization-server endpoint before wiring the server into config.
- Match requested scopes to what the provider actually grants — invalid_scope surfaces here.
- Retry transient 5xx failures before assuming a config problem.
When it happens
Trigger: Running /mcp auth (or a reauth) against an MCP server where the OAuth flow throws an unclassified error: the provider rejects the client_id or redirect_uri, DCR registration fails, the token endpoint returns an unexpected status/body, the callback server port is taken, or the server returns a non-standard error code not containing 'timeout', '403', 'unauthorized', 'invalid_grant', 'ECONNREFUSED', or 'fetch failed'.
Common situations: Misconfigured OAuth client credentials in the MCP server config; the authorization server returning 400 invalid_client or invalid_scope; an IdP that does not support RFC 8414 metadata discovery; a corporate proxy returning a non-standard error page; a server whose token endpoint replies with a 500 that the flow surfaces verbatim.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- No OAuth accounts resolved for provider ${model.provider}
- MCP OAuth credential is missing refresh material
- Broker returned non-OAuth credential for ${provider}
- Token exchange returned no access token${providerError ? `:
- MCP OAuth refresh failed: ${response.status} ${text}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/20b62d9e01bc821d.
Report an issue: GitHub.