@acid-cat/core-ts 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,610 +1,8 @@
1
- // src/app.ts
2
- import { canonicalText } from "@acid-cat/catalog";
3
- var defineCapability = (capability) => capability;
4
- var capabilitiesFor = () => (capability) => capability;
5
- var defineApp = (app) => app;
6
- function catalogOf(app, options) {
7
- const types = {};
8
- const toJson = (schema, where) => {
9
- const { $schema: _, $defs, ...json2 } = options.toJsonSchema(schema);
10
- for (const [name, definition] of Object.entries($defs ?? {})) {
11
- const existing = types[name];
12
- if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(definition)) {
13
- throw new Error(`${where}: two different schemas have the name ${name}. Give each one its own name.`);
14
- }
15
- types[name] = definition;
16
- }
17
- return json2;
18
- };
19
- const capabilities = {};
20
- for (const capability of app.capabilities) {
21
- if (capability.id in capabilities) throw new Error(`The capability ${capability.id} is declared two times.`);
22
- const failures = Object.fromEntries(
23
- Object.entries(capability.failures ?? {}).map(([tag, schema]) => [tag, toJson(schema, capability.id)])
24
- );
25
- capabilities[capability.id] = {
26
- description: capability.description,
27
- audience: capability.audience,
28
- ...capability.input === void 0 ? {} : { input: toJson(capability.input, capability.id) },
29
- ...capability.output === void 0 ? {} : { output: toJson(capability.output, capability.id) },
30
- ...Object.keys(failures).length === 0 ? {} : { failures },
31
- ...capability.readOnly === true ? { readOnly: true } : {},
32
- ...capability.state === void 0 ? {} : { state: toJson(capability.state.schema, capability.id) }
33
- };
34
- }
35
- return {
36
- acidcat: "0.1",
37
- ...app.title === void 0 ? {} : { title: app.title },
38
- ...app.description === void 0 ? {} : { description: app.description },
39
- targets: Object.fromEntries(
40
- Object.entries(app.targets).map(([name, target]) => [
41
- name,
42
- {
43
- bindings: target.bindings,
44
- ...target.description === void 0 ? {} : { description: target.description },
45
- ...target.options === void 0 ? {} : { options: target.options }
46
- }
47
- ])
48
- ),
49
- ...Object.keys(types).length === 0 ? {} : { $defs: types },
50
- capabilities
51
- };
52
- }
53
- var renderCatalog = (app, options) => canonicalText(catalogOf(app, options));
54
-
55
- // src/session.ts
56
- import { AVAILABLE, CallFailureError, callFailure, isCallFailure, viewOf } from "@acid-cat/spec-ts";
57
- var always = { getSnapshot: () => AVAILABLE, subscribe: () => ({ unsubscribe() {
58
- } }) };
59
- function createSession(app, options) {
60
- const view = viewOf(catalogOf(app, { toJsonSchema: options.toJsonSchema }), options.target);
61
- const lifetime = new AbortController();
62
- const running = /* @__PURE__ */ new Set();
63
- const listeners = /* @__PURE__ */ new Set();
64
- const subscriptions = [];
65
- const onDefect = options.onDefect ?? ((id, cause) => console.error(`A capability failed with a defect: ${id}`, cause));
66
- const served = /* @__PURE__ */ new Map();
67
- for (const capability of app.capabilities) {
68
- if (!(capability.id in view.capabilities)) continue;
69
- const availability = capability.availability?.(options.context) ?? always;
70
- subscriptions.push(availability.subscribe(() => listeners.forEach((listener) => listener())));
71
- served.set(capability.id, { capability, availability });
72
- }
73
- async function call(id, input, trusted, signal) {
74
- if (lifetime.signal.aborted || signal?.aborted) return callFailure("cancelled", "The call was cancelled.");
75
- const entry = served.get(id);
76
- if (entry === void 0) return callFailure("unknown-capability", `There is no capability ${id}.`);
77
- const availability = entry.availability.getSnapshot();
78
- if (!availability.available) return callFailure("unavailable", availability.reason);
79
- const { capability } = entry;
80
- let decoded = input ?? {};
81
- if (!trusted && capability.input !== void 0) {
82
- const validated = await capability.input["~standard"].validate(input);
83
- if (validated.issues !== void 0) {
84
- const message = validated.issues.map((issue) => `${(issue.path ?? []).map((key) => typeof key === "object" ? String(key.key) : String(key)).join(".") || "input"}: ${issue.message}`).join("; ");
85
- return callFailure("invalid-input", message);
86
- }
87
- decoded = validated.value;
88
- }
89
- const controller = new AbortController();
90
- const abort = () => controller.abort();
91
- lifetime.signal.addEventListener("abort", abort);
92
- signal?.addEventListener("abort", abort);
93
- const cancelled = new Promise(
94
- (resolve2) => controller.signal.addEventListener("abort", () => resolve2(callFailure("cancelled", "The call was cancelled.")))
95
- );
96
- if (lifetime.signal.aborted || signal?.aborted) controller.abort();
97
- const work = Promise.resolve().then(() => capability.run(decoded, { signal: controller.signal, context: options.context })).then((result) => checkResult(capability, result)).catch((cause) => {
98
- onDefect(id, cause);
99
- return callFailure("internal", "The capability failed.");
100
- });
101
- const outcome = Promise.race([work, cancelled]);
102
- running.add(outcome);
103
- try {
104
- return await outcome;
105
- } finally {
106
- running.delete(outcome);
107
- lifetime.signal.removeEventListener("abort", abort);
108
- signal?.removeEventListener("abort", abort);
109
- }
110
- }
111
- const handles = /* @__PURE__ */ new Map();
112
- for (const [id, { capability, availability }] of served) {
113
- const reject = async (outcome) => {
114
- const settled = await outcome;
115
- if (isCallFailure(settled)) throw new CallFailureError(settled);
116
- return settled;
117
- };
118
- const state = capability.state?.source(options.context);
119
- handles.set(id, {
120
- id,
121
- description: capability.description,
122
- audience: capability.audience,
123
- availability,
124
- ...state === void 0 ? {} : { snapshot: detached(state) },
125
- execute: (input, callOptions) => reject(call(id, input, true, callOptions?.signal)),
126
- invokeExternal: (input, callOptions) => reject(call(id, input, false, callOptions?.signal))
127
- });
128
- }
129
- return {
130
- target: options.target,
131
- view,
132
- handles,
133
- signal: lifetime.signal,
134
- invoke: (id, input, callOptions) => call(id, input, false, callOptions?.signal),
135
- onChange(listener) {
136
- listeners.add(listener);
137
- return { unsubscribe: () => listeners.delete(listener) };
138
- },
139
- async dispose() {
140
- lifetime.abort();
141
- subscriptions.forEach((subscription) => subscription.unsubscribe());
142
- await Promise.allSettled([...running]);
143
- }
144
- };
145
- }
146
- function detached(source) {
147
- return {
148
- getSnapshot: () => structuredClone(source.getSnapshot()),
149
- subscribe: (listener) => source.subscribe((value) => listener(structuredClone(value)))
150
- };
151
- }
152
- async function checkResult(capability, result) {
153
- if (result.ok) {
154
- if (capability.output === void 0) return { ok: true, value: result.value ?? null };
155
- const checked2 = await capability.output["~standard"].validate(result.value);
156
- if (checked2.issues !== void 0) throw new Error(`The output does not agree with its schema: ${checked2.issues[0]?.message}`);
157
- return { ok: true, value: checked2.value };
158
- }
159
- const tag = result.error._tag;
160
- const schema = tag === void 0 ? void 0 : capability.failures?.[tag];
161
- if (schema === void 0) throw new Error(`The failure ${String(tag)} is not in failures.`);
162
- const checked = await schema["~standard"].validate(result.error);
163
- if (checked.issues !== void 0) throw new Error(`The failure ${tag} does not agree with its schema: ${checked.issues[0]?.message}`);
164
- return { ok: false, error: { _tag: tag, ...checked.value } };
165
- }
166
-
167
- // src/mcp.ts
168
- import { bundle, MCP_PROTOCOL_VERSION, MCP_REJECTIONS, MCP_UNKNOWN_TOOL, mcpToolResult, readBounded, screenRequest } from "@acid-cat/spec-ts";
169
- function mcpHandler(app, options) {
170
- const maximumBytes = options.maximumBytes ?? 1048576;
171
- return async (request) => {
172
- const screened = screenRequest(request, {
173
- methods: ["POST"],
174
- allowedOrigins: options.allowedOrigins,
175
- mediaType: "application/json",
176
- maximumBytes
177
- });
178
- if (screened !== void 0) return rejection(screened);
179
- const body = await readBounded(request, maximumBytes);
180
- if ("rejection" in body) return rejection(body.rejection);
181
- let message;
182
- try {
183
- message = JSON.parse(body.text);
184
- } catch {
185
- return rejection("not-utf8");
186
- }
187
- const version = request.headers.get("MCP-Protocol-Version");
188
- const id = message.id ?? null;
189
- if (version === null) return error(400, id, -32020, "MCP-Protocol-Version header is required");
190
- if (version !== MCP_PROTOCOL_VERSION) {
191
- return error(400, id, -32022, `Unsupported protocol version ${version}`, { supported: [MCP_PROTOCOL_VERSION] });
192
- }
193
- if (message.id === void 0) return new Response(null, { status: 200, headers: { "Content-Type": "application/json" } });
194
- const metaVersion = message.params?._meta?.["io.modelcontextprotocol/protocolVersion"];
195
- if (metaVersion === void 0) return error(400, id, -32602, "Required request metadata is missing");
196
- if (metaVersion !== version) return error(400, id, -32020, "MCP-Protocol-Version header does not match request metadata");
197
- const session = createSession(app, {
198
- target: options.target ?? "mcp",
199
- context: options.context,
200
- toJsonSchema: options.toJsonSchema
201
- });
202
- try {
203
- return await answer(session, message, options, request.signal);
204
- } finally {
205
- await session.dispose();
206
- }
207
- };
208
- }
209
- async function answer(session, message, options, signal) {
210
- const id = message.id ?? null;
211
- const complete = (result, list = false) => reply(id, {
212
- _meta: { "io.modelcontextprotocol/serverInfo": { name: options.name, version: options.version } },
213
- resultType: "complete",
214
- ...list ? { ttlMs: 0, cacheScope: "private" } : {},
215
- ...result
216
- });
217
- switch (message.method) {
218
- case "server/discover":
219
- return complete({ supportedVersions: [MCP_PROTOCOL_VERSION], capabilities: { tools: { listChanged: false } } }, true);
220
- case "tools/list":
221
- return complete({ tools: listTools(session) }, true);
222
- case "tools/call": {
223
- const name = message.params?.name ?? "";
224
- const handle = session.handles.get(name);
225
- if (handle === void 0 || !handle.availability.getSnapshot().available) {
226
- return error(200, id, MCP_UNKNOWN_TOOL, `Tool '${name}' not found`);
227
- }
228
- const outcome = await session.invoke(name, message.params?.arguments ?? {}, { signal });
229
- return complete(mcpToolResult(outcome, hasObjectOutput(session, name)));
230
- }
231
- default:
232
- return error(404, id, -32601, `Method not found: ${message.method}`);
233
- }
234
- }
235
- function listTools(session) {
236
- const types = session.view.$defs ?? {};
237
- return [...session.handles.values()].filter((handle) => handle.availability.getSnapshot().available).map((handle) => {
238
- const capability = session.view.capabilities[handle.id];
239
- const output = capability.output === void 0 ? void 0 : bundle(capability.output, types);
240
- return {
241
- name: handle.id,
242
- description: capability.description,
243
- inputSchema: bundle(capability.input ?? { type: "object" }, types),
244
- ...output?.type === "object" ? { outputSchema: output } : {},
245
- annotations: { readOnlyHint: capability.readOnly === true }
246
- };
247
- });
248
- }
249
- function hasObjectOutput(session, id) {
250
- const output = session.view.capabilities[id]?.output;
251
- return output !== void 0 && bundle(output, session.view.$defs ?? {}).type === "object";
252
- }
253
- function reply(id, result) {
254
- return new Response(JSON.stringify({ jsonrpc: "2.0", id, result }), { status: 200, headers: { "Content-Type": "application/json" } });
255
- }
256
- function error(status, id, code, message, data) {
257
- const body = { jsonrpc: "2.0", id, error: { code, message, ...data === void 0 ? {} : { data } } };
258
- return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
259
- }
260
- function rejection(reason) {
261
- const { status, code } = MCP_REJECTIONS[reason];
262
- const headers = new Headers({ "Cache-Control": "no-store" });
263
- if (reason === "method") headers.set("Allow", "POST");
264
- if (code === void 0) return new Response(null, { status, headers });
265
- headers.set("Content-Type", "application/json");
266
- return new Response(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code, message: messages[reason] } }), { status, headers });
267
- }
268
- var messages = {
269
- method: "Use POST.",
270
- origin: "This origin is not allowed.",
271
- "media-type": "Use application/json.",
272
- "too-large": "The request body is too large.",
273
- "not-utf8": "The request body is not UTF-8 JSON."
274
- };
275
-
276
- // src/http.ts
277
- import { callFailure as callFailure2, httpStatus, isCallFailure as isCallFailure2, readBounded as readBounded2, screenRequest as screenRequest2 } from "@acid-cat/spec-ts";
278
- function httpHandler(app, options) {
279
- const maximumBytes = options.maximumBytes ?? 1048576;
280
- return async (request) => {
281
- const screened = screenRequest2(request, {
282
- methods: ["GET", "POST"],
283
- allowedOrigins: options.allowedOrigins,
284
- mediaType: "application/json",
285
- maximumBytes
286
- });
287
- if (screened !== void 0) return rejection2(screened);
288
- const path = new URL(request.url).pathname.slice((options.base ?? "").length);
289
- const session = createSession(app, { target: options.target ?? "api", context: options.context, toJsonSchema: options.toJsonSchema });
290
- try {
291
- if (request.method === "GET" && path === "/capabilities") {
292
- return json(
293
- 200,
294
- [...session.handles.values()].map((handle) => ({ id: handle.id, ...handle.availability.getSnapshot() }))
295
- );
296
- }
297
- if (request.method === "GET" && path === "/catalog" && options.serveCatalog !== false) return json(200, session.view);
298
- const id = request.method === "POST" && path.startsWith("/capabilities/") ? path.slice("/capabilities/".length) : void 0;
299
- if (id === void 0) return json(404, callFailure2("unknown-capability", "There is no such route."));
300
- const body = await readBounded2(request, maximumBytes);
301
- if ("rejection" in body) return rejection2(body.rejection);
302
- let input;
303
- try {
304
- input = body.text === "" ? {} : JSON.parse(body.text);
305
- } catch {
306
- return json(422, callFailure2("invalid-input", "The request body is not JSON."));
307
- }
308
- const outcome = await session.invoke(decodeURIComponent(id), input, { signal: request.signal });
309
- return json(httpStatus(outcome), isCallFailure2(outcome) ? { kind: outcome.kind, message: outcome.message } : outcome);
310
- } finally {
311
- await session.dispose();
312
- }
313
- };
314
- }
315
- var json = (status, body) => new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
316
- var statuses = {
317
- method: 405,
318
- origin: 403,
319
- "media-type": 415,
320
- "too-large": 413,
321
- "not-utf8": 400
322
- };
323
- function rejection2(reason) {
324
- const headers = new Headers({ "Cache-Control": "no-store" });
325
- if (reason === "method") headers.set("Allow", "GET, POST");
326
- return new Response(null, { status: statuses[reason], headers });
327
- }
328
-
329
- // src/cli.ts
330
- import { bundle as bundle2, CLI_EXIT_CODES, cliExitCode, commandPath, isCallFailure as isCallFailure3, REFERENCE } from "@acid-cat/spec-ts";
331
- var UsageError = class extends Error {
332
- exitCode;
333
- constructor(message, exitCode) {
334
- super(message);
335
- this.exitCode = exitCode;
336
- }
337
- };
338
- function main(app, options) {
339
- const interrupt = new AbortController();
340
- process.once("SIGINT", () => interrupt.abort());
341
- void runCli(app, options, process.argv.slice(2), {
342
- stdout: (text) => process.stdout.write(`${text}
343
- `),
344
- stderr: (text) => process.stderr.write(`${text}
345
- `),
346
- signal: interrupt.signal
347
- }).then((code) => {
348
- process.exitCode = code;
349
- });
350
- }
351
- async function runCli(app, options, args, io) {
352
- const session = createSession(app, { target: options.target ?? "cli", context: options.context, toJsonSchema: options.toJsonSchema });
353
- const types = session.view.$defs ?? {};
354
- const commands = Object.keys(session.view.capabilities).map((id) => ({ id, path: commandPath(id) }));
355
- for (const { path } of commands) {
356
- if (path[0] === "call" || path[0] === "list") throw new Error(`The command ${path.join(" ")} has the same name as a command of the command line.`);
357
- }
358
- const print = (outcome) => {
359
- if (isCallFailure3(outcome)) io.stderr(JSON.stringify({ kind: outcome.kind, message: outcome.message }));
360
- else io.stdout(JSON.stringify(outcome.ok ? outcome.value : outcome.error, null, 2));
361
- return cliExitCode(outcome);
362
- };
363
- try {
364
- const [first, ...rest] = args;
365
- if (first === void 0 || first === "--help" || first === "-h") {
366
- io.stdout(help(options.name, commands.map(({ path }) => path.join(" "))));
367
- return CLI_EXIT_CODES.value;
368
- }
369
- if (first === "--version" || first === "-v") {
370
- io.stdout(options.version);
371
- return CLI_EXIT_CODES.value;
372
- }
373
- if (first === "list") {
374
- io.stdout(
375
- JSON.stringify(
376
- [...session.handles.values()].map((handle) => ({
377
- id: handle.id,
378
- description: handle.description,
379
- ...handle.availability.getSnapshot(),
380
- usage: `${options.name} ${commandPath(handle.id).join(" ")}`
381
- })),
382
- null,
383
- 2
384
- )
385
- );
386
- return CLI_EXIT_CODES.value;
387
- }
388
- if (first === "call") {
389
- const [id, text = "{}"] = rest;
390
- if (id === void 0) throw new UsageError("Give the id of a capability.", CLI_EXIT_CODES["invalid-input"]);
391
- let input;
392
- try {
393
- input = JSON.parse(text);
394
- } catch {
395
- return print({ kind: "invalid-input", message: "The input is not JSON." });
396
- }
397
- return print(await session.invoke(id, input, { signal: io.signal }));
398
- }
399
- const match = commands.filter(({ path }) => path.every((word, index) => args[index] === word)).sort((a, b) => b.path.length - a.path.length)[0];
400
- if (match === void 0) throw new UsageError(`There is no command ${args.filter((arg) => !arg.startsWith("-")).join(" ")}.`, CLI_EXIT_CODES["unknown-capability"]);
401
- const capability = session.view.capabilities[match.id];
402
- const schema = bundle2(capability.input ?? { type: "object" }, types);
403
- const flagArgs = args.slice(match.path.length);
404
- if (flagArgs.includes("--help") || flagArgs.includes("-h")) {
405
- io.stdout(usage(options.name, match.path, capability.description, schema));
406
- return CLI_EXIT_CODES.value;
407
- }
408
- return print(await session.invoke(match.id, parseFlags(flagArgs, schema), { signal: io.signal }));
409
- } catch (error2) {
410
- if (!(error2 instanceof UsageError)) throw error2;
411
- io.stderr(error2.message);
412
- return error2.exitCode;
413
- } finally {
414
- await session.dispose();
415
- }
416
- }
417
- function parseFlags(args, schema) {
418
- const properties = schema.properties ?? {};
419
- const definitions = schema.$defs ?? {};
420
- const input = {};
421
- for (let index = 0; index < args.length; index += 1) {
422
- const arg = args[index];
423
- if (!arg.startsWith("--")) throw new UsageError(`Unexpected argument ${arg}.`, CLI_EXIT_CODES["invalid-input"]);
424
- const [name, inline] = arg.slice(2).split(/=(.*)/s, 2);
425
- const property = properties[name];
426
- if (property === void 0) throw new UsageError(`Unknown flag --${name}.`, CLI_EXIT_CODES["invalid-input"]);
427
- const type = resolve(property, definitions);
428
- if (type.type === "boolean") {
429
- input[name] = inline === void 0 ? true : inline === "true";
430
- continue;
431
- }
432
- const text = inline ?? args[index += 1];
433
- if (text === void 0) throw new UsageError(`The flag --${name} needs a value.`, CLI_EXIT_CODES["invalid-input"]);
434
- input[name] = valueOf(name, text, type);
435
- }
436
- for (const name of schema.required ?? []) {
437
- if (!(name in input)) throw new UsageError(`The flag --${name} is required.`, CLI_EXIT_CODES["invalid-input"]);
438
- }
439
- return input;
440
- }
441
- function valueOf(name, text, type) {
442
- if (Array.isArray(type.enum) && type.enum.every((value) => typeof value === "string")) {
443
- if (!type.enum.includes(text)) throw new UsageError(`The flag --${name} must be one of: ${type.enum.join(", ")}.`, CLI_EXIT_CODES["invalid-input"]);
444
- return text;
445
- }
446
- if (type.type === "integer" || type.type === "number") {
447
- const number = Number(text);
448
- if (text.trim() === "" || !Number.isFinite(number) || type.type === "integer" && !Number.isInteger(number)) {
449
- throw new UsageError(`The flag --${name} must be a ${type.type}.`, CLI_EXIT_CODES["invalid-input"]);
450
- }
451
- return number;
452
- }
453
- if (type.type === "string") return text;
454
- try {
455
- return JSON.parse(text);
456
- } catch {
457
- throw new UsageError(`The flag --${name} must be JSON.`, CLI_EXIT_CODES["invalid-input"]);
458
- }
459
- }
460
- function resolve(property, definitions) {
461
- const name = typeof property.$ref === "string" ? REFERENCE.exec(property.$ref)?.[1] : void 0;
462
- return name === void 0 ? property : definitions[name] ?? property;
463
- }
464
- function help(name, paths) {
465
- return [
466
- `Usage: ${name} <command> [flags]`,
467
- "",
468
- "Commands:",
469
- ...paths.map((path) => ` ${path}`),
470
- " call <id> [input]",
471
- " list"
472
- ].join("\n");
473
- }
474
- function usage(name, path, description, schema) {
475
- const properties = Object.keys(schema.properties ?? {});
476
- const required = new Set(schema.required ?? []);
477
- const flags = properties.map((property) => required.has(property) ? `--${property} <value>` : `[--${property} <value>]`);
478
- return [`Usage: ${name} ${[...path, ...flags].join(" ")}`, "", description].join("\n");
479
- }
480
-
481
- // src/webmcp.ts
482
- import { bundle as bundle3, CallFailureError as CallFailureError2, isCallFailure as isCallFailure4 } from "@acid-cat/spec-ts";
483
- function findHost() {
484
- const scope = globalThis;
485
- for (const candidate of [scope.document?.modelContext, scope.navigator?.modelContext]) {
486
- if (typeof candidate?.registerTool === "function") return candidate;
487
- }
488
- return void 0;
489
- }
490
- function installWebMcp(session, options = {}) {
491
- const host = options.host ?? findHost();
492
- if (host === void 0) return void 0;
493
- let status = { phase: "loading", message: null };
494
- const listeners = /* @__PURE__ */ new Set();
495
- const setStatus = (next) => {
496
- status = next;
497
- listeners.forEach((listener) => listener(next));
498
- };
499
- const lifetime = new AbortController();
500
- const registered = /* @__PURE__ */ new Map();
501
- const calls = /* @__PURE__ */ new Set();
502
- const types = session.view.$defs ?? {};
503
- let running = false;
504
- let dirty = false;
505
- const toolFor = (id) => {
506
- const capability = session.view.capabilities[id];
507
- return {
508
- name: id,
509
- description: capability.description,
510
- inputSchema: bundle3(capability.input ?? { type: "object" }, types),
511
- execute: (input, callOptions) => {
512
- const signal = AbortSignal.any([lifetime.signal, ...callOptions?.signal ? [callOptions.signal] : []]);
513
- const call = session.invoke(id, input, { signal }).then((outcome) => {
514
- if (isCallFailure4(outcome)) throw new CallFailureError2(outcome);
515
- return outcome;
516
- });
517
- calls.add(call);
518
- void call.finally(() => calls.delete(call)).catch(() => void 0);
519
- return call;
520
- }
521
- };
522
- };
523
- async function pass() {
524
- const wanted = new Set([...session.handles.values()].filter((handle) => handle.availability.getSnapshot().available).map((handle) => handle.id));
525
- const added = /* @__PURE__ */ new Map();
526
- try {
527
- for (const id of wanted) {
528
- if (registered.has(id)) continue;
529
- const controller = new AbortController();
530
- added.set(id, controller);
531
- await host.registerTool(toolFor(id), { signal: AbortSignal.any([controller.signal, lifetime.signal]) });
532
- }
533
- } catch (error2) {
534
- for (const controller of added.values()) controller.abort();
535
- throw error2;
536
- }
537
- for (const [id, controller] of added) registered.set(id, controller);
538
- for (const [id, controller] of registered) {
539
- if (!wanted.has(id)) {
540
- controller.abort();
541
- registered.delete(id);
542
- }
543
- }
544
- }
545
- async function reconcile() {
546
- if (lifetime.signal.aborted) return;
547
- if (running) {
548
- dirty = true;
549
- return;
550
- }
551
- running = true;
552
- try {
553
- do {
554
- dirty = false;
555
- setStatus({ phase: "reconciling", message: null });
556
- try {
557
- await pass();
558
- setStatus({ phase: "ready", message: null });
559
- } catch (error2) {
560
- setStatus({ phase: "failed", message: error2 instanceof Error ? error2.message : "A tool could not be registered." });
561
- dirty = false;
562
- }
563
- } while (dirty && !lifetime.signal.aborted);
564
- } finally {
565
- running = false;
566
- }
567
- }
568
- const subscription = session.onChange(() => void reconcile());
569
- void reconcile();
570
- return {
571
- status: {
572
- getSnapshot: () => status,
573
- subscribe(listener) {
574
- listeners.add(listener);
575
- return { unsubscribe: () => listeners.delete(listener) };
576
- }
577
- },
578
- retry: () => void reconcile(),
579
- async dispose() {
580
- setStatus({ phase: "closing", message: null });
581
- subscription.unsubscribe();
582
- lifetime.abort();
583
- registered.clear();
584
- await Promise.allSettled([...calls]);
585
- setStatus({ phase: "disposed", message: null });
586
- }
587
- };
588
- }
589
-
590
- // src/index.ts
591
- import { AVAILABLE as AVAILABLE2, CallFailureError as CallFailureError3, fail, ok, unavailable } from "@acid-cat/spec-ts";
592
- export {
593
- AVAILABLE2 as AVAILABLE,
594
- CallFailureError3 as CallFailureError,
595
- capabilitiesFor,
596
- catalogOf,
597
- main as cliMain,
598
- createSession,
599
- defineApp,
600
- defineCapability,
601
- fail,
602
- findHost,
603
- httpHandler,
604
- installWebMcp,
605
- mcpHandler,
606
- ok,
607
- renderCatalog,
608
- runCli,
609
- unavailable
610
- };
1
+ import { capabilitiesFor, catalogOf, defineApp, defineCapability, renderCatalog } from "./app.js";
2
+ import { createSession } from "./session.js";
3
+ import { mcpHandler } from "./mcp.js";
4
+ import { httpHandler } from "./http.js";
5
+ import { main, runCli } from "./cli.js";
6
+ import { findHost, installWebMcp } from "./webmcp.js";
7
+ import { AVAILABLE, CallFailureError, fail, ok, unavailable } from "@acid-cat/spec-ts";
8
+ export { AVAILABLE, CallFailureError, capabilitiesFor, catalogOf, main as cliMain, createSession, defineApp, defineCapability, fail, findHost, httpHandler, installWebMcp, mcpHandler, ok, renderCatalog, runCli, unavailable };
package/dist/mcp.d.ts CHANGED
@@ -1,15 +1,22 @@
1
1
  import type { App, ToJsonSchema } from "./app.js";
