{"record":{"id":"92b66c0afbf14e4d","repo":"can1357/oh-my-pi","slug":"exa-mcp-returned-an-error-falls-back-to-messag","errorCode":null,"errorMessage":"Exa MCP returned an error (falls back to `${message}` when present)","messagePattern":"Exa MCP returned an error \\(falls back to `(.+?)` when present\\)","errorType":"exception","errorClass":"SearchProviderError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/web/search/providers/exa.ts","lineNumber":414,"sourceCode":"\t\t\tcontent?: Array<{ type: string; text?: string }>;\n\t\t\tisError?: boolean;\n\t\t};\n\t\terror?: {\n\t\t\tcode: number;\n\t\t\tmessage: string;\n\t\t};\n\t} | null;\n\tif (!mcpResponse) {\n\t\tthrow new Error(\"Failed to parse MCP response\");\n\t}\n\tif (mcpResponse.error) {\n\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","sourceCodeStart":396,"sourceCodeEnd":432,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/web/search/providers/exa.ts#L396-L432","documentation":"The MCP result was flagged isError=true (tool-level execution failure) and the result content contained no extractable text message. SearchProviderError is thrown with the generic fallback message 'Exa MCP returned an error' when no text content is found; if text is present, that text is used instead.","triggerScenarios":"The Exa MCP tool executed but reported failure via mcpResponse.result.isError with no { type: \"text\", text } item in result.content, or the text item was empty/whitespace after trim.","commonSituations":"Exa backend tool failure with a non-text error payload; protocol shape change removing the text content; upstream search failure reported as a tool error without details.","solutions":["Retry the search — tool-level errors are often transient backend failures.","Switch to the REST API path (EXA_API_KEY) for more descriptive error reporting.","Use another search provider if Exa MCP errors persist.","Log full MCP result content when debugging to see what was actually returned."],"exampleFix":null,"handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  const res = await searchExa({ query });\n} catch (e) {\n  if (e instanceof SearchProviderError && e.message === \"Exa MCP returned an error\") {\n    const res2 = await search({ provider: \"brave\", query }); // opaque tool failure: switch providers\n  } else throw e;\n}","preventionTips":["Treat isError MCP results as transient and retry once before falling back","Keep an alternative provider configured — this error can be opaque","Prefer the REST path with an API key for richer error reporting","Log full result.content to diagnose missing text payloads"],"tags":["mcp","exa","search-provider","tool-error"],"backgroundTag":"mcp-protocol-error","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}