| import { Client } from "@modelcontextprotocol/sdk/client"; |
| import { getClient, evictFromPool } from "./clientPool"; |
| import { isExaServer, getExaApiKey, callExaDirectApi } from "./exaDirect"; |
|
|
| function isConnectionClosedError(err: unknown): boolean { |
| const message = err instanceof Error ? err.message : String(err); |
| return message.includes("-32000") || message.toLowerCase().includes("connection closed"); |
| } |
|
|
| export interface McpServerConfig { |
| name: string; |
| url: string; |
| headers?: Record<string, string>; |
| } |
|
|
| const DEFAULT_TIMEOUT_MS = 60_000; |
|
|
| export type McpToolTextResponse = { |
| text: string; |
| |
| structured?: unknown; |
| |
| content?: unknown[]; |
| }; |
|
|
| export type McpToolProgress = { |
| progress: number; |
| total?: number; |
| message?: string; |
| }; |
|
|
| export async function callMcpTool( |
| server: McpServerConfig, |
| tool: string, |
| args: unknown = {}, |
| { |
| timeoutMs = DEFAULT_TIMEOUT_MS, |
| signal, |
| client, |
| onProgress, |
| }: { |
| timeoutMs?: number; |
| signal?: AbortSignal; |
| client?: Client; |
| onProgress?: (progress: McpToolProgress) => void; |
| } = {} |
| ): Promise<McpToolTextResponse> { |
| |
| if (isExaServer(server)) { |
| const apiKey = getExaApiKey(server); |
| if (!apiKey) { |
| throw new Error( |
| "Exa API key not found. Set EXA_API_KEY environment variable or add ?exaApiKey= to the server URL." |
| ); |
| } |
| const normalizedArgs = |
| typeof args === "object" && args !== null && !Array.isArray(args) |
| ? (args as Record<string, unknown>) |
| : {}; |
| return callExaDirectApi(tool, normalizedArgs, apiKey, { signal, timeoutMs }); |
| } |
|
|
| const normalizedArgs = |
| typeof args === "object" && args !== null && !Array.isArray(args) |
| ? (args as Record<string, unknown>) |
| : undefined; |
|
|
| |
| |
| let activeClient = client ?? (await getClient(server, signal)); |
|
|
| const callToolOptions = { |
| signal, |
| timeout: timeoutMs, |
| |
| onprogress: (progress: McpToolProgress) => { |
| onProgress?.({ |
| progress: progress.progress, |
| total: progress.total, |
| message: progress.message, |
| }); |
| }, |
| resetTimeoutOnProgress: true, |
| }; |
|
|
| let response; |
| try { |
| response = await activeClient.callTool( |
| { name: tool, arguments: normalizedArgs }, |
| undefined, |
| callToolOptions |
| ); |
| } catch (err) { |
| if (!isConnectionClosedError(err)) { |
| throw err; |
| } |
|
|
| |
| const stale = evictFromPool(server); |
| stale?.close?.().catch(() => {}); |
|
|
| |
| activeClient = await getClient(server, signal); |
| response = await activeClient.callTool( |
| { name: tool, arguments: normalizedArgs }, |
| undefined, |
| callToolOptions |
| ); |
| } |
|
|
| const parts = Array.isArray(response?.content) ? (response.content as Array<unknown>) : []; |
| const textParts = parts |
| .filter((part): part is { type: "text"; text: string } => { |
| if (typeof part !== "object" || part === null) return false; |
| const obj = part as Record<string, unknown>; |
| return obj["type"] === "text" && typeof obj["text"] === "string"; |
| }) |
| .map((p) => p.text); |
|
|
| const text = textParts.join("\n"); |
| const structured = (response as unknown as { structuredContent?: unknown })?.structuredContent; |
| const contentBlocks = Array.isArray(response?.content) |
| ? (response.content as unknown[]) |
| : undefined; |
| return { text, structured, content: contentBlocks }; |
| } |
|
|