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.

282 lines (281 loc) • 12.4 kB
import { ServerTool, ToolDefinition } from '@tanstack/ai'; import { ClientOptions, PriorDiscovery, Tool as McpToolDef, ToolAnnotations } from '@modelcontextprotocol/client'; import { TransportInput } from './transport.js'; import { MCPEventsOptions } from './events/types.js'; /** A bare tool definition (from `toolDefinition({...})`, no `.server()`/`.client()` called). */ export type AnyToolDefinition = ToolDefinition<any, any, string>; /** * The `mcp` block stamped onto every tool this package produces * (`tool.metadata.mcp`), on BOTH the auto-discovery and explicit * `tools(defs)` paths. * * You rarely name this type: `tools()` returns {@link McpServerTool}s, whose * `metadata.mcp` is already typed as this shape, so the read needs no * annotation and no cast: * * ```ts * const tools = await mcp.tools() * for (const tool of tools) { * if (tool.metadata.mcp.annotations?.readOnlyHint) { * // e.g. skip the approval prompt for a read-only tool * } * } * ``` */ export interface McpToolMetadata { /** Server-native (UNPREFIXED) tool name, even when the client sets a `prefix`. */ serverToolName: string; /** * Human-readable display name, resolved with the MCP spec's precedence: * `title` → `annotations.title` → `name`. Always set, so a UI can render it * without re-implementing the fallback chain. */ title: string; /** The owning client's `prefix` (the value a widget sends as `serverId`). */ serverId?: string; /** MCP Apps widget link, from the tool def's `_meta.ui.resourceUri`. */ uiResourceUri?: string; /** * A frozen copy of the server's `annotations` for this tool (absent when * the server declares none). All fields are **hints**: useful for display * and for shaping an approval UI, never a security boundary. */ annotations?: ToolAnnotations; } /** * A `ServerTool` produced by this package — structurally a plain `ServerTool` * (so it drops straight into `chat({ tools })`) with one difference: its * `metadata.mcp` block is statically known to be present and typed as * {@link McpToolMetadata}. * * `ServerTool['metadata']` is `Record<string, any> | undefined`, so reading * `tool.metadata.mcp` off a bare `ServerTool` neither compiles (possibly * undefined) nor type-checks the fields under it (`any`). Every `tools()` * overload returns these instead, which makes the natural read work and a * misspelling a compile error: * * ```ts * const [tool] = await mcp.tools() * tool.metadata.mcp.title // string * tool.metadata.mcp.annotaions // compile error (typo) * ``` */ export type McpServerTool<TTool extends ServerTool<any, any, any> = ServerTool> = Omit<TTool, 'metadata'> & { metadata: Record<string, any> & { mcp: McpToolMetadata; }; }; /** Compile-time-only descriptor of an MCP server, emitted by the codegen CLI. */ export interface ServerDescriptor { tools: Record<string, { input: unknown; output: unknown; }>; resources: Record<string, { uri: string; data: unknown; }>; prompts: Record<string, { args: unknown; messages: unknown; }>; capabilities: Record<string, unknown>; } /** The "no generated types" default — discovery yields untyped tools. */ export type AutomaticDescriptor = ServerDescriptor; export interface MCPClientOptions { transport: TransportInput; /** Tool-name prefix (e.g. 'github' → 'github_search'). Default: none. */ prefix?: string; /** * The name the model sees for each tool. Wins over `prefix`. * Receives the raw MCP tool definition (native `name`). * Default: `${prefix}_${tool.name}` with a prefix, else `tool.name`. * * Applies to `tools()` and `tools([...defs])`. Two tools with the same * final name throw `DuplicateToolNameError`. * `metadata.mcp.serverToolName` keeps the server's own name, so * `callTool()` and MCP Apps widget calls still reach the tool. * * ```ts * const mcp = await createMCPClient({ * transport: { type: 'http', url: 'https://mcp.example.com/mcp' }, * toolName: (tool) => `mcp__github__${tool.name}`, * }) * ``` */ toolName?: (tool: McpToolDef) => string; /** Client identity sent to the server. */ name?: string; version?: string; /** * Options forwarded verbatim to the MCP SDK's `Client`. * * The one that matters in practice is `jsonSchemaValidator`. The SDK * validates a tool's `structuredContent` against its declared `outputSchema`, * and its default validator is AJV — which compiles each schema by building * JavaScript source and passing it to `new Function`. Edge runtimes forbid * that: on Cloudflare Workers every call to a tool with an `outputSchema` * fails with `Code generation from strings disallowed for this context`, * wrapped by AJV as `Error compiling schema`. * * The SDK ships the fix (`CfWorkerJsonSchemaValidator`, backed by the * optional peer `@cfworker/json-schema`) but it can only be installed through * `ClientOptions`, which this package did not expose. * * ```ts * import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/client/validators/cf-worker' * * const mcp = await createMCPClient({ * transport: { type: 'http', url: 'https://mcp.example.com/mcp' }, * clientOptions: { jsonSchemaValidator: new CfWorkerJsonSchemaValidator() }, * }) * ``` */ clientOptions?: ClientOptions; /** * Sent with the connect handshake, tool lists, tool calls, resources, and * prompts. SDK defaults when unset. * * A spec 2026 `tools/call` gets no progress notifications, so there only * `timeout` applies. * * ```ts * const mcp = await createMCPClient({ * transport: { type: 'http', url: 'https://mcp.example.com/mcp' }, * requestOptions: { timeout: 120_000, resetTimeoutOnProgress: true }, * }) * ``` */ requestOptions?: { /** Milliseconds. The SDK default is 60,000. */ timeout?: number; /** Restart the timeout when the server sends a progress notification. */ resetTimeoutOnProgress?: boolean; }; /** * A saved era verdict for the SDK `Client.connect`. Save the discover * result from `MCPClient.getDiscoverResult()` and pass * `{ kind: 'modern', discover }` to skip the `server/discover` request. * Reuse it only for the same authorization context, and expire it * yourself. With `prior`, connect sends no request, so a dead server or * a revoked token fails on the first call. A prior with no shared * protocol version throws `MCPConnectionError`. */ prior?: PriorDiscovery; /** * Choose the server tools the model gets. Default: every tool. * * - A function: keep the tools it returns `true` for. It receives the raw * MCP tool definition (native `name`, `title`, `annotations`). * - A list of server tool names: keep exactly these, in this order. A name * the server does not have, or a repeated name, throws * `MCPToolFilterError`. A listed tool that needs task execution, on a * server without task support, throws `MCPTaskRequiredToolError`. An * empty list keeps no tools. * * Applies to `tools()`, `chat({ mcp })`, and MCP Apps widget calls. In * `tools([...defs])` a hidden definition throws `MCPToolNotFoundError`. * `callTool()` is not filtered. `annotations` are server-declared hints, not * a security boundary. * * ```ts * const readOnly = await createMCPClient({ * transport: { type: 'http', url: 'https://mcp.example.com/mcp' }, * toolFilter: (tool) => tool.annotations?.readOnlyHint === true, * }) * * const listed = await createMCPClient({ * transport: { type: 'http', url: 'https://mcp.example.com/mcp' }, * toolFilter: ['search_issues', 'get_issue'], * }) * ``` */ toolFilter?: ((tool: McpToolDef) => boolean) | ReadonlyArray<string>; /** * Return `true` to require user approval before a discovered tool runs. * Default: no approval. `tools([...defs])` keeps each definition's own * `needsApproval`. * * ```ts * const mcp = await createMCPClient({ * transport: { type: 'http', url: 'https://mcp.example.com/mcp' }, * needsApproval: (tool) => tool.annotations?.readOnlyHint !== true, * }) * ``` */ needsApproval?: (tool: McpToolDef) => boolean; /** * Runs when the connection closes and this client did not close it. For * example, the process of a stdio server stops. `close()` does not call * it. After it runs, the client cannot send requests: connect a new one. * * ```ts * const mcp = await createMCPClient({ * transport: { type: 'stdio', command: 'npx', args: ['my-server'] }, * onClose: () => console.warn('The MCP server stopped.'), * }) * ``` */ onClose?: () => void; /** * Runs when the server sends `notifications/tools/list_changed`. Call * `tools()` again to get the new list. */ onToolListChanged?: () => void; /** * MCP Events (SEP-3415). Omit for discovery-only (`list()`). * * Direct mode (`{ store }`) talks to the MCP server and stores webhook * subscriptions. Managed mode (`{ adapter }`) delegates subscribe to a * hosted backend such as Vercel Connect. * * @experimental * * @example * ```ts * import { inMemoryMCPEventStore } from '@tanstack/ai-mcp/events' * * const mcp = await createMCPClient({ * transport: { type: 'http', url: 'https://mcp.example.com/mcp' }, * experimental_events: { store: inMemoryMCPEventStore() }, * }) * ``` */ experimental_events?: MCPEventsOptions; } export interface ToolsOptions { /** Mark tools `lazy: true` to defer schema-sending via LazyToolManager. */ lazy?: boolean; } /** * Per-element ServerTool type from a tool definition. `def.server(execute)` * already returns a fully-typed `ServerTool<TInput, TOutput, TName>`, so a * mapped tuple over the passed definitions preserves per-tool types. Wrapped * in {@link McpServerTool} because the explicit path stamps `metadata.mcp` too. */ export type ServerToolFromDef<TDef> = TDef extends ToolDefinition<infer TInput, infer TOutput, infer TName> ? McpServerTool<ServerTool<TInput, TOutput, TName>> : never; export type MappedServerTools<TDefs extends ReadonlyArray<AnyToolDefinition>> = { -readonly [K in keyof TDefs]: ServerToolFromDef<TDefs[K]>; }; /** * ServerTool named by one descriptor tool key `TKey`. * * Only the tool **name** survives into the discovery result — input/output * stay `any` because `ServerTool`'s generics are *schema* types * (`extends SchemaInput`), while the descriptor carries plain *value* types * emitted by the codegen CLI. Per-tool argument/result typing comes from the * explicit `tools(defs)` overload via `MappedServerTools`. */ type DescribedTool<TKey extends string> = McpServerTool<ServerTool<any, any, TKey>>; /** * Discovery result typed from the generated descriptor: an array whose * elements' `name` is the union of the descriptor's tool-name literals. * Arguments/results are untyped (`any`) on this path — use the `tools(defs)` * overload for typed args. When TServer is the AutomaticDescriptor (no * generated types), this collapses to `Array<ServerTool>`. */ export type DescriptorTools<TServer extends ServerDescriptor> = Array<{ [K in keyof TServer['tools'] & string]: DescribedTool<K>; }[keyof TServer['tools'] & string]>; export type { DirectMCPEventsOptions, JSONObject, ManagedMCPEvents, ManagedMCPEventsOptions, ManagedMCPSubscribeInput, ManagedMCPSubscription, ManagedMCPSubscriptionStatus, MCPEventAdapter, MCPEventAdapterTransport, MCPEventDefinition, MCPEventDeliveryMode, MCPEventDeliveryStatus, MCPEventOccurrence, MCPEventOperations, MCPEvents, MCPEventsOptions, MCPEventStore, MCPEventSubscribeResult, MCPEventSubscription, MCPEventSubscriptionInfo, MCPEventSubscriptionRef, MCPEventWebhookOptions, SubscribeMCPEventOptions, } from './events/types.js';