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