@acid-cat/cli 0.8.0 → 0.10.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.
@@ -0,0 +1,289 @@
1
+ import { isArrayOf, isObject, memberOf } from "../json.js";
2
+ import { expect, expectEqual, expectOccurrence, until } from "./check.js";
3
+ import { eventRequests } from "./mcp.js";
4
+ import { randomUUID } from "node:crypto";
5
+ //#region src/conform/poll.ts
6
+ /**
7
+ * Conformance checks for poll delivery of the MCP events extension (spec/bindings/mcp.md#poll), through a channel. Each
8
+ * transport runs them. The checks read the state of a lease only through `events/poll` and `fixture.watching`, and wait
9
+ * for states that they can observe, not for times.
10
+ *
11
+ * A lease continues after the check that started it, until the lease time of the deployment ends. So each check uses a
12
+ * new key in its input: the leases of one check, or of an earlier run of conform, never have the key of another check.
13
+ */
14
+ const RULE = "spec/bindings/mcp.md#poll";
15
+ /** The largest count of `fixture.ticks`. */
16
+ const MAXIMUM_TICKS = 100;
17
+ /** The most occurrences that a check asks for in one poll. */
18
+ const MAXIMUM_EVENTS = 100;
19
+ async function checkPoll(checks, channel, settings) {
20
+ const { errorOf, watching, signalKey } = eventRequests(channel);
21
+ /** The form of a result of `events/poll` (spec/bindings/mcp.md#poll). */
22
+ const polledOf = (message, name) => {
23
+ const result = memberOf(message, "result");
24
+ expect(isObject(result), `events/poll must give a result, got ${JSON.stringify(message)}`);
25
+ expectEqual(result.resultType, "complete", "resultType");
26
+ const { events, cursor, truncated, hasMore, nextPollMs } = result;
27
+ if (!isArrayOf(events, isObject)) throw new Error(`events must be a list of occurrences, got ${JSON.stringify(events)}`);
28
+ for (const each of events) expect(!("truncated" in each), `an item of events must have no truncated member, got ${JSON.stringify(each)}`);
29
+ expect(typeof cursor === "string", `cursor must be a string, got ${JSON.stringify(cursor)}`);
30
+ expect(typeof truncated === "boolean", `truncated must be a boolean, got ${JSON.stringify(truncated)}`);
31
+ expect(typeof hasMore === "boolean", `hasMore must be a boolean, got ${JSON.stringify(hasMore)}`);
32
+ expect(typeof nextPollMs === "number" && Number.isSafeInteger(nextPollMs) && nextPollMs >= 0, `nextPollMs must be an integer of 0 or more, got ${JSON.stringify(nextPollMs)}`);
33
+ return {
34
+ events: events.map((each) => expectOccurrence(each, name)),
35
+ cursor,
36
+ truncated,
37
+ hasMore
38
+ };
39
+ };
40
+ /** Poll once. The result must have the form of a result. */
41
+ const poll = async (name, input, extra, signal) => polledOf(await channel.request("events/poll", {
42
+ name,
43
+ arguments: input,
44
+ ...extra
45
+ }, signal), name);
46
+ /** Poll with the same params until `done` is true for the result. Each poll renews the lease. */
47
+ const pollUntil = (what, name, input, extra, done, signal) => until(what, () => poll(name, input, extra, signal), done, signal);
48
+ /** The JSON-RPC error of a poll. */
49
+ const pollError = async (name, input, extra, signal) => errorOf(await channel.request("events/poll", {
50
+ name,
51
+ arguments: input,
52
+ ...extra
53
+ }, signal));
54
+ const dataOf = (polled) => polled.events.map((each) => each.data);
55
+ const watched = (key, ...values) => values.map((value) => ({
56
+ key,
57
+ value
58
+ }));
59
+ const ticks = (from, to) => Array.from({ length: to - from + 1 }, (_, index) => ({ n: from + index }));
60
+ const hasKey = (key) => (keys) => keys.includes(key);
61
+ const lacksKey = (key) => (keys) => !keys.includes(key);
62
+ const expectStart = (polled, truncated) => {
63
+ expectEqual(polled.events, [], "the occurrences of the poll that starts the lease");
64
+ expectEqual(polled.truncated, truncated, "truncated");
65
+ expectEqual(polled.hasMore, false, "hasMore");
66
+ };
67
+ /** Start a lease of `fixture.watch` for the key, and wait until its subscription is active. The result is the cursor of the start. */
68
+ const startWatch = async (key, signal) => {
69
+ const started = await poll("fixture.watch", { key }, {}, signal);
70
+ expectStart(started, false);
71
+ await until("fixture.watching gives the key of the lease", () => watching(signal), hasKey(key), signal);
72
+ return started.cursor;
73
+ };
74
+ /** Give values to the subscriptions of the key, one at a time. Each must reach one subscription. */
75
+ const signalAll = async (key, values, signal) => {
76
+ for (const value of values) expectEqual(await signalKey(key, value, signal), 1, "the subscriptions that fixture.signal reached");
77
+ };
78
+ await checks.run("events/poll with no cursor and no lease starts a lease with no occurrences, whose subscription starts now", RULE, async (signal) => {
79
+ const key = randomUUID();
80
+ const cursor = await startWatch(key, signal);
81
+ await signalAll(key, ["first"], signal);
82
+ const polled = await pollUntil("the lease has the occurrence", "fixture.watch", { key }, { cursor }, (each) => each.events.length > 0, signal);
83
+ expectEqual(dataOf(polled), watched(key, "first"), "the data of the occurrences");
84
+ expectEqual([polled.truncated, polled.hasMore], [false, false], "truncated and hasMore");
85
+ });
86
+ await checks.run("a cursor of the lease gives the occurrences after it, in order, and no cursor gives the newest position", RULE, async (signal) => {
87
+ const key = randomUUID();
88
+ const start = await startWatch(key, signal);
89
+ await signalAll(key, ["a", "b"], signal);
90
+ const both = await pollUntil("the lease has both occurrences", "fixture.watch", { key }, { cursor: start }, (each) => each.events.length >= 2, signal);
91
+ expectEqual(dataOf(both), watched(key, "a", "b"), "the data of the occurrences after the start");
92
+ expectEqual(both.truncated, false, "truncated");
93
+ const newest = await poll("fixture.watch", { key }, {}, signal);
94
+ expectEqual([newest.events, newest.truncated], [[], false], "the occurrences and truncated of a poll with no cursor");
95
+ await signalAll(key, ["c"], signal);
96
+ const after = await pollUntil("the lease has the next occurrence", "fixture.watch", { key }, { cursor: newest.cursor }, (each) => each.events.length > 0, signal);
97
+ expectEqual(dataOf(after), watched(key, "c"), "the data of the occurrences after the newest position");
98
+ const next = await poll("fixture.watch", { key }, { cursor: both.cursor }, signal);
99
+ expectEqual(dataOf(next), watched(key, "c"), "the data of the occurrences after the cursor of the last result");
100
+ });
101
+ await checks.run("maxEvents limits the occurrences of a result, and hasMore tells that the buffer has more", RULE, async (signal) => {
102
+ const key = randomUUID();
103
+ const start = await startWatch(key, signal);
104
+ await signalAll(key, [
105
+ "1",
106
+ "2",
107
+ "3"
108
+ ], signal);
109
+ const first = await pollUntil("the lease has three occurrences", "fixture.watch", { key }, {
110
+ cursor: start,
111
+ maxEvents: 2
112
+ }, (each) => each.hasMore, signal);
113
+ expectEqual(dataOf(first), watched(key, "1", "2"), "the data of the first result");
114
+ const second = await poll("fixture.watch", { key }, {
115
+ cursor: first.cursor,
116
+ maxEvents: 2
117
+ }, signal);
118
+ expectEqual(dataOf(second), watched(key, "3"), "the data of the second result");
119
+ expectEqual(second.hasMore, false, "hasMore of the second result");
120
+ });
121
+ await checks.run("a maxEvents that is not an integer of 1 or more is the error -32602", RULE, async (signal) => {
122
+ for (const maxEvents of [
123
+ 0,
124
+ -1,
125
+ 1.5,
126
+ "2"
127
+ ]) {
128
+ const error = await pollError("fixture.watch", { key: randomUUID() }, { maxEvents }, signal);
129
+ expectEqual(error.code, -32602, `the JSON-RPC error code for maxEvents ${JSON.stringify(maxEvents)}`);
130
+ }
131
+ });
132
+ await checks.run("a cursor that is not of the lease, while the lease exists, gives no occurrences, the newest position and truncated: true", RULE, async (signal) => {
133
+ const key = randomUUID();
134
+ const start = await startWatch(key, signal);
135
+ const other = await poll("fixture.watch", { key: randomUUID() }, {}, signal);
136
+ await signalAll(key, ["before"], signal);
137
+ await pollUntil("the lease has the occurrence", "fixture.watch", { key }, { cursor: start }, (each) => each.events.length > 0, signal);
138
+ const foreign = await poll("fixture.watch", { key }, { cursor: other.cursor }, signal);
139
+ expectEqual([foreign.events, foreign.truncated], [[], true], "the occurrences and truncated");
140
+ await signalAll(key, ["after"], signal);
141
+ const after = await pollUntil("the lease has the next occurrence", "fixture.watch", { key }, { cursor: foreign.cursor }, (each) => each.events.length > 0, signal);
142
+ expectEqual(dataOf(after), watched(key, "after"), "the data of the occurrences after the newest position");
143
+ });
144
+ await checks.run("a cursor that is not of a lease, with no lease, starts a lease that starts now, with truncated: true", RULE, async (signal) => {
145
+ const other = await poll("fixture.watch", { key: randomUUID() }, {}, signal);
146
+ const key = randomUUID();
147
+ const started = await poll("fixture.watch", { key }, { cursor: other.cursor }, signal);
148
+ expectStart(started, true);
149
+ await until("fixture.watching gives the key of the new lease", () => watching(signal), hasKey(key), signal);
150
+ await signalAll(key, ["now"], signal);
151
+ const polled = await pollUntil("the lease has the occurrence", "fixture.watch", { key }, { cursor: started.cursor }, (each) => each.events.length > 0, signal);
152
+ expectEqual(dataOf(polled), watched(key, "now"), "the data of the occurrences");
153
+ });
154
+ await checks.run("a cursor of push delivery has no cursor of the provider: the new lease starts now, with truncated: true", RULE, async (signal) => {
155
+ const input = {
156
+ count: 3,
157
+ key: randomUUID()
158
+ };
159
+ const started = await poll("fixture.ticks", input, { cursor: "1" }, signal);
160
+ expectStart(started, true);
161
+ const polled = await pollUntil("the lease has the last tick", "fixture.ticks", input, { cursor: started.cursor }, (each) => each.events.some((event) => memberOf(event.data, "n") === 3), signal);
162
+ expectEqual(dataOf(polled), ticks(1, 3), "the data of the occurrences, from n = 1");
163
+ });
164
+ await checks.run("a cursor of another lease with a cursor of the provider starts a lease that resumes after it", RULE, async (signal) => {
165
+ const first = {
166
+ count: 3,
167
+ key: randomUUID()
168
+ };
169
+ const start = await poll("fixture.ticks", first, {}, signal);
170
+ expectStart(start, false);
171
+ const one = await pollUntil("the first lease has the first tick", "fixture.ticks", first, {
172
+ cursor: start.cursor,
173
+ maxEvents: 1
174
+ }, (each) => each.events.length > 0, signal);
175
+ expectEqual(dataOf(one), ticks(1, 1), "the data of the first occurrence");
176
+ const second = {
177
+ count: 4,
178
+ key: randomUUID()
179
+ };
180
+ const resumed = await poll("fixture.ticks", second, { cursor: one.cursor }, signal);
181
+ expectStart(resumed, false);
182
+ const polled = await pollUntil("the new lease has the last tick", "fixture.ticks", second, { cursor: resumed.cursor }, (each) => each.events.some((event) => memberOf(event.data, "n") === 4), signal);
183
+ expectEqual(dataOf(polled), ticks(2, 4), "the data of the occurrences after the cursor of the provider");
184
+ expectEqual(polled.truncated, false, "truncated");
185
+ });
186
+ await checks.run("an occurrence after a gap gives a result with truncated: true, and its item has no truncated", RULE, async (signal) => {
187
+ const input = {
188
+ count: 3,
189
+ gap: 2,
190
+ key: randomUUID()
191
+ };
192
+ const start = await poll("fixture.ticks", input, {}, signal);
193
+ expectStart(start, false);
194
+ const before = await pollUntil("the lease has the first tick", "fixture.ticks", input, {
195
+ cursor: start.cursor,
196
+ maxEvents: 1
197
+ }, (each) => each.events.length > 0, signal);
198
+ expectEqual([dataOf(before), before.truncated], [ticks(1, 1), false], "the data and truncated of the result before the gap");
199
+ const after = await pollUntil("the lease has the last tick", "fixture.ticks", input, { cursor: before.cursor }, (each) => each.events.some((event) => memberOf(event.data, "n") === 3), signal);
200
+ expectEqual([dataOf(after), after.truncated], [ticks(2, 3), true], "the data and truncated of the result with the gap");
201
+ });
202
+ if (settings.bufferLimit !== void 0) {
203
+ const limit = settings.bufferLimit;
204
+ await checks.run("a cursor older than the buffer gives the occurrences in the buffer, and truncated: true", RULE, async (signal) => {
205
+ expect(Number.isSafeInteger(limit) && limit >= 1 && limit + 3 <= MAXIMUM_TICKS, `conform tests the buffer only when it keeps 1 to 97 occurrences, got ${limit}`);
206
+ const total = limit + 3;
207
+ const input = {
208
+ count: total,
209
+ key: randomUUID()
210
+ };
211
+ const start = await poll("fixture.ticks", input, {}, signal);
212
+ expectStart(start, false);
213
+ const polled = await pollUntil("the lease has the last tick", "fixture.ticks", input, {
214
+ cursor: start.cursor,
215
+ maxEvents: MAXIMUM_EVENTS
216
+ }, (each) => each.events.some((event) => memberOf(event.data, "n") === total), signal);
217
+ expectEqual(dataOf(polled), ticks(4, total), "the data of the occurrences in the buffer");
218
+ expectEqual([polled.truncated, polled.hasMore], [true, false], "truncated and hasMore");
219
+ });
220
+ }
221
+ await checks.run("a failure before the first occurrence is a JSON-RPC error, and starts no lease", RULE, async (signal) => {
222
+ for (const [name, input, code] of [
223
+ [
224
+ "fixture.secret",
225
+ {},
226
+ -32023
227
+ ],
228
+ [
229
+ "fixture.echo",
230
+ { text: "x" },
231
+ -32023
232
+ ],
233
+ [
234
+ "fixture.ticks",
235
+ { count: 101 },
236
+ -32602
237
+ ]
238
+ ]) for (const attempt of ["first", "second"]) {
239
+ const error = await pollError(name, input, {}, signal);
240
+ expectEqual(error.code, code, `the JSON-RPC error code of the ${attempt} poll of ${name}`);
241
+ if (code === -32023) expectEqual(memberOf(error.data, "kind"), "event", `data.kind of the ${attempt} poll of ${name}`);
242
+ }
243
+ });
244
+ await checks.run("after a defect, the next poll is the error -32603 and ends the lease, and a later poll starts a new lease", RULE, async (signal) => {
245
+ /** Start a lease of `fixture.broken`. A lease of an earlier run can still have the failure: its poll ends it first. */
246
+ const start = async () => {
247
+ const message = await channel.request("events/poll", {
248
+ name: "fixture.broken",
249
+ arguments: {}
250
+ }, signal);
251
+ const polled = memberOf(message, "error") === void 0 ? polledOf(message, "fixture.broken") : await poll("fixture.broken", {}, {}, signal);
252
+ expectStart(polled, false);
253
+ return polled.cursor;
254
+ };
255
+ /** Poll until the error. A result before the error can only have the occurrence n = 1. */
256
+ const failure = async (cursor) => {
257
+ const message = await until("a poll gets the error", async () => {
258
+ const each = await channel.request("events/poll", {
259
+ name: "fixture.broken",
260
+ arguments: {},
261
+ cursor
262
+ }, signal);
263
+ if (memberOf(each, "error") === void 0) expect(dataOf(polledOf(each, "fixture.broken")).every((data) => memberOf(data, "n") === 1), "a result before the error has only the occurrence n = 1");
264
+ return each;
265
+ }, (each) => memberOf(each, "error") !== void 0, signal);
266
+ const error = errorOf(message);
267
+ expectEqual(error.code, -32603, "the JSON-RPC error code");
268
+ expect(!/\n\s+at /.test(String(error.message)), "the message must not contain a stack trace");
269
+ };
270
+ await failure(await start());
271
+ await failure(await start());
272
+ });
273
+ if (settings.leaseMs !== void 0) {
274
+ const leaseMs = settings.leaseMs;
275
+ await checks.run("a lease that gets no poll for the lease time ends, and cancels its subscription", RULE, async (signal) => {
276
+ const key = randomUUID();
277
+ const before = Date.now();
278
+ const cursor = await startWatch(key, signal);
279
+ await until("the lease ends, and the key leaves fixture.watching", () => watching(signal), lacksKey(key), signal);
280
+ const elapsed = Date.now() - before;
281
+ expect(elapsed >= leaseMs, `the lease ended ${elapsed} ms after the last poll, before the lease time of ${leaseMs} ms`);
282
+ const again = await poll("fixture.watch", { key }, { cursor }, signal);
283
+ expectStart(again, true);
284
+ await until("fixture.watching gives the key of the new lease", () => watching(signal), hasKey(key), signal);
285
+ });
286
+ }
287
+ }
288
+ //#endregion
289
+ export { checkPoll };
@@ -1,4 +1,5 @@
1
1
  import type { Outcome } from "@acid-cat/spec-ts";
