@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
TypeScript
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'];