{"record":{"id":"c0f967013226dcc9","repo":"can1357/oh-my-pi","slug":"mcp-oauth-credential-is-missing-refresh-material","errorCode":null,"errorMessage":"MCP OAuth credential is missing refresh material","messagePattern":"MCP OAuth credential is missing refresh material","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/mcp/oauth-credentials.ts","lineNumber":111,"sourceCode":" * local MCP manager and the `omp auth-broker serve` refresh path so a broker\n * with no access to the MCP config can still refresh `mcp_oauth:*` credentials\n * from the vault.\n *\n * `serverUrl` supplies the RFC 8707 fallback resource indicator when neither\n * the credential nor the auth block advertised one; the manager passes the\n * configured server URL, the broker recovers it from the credential id via\n * {@link mcpOAuthServerUrlFromCredentialId}.\n *\n * @throws when no usable refresh token or token endpoint is available.\n */\nexport function refreshManagedMcpOAuthCredential(\n\tcredential: MCPStoredOAuthCredential,\n\topts: { serverUrl?: string; auth?: MCPAuthConfig; signal?: AbortSignal } = {},\n): Promise<OAuthCredentials> {\n\tconst material = selectMcpOAuthRefreshMaterial(credential, opts.auth);\n\tconst tokenUrl = material?.tokenUrl;\n\tif (!credential.refresh || !tokenUrl) {\n\t\tthrow new Error(\"MCP OAuth credential is missing refresh material\");\n\t}\n\tconst authorizationUrl = material && \"authorizationUrl\" in material ? material.authorizationUrl : undefined;\n\tconst resourceIsFallback = !material?.resource && Boolean(opts.serverUrl);\n\tconst resource = material?.resource ?? (resourceIsFallback ? opts.serverUrl : undefined);\n\treturn refreshMCPOAuthToken(tokenUrl, credential.refresh, material?.clientId, material?.clientSecret, resource, {\n\t\tauthorizationUrl,\n\t\tstripSameOriginResource: resourceIsFallback,\n\t\tsignal: opts.signal,\n\t});\n}\n\nasync function refreshBrokeredMcpOAuthCredential(\n\tauthStorage: AuthStorage,\n\tcredentialId: number,\n\tprovider: string,\n\tsignal?: AbortSignal,\n): Promise<OAuthCredentials> {\n\tconst entry = await authStorage.forceRefreshCredentialById(credentialId, signal);","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/mcp/oauth-credentials.ts#L93-L129","documentation":"refreshManagedMcpOAuthCredential refreshes a stored MCP OAuth credential via the standard refresh_token grant. It requires two things: a refresh token on the credential itself, and a token endpoint URL resolved from either the credential (modern self-contained credentials) or the server's `auth` config block (legacy credentials). If either is missing, the credential cannot be refreshed through the standard grant and this error is thrown instead of issuing a doomed network request.","triggerScenarios":"Calling refreshManagedMcpOAuthCredential (directly, or via refreshBrokerOAuthCredential / refreshStoredManagedMcpOAuthCredential) when credential.refresh is falsy (the token was never stored or was consumed as a one-time token), or when neither the stored credential nor the passed opts.auth block carries a tokenUrl.","commonSituations":"Legacy MCP server configs that only stored an access token without a refresh token; auth blocks that specify only an authorization server but omit the token endpoint; OAuth providers that never issue refresh tokens; credentials whose refresh token was rotated and dropped by the server.","solutions":["Re-run the full OAuth authorization flow for the MCP server so a fresh access token and refresh token are stored","Add a tokenUrl to the server's `auth` config block (or to the stored credential) so legacy credentials can locate the token endpoint","Check whether the OAuth provider issues refresh tokens at all; if it does not, plan for periodic re-authorization instead of refresh","Verify the stored credential in the vault was not partially written (missing refresh field) by an interrupted flow"],"exampleFix":"// before: legacy config with no token endpoint\n{\"mcpServers\": {\"acme\": {\"url\": \"https://acme.example/mcp\", \"oauth\": {\"clientId\": \"x\"}}}}\n// after: supply the token endpoint so the refresh grant can run\n{\"mcpServers\": {\"acme\": {\"url\": \"https://acme.example/mcp\", \"auth\": {\"tokenUrl\": \"https://acme.example/oauth/token\", \"clientId\": \"x\"}}}}","handlingStrategy":"validation","validationCode":"function canRefresh(c) {\n  const material = c.tokenUrl ? c : authConfig;\n  return Boolean(c.refresh && material?.tokenUrl);\n}\nif (!canRefresh(credential)) await runFullAuthorizationFlow(serverUrl);","typeGuard":"function hasRefreshMaterial(c, auth) {\n  const material = c.tokenUrl ? c : auth;\n  return typeof c.refresh === 'string' && c.refresh.length > 0 &&\n         typeof material?.tokenUrl === 'string' && material.tokenUrl.length > 0;\n}","tryCatchPattern":"try {\n  creds = await refreshManagedMcpOAuthCredential(credential, { serverUrl, auth });\n} catch (e) {\n  if (e.message.includes('missing refresh material')) {\n    creds = await runFullAuthorizationFlow(serverUrl); // re-auth instead of refresh\n  } else throw e;\n}","preventionTips":["Always complete the full authorization flow at least once so a refresh token is stored","Include tokenUrl in the server's auth config block for legacy credentials","Check provider docs for refresh-token support before relying on silent refresh","Validate stored credentials at startup and flag any missing refresh material early"],"tags":["oauth","mcp","configuration","refresh-token"],"backgroundTag":"oauth-refresh-token-missing","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}