@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.
248 lines (247 loc) • 9.64 kB
JavaScript
import { DuplicateToolNameError, MCPConnectionError, MCPTaskRequiredToolError, MCPToolFilterError, MCPToolNotFoundError } from "./errors.js";
import { listPages } from "./list-pages.js";
import { callMcpTool, makeMcpExecute, requiresTaskExecution, sdkRequestOptions, 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 = {};
instructions;
#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, toolName, requestOptions } = this.#policy;
return {
transport: this.#transport,
prefix: this.prefix,
...this.#clientOptions ? { clientOptions: this.#clientOptions } : {},
...toolFilter ? { toolFilter } : {},
...needsApproval ? { needsApproval } : {},
...toolName ? { toolName } : {},
...requestOptions ? { requestOptions } : {}
};
}
async connect(transport) {
try {
this.#client.setNotificationHandler("notifications/tools/list_changed", () => {
this.#toolDefinitions = void 0;
});
await this.#client.connect(transport, sdkRequestOptions(this.#policy.requestOptions));
this.capabilities = this.#client.getServerCapabilities() ?? {};
this.instructions = this.#client.getInstructions();
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 requestOptions = sdkRequestOptions(this.#policy.requestOptions);
const defs = await listPages(async (cursor) => {
const page = cursor === void 0 ? await client.request({ method: "tools/list" }, requestOptions) : await client.request({
method: "tools/list",
params: { cursor }
}, requestOptions);
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(void 0, requestOptions);
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, toolName, requestOptions } = this.#policy;
const defs = applyToolFilter(await this.#listTools(), toolFilter, serverSupportsTaskCalls(this.#client));
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), requestOptions));
const existingMcp = bound.metadata?.mcp;
const mcpBase = existingMcp !== null && typeof existingMcp === "object" ? existingMcp : {};
const name = toolName?.(serverTool) ?? (this.prefix ? `${this.prefix}_${def.name}` : def.name);
return {
...bound,
name,
...options.lazy ? { lazy: true } : {},
metadata: {
...bound.metadata,
mcp: {
...mcpBase,
...toolMcpMetadata(serverTool, this.prefix)
}
}
};
});
} else tools = toServerTools(this.#client, defs, {
prefix: this.prefix,
toolName,
lazy: options.lazy,
needsApproval,
requestOptions
});
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");
const options = sdkRequestOptions(this.#policy.requestOptions);
return (await this.#client.listResources(void 0, options)).resources;
}
async readResource(uri) {
if (this.#closed) throw new MCPConnectionError("MCP client is closed");
const options = sdkRequestOptions(this.#policy.requestOptions);
return this.#client.readResource({ uri }, options);
}
async resourceTemplates() {
if (this.#closed) throw new MCPConnectionError("MCP client is closed");
const options = sdkRequestOptions(this.#policy.requestOptions);
return (await this.#client.listResourceTemplates(void 0, options)).resourceTemplates;
}
async prompts() {
if (this.#closed) throw new MCPConnectionError("MCP client is closed");
const options = sdkRequestOptions(this.#policy.requestOptions);
return (await this.#client.listPrompts(void 0, options)).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;
const options = sdkRequestOptions(this.#policy.requestOptions);
return this.#client.getPrompt({
name,
arguments: promptArgs
}, options);
}
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, void 0, this.#policy.requestOptions);
}
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);
}
/**
* Applies the client `toolFilter` to the listed tools.
* A function keeps the tools it returns `true` for.
* A list keeps exactly the listed tools, in list order. A missing or
* repeated name throws `MCPToolFilterError`. A listed tool that needs a task
* on a server without task support throws `MCPTaskRequiredToolError`.
*/
function applyToolFilter(listed, toolFilter, supportsTasks) {
if (toolFilter === void 0) return listed;
if (typeof toolFilter === "function") return listed.filter((def) => toolFilter(def));
const byName = new Map(listed.map((def) => [def.name, def]));
const seen = /* @__PURE__ */ new Set();
const repeated = /* @__PURE__ */ new Set();
const missing = [];
const kept = [];
for (const name of toolFilter) {
if (seen.has(name)) {
repeated.add(name);
continue;
}
seen.add(name);
const def = byName.get(name);
if (def === void 0) missing.push(name);
else kept.push(def);
}
if (missing.length > 0 || repeated.size > 0) throw new MCPToolFilterError({
missing,
repeated: [...repeated],
available: listed.map((def) => def.name)
});
const unsupported = kept.find((def) => requiresTaskExecution(def) && !supportsTasks);
if (unsupported !== void 0) throw new MCPTaskRequiredToolError(unsupported.name);
return kept;
}
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,
toolName: options.toolName,
requestOptions: options.requestOptions
});
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