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.

108 lines (102 loc) 3.92 kB
import type { ClientOptions } from '@modelcontextprotocol/sdk/client/index.js' import type { TransportConfig } from '../transport' export interface McpServerDescriptor { /** * The serializable connection config the call handler reconnects from. * `undefined` for a client built from a ready-made `Transport` instance * (single-use, not reconnectable) — the handler rejects such a call with a * clear error. */ transport: TransportConfig | undefined prefix?: string /** * Options to rebuild the client with. Carried so a reconnect keeps a custom * `jsonSchemaValidator` — an edge runtime cannot use the SDK's AJV default. */ clientOptions?: ClientOptions } export interface McpSessionStore { /** * Resolve the server descriptor for a thread+serverId, or null if unknown. * * `serverId` may be undefined when the widget omits it (single-server setups); * implementations should default to the sole recorded server in that case. */ get: ( threadId: string, serverId: string | undefined, ) => Promise<McpServerDescriptor | null> /** Record the servers a thread may interact with (called from the chat route). */ set: ( threadId: string, servers: Record<string, McpServerDescriptor>, ) => Promise<void> } /** Call-handler request shape; imported by call-handler.ts from this module. */ export interface McpAppCallRequest { threadId: string /** * The server the widget targets. May be undefined when the widget omits it * (single-server setups) — the handler defaults to the sole configured server. */ serverId?: string toolName: string args?: unknown /** * Reserved — forwarded by the bridge for correlation purposes but not * consumed by the call handler. Mirrors the `meta` convention: accepted * on the wire, carried through, but the handler does not read it. */ messageId?: string } /** * Creates a simple in-memory McpSessionStore. * * TTL is enforced on read (prune-on-read) and slides on each successful hit — * this is single-instance only. The `McpSessionStore` interface is the * extension point for persistent/SQL backends, which can drop in later with no * API change. * * Growth is bounded by an opportunistic sweep on `set()`: prune-on-read alone * never reclaims a thread that is recorded but never has a widget interaction * (the common case — most threads never touch a `ui://` widget), so without the * sweep the map would grow by one entry per chat thread for the process * lifetime. The sweep drops every entry older than the TTL whenever a new one is * written, keeping the map bounded to threads active within the TTL window. */ export function inMemoryMcpSessionStore( opts: { ttlMs?: number } = {}, ): McpSessionStore { const map = new Map< string, { at: number; servers: Record<string, McpServerDescriptor> } >() const ttl = opts.ttlMs ?? 30 * 60_000 return { async set(threadId, servers) { // Opportunistic sweep: reclaim every expired entry, not just this thread, // so set-but-never-read threads can't accumulate unbounded. const now = Date.now() for (const [id, e] of map) { if (now - e.at > ttl) map.delete(id) } map.set(threadId, { at: now, servers }) }, async get(threadId, serverId) { const e = map.get(threadId) if (!e || Date.now() - e.at > ttl) { map.delete(threadId) return null } // Sliding TTL: refresh on a successful hit so an actively-used thread // doesn't expire by absolute time mid-session. e.at = Date.now() // serverId omitted (single-server setups): default to the sole server. if (serverId === undefined) { const entries = Object.entries(e.servers) return entries.length === 1 ? (entries[0]?.[1] ?? null) : null } return e.servers[serverId] ?? null }, } }