UNPKG

@tanstack/ai-mcp

Version:

Host-side Model Context Protocol client for TanStack AI: discover and run MCP server tools, resources, and prompts in any adapter's chat() loop, with generated end-to-end types.

200 lines (199 loc) • 7.36 kB
import { DuplicateToolNameError, MCPConnectionError, MCPTaskRequiredToolError, MCPToolNotFoundError } from "./errors.js"; import { listPages } from "./list-pages.js"; import { callMcpTool, makeMcpExecute, requiresTaskExecution, serverSupportsTaskCalls, toServerTools, toolMcpMetadata } from "./tools.js"; import { directMCPClient } from "./direct-client.js"; import { isTransportInstance, resolveTransport } from "./transport.js"; import { Client } from "@modelcontextprotocol/client"; //#region src/client.ts var MCPClientImpl = class { capabilities = {}; #client; #closed = false; #toolDefinitions; #toolListSubscription; prefix; #transport; #clientOptions; #policy; constructor(prefix, name = "tanstack-ai-mcp", version = "0.0.1", transport, clientOptions, policy = {}) { this.prefix = prefix; this.#transport = transport; this.#clientOptions = clientOptions; this.#policy = policy; this.#client = new Client({ name, version }, { ...clientOptions, capabilities: { elicitation: { form: {} }, sampling: {}, ...clientOptions?.capabilities }, versionNegotiation: { ...clientOptions?.versionNegotiation, mode: "auto" } }); } getInfo() { const { toolFilter, needsApproval } = this.#policy; return { transport: this.#transport, prefix: this.prefix, ...this.#clientOptions ? { clientOptions: this.#clientOptions } : {}, ...toolFilter ? { toolFilter } : {}, ...needsApproval ? { needsApproval } : {} }; } async connect(transport) { try { this.#client.setNotificationHandler("notifications/tools/list_changed", () => { this.#toolDefinitions = void 0; }); await this.#client.connect(transport); this.capabilities = this.#client.getServerCapabilities() ?? {}; await this.#listenForToolListChanges().catch(() => void 0); } catch (err) { await this.#toolListSubscription?.close().catch(() => void 0); await this.#client.close().catch(() => void 0); throw new MCPConnectionError("Failed to connect to MCP server", err); } } async #listenForToolListChanges() { if (this.#client.getProtocolEra() !== "modern") return; if ((this.#client.getServerCapabilities()?.tools)?.listChanged !== true) return; this.#toolListSubscription = await this.#client.listen({ toolsListChanged: true }); } async #listTools(options) { const client = this.#client; const defs = await listPages(async (cursor) => { const page = cursor === void 0 ? await client.request({ method: "tools/list" }) : await client.request({ method: "tools/list", params: { cursor } }); return { items: page.tools, nextCursor: page.nextCursor }; }); this.#toolDefinitions = new Map(defs.map((def) => [def.name, def])); if (options?.raw !== true && client.getProtocolEra() !== "modern") await client.listTools(); return defs; } async tools(defsOrOptions, maybeOptions = {}) { if (this.#closed) throw new MCPConnectionError("MCP client is closed"); const isDefs = Array.isArray(defsOrOptions); const options = isDefs ? maybeOptions : defsOrOptions ?? {}; const { toolFilter, needsApproval } = this.#policy; const listed = await this.#listTools(); const defs = toolFilter ? listed.filter((def) => toolFilter(def)) : listed; let tools; if (isDefs) { const available = new Map(defs.map((tool) => [tool.name, tool])); tools = defsOrOptions.map((def) => { const serverTool = available.get(def.name); if (!serverTool) throw new MCPToolNotFoundError(def.name); if (requiresTaskExecution(serverTool) && !serverSupportsTaskCalls(this.#client)) throw new MCPTaskRequiredToolError(def.name); const bound = def.server(makeMcpExecute(this.#client, def.name, Boolean(def.outputSchema), requiresTaskExecution(serverTool))); const existingMcp = bound.metadata?.mcp; const mcpBase = existingMcp !== null && typeof existingMcp === "object" ? existingMcp : {}; return { ...bound, ...this.prefix ? { name: `${this.prefix}_${def.name}` } : {}, ...options.lazy ? { lazy: true } : {}, metadata: { ...bound.metadata, mcp: { ...mcpBase, ...toolMcpMetadata(serverTool, this.prefix) } } }; }); } else tools = toServerTools(this.#client, defs, { prefix: this.prefix, lazy: options.lazy, needsApproval }); const seen = /* @__PURE__ */ new Set(); for (const t of tools) { if (seen.has(t.name)) throw new DuplicateToolNameError(t.name); seen.add(t.name); } return tools; } async resources() { if (this.#closed) throw new MCPConnectionError("MCP client is closed"); return (await this.#client.listResources()).resources; } async readResource(uri) { if (this.#closed) throw new MCPConnectionError("MCP client is closed"); return this.#client.readResource({ uri }); } async resourceTemplates() { if (this.#closed) throw new MCPConnectionError("MCP client is closed"); return (await this.#client.listResourceTemplates()).resourceTemplates; } async prompts() { if (this.#closed) throw new MCPConnectionError("MCP client is closed"); return (await this.#client.listPrompts()).prompts; } async getPrompt(name, args) { if (this.#closed) throw new MCPConnectionError("MCP client is closed"); const promptArgs = isArgs(args) ? Object.fromEntries(Object.entries(args).map(([key, value]) => [key, String(value)])) : void 0; return this.#client.getPrompt({ name, arguments: promptArgs }); } async callTool(name, args, options) { if (this.#closed) throw new MCPConnectionError("MCP client is closed"); if (!this.#toolDefinitions) try { await this.#listTools({ raw: true }); } catch {} const definition = this.#toolDefinitions?.get(name); const taskRequired = definition !== void 0 && requiresTaskExecution(definition); if (taskRequired && !serverSupportsTaskCalls(this.#client)) throw new MCPTaskRequiredToolError(name); return await callMcpTool(this.#client, name, isArgs(args) ? args : {}, taskRequired, options?.signal); } async close() { if (this.#closed) return; this.#closed = true; const subscription = this.#toolListSubscription; this.#toolListSubscription = void 0; try { await subscription?.close(); } finally { await this.#client.close(); } } async [Symbol.asyncDispose]() { await this.close(); } }; function isArgs(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } async function createMCPClient(options) { if ("server" in options) return directMCPClient(options.server); return connectTransport(options); } async function connectTransport(options) { const transport = await resolveTransport(options.transport); const impl = new MCPClientImpl(options.prefix, options.name, options.version, isTransportInstance(options.transport) ? void 0 : options.transport, options.clientOptions, { toolFilter: options.toolFilter, needsApproval: options.needsApproval }); await impl.connect(transport); return impl; } /** Test-only: connect directly from a transport instance (skips resolveTransport). */ async function createMCPClientFromTransport(transport, prefix, clientOptions) { const impl = new MCPClientImpl(prefix, void 0, void 0, void 0, clientOptions); await impl.connect(transport); return impl; } //#endregion export { createMCPClient, createMCPClientFromTransport }; //# sourceMappingURL=client.js.map