{"record":{"id":"63bc4a38261eedd3","repo":"NousResearch/hermes-agent","slug":"oauth-server-did-not-provide-an-authorization-url-63bc4a","errorCode":null,"errorMessage":"OAuth server did not provide an authorization URL","messagePattern":"OAuth server did not provide an authorization URL","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/src/lib/mcp-dashboard-oauth.ts","lineNumber":37,"sourceCode":"  open,\n  sleep = defaultSleep,\n  maxPollFailures = 3,\n}: CompleteOptions): Promise<McpOAuthFlow> {\n  // Open synchronously from the click handler, before the first await. Browsers\n  // otherwise classify the later OAuth popup as unsolicited and block it.\n  const authWindow = open(\"about:blank\", \"_blank\") as Window | null;\n  if (!authWindow) {\n    throw new Error(\"OAuth popup was blocked — allow popups for this dashboard and retry\");\n  }\n  authWindow.opener = null;\n  let started: McpOAuthFlow;\n  try {\n    started = await start(serverName);\n    if (started.status === \"error\") {\n      throw new Error(started.error || \"OAuth failed to start\");\n    }\n    if (!started.authorization_url) {\n      throw new Error(\"OAuth server did not provide an authorization URL\");\n    }\n    authWindow.location.href = started.authorization_url;\n  } catch (error) {\n    authWindow.close();\n    throw error;\n  }\n\n  let pollFailures = 0;\n  for (;;) {\n    let current: McpOAuthFlow;\n    try {\n      current = await status(started.flow_id);\n      pollFailures = 0;\n    } catch (error) {\n      pollFailures += 1;\n      if (pollFailures >= maxPollFailures) throw error;\n      await sleep(1000);\n      continue;","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/web/src/lib/mcp-dashboard-oauth.ts#L19-L55","documentation":"The MCP OAuth start call succeeded (status was not 'error') but the returned flow object has no authorization_url, so there is nothing to navigate the popup to. This indicates a malformed or partial response from the gateway's OAuth start handler — the flow was registered but the provider's consent URL was never produced.","triggerScenarios":"A gateway bug or version where the start handler returns `{status:'ok', flow_id}` without building the authorization URL; an OAuth provider whose metadata lacks an authorization_endpoint so the URL cannot be constructed; response shape mismatch between gateway and web client.","commonSituations":"Version skew between dashboard frontend and gateway; an MCP server advertising OAuth (RFC 9725 metadata) but missing authorization_endpoint; middleware stripping fields from the JSON response.","solutions":["Inspect the start endpoint's raw JSON response in the network tab to confirm authorization_url is genuinely absent.","Update/restart the gateway so its MCP OAuth handler matches the dashboard version and always returns authorization_url on success.","Verify the MCP server's OAuth discovery metadata includes an authorization_endpoint."],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function isStartedFlow(v: unknown): v is { flow_id: string; authorization_url: string } {\n  const f = v as { flow_id?: unknown; authorization_url?: unknown }\n  return typeof f?.flow_id === 'string'\n    && typeof f?.authorization_url === 'string'\n    && f.authorization_url.length > 0\n}","tryCatchPattern":"const started = await start(serverName)\nif (!isStartedFlow(started)) {\n  throw new Error(`OAuth start response incomplete: ${JSON.stringify(started)}`)\n}","preventionTips":["Validate flow responses before navigating the popup.","Keep gateway and dashboard versions matched.","Add contract tests for the start endpoint's response shape."],"tags":["oauth","mcp","api-contract"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}