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.

134 lines (133 loc) 4.98 kB
//#region src/tools.ts /** Reads the MCP Apps `_meta.ui.resourceUri` link from a tool def, if present. */ function extractUiResourceUri(def) { const uri = def._meta?.ui?.resourceUri; return typeof uri === "string" ? uri : void 0; } /** * The human-readable display name for a tool, following the MCP spec's * precedence: the top-level `title` field wins, then the legacy * `annotations.title`, and finally the programmatic `name`. */ function toolDisplayTitle(def) { return def.title ?? def.annotations?.title ?? def.name; } /** * Build the `metadata.mcp` block stamped onto every discovered/bound tool. * Shared by auto-discovery (`toServerTools`) and the explicit `tools(defs)` * path in `client.ts` so the two cannot drift. * * `annotations` is the server's own object, forwarded verbatim. Per the MCP * spec its fields (including `title`) are **hints** — a host may use them for * display or to shape an approval UI, but never as a security boundary. * * Fields the server didn't declare are OMITTED rather than set to `undefined`: * the explicit path merges this over any `mcp` block the caller already put on * their tool definition, and an `undefined` value would blank out what they set. */ function toolMcpMetadata(def, serverId) { const uiResourceUri = extractUiResourceUri(def); const annotations = def.annotations; return { serverToolName: def.name, serverId, title: toolDisplayTitle(def), ...uiResourceUri !== void 0 ? { uiResourceUri } : {}, ...annotations !== void 0 ? { annotations } : {} }; } function mcpContentToTanstack(content) { if (!Array.isArray(content)) return ""; if (content.length === 1 && content[0]?.type === "text") return content[0].text; const parts = content.map((c) => { switch (c.type) { case "text": return { type: "text", content: c.text }; case "image": return { type: "image", source: { type: "data", value: c.data, mimeType: c.mimeType } }; case "resource": { const uri = c.resource?.uri; if (typeof uri === "string" && uri.startsWith("ui://")) return { type: "text", content: "" }; return { type: "text", content: JSON.stringify(c.resource) }; } default: return { type: "text", content: JSON.stringify(c) }; } }).filter((p) => !(p.type === "text" && p.content === "")); return parts.length ? parts : ""; } /** * Build the execute body that proxies a TanStack tool call to an MCP server's * `callTool`. Shared by auto-discovery and the definition path. * * @param preferStructured when true (i.e. the tool declares an outputSchema), * return `result.structuredContent` if present so the existing output * validation in `executeServerTool` validates MCP's typed payload rather than * a JSON-in-text blob. Otherwise normalize `content[]` → string | ContentPart[]. */ function makeMcpExecute(client, mcpName, preferStructured) { return async (args, ctx) => { ctx?.abortSignal?.throwIfAborted(); const result = await client.callTool({ name: mcpName, arguments: args ?? {} }, void 0, { signal: ctx?.abortSignal }); if (result.isError) { const text = Array.isArray(result.content) ? mcpContentToTanstack(result.content) : void 0; const detail = typeof text === "string" ? text : text === void 0 ? void 0 : JSON.stringify(text); throw new Error(!detail ? `MCP tool "${mcpName}" returned an error` : `MCP tool "${mcpName}" returned an error: ${detail}`); } if (preferStructured && result.structuredContent !== void 0) return result.structuredContent; return mcpContentToTanstack(result.content); }; } /** * A tool with `execution.taskSupport: 'required'` can only run through the * SDK's experimental task-based execution (`tasks/callToolStream`) — plain * `callTool` is rejected by the server with -32600. Until task execution is * supported, such tools must not be offered to the model. */ function requiresTaskExecution(def) { return def.execution?.taskSupport === "required"; } /** * Auto-discovery path: turn raw MCP tool defs into ServerTools (args typed * `unknown`). Task-required tools are excluded — they cannot be invoked via * plain `callTool` (see {@link requiresTaskExecution}). */ function toServerTools(client, defs, options) { return defs.filter((def) => !requiresTaskExecution(def)).map((def) => { return { __toolSide: "server", name: options.prefix ? `${options.prefix}_${def.name}` : def.name, description: def.description ?? "", inputSchema: def.inputSchema ?? { type: "object", properties: {} }, ...def.outputSchema ? { outputSchema: def.outputSchema } : {}, ...options.lazy ? { lazy: true } : {}, metadata: { mcp: toolMcpMetadata(def, options.prefix) }, execute: makeMcpExecute(client, def.name, Boolean(def.outputSchema)) }; }); } //#endregion export { extractUiResourceUri, makeMcpExecute, mcpContentToTanstack, requiresTaskExecution, toServerTools, toolMcpMetadata }; //# sourceMappingURL=tools.js.map