{"record":{"id":"3491d30259387b30","repo":"can1357/oh-my-pi","slug":"mcp-oauth-refresh-failed-response-status-tex","errorCode":null,"errorMessage":"MCP OAuth refresh failed: ${response.status} ${text}","messagePattern":"MCP OAuth refresh failed: (.+?) (.+?)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/mcp/oauth-flow.ts","lineNumber":827,"sourceCode":"\tif (normalizedClientId) params.set(\"client_id\", normalizedClientId);\n\t// Drop redundant indicators so refresh stays consistent with the initial\n\t// grant; see {@link filterResourceIndicator} for context.\n\tconst resolvedResource = filterResourceIndicator(resolveResourceUri(resource), filterAnchor, {\n\t\tstripSameOriginResource: optsFromTrailing?.stripSameOriginResource,\n\t});\n\tif (resolvedResource) params.set(\"resource\", resolvedResource);\n\tif (clientSecret) params.set(\"client_secret\", clientSecret);\n\n\tconst response = await fetchImpl(tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: params.toString(),\n\t\tsignal: optsFromTrailing?.signal,\n\t});\n\n\tif (!response.ok) {\n\t\tconst text = await response.text();\n\t\tthrow new Error(`MCP OAuth refresh failed: ${response.status} ${text}`);\n\t}\n\n\tconst data = (await response.json()) as {\n\t\taccess_token: string;\n\t\trefresh_token?: string;\n\t\texpires_in?: number;\n\t};\n\tconst expiresIn = data.expires_in ?? 3600;\n\treturn {\n\t\taccess: data.access_token,\n\t\trefresh: data.refresh_token ?? refreshToken,\n\t\texpires: Date.now() + expiresIn * 1000,\n\t};\n}\n","sourceCodeStart":809,"sourceCodeEnd":842,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/mcp/oauth-flow.ts#L809-L842","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Read the body after the status code — `invalid_grant`/`invalid_token` means re-authentication is required","Delete the stored token file / clear cached credentials for that MCP server and run the OAuth login flow again","Confirm the stored refresh token is current if your provider rotates refresh tokens (store the new one from every refresh response)","Check provider status pages if the failure is a 5xx rather than 4xx"],"exampleFix":"// before: retrying refresh forever on invalid_grant\ntry { await refresh(token.refreshToken); } catch { /* retry */ }\n// after: detect invalid_grant and fall back to interactive login\ncatch (e) {\n  if (String(e.message).includes(\"invalid_grant\")) await runOAuthLogin(server);\n  else throw e;\n}","handlingStrategy":"try-catch","validationCode":"// check a refresh token exists and isn't obviously stale before refreshing\nif (!stored.refreshToken) throw new Error(\"No refresh token stored; interactive login required\");\nif (Date.now() - stored.refreshedAt > MAX_REFRESH_TOKEN_AGE_MS) promptRelogin();","typeGuard":"function isInvalidGrant(err: unknown): boolean {\n  return err instanceof Error && /invalid_grant|invalid_token/i.test(err.message);\n}","tryCatchPattern":"try {\n  await refreshAccessToken(refreshToken);\n} catch (err) {\n  if (isInvalidGrant(err)) {\n    await clearStoredCredentials(serverId);\n    await startInteractiveOAuthLogin(serverId); // re-auth is the only fix\n  } else throw err;\n}","preventionTips":["Persist rotated refresh tokens — many providers invalidate the old token on each refresh","Schedule proactive refresh before access_token expiry to reduce refresh-path failures","Never store refresh tokens in shared/cache locations that other tooling may overwrite","Treat invalid_grant as terminal: clear credentials and re-login rather than retrying"],"tags":["oauth","mcp","refresh-token","http"],"backgroundTag":"oauth-refresh-token-invalid","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}