can1357/oh-my-pi · error · Error
Token exchange returned no access token${providerError ? `:
Error message
Token exchange returned no access token${providerError ? `: ${providerError}` : ""} What it means
Thrown by exchangeToken in the MCP OAuth flow when the token endpoint responds successfully but the JSON body has no non-empty `access_token`. This catches providers like the Slack Web API that return HTTP 200 with an `{ ok: false, error }` body instead of a real HTTP error, which would otherwise store an empty token that only fails later with `invalid_token`. The message appends the provider's `error_description` or `error` field when present.
Source
Thrown at packages/coding-agent/src/mcp/oauth-flow.ts:533
const errorText = await response.text();
throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
}
const data = (await response.json()) as {
access_token?: string;
refresh_token?: string;
expires_in?: number;
token_type?: string;
error?: string;
error_description?: string;
};
// Some providers (e.g. the Slack Web API) signal failure with HTTP 200 and
// an `{ ok: false, error }` body. Accepting such a response would store an
// empty access token and only surface `invalid_token` on a later request.
if (typeof data.access_token !== "string" || data.access_token.length === 0) {
const providerError = data.error_description ?? data.error;
throw new Error(`Token exchange returned no access token${providerError ? `: ${providerError}` : ""}`);
}
// Calculate expiry timestamp
const expiresIn = data.expires_in ?? 3600; // Default to 1 hour
const expires = Date.now() + expiresIn * 1000;
return {
access: data.access_token,
refresh: data.refresh_token ?? "",
expires,
};
}
/**
* Generate PKCE code verifier (random string).
*/
#generateCodeVerifier(): string {
const bytes = new Uint8Array(32);View on GitHub (pinned to 9690622007)
Solutions
- Read the provider error appended after the colon in the message (e.g. `bad_verification_code`, `invalid_client`) and fix the corresponding OAuth parameter
- Verify client_id, client_secret, and redirect_uri exactly match the app registration in the provider's dashboard
- Re-run the full OAuth flow to get a fresh authorization code — codes are single-use and expire quickly
- Check the MCP server's OAuth documentation for non-standard token endpoint behavior
Example fix
// before: blindly trusting any 200 response
const data = await response.json();
await storeToken(data.access_token);
// after: validate before storing (this is what the library does)
if (typeof data.access_token !== "string" || data.access_token.length === 0) {
throw new Error(`Token exchange returned no access token${providerError ? `: ${providerError}` : ""}`);
} Defensive patterns
Strategy: validation
Validate before calling
// validate before calling the OAuth flow
const clientId = config.clientId?.trim();
const clientSecret = config.clientSecret?.trim();
if (!clientId || !clientSecret) throw new Error("OAuth client_id/client_secret required before token exchange"); Type guard
function hasAccessToken(d: unknown): d is { access_token: string } {
return typeof d === "object" && d !== null && typeof (d as any).access_token === "string" && (d as any).access_token.length > 0;
} Try / catch
try {
const token = await exchangeToken(code, verifier, ...);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (msg.startsWith("Token exchange returned no access token")) {
// surface msg tail (provider error) to user, prompt re-login
} else throw err;
} Prevention
- Double-check client_id/client_secret/redirect_uri against the provider dashboard before the flow
- Always run a fresh authorization code — codes expire and are single-use
- Read the provider error tail in the message; it names the exact OAuth failure
- Test OAuth flows against the specific MCP server's documented provider quirks
When it happens
Trigger: Calling exchangeToken after the OAuth code-for-token exchange when the provider replies 200 with a body missing `access_token`, an empty-string `access_token`, or a Slack-style `{ ok: false, error: ... }` payload.
Common situations: Misconfigured OAuth client (wrong client_secret so the provider returns an error object with 200), authorization code already used/expired, provider-specific non-standard OAuth implementations (Slack-style APIs), or the redirect URL not matching the registered callback.
Related errors
- GitLab OAuth token exchange failed: ${response.status} ${awa
- Token exchange failed: ${formatOpenAICodexTokenEndpointError
- Z.ai ${operation} failed: ${envelope.msg ?? `code ${String(e
- Z.ai business login returned no access token
- MCP OAuth credential is missing refresh material
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/845b9192956e5e1a.
Report an issue: GitHub.