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.

147 lines (146 loc) • 4.94 kB
import { MCPEventConfigError, MCPEventRequestError, MCPEventSubscriptionNotFoundError, MCPEventsUnsupportedError, MCPUnknownEventError } from "../errors.js"; import { isSafeInteger } from "./value.js"; import { validateCallbackUrl, withCallbackKey } from "./callback-url.js"; import { isMethodNotFound, toMCPEventRequestError } from "./errors-map.js"; import { emptyResult, subscribeResult } from "./wire.js"; import { listEventDefinitions } from "./list.js"; import { createMCPEventSecret, decodeMCPEventSecret } from "./secret.js"; //#region src/events/direct.ts function rethrowEvents(err) { if (isMethodNotFound(err)) throw new MCPEventsUnsupportedError(); throw toMCPEventRequestError(err); } function assertWebhookMode(mode) { if (mode !== "webhook") throw new MCPEventConfigError("delivery.mode must be webhook"); } function assertTtlMs(value) { if (value === void 0 || value === null) return; if (!isSafeInteger(value) || value <= 0) throw new MCPEventConfigError("ttlMs must be a positive safe integer or null"); } function assertMaxAgeMs(value) { if (value === void 0) return; if (!isSafeInteger(value) || value < 0) throw new MCPEventConfigError("maxAgeMs must be a non-negative safe integer"); } async function resolveRecord(store, ref) { const found = ref.key !== void 0 ? await store.get(ref.key) : await store.getById(ref.id); if (found === void 0) throw new MCPEventSubscriptionNotFoundError(ref.key !== void 0 ? { key: ref.key } : { id: ref.id }); return found; } function subscribeParams(record) { const params = { name: record.name, arguments: record.arguments, delivery: { mode: "webhook", url: record.delivery.url, secret: record.delivery.secret }, cursor: record.cursor }; if (record.ttlMs !== void 0) params.ttlMs = record.ttlMs; if (record.maxAgeMs !== void 0) params.maxAgeMs = record.maxAgeMs; return params; } /** * Direct-mode events API: MCP requests plus a local subscription store. * * @experimental */ function createDirectMCPEvents(ctx, options) { const store = options.store; function assertReady() { ctx.assertOpen(); if (!ctx.supportsEvents) throw new MCPEventsUnsupportedError(); } async function send(record, signal) { try { return await ctx.request("events/subscribe", subscribeParams(record), subscribeResult, signal); } catch (err) { rethrowEvents(err); } } async function save(key, result) { await store.update(key, { id: result.id, refreshBefore: result.refreshBefore, cursor: result.cursor, truncated: result.truncated, status: "active" }); return { key, ...result }; } return { list(listOptions) { return listEventDefinitions(ctx, listOptions?.signal); }, async subscribe(input) { assertReady(); assertWebhookMode(input.delivery.mode); assertTtlMs(input.ttlMs); assertMaxAgeMs(input.maxAgeMs); const parsedUrl = validateCallbackUrl(input.delivery.url, { allowInsecureLocalhost: input.delivery.allowInsecureLocalhost }); const secret = input.delivery.secret ?? createMCPEventSecret(); decodeMCPEventSecret(secret); const definition = (await listEventDefinitions(ctx, input.signal)).find((item) => item.name === input.name); if (definition === void 0) throw new MCPUnknownEventError(input.name, "not-found"); if (!definition.delivery.includes("webhook")) throw new MCPUnknownEventError(input.name, "webhook-unsupported"); const args = input.arguments ?? {}; await options.validateArguments?.({ definition, arguments: args }); const key = crypto.randomUUID(); const url = withCallbackKey(parsedUrl, key); const record = { key, name: input.name, arguments: args, definition, delivery: { mode: "webhook", url, secret }, cursor: input.cursor === void 0 ? null : input.cursor, truncated: false, status: "pending", ...input.ttlMs === void 0 ? {} : { ttlMs: input.ttlMs }, ...input.maxAgeMs === void 0 ? {} : { maxAgeMs: input.maxAgeMs } }; await store.set(record); return await save(key, await send(record, input.signal).catch(async (err) => { await store.delete(key); throw err; })); }, async refresh(ref) { assertReady(); const record = await resolveRecord(store, ref); return await save(record.key, await send(record, ref.signal)); }, async unsubscribe(ref) { assertReady(); const record = await resolveRecord(store, ref); try { await ctx.request("events/unsubscribe", { name: record.name, arguments: record.arguments, delivery: { mode: "webhook", url: record.delivery.url } }, emptyResult, ref.signal); } catch (err) { const mapped = toMCPEventRequestError(err); if (!(mapped instanceof MCPEventRequestError) || mapped.kind !== "not-found") throw mapped; } await store.delete(record.key); } }; } //#endregion export { createDirectMCPEvents }; //# sourceMappingURL=direct.js.map