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.

114 lines (109 loc) • 4.02 kB
import { Client } from '@modelcontextprotocol/client' import type { Transport } from '@modelcontextprotocol/client' import { listPages } from '../list-pages' import { resolveTransport } from '../transport' import type { CodegenServerConfig } from './define-config' /** * Tools, resources, prompts, and capabilities read from one MCP server. */ export interface ServerSurface { tools: Array<{ name: string inputSchema: unknown outputSchema?: unknown description?: string }> resources: Array<{ uri: string; name?: string }> prompts: Array<{ name: string arguments?: Array<{ name: string; required?: boolean }> }> capabilities: Record<string, unknown> } // A list call with no cursor reads every page and removes nextCursor. // listPages must see each cursor. A repeated cursor then throws an Error. function fetchListPage< MethodName extends 'tools/list' | 'resources/list' | 'prompts/list', >(client: Client, method: MethodName, cursor: string | undefined) { if (cursor === undefined) { return client.request({ method }) } const params: Record<string, unknown> = { cursor } return client.request({ method, params }) } /** * Read tools, resources, prompts, and capabilities from one MCP server. * * `transport` is a client transport that is not connected yet. * The client tries protocol `2026-07-28` first. * If the server does not support that protocol, the client uses the 2025 initialize handshake. * This function throws an Error when connect fails, when a list repeats a cursor, or when a list passes the page cap. */ export async function introspectFromTransport(transport: Transport) { const client = new Client( { name: 'tanstack-ai-mcp-codegen', version: '0.0.1' }, { versionNegotiation: { mode: 'auto' } }, ) try { await client.connect(transport) const reported = client.getServerCapabilities() const serverListsTools = reported?.tools !== undefined const serverListsResources = reported?.resources !== undefined const serverListsPrompts = reported?.prompts !== undefined const tools = serverListsTools ? await listPages(async (cursor) => { const page = await fetchListPage(client, 'tools/list', cursor) return { items: page.tools, nextCursor: page.nextCursor } }) : [] const resources = serverListsResources ? await listPages(async (cursor) => { const page = await fetchListPage(client, 'resources/list', cursor) return { items: page.resources, nextCursor: page.nextCursor } }) : [] const prompts = serverListsPrompts ? await listPages(async (cursor) => { const page = await fetchListPage(client, 'prompts/list', cursor) return { items: page.prompts, nextCursor: page.nextCursor } }) : [] const capabilities: Record<string, unknown> = {} if (reported !== undefined) { const entries = Object.entries(reported) for (const [key, entry] of entries) { capabilities[key] = entry } } return { tools: tools.map((tool) => ({ name: tool.name, inputSchema: tool.inputSchema, outputSchema: tool.outputSchema, description: tool.description, })), resources: resources.map((resource) => ({ uri: resource.uri, name: resource.name, })), prompts: prompts.map((prompt) => ({ name: prompt.name, arguments: prompt.arguments, })), capabilities, } } finally { // A close failure must not hide the original error. await client.close().catch(() => undefined) } } /** * Connect to the server in `config` and read its surface. * * `config` is one server from the codegen config. * This function throws an Error for the same failures as `introspectFromTransport`. */ export async function introspectServer(config: CodegenServerConfig) { const transport = await resolveTransport(config.transport) return introspectFromTransport(transport) }