2
- export interface McpOptions<Context> {
2
+ import type { ContextOption } from "./context.js";
3
+ /** The options of `mcpHandler`. Give `context` to share one context, or `createContext` to make one for each request. */
4
+ export type McpOptions<Context> = McpSettings & ContextOption<Context>;
5
+ export interface McpSettings {
3
6
  readonly name: string;
4
7
  readonly version: string;
8
+ /**
9
+ * Guidance for a model about the whole server, which `server/discover` gives as `instructions`. The default is the
10
+ * description of the application.
11
+ */
12
+ readonly instructions?: string | undefined;
5
13
  /** The origins that can call the server. A request with no `Origin` header is accepted. */
6
14
  readonly allowedOrigins: readonly string[];
7
- readonly context: Context;
8
15
  readonly toJsonSchema: ToJsonSchema;
9
16
  /** The target that the server serves. The default is `"mcp"`. */
10
17
  readonly target?: string | undefined;
11
18
  /** The maximum size of a request body, in bytes. The default is 1 MiB. */
12
19
  readonly maximumBytes?: number | undefined;
13
20
  }
14
- /** A web request handler for the MCP server. Each request has its own session. */
21
+ /** A web request handler for the MCP server. Each request has its own session, and with `createContext` its own context. */
15
22
  export declare function mcpHandler<Context>(app: App<Context>, options: McpOptions<Context>): (request: Request) => Promise<Response>;