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.

409 lines (408 loc) • 10.7 kB
import { JSONObject, StandardSchemaV1 } from '@modelcontextprotocol/client'; export type { JSONObject, JSONValue } from '@modelcontextprotocol/client'; /** * Delivery modes an event type may advertise. * * @experimental */ export type MCPEventDeliveryMode = 'poll' | 'push' | 'webhook'; /** * One event type from `events/list`. * * @experimental * * @example * ```ts * const events = await client.experimental_events.list() * const incident = events.find((event) => event.name === 'incident.created') * ``` */ export interface MCPEventDefinition { name: string; description?: string; delivery: Array<MCPEventDeliveryMode>; inputSchema: unknown; payloadSchema: unknown; _meta?: JSONObject; } /** * Server-reported webhook delivery health on subscribe/refresh. * * @experimental */ export interface MCPEventDeliveryStatus { active?: boolean; lastDeliveryAt?: string; lastError?: string | null; failedSince?: string; throttled?: boolean; retryAfterMs?: number; } /** * Result of `events/subscribe` after the client stores a local key. * * @experimental */ export interface MCPEventSubscribeResult { key: string; id: string; refreshBefore: string | null; cursor: string | null; truncated: boolean; deliveryStatus?: MCPEventDeliveryStatus; } /** * One delivered event occurrence. * * @experimental */ export interface MCPEventOccurrence { eventId: string; name: string; timestamp: string; data: JSONObject; cursor?: string | null; } /** * Stored subscription. Must stay server-side: it holds the HMAC secret. * * @experimental */ export interface MCPEventSubscription { key: string; id?: string; name: string; arguments: JSONObject; definition: MCPEventDefinition; delivery: { mode: 'webhook'; url: string; secret: string; }; refreshBefore?: string | null; cursor: string | null; truncated: boolean; ttlMs?: number | null; maxAgeMs?: number; status: 'pending' | 'active'; } /** * Handler-safe view of a subscription. No secret. * * @experimental */ export interface MCPEventSubscriptionInfo { key: string; id: string; name: string; arguments: JSONObject; delivery: { mode: 'webhook'; url: string; }; refreshBefore: string | null; } /** * Client store for webhook subscriptions. * * `update` must merge only the supplied fields. * `list` is optional so a cron job can refresh every subscription. * * @experimental * * @example * ```ts * import { inMemoryMCPEventStore } from '@tanstack/ai-mcp/events' * * const store = inMemoryMCPEventStore() * const mcp = await createMCPClient({ * transport: { type: 'http', url: 'https://mcp.example.com/mcp' }, * experimental_events: { store }, * }) * ``` */ export interface MCPEventStore { get: (key: string) => Promise<MCPEventSubscription | undefined>; getById: (id: string) => Promise<MCPEventSubscription | undefined>; set: (subscription: MCPEventSubscription) => Promise<void>; update: (key: string, patch: Partial<Omit<MCPEventSubscription, 'key'>>) => Promise<void>; delete: (key: string) => Promise<void>; list?: () => AsyncIterable<MCPEventSubscription>; } /** * Direct mode: the client talks to the MCP server and this store. * * @experimental */ export interface DirectMCPEventsOptions { store: MCPEventStore; adapter?: never; /** * Called on each `subscribe()` call, not on `refresh()`. Throw to reject. * The SDK does not validate against `inputSchema`. * * @example * ```ts * experimental_events: { * store, * validateArguments: ({ arguments: args }) => { * if (typeof args.repo !== 'string') throw new Error('repo required') * }, * } * ``` */ validateArguments?: (args: { definition: MCPEventDefinition; arguments: JSONObject; }) => void | PromiseLike<void>; } /** * Transport context passed to a managed events adapter. * * @experimental */ export type MCPEventAdapterTransport = { type: 'http'; url: string; } | { type: 'sse'; url: string; } | { type: 'custom'; }; /** * Status values used by managed adapters such as Vercel Connect. * * @experimental */ export type ManagedMCPSubscriptionStatus = 'pending' | 'active' | 'needs_auth' | 'stopped' | 'expired' | 'failed'; /** * One managed subscription record. * * @experimental */ export interface ManagedMCPSubscription { id: string; name: string; arguments: JSONObject; status: ManagedMCPSubscriptionStatus; expiresAt: string | null; idempotencyKey?: string; context?: JSONObject; } /** * Input for a managed adapter `subscribe` call. * * @experimental */ export interface ManagedMCPSubscribeInput { name: string; arguments: JSONObject; context?: JSONObject; idempotencyKey: string; ttlMs: number | null; options?: { signal?: AbortSignal; timeout?: number; }; } /** * Operations returned by `adapter.createAdapter`. * * @experimental */ export interface MCPEventOperations { subscribe: (input: ManagedMCPSubscribeInput) => Promise<ManagedMCPSubscription>; getSubscription: (id: string) => Promise<ManagedMCPSubscription | undefined>; listSubscriptions: () => Promise<Array<ManagedMCPSubscription>>; unsubscribe: (id: string) => Promise<void>; } /** * Hosted-backend adapter. `createAdapter` is synchronous and called once. * * @experimental * * @example * ```ts * const mcp = await createMCPClient({ * transport: { type: 'http', url: 'https://mcp.example.com/mcp' }, * experimental_events: { adapter }, * }) * await mcp.experimental_events.subscribe({ * name: 'incident.created', * arguments: { repo: 'acme/app' }, * idempotencyKey: 'incident:acme/app', * ttlMs: 3_600_000, * }) * ``` */ export interface MCPEventAdapter { createAdapter: (context: { transport: MCPEventAdapterTransport; }) => MCPEventOperations; } /** * Managed mode: subscribe goes through the adapter. `list` still uses MCP. * * @experimental */ export interface ManagedMCPEventsOptions { adapter: MCPEventAdapter; store?: never; validateArguments?: never; } /** * `createMCPClient({ experimental_events })` option. * Omit it for discovery-only (`list()`). * * @experimental */ export type MCPEventsOptions = DirectMCPEventsOptions | ManagedMCPEventsOptions; /** * Lookup key for refresh and unsubscribe in direct mode. * * @experimental */ export type MCPEventSubscriptionRef = ({ key: string; id?: never; } | { id: string; key?: never; }) & { signal?: AbortSignal; }; /** * Options for a direct-mode subscribe call. * * @experimental * * @example * ```ts * const sub = await client.experimental_events.subscribe({ * name: 'incident.created', * arguments: { repo: 'acme/app' }, * delivery: { * mode: 'webhook', * url: 'https://app.example.com/api/mcp-events', * }, * }) * ``` */ export interface SubscribeMCPEventOptions { name: string; arguments?: JSONObject; delivery: { mode: 'webhook'; url: string; secret?: string; allowInsecureLocalhost?: boolean; }; cursor?: string | null; ttlMs?: number | null; maxAgeMs?: number; signal?: AbortSignal; } /** * Direct and discovery events API on `client.experimental_events`. * * @experimental */ export interface MCPEvents { list: (options?: { signal?: AbortSignal; }) => Promise<Array<MCPEventDefinition>>; subscribe: (options: SubscribeMCPEventOptions) => Promise<MCPEventSubscribeResult>; refresh: (options: MCPEventSubscriptionRef) => Promise<MCPEventSubscribeResult>; unsubscribe: (options: MCPEventSubscriptionRef) => Promise<void>; } /** * Managed events API: MCP `list()` plus adapter operations. No `refresh`. * * @experimental */ export interface ManagedMCPEvents extends MCPEventOperations { list: (options?: { signal?: AbortSignal; }) => Promise<Array<MCPEventDefinition>>; } /** * What `client.ts` hands to the `events/*` modules. Internal. */ export interface MCPEventsContext { /** Sends one `events/*` request. The SDK validates the result with `schema`. */ request: <T>(method: 'events/list' | 'events/subscribe' | 'events/unsubscribe', params: Record<string, unknown>, schema: StandardSchemaV1<unknown, T>, signal?: AbortSignal) => Promise<T>; assertOpen: () => void; readonly supportsEvents: boolean; readonly transport: MCPEventAdapterTransport; } /** * Receiver options for `createMCPEventWebhook`. * * @experimental * * @example * ```ts * import { createMCPEventWebhook } from '@tanstack/ai-mcp/events' * * const webhook = createMCPEventWebhook({ * store, * onEvent: async ({ event }) => { * await queue.send({ eventId: event.eventId, data: event.data }) * }, * }) * ``` */ export interface MCPEventWebhookOptions { store: MCPEventStore; onEvent: (args: { subscription: MCPEventSubscriptionInfo; event: MCPEventOccurrence; messageId: string; }) => Promise<void> | void; onGap?: (args: { subscription: MCPEventSubscriptionInfo; messageId: string; gap: { cursor: string; truncated: true; }; }) => Promise<void> | void; onTerminated?: (args: { subscription: MCPEventSubscriptionInfo; messageId: string; termination: { code: number; message: string; data?: unknown; }; }) => Promise<void> | void; /** * Throw to reject the event with HTTP 400. */ validatePayload?: (args: { definition: MCPEventDefinition; data: JSONObject; }) => void | PromiseLike<void>; /** * Called on failed checks. Never receives secrets. */ onError?: (error: MCPEventWebhookError) => void; toleranceSeconds?: number; maxBodyBytes?: number; now?: () => number; } /** * Receiver error passed to `onError`. Messages never include the secret. * * @experimental */ export declare class MCPEventWebhookError extends Error { readonly stage: 'headers' | 'routing' | 'signature' | 'timestamp' | 'body' | 'envelope' | 'payload' | 'handler' | 'store'; readonly subscriptionKey?: string; constructor(stage: MCPEventWebhookError['stage'], message: string, options?: { subscriptionKey?: string; cause?: unknown; }); } export type MCPEventWebhookStage = MCPEventWebhookError['stage'];