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.

170 lines (169 loc) • 7.73 kB
import { AnyTool } from '@tanstack/ai'; import { AnyCommand, PluginPrompt, PluginSessionApi, PluginSetupContext, RunContext, TurnInfo } from '@tanstack/ai-harness'; import { OnElicit } from './client.js'; import { McpConnectorOptions } from './connector.js'; import { MCPClientOptions } from './types.js'; import { HttpTransportConfig, StdioTransportConfig } from './transport.js'; /** One server of {@link mcp}, keyed by its name. */ export type McpServerConfig = (StdioTransportConfig | (HttpTransportConfig & { /** * Sign in with OAuth, like `mcpConnector`. This adds `/connect <name>` * and `/disconnect <name>`. The tools of the server come after the * sign-in, and the tools that are not read-only ask for approval. * `headers` and `authProvider` do not apply then. */ oauth?: boolean; })) & { /** * The time in milliseconds that this server can take to connect, to send * its tool list, or to answer a tool call. Default: the MCP SDK default, * 60,000. */ timeoutMs?: number; /** * Give the tools of this server to code mode: the `codeMode()` plugin of * `@tanstack/ai-code-mode` takes them out of the tool list and lets the * model call them from `execute_typescript`. Without that plugin, nothing * changes. */ codeMode?: boolean; /** * Ask the user for approval before a tool of this server runs. `true` asks * for every tool. A function gets the MCP tool definition and returns * `true` to ask. Default: no approval, and for `oauth: true` every tool * that the server does not mark `readOnlyHint`. */ needsApproval?: boolean | McpConnectorOptions['needsApproval']; /** * The name the model sees for each tool of this server. It gets the MCP * tool definition. Default: `<server>_<tool>`. */ toolName?: MCPClientOptions['toolName']; }; /** * The status of one server. UIs read the list from * `snapshot().plugins['tanstack/mcp'].servers`. A server is `reconnecting` * after its connection closed, until a new connection is open. */ export type McpServerStatus = { name: string; /** How many tools the server gives the model. */ toolCount: number; } & ({ status: 'connecting' | 'connected' | 'reconnecting'; } | { status: 'failed'; error: string; }); /** * A harness plugin that connects MCP servers and gives the model their * tools, named `<server>_<tool>`. * * - Each server connects on its own. A server that fails does not stop the * others: it gets the status `failed` with the error. * - The servers connect in the background. The first turn waits for them. * - `/mcp` lists the servers and their status. UIs read the same list from * the plugin state. * - A `stdio` server runs as one child process (Node only) for every * session of the harness on the host that uses this `mcp()` object. It * stops when the last of those sessions closes, or at `host.close()`. * Create the plugin once, outside the `plugins` factory: a factory that * calls `mcp()` for each session starts a process for each session. The * other servers connect for each session. * - An `http` server with `oauth: true` signs in through `/connect <name>`. * - When the connection of a server closes, the server is `reconnecting`. * The plugin connects again after 0.5 s, 1 s, 2 s, and so on (at most * 30 s), until it is connected or its session or host closes. The wait * starts again at 0.5 s only after a connection stayed open for 30 s, so a * server that crashes right after it connects waits longer each time. The * next turn gets the tools of the new connection. A server that fails its * first connect is `failed`, and connects again the same way. * - A spec 2025 server that asks for input (`elicitation/create`) does not * name the tool call that asks. The user of the call is asked only when * all the calls of that server that run now belong to one run. Else the * request is declined, so a question never goes to another user. * - When a server sends `tools/list_changed`, the next turn lists its tools * again. * - When two servers give a tool with the same name, the model gets the * tool of the server that comes first in `servers`. The plugin warns once, * to the host logger (`createHarnessHost({ logger })`). With * `onDuplicateTool: 'error'`, the later server fails instead. * * @param options.servers - The servers, keyed by name. The name is the tool * prefix. * @param options.onDuplicateTool - What happens when a server gives a tool * name that a server earlier in `servers` gives too. `'warn'` (default): * the model does not get that one tool, and the plugin warns once. * `'error'`: the later server gets the status `failed` with an error that * names both servers and the tool, and the model gets none of its tools. * The other servers keep working. * * @example * ```ts * import { mcp } from '@tanstack/ai-mcp/harness' * * // Once, so the sessions share the stdio process. * const tools = mcp({ * servers: { * files: { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', '.'] }, * docs: { type: 'http', url: 'https://mcp.example.com/mcp', timeoutMs: 10_000 }, * notion: { type: 'http', url: 'https://mcp.notion.com/mcp', oauth: true }, * }, * }) * * plugins: () => [tools] * ``` */ export declare function mcp(options: { servers: Readonly<Record<string, McpServerConfig>>; onDuplicateTool?: 'warn' | 'error'; }): import('@tanstack/ai-harness').HarnessPlugin<NoInfer<{ readonly name: "tanstack/mcp"; readonly lifetime: "host"; readonly setup: (host: import('@tanstack/ai-harness').HostPluginContext) => { session: (ctx: PluginSetupContext<Record<string, unknown>>) => Promise<{ commands: Record<string, AnyCommand>; prompts: PluginPrompt[]; discoverTools: (turn: TurnInfo) => Promise<AnyTool[]>; }>; }; }>, Record<string, unknown>>; /** A tool call that runs now: its session, and the run that calls. */ export interface Caller { session: PluginSessionApi; run?: RunContext; } /** * Puts each call of `tools` in `calling` while it runs, with its session * and its run, so a spec 2025 elicitation can find the run that calls. */ export declare function trackCaller(tools: ReadonlyArray<AnyTool>, session: PluginSessionApi, calling: Array<Caller>): AnyTool[]; /** * The harness asks the user for form and URL elicitations, so its clients * declare both. `chat()` clients keep the default: form only. */ export declare const harnessClientOptions: { capabilities: { elicitation: { form: {}; url: {}; }; }; }; /** * Ask the session user when an MCP tool asks for input (a form or a URL * elicitation). The tool call waits for the answer, then sends it to the * server. On spec 2026, a tool call can ask up to 5 times. The answer is the * form content, or an MCP result such as `{ action: 'decline' }` or * `{ action: 'cancel' }`. */ export declare function askForInput(tools: ReadonlyArray<AnyTool>, session: PluginSessionApi): AnyTool[]; /** * Answers the `elicitation/create` requests of a spec 2025 server. The * request does not name the tool call that it belongs to. So it asks only * when the calls of `calling` all belong to one run of one session, as a * live wait of that run. With no call, or with calls of more than one run * (two users or tenants can share a server), it declines: a question must * never go to the user of another run. */ export declare function elicitFor(calling: ReadonlyArray<Caller>): OnElicit;