can1357/oh-my-pi · error · Error

MCP OAuth refresh failed: ${response.status} ${text}

Error message

MCP OAuth refresh failed: ${response.status} ${text}

What it means

Thrown when the OAuth token refresh POST to the provider returns a non-OK HTTP status. The message embeds the status code and the raw response body text, which usually contains a JSON error like `invalid_grant` (refresh token revoked/expired). The caller must re-authenticate interactively; silent refresh is impossible at that point.

Source

Thrown at packages/coding-agent/src/mcp/oauth-flow.ts:827

	if (normalizedClientId) params.set("client_id", normalizedClientId);
	// Drop redundant indicators so refresh stays consistent with the initial
	// grant; see {@link filterResourceIndicator} for context.
	const resolvedResource = filterResourceIndicator(resolveResourceUri(resource), filterAnchor, {
		stripSameOriginResource: optsFromTrailing?.stripSameOriginResource,
	});
	if (resolvedResource) params.set("resource", resolvedResource);
	if (clientSecret) params.set("client_secret", clientSecret);

	const response = await fetchImpl(tokenUrl, {
		method: "POST",
		headers: { "Content-Type": "application/x-www-form-urlencoded" },
		body: params.toString(),
		signal: optsFromTrailing?.signal,
	});

	if (!response.ok) {
		const text = await response.text();
		throw new Error(`MCP OAuth refresh failed: ${response.status} ${text}`);
	}

	const data = (await response.json()) as {
		access_token: string;
		refresh_token?: string;
		expires_in?: number;
	};
	const expiresIn = data.expires_in ?? 3600;
	return {
		access: data.access_token,
		refresh: data.refresh_token ?? refreshToken,
		expires: Date.now() + expiresIn * 1000,
	};
}

View on GitHub (pinned to 9690622007)

Solutions

  1. Read the body after the status code — `invalid_grant`/`invalid_token` means re-authentication is required
  2. Delete the stored token file / clear cached credentials for that MCP server and run the OAuth login flow again
  3. Confirm the stored refresh token is current if your provider rotates refresh tokens (store the new one from every refresh response)
  4. Check provider status pages if the failure is a 5xx rather than 4xx

Example fix

// before: retrying refresh forever on invalid_grant
try { await refresh(token.refreshToken); } catch { /* retry */ }
// after: detect invalid_grant and fall back to interactive login
catch (e) {
  if (String(e.message).includes("invalid_grant")) await runOAuthLogin(server);
  else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// check a refresh token exists and isn't obviously stale before refreshing
if (!stored.refreshToken) throw new Error("No refresh token stored; interactive login required");
if (Date.now() - stored.refreshedAt > MAX_REFRESH_TOKEN_AGE_MS) promptRelogin();

Type guard

function isInvalidGrant(err: unknown): boolean {
  return err instanceof Error && /invalid_grant|invalid_token/i.test(err.message);
}

Try / catch

try {
  await refreshAccessToken(refreshToken);
} catch (err) {
  if (isInvalidGrant(err)) {
    await clearStoredCredentials(serverId);
    await startInteractiveOAuthLogin(serverId); // re-auth is the only fix
  } else throw err;
}

Prevention

When it happens

Trigger: Refreshing an expired MCP OAuth access token when the server responds 400/401/403 — e.g. refresh token expired, revoked, or rotated and the stored one is stale.

Common situations: User revoked the app in the provider's dashboard, provider rotated refresh tokens on each use and an old refresh_token was reused, long-idle sessions past the refresh token's absolute lifetime, or provider changed endpoint requirements.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/3491d30259387b30. Report an issue: GitHub.