2
+ import type { Client } from "@modelcontextprotocol/client";
2
3
  import type { JsonObject } from "../json.js";
3
4
  import { Checks } from "./check.js";
4
5
  /** A call through one binding. When the signal aborts, the client stops the call and rejects with the reason of the signal. */
@@ -8,4 +9,8 @@ export declare function mcpInvoke(url: string, signal: AbortSignal): Promise<{
8
9
  readonly invoke: Invoke;
9
10
  close(): Promise<void>;
10
11
  }>;
12
+ /** Calls through a connected MCP client, with any transport. */
13
+ export declare function clientInvoke(client: Client): Invoke;
14
+ /** The keys of the calls that hold now, as fixture.held gives them through the binding. */
15
+ export declare function heldKeys(invoke: Invoke, signal: AbortSignal): Promise<readonly string[]>;
11
16
  export declare function checkProperties(binding: string, invoke: Invoke, deadline: number): Promise<Checks>;
@@ -1,8 +1,8 @@
1
1
  import { isStringArray, memberOf, parseJson, readJson } from "../json.js";
2
2
  import { Checks, expect, isOutcome, until } from "./check.js";
3
3
  import { mcpClient } from "./mcp.js";
4
- import { SdkError } from "@modelcontextprotocol/client";
5
4
  import { randomUUID } from "node:crypto";
