@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
JavaScript
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