{"record":{"id":"653b92bb3bf5a183","repo":"can1357/oh-my-pi","slug":"exa-mcp-search-returned-unexpected-response-shape","errorCode":null,"errorMessage":"Exa MCP search returned unexpected response shape.","messagePattern":"Exa MCP search returned unexpected response shape\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/web/search/providers/exa.ts","lineNumber":426,"sourceCode":"\t\tthrow new Error(`MCP error: ${mcpResponse.error.message}`);\n\t}\n\tif (mcpResponse.result?.isError) {\n\t\tconst message = mcpResponse.result.content\n\t\t\t?.find(item => item.type === \"text\" && typeof item.text === \"string\")\n\t\t\t?.text?.trim();\n\t\tthrow new SearchProviderError(\"exa\", message || \"Exa MCP returned an error\");\n\t}\n\tconst responsePayload = normalizeExaMcpPayload(mcpResponse.result);\n\tif (isSearchResponse(responsePayload)) {\n\t\treturn responsePayload as ExaSearchResponse;\n\t}\n\n\tconst parsed = parseExaMcpTextPayload(responsePayload);\n\tif (parsed) {\n\t\treturn parsed;\n\t}\n\n\tthrow new Error(\"Exa MCP search returned unexpected response shape.\");\n}\n\n/** Execute Exa web search */\nexport async function searchExa(params: ExaSearchParams): Promise<SearchResponse> {\n\t// AuthStorage-backed key takes precedence (existing behavior); probe it once\n\t// so the env-key and keyless-MCP fallbacks below stay intact, then drive the\n\t// authStorage path through the central force-refresh/rotate retry policy.\n\tconst storedKey = params.authStorage\n\t\t? await params.authStorage.getApiKey(\"exa\", params.sessionId, { signal: params.signal })\n\t\t: undefined;\n\tconst keyOrResolver: ApiKey | undefined =\n\t\tstoredKey && params.authStorage\n\t\t\t? params.authStorage.resolver(\"exa\", { sessionId: params.sessionId })\n\t\t\t: getEnvApiKey(\"exa\");\n\tconst response = keyOrResolver\n\t\t? await withAuth(keyOrResolver, key => callExaSearch(key, params), { signal: params.signal })\n\t\t: await callExaMcpSearch(params);\n","sourceCodeStart":408,"sourceCodeEnd":444,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/web/search/providers/exa.ts#L408-L444","documentation":"The Exa MCP call succeeded at the transport level and the result was not flagged as an error, but the payload did not match the expected ExaSearchResponse shape and could not be normalized/parsed into one (normalizeExaMcpPayload, isSearchResponse, and parseExaMcpTextPayload all failed). Thrown as a plain Error indicating a contract violation from the MCP endpoint.","triggerScenarios":"Exa changed its MCP result schema (renamed fields, nested differently); the result contained a different tool's payload; an empty/odd result object passed all error checks but failed shape validation.","commonSituations":"Exa MCP API version drift without a corresponding client update; the agent requesting a tool whose payload shape differs from web_search; caching layers returning stale/reshaped payloads.","solutions":["Check for a client/package update that matches the current Exa MCP schema.","Inspect the raw mcpResponse.result payload to identify the new shape and update normalizeExaMcpPayload/isSearchResponse accordingly.","Pin/verify the MCP endpoint version if Exa exposes one.","Fall back to the REST API path or another search provider."],"exampleFix":"// before\nconst results = await searchExa({ query }); // keyless MCP path, new schema -> throws\n// after\nconst results = await searchExa({ query, apiKey: process.env.EXA_API_KEY }); // stable REST path","handlingStrategy":"validation","validationCode":"function looksLikeExaSearchResult(r: unknown): boolean {\n  const o = r as { results?: unknown[] } | null;\n  return !!o && Array.isArray(o.results);\n}","typeGuard":"function isExaSearchResponse(x: unknown): x is ExaSearchResponse {\n  return typeof x === \"object\" && x !== null && \"results\" in x && Array.isArray((x as ExaSearchResponse).results);\n}","tryCatchPattern":"try {\n  const res = await searchExa({ query });\n} catch (e) {\n  if (e instanceof Error && e.message.includes(\"unexpected response shape\")) {\n    // schema drift: update client or fall back to REST provider\n  } else throw e;\n}","preventionTips":["Keep the Exa client package updated to track MCP schema changes","Validate payloads with isSearchResponse before consuming results","Pin or verify the MCP endpoint version in production","Prefer the versioned REST API when schema stability matters"],"tags":["schema","mcp","exa","parse-error","search-provider"],"backgroundTag":"schema-validation-failed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}