44 lines
2.6 KiB
TypeScript
44 lines
2.6 KiB
TypeScript
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<string, unknown> => !!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<string, unknown> | 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<void> {
|
|
signal.throwIfAborted();
|
|
try { await delay(attempt === 0 ? 100 : 300, undefined, { signal }); }
|
|
catch (cause) { signal.throwIfAborted(); throw cause; }
|
|
}
|