5
+ import { SdkError } from "@modelcontextprotocol/client";
6
6
  import fc from "fast-check";
7
7
  //#region src/conform/properties.ts
8
8
  /**
@@ -26,32 +26,36 @@ function httpInvoke(base) {
26
26
  async function mcpInvoke(url, signal) {
27
27
  const client = await mcpClient(url, signal);
28
28
  return {
29
- invoke: async (id, input, signal) => {
30
- try {
31
- const result = await client.callTool({
32
- name: id,
33
- arguments: input
34
- }, signal ? { signal } : void 0);
35
- const first = result.content[0];
36
- const body = parseJson(first?.type === "text" ? first.text : "null");
37
- if (result.isError !== true) return {
38
- ok: true,
39
- value: result.structuredContent ?? body
40
- };
41
- if (!isOutcome(body)) throw new Error(`the error result of ${id} gave ${JSON.stringify(body)}, which is not an outcome`);
42
- return body;
43
- } catch (error) {
44
- if (signal?.aborted && error instanceof SdkError && error.message === String(signal.reason)) throw signal.reason;
45
- if (memberOf(error, "code") === -32602) return {
46
- kind: "unknown-capability",
47
- message: String(error)
48
- };
49
- throw error;
50
- }
51
- },
29
+ invoke: clientInvoke(client),
52
30
  close: () => client.close()
53
31
  };
54
32
  }
33
+ /** Calls through a connected MCP client, with any transport. */
34
+ function clientInvoke(client) {
35
+ return async (id, input, signal) => {
36
+ try {
37
+ const result = await client.callTool({
38
+ name: id,
39
+ arguments: input
40
+ }, signal ? { signal } : void 0);
41
+ const first = result.content[0];
42
+ const body = parseJson(first?.type === "text" ? first.text : "null");
43
+ if (result.isError !== true) return {
44
+ ok: true,
45
+ value: result.structuredContent ?? body
46
+ };
47
+ if (!isOutcome(body)) throw new Error(`the error result of ${id} gave ${JSON.stringify(body)}, which is not an outcome`);
48
+ return body;
49
+ } catch (error) {
50
+ if (signal?.aborted && error instanceof SdkError && error.message === String(signal.reason)) throw signal.reason;
51
+ if (memberOf(error, "code") === -32602) return {
52
+ kind: "unknown-capability",
53
+ message: String(error)
54
+ };
55
+ throw error;
56
+ }
57
+ };
58
+ }
55
59
  const operation = fc.oneof(fc.string({ maxLength: 40 }).map((text) => ({
56
60
  kind: "echo",
57
61
  text
@@ -167,4 +171,4 @@ async function checkProperties(binding, invoke, deadline) {
167
171
  return checks;
168
172
  }
169
173
  //#endregion
170
- export { checkProperties, httpInvoke, mcpInvoke };
174
+ export { checkProperties, clientInvoke, heldKeys, httpInvoke, mcpInvoke };
@@ -55,7 +55,10 @@ async function answer(incoming, outgoing, prefixes, routes) {
55
55
  outgoing.end();
56
56
  return;
57
57
  }
58
- for await (const chunk of response.body) outgoing.write(chunk);
58
+ outgoing.flushHeaders();
59
+ const reader = response.body.getReader();
60
+ abort.signal.addEventListener("abort", () => void reader.cancel().catch(() => void 0), { once: true });
61
+ for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) outgoing.write(chunk.value);
59
62
  outgoing.end();
60
63
  } catch {
61
64
  if (!outgoing.headersSent) outgoing.writeHead(500);
@@ -0,0 +1,15 @@
1
+ /** A reader of a stream of server-sent events, for the checks of the HTTP binding and of MCP through HTTP. */
2
+ /** One message of a stream of server-sent events. A comment line is a message with the event `comment`. */
3
+ export interface SseMessage {
4
+ readonly event?: string;
5
+ readonly id?: string;
6
+ readonly data: string;
7
+ }
8
+ /**
9
+ * Read the messages of a response one at a time. `next` skips comment lines, and resolves with `undefined` at the end of
10
+ * the stream. When the signal of the request aborts, `next` rejects.
11
+ */
12
+ export declare function sseReader(response: Response): {
13
+ next(): Promise<SseMessage | undefined>;
14
+ cancel: () => Promise<void | undefined>;
15
+ };
@@ -0,0 +1,57 @@
1
+ //#region src/conform/sse.ts
2
+ /**
3
+ * Read the messages of a response one at a time. `next` skips comment lines, and resolves with `undefined` at the end of
4
+ * the stream. When the signal of the request aborts, `next` rejects.
5
+ */
6
+ function sseReader(response) {
7
+ if (response.body === null) throw new Error("the response has no body");
8
+ const reader = response.body.getReader();
9
+ const decoder = new TextDecoder();
10
+ let text = "";
11
+ const read = async () => {
12
+ for (;;) {
13
+ const end = /\r?\n\r?\n/.exec(text);
14
+ if (end !== null) {
15
+ const raw = text.slice(0, end.index);
16
+ text = text.slice(end.index + end[0].length);
17
+ return parse(raw);
18
+ }
19
+ const chunk = await reader.read();
20
+ if (chunk.done) return text.trim() === "" ? void 0 : parse(text);
21
+ text += decoder.decode(chunk.value, { stream: true });
22
+ }
23
+ };
24
+ return {
25
+ async next() {
26
+ for (;;) {
27
+ const message = await read();
28
+ if (message?.event !== "comment") return message;
29
+ }
30
+ },
31
+ cancel: () => reader.cancel().catch(() => void 0)
32
+ };
33
+ }
34
+ function parse(raw) {
35
+ const fields = { data: [] };
36
+ const lines = raw.split(/\r?\n/);
37
+ if (lines.every((line) => line.startsWith(":"))) return {
38
+ event: "comment",
39
+ data: ""
40
+ };
41
+ for (const line of lines) {
42
+ if (line.startsWith(":")) continue;
43
+ const colon = line.indexOf(":");
44
+ const name = colon === -1 ? line : line.slice(0, colon);
45
+ const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
46
+ if (name === "event") fields.event = value;
47
+ else if (name === "id") fields.id = value;
48
+ else if (name === "data") fields.data.push(value);
49
+ }
50
+ return {
51
+ ...fields.event === void 0 ? {} : { event: fields.event },
52
+ ...fields.id === void 0 ? {} : { id: fields.id },
53
+ data: fields.data.join("\n")
54
+ };
55
+ }
56
+ //#endregion
57
+ export { sseReader };
@@ -0,0 +1,28 @@
1
+ import { Checks } from "./check.js";
2
+ /** The settings of the checks of webhook delivery. */
3
+ export interface WebhookSettings {
4
+ /** The principal that the deployment gives to a request with the header `Authorization: Bearer <principal>`. */
5
+ readonly principal: string;
6
+ /** The certificate of the receiver, in PEM. It must be valid for the IP address 127.0.0.1. */
7
+ readonly cert: string;
8
+ /** The private key of the certificate, in PEM. */
9
+ readonly key: string;
10
+ /**
11
+ * A second principal, given to a request with the header `Authorization: Bearer <otherPrincipal>`. With it, the check
12
+ * of the limit fills the subscriptions of this principal, which no other check uses, and checks that the first
13
+ * principal can still subscribe.
14
+ */
15
+ readonly otherPrincipal?: string | undefined;
16
+ /** The maximum lifetime of a subscription in the deployment, in milliseconds. The default is the default of the specification, 3 600 000. */
17
+ readonly maxTtlMs?: number | undefined;
18
+ /**
19
+ * The minimum lifetime of a subscription in the deployment, in milliseconds. The default is the default of the
20
+ * specification, 60 000. With it, conform also tests that a subscription with no renewal ends: it must be shorter
21
+ * than the deadline.
22
+ */
23
+ readonly minTtlMs?: number | undefined;
24
+ /** The maximum number of subscriptions of one principal in the deployment. With it, conform tests the limit. */
25
+ readonly maxSubscriptions?: number | undefined;
26
+ }
27
+ /** The checks of webhook delivery through the MCP server at `url`. */
28
+ export declare function checkWebhook(checks: Checks, url: string, settings: WebhookSettings): Promise<void>;