@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
JavaScript
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