import { setTimeout as delay } from "node:timers/promises"; import type { Tool } from "@modelcontextprotocol/client"; import { PRODUCT_MCP_READ_TOOLS } from "../runtime/product-mcp-tools.js"; const record = (value: unknown): value is Record => !!value && typeof value === "object" && !Array.isArray(value); const transientCodes = new Set(["SOURCE_UNAVAILABLE", "QUERY_TIMEOUT", "RATE_LIMIT", "RATE_LIMITED", "REQUEST_TIMEOUT"]); const networkCodes = new Set(["ECONNRESET", "ECONNREFUSED", "ETIMEDOUT", "EAI_AGAIN", "ENOTFOUND", "EPIPE", "UND_ERR_SOCKET", "UND_ERR_CONNECT_TIMEOUT"]); export const transientMcpHttpStatus = (status: unknown) => status === 429 || status === 502 || status === 503 || status === 504; export function mayRetryMcpRead(tool: Tool): boolean { // The calculation waiter already owns four bounded status attempts. Do not // multiply its polling budget, or trust readOnlyHint as an independent grant. return tool.name !== "list_calculation_jobs" && (PRODUCT_MCP_READ_TOOLS as readonly string[]).includes(tool.name) && tool.annotations?.readOnlyHint === true && tool.annotations.destructiveHint !== true; } export function mcpToolFailure(result: unknown): Record | undefined { if (!record(result) || result.isError !== true || !record(result.structuredContent) || !record(result.structuredContent.error)) return; return result.structuredContent.error; } export function isMcpAuthenticationFailure(value: unknown): boolean { if (!record(value)) return false; const status = value.status ?? value.statusCode; return typeof status === "number" ? status === 401 : value.code === "UNAUTHORIZED" || value.code === "AUTH_REQUIRED"; } export function isTransientMcpFailure(value: unknown): boolean { if (!record(value) || value.retryable === false) return false; const status = value.status ?? value.statusCode; if (typeof status === "number") return transientMcpHttpStatus(status); if (typeof value.code === "string" && (transientCodes.has(value.code) || networkCodes.has(value.code))) return true; if (value.name === "TimeoutError" || value.code === -32001) return true; if (value.name === "TypeError" && typeof value.message === "string" && /fetch|network|connection|connect|load failed|socket/i.test(value.message)) return true; return record(value.cause) && typeof value.cause.code === "string" && networkCodes.has(value.cause.code); } export async function waitForMcpRetry(attempt: number, signal: AbortSignal): Promise { signal.throwIfAborted(); try { await delay(attempt === 0 ? 100 : 300, undefined, { signal }); } catch (cause) { signal.throwIfAborted(); throw cause; } }