@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.
132 lines (131 loc) • 4.52 kB
JavaScript
import { DuplicateToolNameError, MCPConnectionError, MCPTaskRequiredToolError, MCPToolNotFoundError } from "./errors.js";
import { makeMcpExecute, requiresTaskExecution, toServerTools, toolMcpMetadata } from "./tools.js";
import { isTransportInstance, resolveTransport } from "./transport.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
//#region src/client.ts
var MCPClientImpl = class {
capabilities = {};
#client;
#closed = false;
prefix;
#transport;
#clientOptions;
constructor(prefix, name = "tanstack-ai-mcp", version = "0.0.1", transport, clientOptions) {
this.prefix = prefix;
this.#transport = transport;
this.#clientOptions = clientOptions;
this.#client = new Client({
name,
version
}, clientOptions);
}
getInfo() {
return {
transport: this.#transport,
prefix: this.prefix,
...this.#clientOptions ? { clientOptions: this.#clientOptions } : {}
};
}
async connect(transport) {
try {
await this.#client.connect(transport);
this.capabilities = this.#client.getServerCapabilities() ?? {};
} catch (err) {
throw new MCPConnectionError("Failed to connect to MCP server", err);
}
}
async tools(defsOrOptions, maybeOptions = {}) {
if (this.#closed) throw new MCPConnectionError("MCP client is closed");
const isDefs = Array.isArray(defsOrOptions);
const options = isDefs ? maybeOptions : defsOrOptions ?? {};
let tools;
if (isDefs) {
const available = new Map((await this.#client.listTools()).tools.map((t) => [t.name, t]));
tools = defsOrOptions.map((def) => {
const serverTool = available.get(def.name);
if (!serverTool) throw new MCPToolNotFoundError(def.name);
if (requiresTaskExecution(serverTool)) throw new MCPTaskRequiredToolError(def.name);
const bound = def.server(makeMcpExecute(this.#client, def.name, Boolean(def.outputSchema)));
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 {
const defs = (await this.#client.listTools()).tools;
tools = toServerTools(this.#client, defs, {
prefix: this.prefix,
lazy: options.lazy
});
}
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");
return this.#client.getPrompt({
name,
arguments: args
});
}
async callTool(name, args) {
if (this.#closed) throw new MCPConnectionError("MCP client is closed");
return this.#client.callTool({
name,
arguments: args ?? {}
});
}
async close() {
if (this.#closed) return;
this.#closed = true;
await this.#client.close();
}
async [Symbol.asyncDispose]() {
await this.close();
}
};
async function createMCPClient(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);
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