@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.
183 lines (182 loc) • 7.26 kB
TypeScript
import { Client, Tool as McpToolDef } from '@modelcontextprotocol/client';
import { ContentPart, ToolInputResponse } from '@tanstack/ai';
import { McpServerTool, McpToolMetadata } from './types.js';
interface ConvertOptions {
prefix?: string;
lazy?: boolean;
needsApproval?: (tool: McpToolDef) => boolean;
}
/** Reads the MCP Apps `_meta.ui.resourceUri` link from a tool def, if present. */
export declare function extractUiResourceUri(def: McpToolDef): string | undefined;
/**
* 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.
*/
export declare function toolMcpMetadata(def: McpToolDef, serverId: string | undefined): McpToolMetadata;
export declare function mcpContentToTanstack(content: unknown): string | Array<ContentPart>;
/**
* Calls one MCP tool and returns the tool result.
*
* A spec 2025 task waits on `tasks/get`, then reads `tasks/result`.
* Spec 2026-07-28 has no tasks, so a 2026 call returns the tool result.
* `chat()` receives the tool result after the task ends.
*
* `signal` stops the wait. This function then sends `tasks/cancel`.
* It does not wait for that cancel request.
*
* If the tool result asks for input, this function throws
* {@link MCPInputRequiredError}.
* `kind` is `form` for user input, or `sampling` for a model request.
* `request` is the input request body.
* This function does not catch that error.
*
* On spec 2026, pass `inputResponse` to answer an input request.
* The call gets the request again, then sends the answer at once
* with `inputResponses` and the server's `requestState`.
* If the server asks for input again after that answer, this throws an Error.
*
* @param client - Connected MCP client
* @param mcpName - Server tool name
* @param args - Tool arguments
* @param taskRequired - True when the tool requires a spec 2025 task
* @param signal - Stops the wait when the caller aborts
* @param inputResponse - The user's answer from an `mcp_input` interrupt
*/
export declare function callMcpTool(client: Client, mcpName: string, args: Record<string, unknown>, taskRequired: boolean, signal?: AbortSignal, inputResponse?: ToolInputResponse): Promise<{
[x: string]: unknown;
content: ({
type: "text";
text: string;
annotations?: {
audience?: ("user" | "assistant")[] | undefined;
priority?: number | undefined;
lastModified?: string | undefined;
} | undefined;
_meta?: {
[x: string]: unknown;
} | undefined;
} | {
type: "image";
data: string;
mimeType: string;
annotations?: {
audience?: ("user" | "assistant")[] | undefined;
priority?: number | undefined;
lastModified?: string | undefined;
} | undefined;
_meta?: {
[x: string]: unknown;
} | undefined;
} | {
type: "audio";
data: string;
mimeType: string;
annotations?: {
audience?: ("user" | "assistant")[] | undefined;
priority?: number | undefined;
lastModified?: string | undefined;
} | undefined;
_meta?: {
[x: string]: unknown;
} | undefined;
} | {
uri: string;
name: string;
type: "resource_link";
description?: string | undefined;
mimeType?: string | undefined;
size?: number | undefined;
annotations?: {
audience?: ("user" | "assistant")[] | undefined;
priority?: number | undefined;
lastModified?: string | undefined;
} | undefined;
_meta?: {
[x: string]: unknown;
} | undefined;
icons?: {
src: string;
mimeType?: string | undefined;
sizes?: string[] | undefined;
theme?: "light" | "dark" | undefined;
}[] | undefined;
title?: string | undefined;
} | {
type: "resource";
resource: {
uri: string;
text: string;
mimeType?: string | undefined;
_meta?: {
[x: string]: unknown;
} | undefined;
} | {
uri: string;
blob: string;
mimeType?: string | undefined;
_meta?: {
[x: string]: unknown;
} | undefined;
};
annotations?: {
audience?: ("user" | "assistant")[] | undefined;
priority?: number | undefined;
lastModified?: string | undefined;
} | undefined;
_meta?: {
[x: string]: unknown;
} | undefined;
})[];
_meta?: {
[x: string]: unknown;
"io.modelcontextprotocol/serverInfo"?: {
version: string;
name: string;
websiteUrl?: string | undefined;
description?: string | undefined;
icons?: {
src: string;
mimeType?: string | undefined;
sizes?: string[] | undefined;
theme?: "light" | "dark" | undefined;
}[] | undefined;
title?: string | undefined;
} | undefined;
} | undefined;
structuredContent?: unknown;
isError?: boolean | undefined;
}>;
/**
* Build the execute body that proxies a TanStack tool call to an MCP server.
* 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[].
*/
export declare function makeMcpExecute(client: Client, mcpName: string, preferStructured: boolean, taskRequired?: boolean): (args: unknown, ctx?: {
abortSignal?: AbortSignal;
inputResponse?: ToolInputResponse;
}) => Promise<{} | null>;
/** A tool that must run as a task. */
export declare function requiresTaskExecution(def: McpToolDef): boolean;
/** The server declares task-based execution support for tools/call. */
export declare function serverSupportsTaskCalls(client: Client): boolean;
/**
* Auto-discovery path: turn raw MCP tool defs into ServerTools. Task-required
* tools are excluded when the server does not declare the tasks capability
* for tools/call — every invocation would fail, so they must not be offered
* to the model.
*/
export declare function toServerTools(client: Client, defs: Array<McpToolDef>, options: ConvertOptions): Array<McpServerTool>;
export {};