can1357/oh-my-pi · error · Error
MCP OAuth credential is missing refresh material
Error message
MCP OAuth credential is missing refresh material
What it means
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.
Source
Thrown at packages/coding-agent/src/mcp/oauth-credentials.ts:111
* local MCP manager and the `omp auth-broker serve` refresh path so a broker
* with no access to the MCP config can still refresh `mcp_oauth:*` credentials
* from the vault.
*
* `serverUrl` supplies the RFC 8707 fallback resource indicator when neither
* the credential nor the auth block advertised one; the manager passes the
* configured server URL, the broker recovers it from the credential id via
* {@link mcpOAuthServerUrlFromCredentialId}.
*
* @throws when no usable refresh token or token endpoint is available.
*/
export function refreshManagedMcpOAuthCredential(
credential: MCPStoredOAuthCredential,
opts: { serverUrl?: string; auth?: MCPAuthConfig; signal?: AbortSignal } = {},
): Promise<OAuthCredentials> {
const material = selectMcpOAuthRefreshMaterial(credential, opts.auth);
const tokenUrl = material?.tokenUrl;
if (!credential.refresh || !tokenUrl) {
throw new Error("MCP OAuth credential is missing refresh material");
}
const authorizationUrl = material && "authorizationUrl" in material ? material.authorizationUrl : undefined;
const resourceIsFallback = !material?.resource && Boolean(opts.serverUrl);
const resource = material?.resource ?? (resourceIsFallback ? opts.serverUrl : undefined);
return refreshMCPOAuthToken(tokenUrl, credential.refresh, material?.clientId, material?.clientSecret, resource, {
authorizationUrl,
stripSameOriginResource: resourceIsFallback,
signal: opts.signal,
});
}
async function refreshBrokeredMcpOAuthCredential(
authStorage: AuthStorage,
credentialId: number,
provider: string,
signal?: AbortSignal,
): Promise<OAuthCredentials> {
const entry = await authStorage.forceRefreshCredentialById(credentialId, signal);View on GitHub (pinned to 9690622007)
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
Example fix
// before: legacy config with no token endpoint
{"mcpServers": {"acme": {"url": "https://acme.example/mcp", "oauth": {"clientId": "x"}}}}
// after: supply the token endpoint so the refresh grant can run
{"mcpServers": {"acme": {"url": "https://acme.example/mcp", "auth": {"tokenUrl": "https://acme.example/oauth/token", "clientId": "x"}}}} Defensive patterns
Strategy: validation
Validate before calling
function canRefresh(c) {
const material = c.tokenUrl ? c : authConfig;
return Boolean(c.refresh && material?.tokenUrl);
}
if (!canRefresh(credential)) await runFullAuthorizationFlow(serverUrl); Type guard
function hasRefreshMaterial(c, auth) {
const material = c.tokenUrl ? c : auth;
return typeof c.refresh === 'string' && c.refresh.length > 0 &&
typeof material?.tokenUrl === 'string' && material.tokenUrl.length > 0;
} Try / catch
try {
creds = await refreshManagedMcpOAuthCredential(credential, { serverUrl, auth });
} catch (e) {
if (e.message.includes('missing refresh material')) {
creds = await runFullAuthorizationFlow(serverUrl); // re-auth instead of refresh
} else throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- MCP OAuth refresh failed: ${response.status} ${text}
- Unknown OAuth provider: ${provider}
- OAuth provider "${provider}" does not support token refresh
- Alibaba Coding Plan
- QwenCloud Token Plan
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/c0f967013226dcc9.
Report an issue: GitHub.