@acid-cat/core-ts 0.10.0 → 0.10.2
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/README.md +3 -2
- package/dist/app.js +1 -2
- package/dist/cli.js +5 -2
- package/dist/clock.js +1 -2
- package/dist/context.js +1 -2
- package/dist/http.js +32 -10
- package/dist/index.js +2 -2
- package/dist/mcp-events.d.ts +3 -0
- package/dist/mcp-events.js +5 -4
- package/dist/mcp-poll.js +6 -4
- package/dist/mcp-stdio.js +3 -4
- package/dist/mcp.js +3 -4
- package/dist/node.js +1 -2
- package/dist/session.d.ts +5 -0
- package/dist/session.js +15 -4
- package/dist/stream.js +1 -2
- package/dist/subscription.js +1 -2
- package/dist/webhook.js +16 -17
- package/dist/webmcp.js +1 -2
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -78,6 +78,7 @@ export const aircon = defineApp<{ unit: Unit }>({ targets, capabilities: [powerS
|
|
|
78
78
|
## Write the catalog
|
|
79
79
|
|
|
80
80
|
```ts
|
|
81
|
+
import { writeFile } from "node:fs/promises";
|
|
81
82
|
import { renderCatalog } from "@acid-cat/core-ts";
|
|
82
83
|
|
|
83
84
|
const toJsonSchema = (schema) => z.toJSONSchema(schema, { target: "draft-2020-12" });
|
|
@@ -248,7 +249,7 @@ for await (const occurrence of await status.subscribe({})) {
|
|
|
248
249
|
}
|
|
249
250
|
```
|
|
250
251
|
|
|
251
|
-
- The promise of `subscribe` resolves when the subscription has started. Before that, it rejects with a `CallFailureError`: `unknown-capability` for an event that the session does not serve, `invalid-input` for an input that does not agree with the schema, and `cancelled` when the session closes or the signal aborts.
|
|
252
|
+
- The promise of `subscribe` resolves when the subscription has started. Before that, it rejects with a `CallFailureError`: `unknown-capability` for an event that the session does not serve, `invalid-input` for an input that does not agree with the schema or a `maxAgeMs` that is not an integer of 0 or more, and `cancelled` when the session closes or the signal aborts.
|
|
252
253
|
- The result is an async iterator of occurrences: `eventId`, `name`, `timestamp`, `data`, and `cursor` and `truncated` if they are present. `return()` cancels the subscription, also while a `next()` waits: that `next()` resolves with `done: true`. `await using` also cancels it at the end of the block.
|
|
253
254
|
- The options are `signal`, `cursor` and `maxAgeMs`.
|
|
254
255
|
- When the session closes, each subscription is complete. With `internal`, `next()` rejects with a `CallFailureError`, then resolves with `done: true`.
|
|
@@ -274,7 +275,7 @@ const handler = httpHandler(aircon, {
|
|
|
274
275
|
|
|
275
276
|
### Command line
|
|
276
277
|
|
|
277
|
-
The command line has one command for each capability in the view of the `cli` target. Each top-level property of the input is a flag with the same name, for example `--celsius 22` or `--celsius=22`. A boolean property is a switch, for example `--on`. A property with an array of strings, numbers or values of a string enum is a repeated flag: each occurrence gives one item, in the order of the flags. A repeated flag that occurs once can also give the whole array as JSON text. Other properties take JSON text. For example, with the command line of the air conditioner, [`examples/aircon/core-ts/cli.ts`](../../examples/aircon/core-ts/cli.ts):
|
|
278
|
+
The command line has one command for each capability in the view of the `cli` target. The words of the id give the path of the command. A shared prefix only contains other commands: if the path of a capability is the start of the path of another capability, for example `aircon.status` and `aircon.status.get`, `runCli` rejects and `cliMain` stops the program with an error when it starts. Each top-level property of the input is a flag with the same name, for example `--celsius 22` or `--celsius=22`. A boolean property is a switch, for example `--on`. A property with an array of strings, numbers or values of a string enum is a repeated flag: each occurrence gives one item, in the order of the flags. A repeated flag that occurs once can also give the whole array as JSON text. Other properties take JSON text. For example, with the command line of the air conditioner, [`examples/aircon/core-ts/cli.ts`](../../examples/aircon/core-ts/cli.ts):
|
|
278
279
|
|
|
279
280
|
```sh
|
|
280
281
|
node cli.ts aircon temperature set --celsius 22
|
package/dist/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { canonicalText } from "@acid-cat/catalog";
|
|
2
2
|
import { isJsonObject, objectInput } from "@acid-cat/spec-ts";
|
|
3
|
-
|
|
3
|
+
// src/app.ts
|
|
4
4
|
/**
|
|
5
5
|
* Capabilities and applications in plain TypeScript. An application writes its own catalog.
|
|
6
6
|
* See spec/catalog.md.
|
|
@@ -127,5 +127,4 @@ function write(app, toJsonSchema) {
|
|
|
127
127
|
}
|
|
128
128
|
/** The catalog of an application, as text in canonical form. */
|
|
129
129
|
const renderCatalog = (app, options) => canonicalText(catalogOf(app, options));
|
|
130
|
-
//#endregion
|
|
131
130
|
export { capabilitiesFor, catalogOf, defineApp, defineCapability, defineEvent, eventsFor, renderCatalog };
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSession } from "./session.js";
|
|
2
2
|
import { CLI_EXIT_CODES, CLI_RESUME_FLAGS, CLI_WATCH_EXIT_CODES, CallFailureError, bundle, cliEventUsage, cliExitCode, cliFlags, cliJsonArray, cliRepeatedItems, cliResolve, cliUsage, commandPath, isCallFailure, isJsonObject } from "@acid-cat/spec-ts";
|
|
3
|
-
|
|
3
|
+
// src/cli.ts
|
|
4
4
|
/**
|
|
5
5
|
* The CLI binding (spec/bindings/cli.md): a command line made from the catalog.
|
|
6
6
|
*/
|
|
@@ -42,6 +42,10 @@ async function runCli(app, options, args, io) {
|
|
|
42
42
|
path: commandPath(id)
|
|
43
43
|
}));
|
|
44
44
|
for (const { path } of commands) if (path[0] !== void 0 && GENERIC_COMMANDS.includes(path[0])) throw new Error(`The command ${path.join(" ")} has the same name as a command of the command line.`);
|
|
45
|
+
for (const { path } of commands) {
|
|
46
|
+
const longer = commands.find((other) => other.path.length > path.length && path.every((word, index) => other.path[index] === word));
|
|
47
|
+
if (longer !== void 0) throw new Error(`The command ${path.join(" ")} is also the start of the command ${longer.path.join(" ")}. A shared prefix only contains other commands.`);
|
|
48
|
+
}
|
|
45
49
|
for (const [id, event] of Object.entries(session.view.events ?? {})) {
|
|
46
50
|
const properties = event.input === void 0 ? void 0 : bundle(event.input, types).properties;
|
|
47
51
|
const taken = CLI_RESUME_FLAGS.find((flag) => isJsonObject(properties) && flag in properties);
|
|
@@ -293,5 +297,4 @@ function usage(name, path, description, schema) {
|
|
|
293
297
|
description
|
|
294
298
|
].join("\n");
|
|
295
299
|
}
|
|
296
|
-
//#endregion
|
|
297
300
|
export { main, runCli };
|
package/dist/clock.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/clock.ts
|
|
2
2
|
/** The clock of the system: `Date.now` and `setTimeout`. */
|
|
3
3
|
const systemClock = {
|
|
4
4
|
now: () => Date.now(),
|
|
@@ -22,5 +22,4 @@ function sleep(clock, ms, signal) {
|
|
|
22
22
|
signal.addEventListener("abort", stop, { once: true });
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
-
//#endregion
|
|
26
25
|
export { sleep, systemClock };
|
package/dist/context.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/context.ts
|
|
2
2
|
/**
|
|
3
3
|
* The context of one request, and the function that releases it if the handler made it. A response with a stream, such
|
|
4
4
|
* as the stream of a subscription, releases the context when the stream ends.
|
|
@@ -24,5 +24,4 @@ async function release(value) {
|
|
|
24
24
|
}
|
|
25
25
|
/** Tell if a value is a function. A function can take any arguments, and its result is not known. */
|
|
26
26
|
const isFunction = (value) => typeof value === "function";
|
|
27
|
-
//#endregion
|
|
28
27
|
export { openContext };
|
package/dist/http.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { createSession } from "./session.js";
|
|
2
1
|
import { openContext } from "./context.js";
|
|
2
|
+
import { createSession } from "./session.js";
|
|
3
3
|
import { idlePuller, textStream } from "./stream.js";
|
|
4
4
|
import { CallFailureError, KEEPALIVE_MS, SSE_MEDIA_TYPE, callFailure, httpEventMessage, httpStatus, isCallFailure, readBounded, screenRequest } from "@acid-cat/spec-ts";
|
|
5
|
-
|
|
5
|
+
// src/http.ts
|
|
6
6
|
/**
|
|
7
7
|
* The HTTP binding (spec/bindings/http.md): JSON through HTTP.
|
|
8
8
|
*/
|
|
@@ -42,13 +42,15 @@ function httpHandler(app, options) {
|
|
|
42
42
|
if (request.method === "GET" && path === "/catalog" && options.serveCatalog !== false) return json(200, session.view);
|
|
43
43
|
const route = request.method === "POST" ? postRoute(path) : void 0;
|
|
44
44
|
if (route === void 0) return json(404, callFailure("unknown-capability", "There is no such route."));
|
|
45
|
+
const id = decodedId(route.id);
|
|
46
|
+
if (id === void 0) return json(404, callFailure("unknown-capability", "There is no such route."));
|
|
45
47
|
let input;
|
|
46
48
|
try {
|
|
47
49
|
input = body.text === "" ? {} : JSON.parse(body.text);
|
|
48
50
|
} catch {
|
|
49
|
-
|
|
51
|
+
const failure = notJson(session, route.kind, id, request.signal);
|
|
52
|
+
return json(httpStatus(failure), failureBody(failure));
|
|
50
53
|
}
|
|
51
|
-
const id = decodeURIComponent(route.id);
|
|
52
54
|
if (route.kind === "capability") {
|
|
53
55
|
const outcome = await session.invoke(id, input, { signal: request.signal });
|
|
54
56
|
return json(httpStatus(outcome), isCallFailure(outcome) ? failureBody(outcome) : outcome);
|
|
@@ -75,17 +77,38 @@ function postRoute(path) {
|
|
|
75
77
|
id: path.slice(8)
|
|
76
78
|
};
|
|
77
79
|
}
|
|
80
|
+
/** The id of a route with its escapes decoded, or `undefined` if an escape is malformed, as in `%E0`. */
|
|
81
|
+
function decodedId(encoded) {
|
|
82
|
+
try {
|
|
83
|
+
return decodeURIComponent(encoded);
|
|
84
|
+
} catch {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The call failure of a request whose body is not JSON. The kinds before `invalid-input` apply first
|
|
90
|
+
* (spec/invocation.md#call-failures, spec/events.md#subscribing): `cancelled`, `unknown-capability`, then `unavailable`
|
|
91
|
+
* for a capability.
|
|
92
|
+
*/
|
|
93
|
+
function notJson(session, kind, id, signal) {
|
|
94
|
+
if (session.signal.aborted || signal.aborted) return callFailure("cancelled", "The call was cancelled.");
|
|
95
|
+
if (kind === "event") {
|
|
96
|
+
if (!session.eventHandles.has(id)) return callFailure("unknown-capability", `There is no event ${id}.`);
|
|
97
|
+
} else {
|
|
98
|
+
const handle = session.handles.get(id);
|
|
99
|
+
if (handle === void 0) return callFailure("unknown-capability", `There is no capability ${id}.`);
|
|
100
|
+
const availability = handle.availability.getSnapshot();
|
|
101
|
+
if (!availability.available) return callFailure("unavailable", availability.reason);
|
|
102
|
+
}
|
|
103
|
+
return callFailure("invalid-input", "The request body is not JSON.");
|
|
104
|
+
}
|
|
78
105
|
/**
|
|
79
106
|
* Subscribe to an event (spec/bindings/http.md#events). A call failure before the first occurrence is a JSON answer.
|
|
80
107
|
* After that, the answer is a stream of server-sent events, which closes the session when it ends.
|
|
81
108
|
*/
|
|
82
109
|
async function subscribe(session, id, input, url, request, stream) {
|
|
83
110
|
const maxAge = url.searchParams.get("maxAgeMs");
|
|
84
|
-
const maxAgeMs = maxAge === null ? void 0 : Number(maxAge);
|
|
85
|
-
if (maxAgeMs !== void 0 && (maxAge?.trim() === "" || !Number.isInteger(maxAgeMs) || maxAgeMs < 0)) return {
|
|
86
|
-
response: json(422, callFailure("invalid-input", "maxAgeMs must be a whole number of milliseconds.")),
|
|
87
|
-
streaming: false
|
|
88
|
-
};
|
|
111
|
+
const maxAgeMs = maxAge === null ? void 0 : /^[0-9]+$/.test(maxAge) ? Number(maxAge) : NaN;
|
|
89
112
|
const cursor = request.headers.get("Last-Event-ID") ?? void 0;
|
|
90
113
|
const subscribed = await session.subscribe(id, input, {
|
|
91
114
|
signal: request.signal,
|
|
@@ -151,5 +174,4 @@ function rejection(reason) {
|
|
|
151
174
|
headers
|
|
152
175
|
});
|
|
153
176
|
}
|
|
154
|
-
//#endregion
|
|
155
177
|
export { httpHandler };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { capabilitiesFor, catalogOf, defineApp, defineCapability, defineEvent, eventsFor, renderCatalog } from "./app.js";
|
|
2
|
-
import { DEFAULT_BUFFER_LIMIT } from "./subscription.js";
|
|
3
2
|
import { createSession } from "./session.js";
|
|
4
|
-
import {
|
|
3
|
+
import { DEFAULT_BUFFER_LIMIT } from "./subscription.js";
|
|
5
4
|
import { mcpHandler } from "./mcp.js";
|
|
5
|
+
import { systemClock } from "./clock.js";
|
|
6
6
|
import { main as main$1, runMcpStdio } from "./mcp-stdio.js";
|
|
7
7
|
import { httpHandler } from "./http.js";
|
|
8
8
|
import { main, runCli } from "./cli.js";
|
package/dist/mcp-events.d.ts
CHANGED
|
@@ -51,6 +51,9 @@ export declare const invalidParams: (message: string) => {
|
|
|
51
51
|
/**
|
|
52
52
|
* Read the subscription parameters (spec/bindings/mcp.md#events): the id, the input, the cursor and `maxAgeMs`. An
|
|
53
53
|
* absent cursor and a `null` cursor are the same. The input is `{}` when `arguments` is absent.
|
|
54
|
+
*
|
|
55
|
+
* This function does not check `maxAgeMs`: an unknown id comes before an invalid `maxAgeMs` (spec/events.md#subscribing),
|
|
56
|
+
* so the session checks it after the id. A `maxAgeMs` that is not a number is `NaN`, which the session rejects.
|
|
54
57
|
*/
|
|
55
58
|
export declare function subscriptionParams(params: JsonObject): {
|
|
56
59
|
readonly error: JsonRpcError;
|
package/dist/mcp-events.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { idlePuller } from "./stream.js";
|
|
2
2
|
import { CallFailureError, MCP_EVENTS_EXTENSION, MCP_EVENT_ERRORS, MCP_SUBSCRIPTION_ID, bundle, mcpEventEntry, mcpEventParams, mcpSubscribeError } from "@acid-cat/spec-ts";
|
|
3
|
-
|
|
3
|
+
// src/mcp-events.ts
|
|
4
4
|
/**
|
|
5
5
|
* Events through MCP (spec/bindings/mcp.md#events): the MCP events extension. This module serves push delivery, and
|
|
6
6
|
* declares the parts that poll and webhook delivery share. The HTTP and the stdio transports share these functions.
|
|
@@ -36,16 +36,18 @@ const invalidParams = (message) => ({ error: {
|
|
|
36
36
|
/**
|
|
37
37
|
* Read the subscription parameters (spec/bindings/mcp.md#events): the id, the input, the cursor and `maxAgeMs`. An
|
|
38
38
|
* absent cursor and a `null` cursor are the same. The input is `{}` when `arguments` is absent.
|
|
39
|
+
*
|
|
40
|
+
* This function does not check `maxAgeMs`: an unknown id comes before an invalid `maxAgeMs` (spec/events.md#subscribing),
|
|
41
|
+
* so the session checks it after the id. A `maxAgeMs` that is not a number is `NaN`, which the session rejects.
|
|
39
42
|
*/
|
|
40
43
|
function subscriptionParams(params) {
|
|
41
44
|
const { name, arguments: input, cursor, maxAgeMs } = params;
|
|
42
45
|
if (cursor !== void 0 && cursor !== null && typeof cursor !== "string") return invalidParams("cursor must be a string or null.");
|
|
43
|
-
if (maxAgeMs !== void 0 && maxAgeMs !== null && (typeof maxAgeMs !== "number" || !Number.isFinite(maxAgeMs) || maxAgeMs < 0)) return invalidParams("maxAgeMs must be a number of milliseconds.");
|
|
44
46
|
return {
|
|
45
47
|
name: typeof name === "string" ? name : "",
|
|
46
48
|
input: input ?? {},
|
|
47
49
|
cursor: typeof cursor === "string" ? cursor : void 0,
|
|
48
|
-
maxAgeMs: typeof maxAgeMs === "number" ? maxAgeMs :
|
|
50
|
+
maxAgeMs: maxAgeMs === void 0 || maxAgeMs === null ? void 0 : typeof maxAgeMs === "number" ? maxAgeMs : NaN
|
|
49
51
|
};
|
|
50
52
|
}
|
|
51
53
|
/** The message of the call failure that ends a subscription. A subscription ends only with `internal`. */
|
|
@@ -125,5 +127,4 @@ async function startEventStream(session, id, params, settings) {
|
|
|
125
127
|
cancel: () => void subscribed.return()
|
|
126
128
|
};
|
|
127
129
|
}
|
|
128
|
-
//#endregion
|
|
129
130
|
export { endMessage, eventCapabilities, forbidden, invalidParams, listEvents, servesEvents, startEventStream, subscriptionParams, unsupportedWebhook };
|
package/dist/mcp-poll.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { endMessage, invalidParams, subscriptionParams } from "./mcp-events.js";
|
|
2
1
|
import { systemClock } from "./clock.js";
|
|
2
|
+
import { endMessage, invalidParams, subscriptionParams } from "./mcp-events.js";
|
|
3
|
+
import { isMaxAgeMs } from "./session.js";
|
|
3
4
|
import { MCP_EVENT_ERRORS, MCP_POLL_DEFAULTS, canonicalJson, decodePollCursor, encodePollCursor, mcpSubscribeError } from "@acid-cat/spec-ts";
|
|
4
|
-
|
|
5
|
+
// src/mcp-poll.ts
|
|
5
6
|
/**
|
|
6
7
|
* Poll delivery through MCP (spec/bindings/mcp.md#poll): `events/poll` reads the occurrences of a poll lease. A lease
|
|
7
8
|
* holds one subscription, in a session of its own, and a buffer of its occurrences. The HTTP and the stdio transports
|
|
@@ -55,12 +56,14 @@ var PollLeases = class {
|
|
|
55
56
|
const position = read.cursor === void 0 ? void 0 : decodePollCursor(read.cursor);
|
|
56
57
|
if (lease === void 0) {
|
|
57
58
|
const provider = read.cursor === void 0 ? void 0 : position?.provider;
|
|
58
|
-
const
|
|
59
|
+
const maxAgeMs = provider === void 0 && isMaxAgeMs(read.maxAgeMs) ? void 0 : read.maxAgeMs;
|
|
60
|
+
const started = this.#start(key, read.name, read.input, provider, maxAgeMs, open);
|
|
59
61
|
this.#starting.set(key, started);
|
|
60
62
|
const answer = await started.finally(() => this.#starting.delete(key));
|
|
61
63
|
if ("error" in answer) return answer;
|
|
62
64
|
return this.#result([], this.#cursorOf(answer, 0), read.cursor !== void 0 && provider === void 0, false);
|
|
63
65
|
}
|
|
66
|
+
if (!isMaxAgeMs(read.maxAgeMs)) return invalidParams("maxAgeMs must be an integer that is 0 or more.");
|
|
64
67
|
this.#renew(lease);
|
|
65
68
|
if (read.cursor === void 0) return this.#result([], this.#cursorOf(lease, lease.newest), false, false);
|
|
66
69
|
if (position?.lease === lease.id) return this.#read(lease, position.position, maxEvents, read.cursor);
|
|
@@ -175,5 +178,4 @@ function withoutTruncated(occurrence) {
|
|
|
175
178
|
const { truncated: _, ...rest } = occurrence;
|
|
176
179
|
return { ...rest };
|
|
177
180
|
}
|
|
178
|
-
//#endregion
|
|
179
181
|
export { PollLeases };
|
package/dist/mcp-stdio.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { hasObjectOutput, listTools } from "./mcp.js";
|
|
2
2
|
import { eventCapabilities, listEvents, servesEvents, startEventStream, unsupportedWebhook } from "./mcp-events.js";
|
|
3
3
|
import { PollLeases } from "./mcp-poll.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createSession } from "./session.js";
|
|
5
5
|
import { KEEPALIVE_MS, MCP_PROTOCOL_VERSION, MCP_UNKNOWN_TOOL, isJsonObject, mcpToolResult } from "@acid-cat/spec-ts";
|
|
6
|
-
|
|
6
|
+
// src/mcp-stdio.ts
|
|
7
7
|
/**
|
|
8
8
|
* The MCP binding through standard input and standard output (spec/bindings/mcp.md#stdio). The process is one session.
|
|
9
9
|
* The server serves revision 2026-07-28, and the `initialize` handshake of revisions 2025-11-25 and 2025-06-18.
|
|
@@ -321,5 +321,4 @@ var StdioServer = class {
|
|
|
321
321
|
this.#write(`${JSON.stringify(message)}\n`);
|
|
322
322
|
}
|
|
323
323
|
};
|
|
324
|
-
//#endregion
|
|
325
324
|
export { main, runMcpStdio };
|
package/dist/mcp.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { createSession } from "./session.js";
|
|
2
1
|
import { openContext } from "./context.js";
|
|
3
|
-
import { textStream } from "./stream.js";
|
|
4
2
|
import { eventCapabilities, forbidden, listEvents, servesEvents, startEventStream, unsupportedWebhook } from "./mcp-events.js";
|
|
5
3
|
import { PollLeases } from "./mcp-poll.js";
|
|
4
|
+
import { createSession } from "./session.js";
|
|
5
|
+
import { textStream } from "./stream.js";
|
|
6
6
|
import { KEEPALIVE_MS, MCP_PROTOCOL_VERSION, MCP_REJECTIONS, MCP_UNKNOWN_TOOL, SSE_MEDIA_TYPE, bundle, isJsonObject, mcpToolResult, readBounded, screenRequest, sseMessage } from "@acid-cat/spec-ts";
|
|
7
|
-
|
|
7
|
+
// src/mcp.ts
|
|
8
8
|
/**
|
|
9
9
|
* The MCP binding (spec/bindings/mcp.md): an MCP server through HTTP, revision 2026-07-28. The server keeps no state
|
|
10
10
|
* between requests, except poll leases and webhook subscriptions.
|
|
@@ -253,5 +253,4 @@ const messages = {
|
|
|
253
253
|
"too-large": "The request body is too large.",
|
|
254
254
|
"not-utf8": "The request body is not UTF-8 JSON."
|
|
255
255
|
};
|
|
256
|
-
//#endregion
|
|
257
256
|
export { hasObjectOutput, listTools, mcpHandler };
|
package/dist/node.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { webhookDelivery } from "./webhook.js";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
|
-
|
|
3
|
+
// src/node.ts
|
|
4
4
|
/**
|
|
5
5
|
* The functions that need Node.js. The main entry point, `@acid-cat/core-ts`, works in all runtimes.
|
|
6
6
|
*
|
|
@@ -172,5 +172,4 @@ function bodyOf(incoming) {
|
|
|
172
172
|
}
|
|
173
173
|
}, { highWaterMark: 0 });
|
|
174
174
|
}
|
|
175
|
-
//#endregion
|
|
176
175
|
export { serve, webhookDelivery };
|
package/dist/session.d.ts
CHANGED
|
@@ -123,5 +123,10 @@ export interface SessionOptions<Context> {
|
|
|
123
123
|
*/
|
|
124
124
|
readonly bufferLimit?: number | undefined;
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* True if `maxAgeMs` is absent, or an integer that is 0 or more (spec/events.md#resuming). A session rejects another
|
|
128
|
+
* value as `invalid-input`, after it checks the id of the event.
|
|
129
|
+
*/
|
|
130
|
+
export declare const isMaxAgeMs: (maxAgeMs: number | undefined) => boolean;
|
|
126
131
|
/** Open a session for a target. */
|
|
127
132
|
export declare function createSession<Context>(app: App<Context>, options: SessionOptions<Context>): Session<Context>;
|
package/dist/session.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { catalogOf } from "./app.js";
|
|
2
2
|
import { runSubscription } from "./subscription.js";
|
|
3
3
|
import { AVAILABLE, CallFailureError, callFailure, isCallFailure, viewOf } from "@acid-cat/spec-ts";
|
|
4
|
-
|
|
4
|
+
// src/session.ts
|
|
5
5
|
/**
|
|
6
6
|
* Sessions and handles (spec/invocation.md, spec/bindings/typescript.md).
|
|
7
7
|
*/
|
|
@@ -36,6 +36,11 @@ function outsideView(capability) {
|
|
|
36
36
|
invokeExternal: reject
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* True if `maxAgeMs` is absent, or an integer that is 0 or more (spec/events.md#resuming). A session rejects another
|
|
41
|
+
* value as `invalid-input`, after it checks the id of the event.
|
|
42
|
+
*/
|
|
43
|
+
const isMaxAgeMs = (maxAgeMs) => maxAgeMs === void 0 || Number.isSafeInteger(maxAgeMs) && maxAgeMs >= 0;
|
|
39
44
|
const always = {
|
|
40
45
|
getSnapshot: () => AVAILABLE,
|
|
41
46
|
subscribe: () => ({ unsubscribe() {} })
|
|
@@ -65,7 +70,13 @@ function createSession(app, options) {
|
|
|
65
70
|
if (lifetime.signal.aborted || signal?.aborted) return callFailure("cancelled", "The call was cancelled.");
|
|
66
71
|
const availability = entry.availability.getSnapshot();
|
|
67
72
|
if (!availability.available) return callFailure("unavailable", availability.reason);
|
|
68
|
-
|
|
73
|
+
let prepared;
|
|
74
|
+
try {
|
|
75
|
+
prepared = await prepare();
|
|
76
|
+
} catch (cause) {
|
|
77
|
+
onDefect(id, cause);
|
|
78
|
+
return callFailure("internal", "The capability failed.");
|
|
79
|
+
}
|
|
69
80
|
if ("issues" in prepared) return callFailure("invalid-input", issueMessage(prepared.issues));
|
|
70
81
|
const controller = new AbortController();
|
|
71
82
|
const abort = () => controller.abort();
|
|
@@ -172,6 +183,7 @@ function createSession(app, options) {
|
|
|
172
183
|
const { signal } = subscribeOptions;
|
|
173
184
|
const cancelled = () => lifetime.signal.aborted || signal?.aborted === true;
|
|
174
185
|
if (cancelled()) return callFailure("cancelled", "The subscription was cancelled.");
|
|
186
|
+
if (!isMaxAgeMs(subscribeOptions.maxAgeMs)) return callFailure("invalid-input", "maxAgeMs must be an integer that is 0 or more.");
|
|
175
187
|
let prepared;
|
|
176
188
|
try {
|
|
177
189
|
prepared = await prepare();
|
|
@@ -379,5 +391,4 @@ async function checkFailure(failure, tag, schemas) {
|
|
|
379
391
|
...checked.value
|
|
380
392
|
};
|
|
381
393
|
}
|
|
382
|
-
|
|
383
|
-
export { createSession };
|
|
394
|
+
export { createSession, isMaxAgeMs };
|
package/dist/stream.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/stream.ts
|
|
2
2
|
/**
|
|
3
3
|
* A function that pulls the next step of a subscription. It gives `idle` when no step comes within `idleMs`
|
|
4
4
|
* milliseconds. The `next()` that waits is kept for the next pull, so no occurrence is lost.
|
|
@@ -59,5 +59,4 @@ function textStream(produce, cancel, release) {
|
|
|
59
59
|
}
|
|
60
60
|
}, { highWaterMark: 0 });
|
|
61
61
|
}
|
|
62
|
-
//#endregion
|
|
63
62
|
export { idlePuller, textStream };
|
package/dist/subscription.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CallFailureError, callFailure, occurrenceOf } from "@acid-cat/spec-ts";
|
|
2
|
-
|
|
2
|
+
// src/subscription.ts
|
|
3
3
|
/**
|
|
4
4
|
* One subscription (spec/events.md): the pump that reads the occurrences of the provider, the bounded buffer for a slow
|
|
5
5
|
* client, and the iterator that the client reads.
|
|
@@ -141,5 +141,4 @@ function checkEmitted(value) {
|
|
|
141
141
|
if (truncated !== void 0 && truncated !== true) throw new TypeError("The truncated member of an occurrence is not true.");
|
|
142
142
|
return value;
|
|
143
143
|
}
|
|
144
|
-
//#endregion
|
|
145
144
|
export { DEFAULT_BUFFER_LIMIT, runSubscription };
|
package/dist/webhook.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { endMessage, invalidParams, subscriptionParams } from "./mcp-events.js";
|
|
2
1
|
import { sleep, systemClock } from "./clock.js";
|
|
2
|
+
import { endMessage, invalidParams, subscriptionParams } from "./mcp-events.js";
|
|
3
|
+
import { isMaxAgeMs } from "./session.js";
|
|
3
4
|
import { MCP_EVENT_ERRORS, MCP_WEBHOOK_DEFAULTS, canonicalJson, isGloballyRoutable, isJsonObject, mcpSubscribeError, webhookGrantMs, webhookSecretBytes, webhookSignature, webhookSubscriptionId } from "@acid-cat/spec-ts";
|
|
4
5
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
5
6
|
import { lookup } from "node:dns";
|
|
6
7
|
import { request } from "node:https";
|
|
7
8
|
import { isIP } from "node:net";
|
|
8
9
|
import { rootCertificates } from "node:tls";
|
|
9
|
-
|
|
10
|
+
// src/webhook.ts
|
|
10
11
|
/**
|
|
11
12
|
* Webhook delivery through MCP (spec/bindings/mcp.md#webhook), on Node.js. The server sends the occurrences of each
|
|
12
13
|
* webhook subscription in HTTP `POST` requests to a URL of the client, signed as Standard Webhooks specifies.
|
|
@@ -139,6 +140,7 @@ function webhookDelivery(options = {}) {
|
|
|
139
140
|
secrets: [secret]
|
|
140
141
|
}, webhookIdOf("verification"), body), body, network);
|
|
141
142
|
if ("failure" in posted) return posted.failure;
|
|
143
|
+
if (posted.status >= 300 && posted.status < 400) return "challenge_failed";
|
|
142
144
|
if (posted.status < 200 || posted.status >= 300) return statusError(posted.status);
|
|
143
145
|
if (!sameText(challengeOf(posted.text), challenge)) return "challenge_failed";
|
|
144
146
|
verified.add(pair);
|
|
@@ -164,7 +166,7 @@ function webhookDelivery(options = {}) {
|
|
|
164
166
|
max: settings.maxSubscriptions
|
|
165
167
|
}
|
|
166
168
|
} };
|
|
167
|
-
const id = await webhookSubscriptionId(principal,
|
|
169
|
+
const id = await webhookSubscriptionId(principal, request.rawUrl, request.name, request.input);
|
|
168
170
|
const own = await open();
|
|
169
171
|
const subscribed = await own.session.subscribe(request.name, request.input, {
|
|
170
172
|
...request.cursor === void 0 ? {} : { cursor: request.cursor },
|
|
@@ -207,15 +209,11 @@ function webhookDelivery(options = {}) {
|
|
|
207
209
|
const read = readSubscribe(params);
|
|
208
210
|
if ("error" in read) return read;
|
|
209
211
|
const { url, secret } = read;
|
|
210
|
-
const key =
|
|
211
|
-
principal,
|
|
212
|
-
url.href,
|
|
213
|
-
read.name,
|
|
214
|
-
read.input
|
|
215
|
-
]);
|
|
212
|
+
const key = subscriptionKey(principal, read.rawUrl, read.name, read.input);
|
|
216
213
|
await starting.get(key);
|
|
217
214
|
const existing = subscriptions.get(key);
|
|
218
215
|
if (existing !== void 0) {
|
|
216
|
+
if (!isMaxAgeMs(read.maxAgeMs)) return invalidParams("maxAgeMs must be an integer that is 0 or more.");
|
|
219
217
|
if (existing.secret !== secret) {
|
|
220
218
|
existing.previous = {
|
|
221
219
|
secret: existing.secret,
|
|
@@ -234,13 +232,7 @@ function webhookDelivery(options = {}) {
|
|
|
234
232
|
const { name, arguments: input = {}, delivery } = params;
|
|
235
233
|
const url = isJsonObject(delivery) ? delivery["url"] : void 0;
|
|
236
234
|
if (typeof name !== "string" || typeof url !== "string") return Promise.resolve(invalidParams("events/unsubscribe needs name and delivery.url."));
|
|
237
|
-
const
|
|
238
|
-
const subscription = subscriptions.get(canonicalJson([
|
|
239
|
-
principal,
|
|
240
|
-
parsed,
|
|
241
|
-
name,
|
|
242
|
-
input
|
|
243
|
-
]));
|
|
235
|
+
const subscription = subscriptions.get(subscriptionKey(principal, url, name, input));
|
|
244
236
|
if (subscription === void 0) return Promise.resolve({ error: {
|
|
245
237
|
code: MCP_EVENT_ERRORS.notFound,
|
|
246
238
|
message: "There is no such subscription.",
|
|
@@ -256,6 +248,13 @@ function webhookDelivery(options = {}) {
|
|
|
256
248
|
}
|
|
257
249
|
};
|
|
258
250
|
}
|
|
251
|
+
/** The key of a webhook subscription: the principal, `delivery.url` as the client sent it, the id, and the input. */
|
|
252
|
+
const subscriptionKey = (principal, rawUrl, name, input) => canonicalJson([
|
|
253
|
+
principal,
|
|
254
|
+
rawUrl,
|
|
255
|
+
name,
|
|
256
|
+
input
|
|
257
|
+
]);
|
|
259
258
|
/** Read and check the parameters of `events/subscribe`. */
|
|
260
259
|
function readSubscribe(params) {
|
|
261
260
|
const read = subscriptionParams(params);
|
|
@@ -270,6 +269,7 @@ function readSubscribe(params) {
|
|
|
270
269
|
...read,
|
|
271
270
|
ttlMs,
|
|
272
271
|
url: new URL(url),
|
|
272
|
+
rawUrl: url,
|
|
273
273
|
secret
|
|
274
274
|
};
|
|
275
275
|
}
|
|
@@ -381,5 +381,4 @@ function failureOf(error) {
|
|
|
381
381
|
if (/^(ERR_TLS|ERR_SSL|CERT_|UNABLE_TO_|DEPTH_ZERO|SELF_SIGNED|HOSTNAME_MISMATCH)/.test(code) || /certificate|SSL|TLS/i.test(error.message)) return "tls_error";
|
|
382
382
|
return "connection_refused";
|
|
383
383
|
}
|
|
384
|
-
//#endregion
|
|
385
384
|
export { webhookDelivery };
|
package/dist/webmcp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CallFailureError, bundle, isCallFailure } from "@acid-cat/spec-ts";
|
|
2
|
-
|
|
2
|
+
// src/webmcp.ts
|
|
3
3
|
/**
|
|
4
4
|
* The WebMCP binding (spec/bindings/webmcp.md): register capabilities as WebMCP tools in a web page.
|
|
5
5
|
*/
|
|
@@ -136,5 +136,4 @@ function installWebMcp(session, options = {}) {
|
|
|
136
136
|
}
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
|
-
//#endregion
|
|
140
139
|
export { findHost, installWebMcp };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acid-cat/core-ts",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Serve AcidCat capabilities and events in plain TypeScript, with no Effect dependency.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"acidcat",
|
|
@@ -36,16 +36,16 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@acid-cat/catalog": "0.10.
|
|
40
|
-
"@acid-cat/mcp-handshake": "0.10.
|
|
41
|
-
"@acid-cat/spec-ts": "0.10.
|
|
39
|
+
"@acid-cat/catalog": "0.10.2",
|
|
40
|
+
"@acid-cat/mcp-handshake": "0.10.2",
|
|
41
|
+
"@acid-cat/spec-ts": "0.10.2"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@acid-cat/cli": "0.10.
|
|
45
|
-
"@types/node": "26.6.
|
|
44
|
+
"@acid-cat/cli": "0.10.2",
|
|
45
|
+
"@types/node": "26.6.6",
|
|
46
46
|
"esbuild": "0.28.2",
|
|
47
|
-
"graphql": "17.0
|
|
48
|
-
"playwright": "1.
|
|
47
|
+
"graphql": "17.1.0",
|
|
48
|
+
"playwright": "1.65.0",
|
|
49
49
|
"typescript": "7.0.2",
|
|
50
50
|
"zod": "4.6.7"
|
|
51
51
|
}
|