@acid-cat/spec-ts 0.9.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.
- package/dist/index.d.ts +147 -0
- package/dist/index.js +353 -4
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* implementation. This package has no dependencies.
|
|
4
4
|
*
|
|
5
5
|
* - Outcomes and call failures: spec/invocation.md
|
|
6
|
+
* - Subscriptions and occurrences: spec/events.md
|
|
6
7
|
* - Names, views and bundling: spec/catalog.md
|
|
7
8
|
* - Exit codes, status codes and MCP results: spec/bindings/
|
|
8
9
|
*/
|
|
@@ -36,6 +37,39 @@ export declare class CallFailureError extends Error {
|
|
|
36
37
|
readonly kind: CallFailureKind;
|
|
37
38
|
constructor(failure: CallFailure);
|
|
38
39
|
}
|
|
40
|
+
/** The kinds of call failure that a subscription can fail with before its first occurrence, in order of precedence. */
|
|
41
|
+
export declare const SUBSCRIBE_FAILURE_KINDS: readonly ["cancelled", "unknown-capability", "invalid-input", "internal"];
|
|
42
|
+
/** One thing that happened, in its JSON form (spec/events.md#occurrences). */
|
|
43
|
+
export interface Occurrence<Payload = unknown> {
|
|
44
|
+
readonly eventId: string;
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly timestamp: string;
|
|
47
|
+
readonly data: Payload;
|
|
48
|
+
readonly cursor?: string;
|
|
49
|
+
readonly truncated?: true;
|
|
50
|
+
}
|
|
51
|
+
/** What a provider gives for one occurrence. The binding adds `name`, and makes `eventId` and `timestamp` if they are absent. */
|
|
52
|
+
export interface Emitted<Payload = unknown> {
|
|
53
|
+
readonly data: Payload;
|
|
54
|
+
readonly eventId?: string;
|
|
55
|
+
readonly timestamp?: string;
|
|
56
|
+
readonly cursor?: string;
|
|
57
|
+
readonly truncated?: true;
|
|
58
|
+
}
|
|
59
|
+
/** Where a subscription starts (spec/events.md#resuming). */
|
|
60
|
+
export interface Resume {
|
|
61
|
+
readonly cursor?: string;
|
|
62
|
+
readonly maxAgeMs?: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The occurrence for an emitted value. `makeId` gives an `eventId` that is unique in the subscription, and `now` gives
|
|
66
|
+
* the time, when the provider gives none.
|
|
67
|
+
*/
|
|
68
|
+
export declare function occurrenceOf<Payload>(name: string, emitted: Emitted<Payload>, makeId: () => string, now: () => Date): Occurrence<Payload>;
|
|
69
|
+
/** Tell if a value is an occurrence in its JSON form. `data` can be any JSON object. */
|
|
70
|
+
export declare function isOccurrence(value: unknown): value is Occurrence;
|
|
71
|
+
/** The time with no message on a stream after which a binding sends a keepalive or a heartbeat, in milliseconds. */
|
|
72
|
+
export declare const KEEPALIVE_MS = 15000;
|
|
39
73
|
export type Availability = {
|
|
40
74
|
readonly available: true;
|
|
41
75
|
} | {
|
|
@@ -82,6 +116,19 @@ export declare function cliJsonArray(values: readonly string[]): readonly unknow
|
|
|
82
116
|
* `undefined` when the capability has no input.
|
|
83
117
|
*/
|
|
84
118
|
export declare function cliUsage(program: string, id: string, input: JsonObject | undefined): string;
|
|
119
|
+
/**
|
|
120
|
+
* The `usage` of an event in `list` (spec/bindings/cli.md#events). `input` is the bundled input schema, or `undefined`
|
|
121
|
+
* when the event has no input.
|
|
122
|
+
*/
|
|
123
|
+
export declare function cliEventUsage(program: string, id: string, input: JsonObject | undefined): string;
|
|
124
|
+
/** The flags of `watch` that give the cursor and `maxAgeMs`. No input property of an event can have these names. */
|
|
125
|
+
export declare const CLI_RESUME_FLAGS: readonly ["cursor", "max-age-ms"];
|
|
126
|
+
/** The exit code for each end of a subscription through the CLI (spec/bindings/cli.md#events). */
|
|
127
|
+
export declare const CLI_WATCH_EXIT_CODES: {
|
|
128
|
+
readonly complete: 0;
|
|
129
|
+
readonly cancelled: 130;
|
|
130
|
+
readonly internal: 5;
|
|
131
|
+
};
|
|
85
132
|
export declare const HTTP_STATUS: {
|
|
86
133
|
readonly "unknown-capability": 404;
|
|
87
134
|
readonly "invalid-input": 422;
|
|
@@ -90,6 +137,21 @@ export declare const HTTP_STATUS: {
|
|
|
90
137
|
readonly internal: 500;
|
|
91
138
|
};
|
|
92
139
|
export declare const httpStatus: (outcome: Outcome<unknown, unknown>) => number;
|
|
140
|
+
/** One message of a stream of server-sent events, as text, with the blank line that ends it. */
|
|
141
|
+
export declare function sseMessage(fields: {
|
|
142
|
+
readonly event?: string | undefined;
|
|
143
|
+
readonly id?: string | undefined;
|
|
144
|
+
readonly data: string;
|
|
145
|
+
}): string;
|
|
146
|
+
/** The messages of a subscription through the HTTP binding (spec/bindings/http.md#events). */
|
|
147
|
+
export declare const httpEventMessage: {
|
|
148
|
+
readonly next: (occurrence: Occurrence) => string;
|
|
149
|
+
readonly complete: () => string;
|
|
150
|
+
readonly error: (failure: CallFailure) => string;
|
|
151
|
+
readonly keepalive: () => string;
|
|
152
|
+
};
|
|
153
|
+
/** The media type of a stream of server-sent events. */
|
|
154
|
+
export declare const SSE_MEDIA_TYPE = "text/event-stream";
|
|
93
155
|
export type Rejection = "method" | "origin" | "media-type" | "too-large" | "not-utf8";
|
|
94
156
|
/**
|
|
95
157
|
* The first rule that a request breaks, before its body is read: method, origin, media type, declared length.
|
|
@@ -116,6 +178,80 @@ export declare const MCP_REJECTIONS: {
|
|
|
116
178
|
readonly code?: number;
|
|
117
179
|
};
|
|
118
180
|
};
|
|
181
|
+
/** The identifier of the MCP events extension (spec/bindings/mcp.md#events). */
|
|
182
|
+
export declare const MCP_EVENTS_EXTENSION = "io.modelcontextprotocol/events";
|
|
183
|
+
/** The key in `params._meta` that holds the id of the `events/stream` request. */
|
|
184
|
+
export declare const MCP_SUBSCRIPTION_ID = "io.modelcontextprotocol/subscriptionId";
|
|
185
|
+
/** The JSON-RPC errors of the MCP events extension. Their numbers are not final in SEP-3415. */
|
|
186
|
+
export declare const MCP_EVENT_ERRORS: {
|
|
187
|
+
readonly notFound: -32023;
|
|
188
|
+
readonly forbidden: -32024;
|
|
189
|
+
readonly resourceExhausted: -32025;
|
|
190
|
+
readonly unsupported: -32026;
|
|
191
|
+
readonly callbackEndpoint: -32027;
|
|
192
|
+
readonly invalidParams: -32602;
|
|
193
|
+
readonly internal: -32603;
|
|
194
|
+
};
|
|
195
|
+
/** The JSON-RPC error for a call failure of a subscription before its first occurrence. */
|
|
196
|
+
export declare function mcpSubscribeError(failure: CallFailure): {
|
|
197
|
+
readonly code: number;
|
|
198
|
+
readonly message: string;
|
|
199
|
+
readonly data?: JsonObject;
|
|
200
|
+
};
|
|
201
|
+
/** An entry of `events/list`. `input` and `payload` are the bundled schemas. */
|
|
202
|
+
export declare function mcpEventEntry(id: string, description: string, input: JsonObject | undefined, payload: JsonObject | undefined, webhook?: boolean): JsonObject;
|
|
203
|
+
/** The params of `notifications/events/event` for an occurrence: the occurrence without `truncated`. */
|
|
204
|
+
export declare function mcpEventParams(occurrence: Occurrence, subscriptionId: string | number): JsonObject;
|
|
205
|
+
/** The canonical JSON of a value (spec/bindings/mcp.md#canonical-json): no spaces, and the keys of each object in code point order. */
|
|
206
|
+
export declare function canonicalJson(value: unknown): string;
|
|
207
|
+
/** The defaults of poll delivery (spec/bindings/mcp.md#poll). */
|
|
208
|
+
export declare const MCP_POLL_DEFAULTS: {
|
|
209
|
+
readonly leaseMs: 300000;
|
|
210
|
+
readonly bufferLimit: 1000;
|
|
211
|
+
readonly maxEvents: 100;
|
|
212
|
+
readonly nextPollMs: 2000;
|
|
213
|
+
};
|
|
214
|
+
/** Where a poll cursor points: a lease, a position in its buffer, and the last cursor of the provider. */
|
|
215
|
+
export interface PollPosition {
|
|
216
|
+
readonly lease: string;
|
|
217
|
+
readonly position: number;
|
|
218
|
+
readonly provider?: string;
|
|
219
|
+
}
|
|
220
|
+
/** The cursor of a poll position. Only the server reads it. */
|
|
221
|
+
export declare function encodePollCursor(position: PollPosition): string;
|
|
222
|
+
/** The position of a poll cursor, or `undefined` when the text is not a poll cursor. */
|
|
223
|
+
export declare function decodePollCursor(cursor: string): PollPosition | undefined;
|
|
224
|
+
/** The defaults of webhook delivery (spec/bindings/mcp.md#webhook). */
|
|
225
|
+
export declare const MCP_WEBHOOK_DEFAULTS: {
|
|
226
|
+
readonly maxTtlMs: 3600000;
|
|
227
|
+
readonly minTtlMs: 60000;
|
|
228
|
+
readonly maxSubscriptions: 100;
|
|
229
|
+
readonly attempts: 5;
|
|
230
|
+
readonly retryWindowMs: 900000;
|
|
231
|
+
readonly timeoutMs: 5000;
|
|
232
|
+
readonly rotationGraceMs: 300000;
|
|
233
|
+
};
|
|
234
|
+
/** The reasons of a `CallbackEndpointError`, and the values of `lastError` (spec/bindings/mcp.md#webhook). */
|
|
235
|
+
export declare const WEBHOOK_ERRORS: readonly ["challenge_failed", "connection_refused", "timeout", "tls_error", "http_4xx", "http_5xx"];
|
|
236
|
+
export type WebhookError = (typeof WEBHOOK_ERRORS)[number];
|
|
237
|
+
/** The bytes of a Standard Webhooks secret: `whsec_` and the base64 of 24 to 64 bytes. `undefined` when it is not valid. */
|
|
238
|
+
export declare function webhookSecretBytes(secret: string): Uint8Array<ArrayBuffer> | undefined;
|
|
239
|
+
/** The lifetime that the server grants for a suggested `ttlMs` (spec/bindings/mcp.md#webhook). */
|
|
240
|
+
export declare function webhookGrantMs(ttlMs: number | null | undefined, limits: {
|
|
241
|
+
readonly maxTtlMs: number;
|
|
242
|
+
readonly minTtlMs: number;
|
|
243
|
+
}): number;
|
|
244
|
+
/** The text that a Standard Webhooks signature covers. */
|
|
245
|
+
export declare const webhookSignedText: (webhookId: string, timestamp: number, body: string) => string;
|
|
246
|
+
/** The `webhook-signature` header: one `v1,` signature for each secret, with a space between them. */
|
|
247
|
+
export declare function webhookSignature(secrets: readonly string[], webhookId: string, timestamp: number, body: string): Promise<string>;
|
|
248
|
+
/** The subscription id of a webhook subscription: `sub_` and 16 hexadecimal digits of the SHA-256 of its key. */
|
|
249
|
+
export declare function webhookSubscriptionId(principal: string, url: string, id: string, input: unknown): Promise<string>;
|
|
250
|
+
/**
|
|
251
|
+
* Tell if an IP address is globally routable: not in a special-purpose range of the IANA IPv4 and IPv6 registries that
|
|
252
|
+
* is not global, for example loopback, private, link-local, shared, documentation or multicast addresses.
|
|
253
|
+
*/
|
|
254
|
+
export declare function isGloballyRoutable(address: string): boolean;
|
|
119
255
|
export interface McpToolResult {
|
|
120
256
|
readonly content: readonly [{
|
|
121
257
|
readonly type: "text";
|
|
@@ -140,6 +276,12 @@ export interface CatalogCapability extends JsonObject {
|
|
|
140
276
|
readonly readOnly?: boolean;
|
|
141
277
|
readonly state?: JsonObject;
|
|
142
278
|
}
|
|
279
|
+
export interface CatalogEvent extends JsonObject {
|
|
280
|
+
readonly description: string;
|
|
281
|
+
readonly audience?: readonly string[];
|
|
282
|
+
readonly input?: JsonObject;
|
|
283
|
+
readonly payload?: JsonObject;
|
|
284
|
+
}
|
|
143
285
|
export interface CatalogDocument extends JsonObject {
|
|
144
286
|
readonly acidcat: string;
|
|
145
287
|
readonly title?: string;
|
|
@@ -155,12 +297,17 @@ export interface CatalogDocument extends JsonObject {
|
|
|
155
297
|
readonly capabilities: {
|
|
156
298
|
readonly [id: string]: CatalogCapability;
|
|
157
299
|
};
|
|
300
|
+
readonly events?: {
|
|
301
|
+
readonly [id: string]: CatalogEvent;
|
|
302
|
+
};
|
|
158
303
|
}
|
|
159
304
|
export declare const REFERENCE: RegExp;
|
|
160
305
|
/** Tell if a value is a JSON object: an object that is not `null` and not an array. */
|
|
161
306
|
export declare const isJsonObject: (value: unknown) => value is JsonObject;
|
|
162
307
|
/** The schemas of a capability, each with the member that holds it. */
|
|
163
308
|
export declare function schemasOf(capability: CatalogCapability): readonly (readonly [string, JsonObject])[];
|
|
309
|
+
/** The schemas of an event, each with the member that holds it. */
|
|
310
|
+
export declare function eventSchemasOf(event: CatalogEvent): readonly (readonly [string, JsonObject])[];
|
|
164
311
|
/** Every object in a JSON value, at all levels. */
|
|
165
312
|
export declare function visitObjects(value: unknown, onObject: (node: JsonObject) => void): void;
|
|
166
313
|
/** The types that a schema uses, directly or through other types. */
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* implementation. This package has no dependencies.
|
|
5
5
|
*
|
|
6
6
|
* - Outcomes and call failures: spec/invocation.md
|
|
7
|
+
* - Subscriptions and occurrences: spec/events.md
|
|
7
8
|
* - Names, views and bundling: spec/catalog.md
|
|
8
9
|
* - Exit codes, status codes and MCP results: spec/bindings/
|
|
9
10
|
*/
|
|
@@ -38,6 +39,41 @@ var CallFailureError = class extends Error {
|
|
|
38
39
|
this.name = failure.kind === "cancelled" ? "AbortError" : failure.kind === "invalid-input" ? "TypeError" : "Error";
|
|
39
40
|
}
|
|
40
41
|
};
|
|
42
|
+
/** The kinds of call failure that a subscription can fail with before its first occurrence, in order of precedence. */
|
|
43
|
+
const SUBSCRIBE_FAILURE_KINDS = [
|
|
44
|
+
"cancelled",
|
|
45
|
+
"unknown-capability",
|
|
46
|
+
"invalid-input",
|
|
47
|
+
"internal"
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* The occurrence for an emitted value. `makeId` gives an `eventId` that is unique in the subscription, and `now` gives
|
|
51
|
+
* the time, when the provider gives none.
|
|
52
|
+
*/
|
|
53
|
+
function occurrenceOf(name, emitted, makeId, now) {
|
|
54
|
+
return {
|
|
55
|
+
eventId: emitted.eventId ?? makeId(),
|
|
56
|
+
name,
|
|
57
|
+
timestamp: emitted.timestamp ?? now().toISOString(),
|
|
58
|
+
data: emitted.data,
|
|
59
|
+
...emitted.cursor === void 0 ? {} : { cursor: emitted.cursor },
|
|
60
|
+
...emitted.truncated === true ? { truncated: true } : {}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Tell if a value is an occurrence in its JSON form. `data` can be any JSON object. */
|
|
64
|
+
function isOccurrence(value) {
|
|
65
|
+
return isJsonObject(value) && typeof value.eventId === "string" && value.eventId.length > 0 && typeof value.name === "string" && isId(value.name) && typeof value.timestamp === "string" && !Number.isNaN(Date.parse(value.timestamp)) && isJsonObject(value.data) && (value.cursor === void 0 || typeof value.cursor === "string") && (value.truncated === void 0 || value.truncated === true) && Object.keys(value).every((key) => OCCURRENCE_MEMBERS.has(key));
|
|
66
|
+
}
|
|
67
|
+
const OCCURRENCE_MEMBERS = /* @__PURE__ */ new Set([
|
|
68
|
+
"eventId",
|
|
69
|
+
"name",
|
|
70
|
+
"timestamp",
|
|
71
|
+
"data",
|
|
72
|
+
"cursor",
|
|
73
|
+
"truncated"
|
|
74
|
+
]);
|
|
75
|
+
/** The time with no message on a stream after which a binding sends a keepalive or a heartbeat, in milliseconds. */
|
|
76
|
+
const KEEPALIVE_MS = 15e3;
|
|
41
77
|
const AVAILABLE = { available: true };
|
|
42
78
|
const unavailable = (reason) => ({
|
|
43
79
|
available: false,
|
|
@@ -122,6 +158,29 @@ function cliUsage(program, id, input) {
|
|
|
122
158
|
...input === void 0 ? [] : cliFlags(input)
|
|
123
159
|
].join(" ");
|
|
124
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* The `usage` of an event in `list` (spec/bindings/cli.md#events). `input` is the bundled input schema, or `undefined`
|
|
163
|
+
* when the event has no input.
|
|
164
|
+
*/
|
|
165
|
+
function cliEventUsage(program, id, input) {
|
|
166
|
+
const resume = "[--cursor <string>] [--max-age-ms <integer>]";
|
|
167
|
+
if (input !== void 0 && input.properties === void 0) return `${program} watch ${id} '<json>' ${resume}`;
|
|
168
|
+
return [
|
|
169
|
+
program,
|
|
170
|
+
"watch",
|
|
171
|
+
id,
|
|
172
|
+
...input === void 0 ? [] : cliFlags(input),
|
|
173
|
+
resume
|
|
174
|
+
].join(" ");
|
|
175
|
+
}
|
|
176
|
+
/** The flags of `watch` that give the cursor and `maxAgeMs`. No input property of an event can have these names. */
|
|
177
|
+
const CLI_RESUME_FLAGS = ["cursor", "max-age-ms"];
|
|
178
|
+
/** The exit code for each end of a subscription through the CLI (spec/bindings/cli.md#events). */
|
|
179
|
+
const CLI_WATCH_EXIT_CODES = {
|
|
180
|
+
complete: 0,
|
|
181
|
+
cancelled: 130,
|
|
182
|
+
internal: 5
|
|
183
|
+
};
|
|
125
184
|
const HTTP_STATUS = {
|
|
126
185
|
"unknown-capability": 404,
|
|
127
186
|
"invalid-input": 422,
|
|
@@ -130,6 +189,36 @@ const HTTP_STATUS = {
|
|
|
130
189
|
internal: 500
|
|
131
190
|
};
|
|
132
191
|
const httpStatus = (outcome) => isCallFailure(outcome) ? HTTP_STATUS[outcome.kind] : 200;
|
|
192
|
+
/** One message of a stream of server-sent events, as text, with the blank line that ends it. */
|
|
193
|
+
function sseMessage(fields) {
|
|
194
|
+
return `${[
|
|
195
|
+
...fields.event === void 0 ? [] : [`event: ${fields.event}`],
|
|
196
|
+
...fields.id === void 0 ? [] : [`id: ${fields.id}`],
|
|
197
|
+
...fields.data.split("\n").map((line) => line === "" ? "data:" : `data: ${line}`)
|
|
198
|
+
].join("\n")}\n\n`;
|
|
199
|
+
}
|
|
200
|
+
/** The messages of a subscription through the HTTP binding (spec/bindings/http.md#events). */
|
|
201
|
+
const httpEventMessage = {
|
|
202
|
+
next: (occurrence) => sseMessage({
|
|
203
|
+
event: "next",
|
|
204
|
+
id: occurrence.cursor,
|
|
205
|
+
data: JSON.stringify(occurrence)
|
|
206
|
+
}),
|
|
207
|
+
complete: () => sseMessage({
|
|
208
|
+
event: "complete",
|
|
209
|
+
data: ""
|
|
210
|
+
}),
|
|
211
|
+
error: (failure) => sseMessage({
|
|
212
|
+
event: "error",
|
|
213
|
+
data: JSON.stringify({
|
|
214
|
+
kind: failure.kind,
|
|
215
|
+
message: failure.message
|
|
216
|
+
})
|
|
217
|
+
}),
|
|
218
|
+
keepalive: () => ":\n\n"
|
|
219
|
+
};
|
|
220
|
+
/** The media type of a stream of server-sent events. */
|
|
221
|
+
const SSE_MEDIA_TYPE = "text/event-stream";
|
|
133
222
|
/**
|
|
134
223
|
* The first rule that a request breaks, before its body is read: method, origin, media type, declared length.
|
|
135
224
|
* The body itself is checked as it arrives, with `readBounded`.
|
|
@@ -187,6 +276,256 @@ const MCP_REJECTIONS = {
|
|
|
187
276
|
code: -32700
|
|
188
277
|
}
|
|
189
278
|
};
|
|
279
|
+
/** The identifier of the MCP events extension (spec/bindings/mcp.md#events). */
|
|
280
|
+
const MCP_EVENTS_EXTENSION = "io.modelcontextprotocol/events";
|
|
281
|
+
/** The key in `params._meta` that holds the id of the `events/stream` request. */
|
|
282
|
+
const MCP_SUBSCRIPTION_ID = "io.modelcontextprotocol/subscriptionId";
|
|
283
|
+
/** The JSON-RPC errors of the MCP events extension. Their numbers are not final in SEP-3415. */
|
|
284
|
+
const MCP_EVENT_ERRORS = {
|
|
285
|
+
notFound: -32023,
|
|
286
|
+
forbidden: -32024,
|
|
287
|
+
resourceExhausted: -32025,
|
|
288
|
+
unsupported: -32026,
|
|
289
|
+
callbackEndpoint: -32027,
|
|
290
|
+
invalidParams: -32602,
|
|
291
|
+
internal: -32603
|
|
292
|
+
};
|
|
293
|
+
/** The JSON-RPC error for a call failure of a subscription before its first occurrence. */
|
|
294
|
+
function mcpSubscribeError(failure) {
|
|
295
|
+
if (failure.kind === "unknown-capability") return {
|
|
296
|
+
code: MCP_EVENT_ERRORS.notFound,
|
|
297
|
+
message: failure.message,
|
|
298
|
+
data: { kind: "event" }
|
|
299
|
+
};
|
|
300
|
+
if (failure.kind === "invalid-input") return {
|
|
301
|
+
code: MCP_EVENT_ERRORS.invalidParams,
|
|
302
|
+
message: failure.message
|
|
303
|
+
};
|
|
304
|
+
return {
|
|
305
|
+
code: MCP_EVENT_ERRORS.internal,
|
|
306
|
+
message: failure.message
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/** An entry of `events/list`. `input` and `payload` are the bundled schemas. */
|
|
310
|
+
function mcpEventEntry(id, description, input, payload, webhook = false) {
|
|
311
|
+
return {
|
|
312
|
+
name: id,
|
|
313
|
+
description,
|
|
314
|
+
delivery: webhook ? [
|
|
315
|
+
"push",
|
|
316
|
+
"poll",
|
|
317
|
+
"webhook"
|
|
318
|
+
] : ["push", "poll"],
|
|
319
|
+
inputSchema: input ?? { type: "object" },
|
|
320
|
+
payloadSchema: payload ?? { type: "object" }
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/** The params of `notifications/events/event` for an occurrence: the occurrence without `truncated`. */
|
|
324
|
+
function mcpEventParams(occurrence, subscriptionId) {
|
|
325
|
+
const { truncated: _, ...rest } = occurrence;
|
|
326
|
+
return {
|
|
327
|
+
...rest,
|
|
328
|
+
_meta: { [MCP_SUBSCRIPTION_ID]: subscriptionId }
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/** The canonical JSON of a value (spec/bindings/mcp.md#canonical-json): no spaces, and the keys of each object in code point order. */
|
|
332
|
+
function canonicalJson(value) {
|
|
333
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
334
|
+
if (isJsonObject(value)) return `{${Object.keys(value).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
335
|
+
return JSON.stringify(value) ?? "null";
|
|
336
|
+
}
|
|
337
|
+
/** The defaults of poll delivery (spec/bindings/mcp.md#poll). */
|
|
338
|
+
const MCP_POLL_DEFAULTS = {
|
|
339
|
+
leaseMs: 3e5,
|
|
340
|
+
bufferLimit: 1e3,
|
|
341
|
+
maxEvents: 100,
|
|
342
|
+
nextPollMs: 2e3
|
|
343
|
+
};
|
|
344
|
+
/** The cursor of a poll position. Only the server reads it. */
|
|
345
|
+
function encodePollCursor(position) {
|
|
346
|
+
const parts = position.provider === void 0 ? [position.lease, position.position] : [
|
|
347
|
+
position.lease,
|
|
348
|
+
position.position,
|
|
349
|
+
position.provider
|
|
350
|
+
];
|
|
351
|
+
return `p1.${base64UrlOfText(JSON.stringify(parts))}`;
|
|
352
|
+
}
|
|
353
|
+
/** The position of a poll cursor, or `undefined` when the text is not a poll cursor. */
|
|
354
|
+
function decodePollCursor(cursor) {
|
|
355
|
+
if (!cursor.startsWith("p1.")) return void 0;
|
|
356
|
+
let parts;
|
|
357
|
+
try {
|
|
358
|
+
parts = JSON.parse(textOfBase64Url(cursor.slice(3)));
|
|
359
|
+
} catch {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (!Array.isArray(parts) || parts.length < 2 || parts.length > 3) return void 0;
|
|
363
|
+
const [lease, position, provider] = parts;
|
|
364
|
+
if (typeof lease !== "string" || typeof position !== "number" || !Number.isSafeInteger(position) || position < 0) return void 0;
|
|
365
|
+
if (provider !== void 0 && typeof provider !== "string") return void 0;
|
|
366
|
+
return provider === void 0 ? {
|
|
367
|
+
lease,
|
|
368
|
+
position
|
|
369
|
+
} : {
|
|
370
|
+
lease,
|
|
371
|
+
position,
|
|
372
|
+
provider
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function base64UrlOfText(text) {
|
|
376
|
+
let binary = "";
|
|
377
|
+
for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);
|
|
378
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
379
|
+
}
|
|
380
|
+
function textOfBase64Url(text) {
|
|
381
|
+
const binary = atob(text.replaceAll("-", "+").replaceAll("_", "/"));
|
|
382
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(binary, (character) => character.charCodeAt(0)));
|
|
383
|
+
}
|
|
384
|
+
/** The defaults of webhook delivery (spec/bindings/mcp.md#webhook). */
|
|
385
|
+
const MCP_WEBHOOK_DEFAULTS = {
|
|
386
|
+
maxTtlMs: 36e5,
|
|
387
|
+
minTtlMs: 6e4,
|
|
388
|
+
maxSubscriptions: 100,
|
|
389
|
+
attempts: 5,
|
|
390
|
+
retryWindowMs: 9e5,
|
|
391
|
+
timeoutMs: 5e3,
|
|
392
|
+
rotationGraceMs: 3e5
|
|
393
|
+
};
|
|
394
|
+
/** The reasons of a `CallbackEndpointError`, and the values of `lastError` (spec/bindings/mcp.md#webhook). */
|
|
395
|
+
const WEBHOOK_ERRORS = [
|
|
396
|
+
"challenge_failed",
|
|
397
|
+
"connection_refused",
|
|
398
|
+
"timeout",
|
|
399
|
+
"tls_error",
|
|
400
|
+
"http_4xx",
|
|
401
|
+
"http_5xx"
|
|
402
|
+
];
|
|
403
|
+
/** The bytes of a Standard Webhooks secret: `whsec_` and the base64 of 24 to 64 bytes. `undefined` when it is not valid. */
|
|
404
|
+
function webhookSecretBytes(secret) {
|
|
405
|
+
if (!secret.startsWith("whsec_")) return void 0;
|
|
406
|
+
const text = secret.slice(6);
|
|
407
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(text) || text.length % 4 !== 0) return void 0;
|
|
408
|
+
let binary;
|
|
409
|
+
try {
|
|
410
|
+
binary = atob(text);
|
|
411
|
+
} catch {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
return binary.length >= 24 && binary.length <= 64 ? Uint8Array.from(binary, (character) => character.charCodeAt(0)) : void 0;
|
|
415
|
+
}
|
|
416
|
+
/** The lifetime that the server grants for a suggested `ttlMs` (spec/bindings/mcp.md#webhook). */
|
|
417
|
+
function webhookGrantMs(ttlMs, limits) {
|
|
418
|
+
if (ttlMs === null || ttlMs === void 0) return limits.maxTtlMs;
|
|
419
|
+
return Math.min(limits.maxTtlMs, Math.max(limits.minTtlMs, ttlMs));
|
|
420
|
+
}
|
|
421
|
+
/** The text that a Standard Webhooks signature covers. */
|
|
422
|
+
const webhookSignedText = (webhookId, timestamp, body) => `${webhookId}.${timestamp}.${body}`;
|
|
423
|
+
/** The `webhook-signature` header: one `v1,` signature for each secret, with a space between them. */
|
|
424
|
+
async function webhookSignature(secrets, webhookId, timestamp, body) {
|
|
425
|
+
const content = new TextEncoder().encode(webhookSignedText(webhookId, timestamp, body));
|
|
426
|
+
const signatures = [];
|
|
427
|
+
for (const secret of secrets) {
|
|
428
|
+
const bytes = webhookSecretBytes(secret);
|
|
429
|
+
if (bytes === void 0) throw new TypeError("A webhook secret is not valid.");
|
|
430
|
+
const key = await crypto.subtle.importKey("raw", bytes, {
|
|
431
|
+
name: "HMAC",
|
|
432
|
+
hash: "SHA-256"
|
|
433
|
+
}, false, ["sign"]);
|
|
434
|
+
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, content));
|
|
435
|
+
let binary = "";
|
|
436
|
+
for (const byte of mac) binary += String.fromCharCode(byte);
|
|
437
|
+
signatures.push(`v1,${btoa(binary)}`);
|
|
438
|
+
}
|
|
439
|
+
return signatures.join(" ");
|
|
440
|
+
}
|
|
441
|
+
/** The subscription id of a webhook subscription: `sub_` and 16 hexadecimal digits of the SHA-256 of its key. */
|
|
442
|
+
async function webhookSubscriptionId(principal, url, id, input) {
|
|
443
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalJson([
|
|
444
|
+
principal,
|
|
445
|
+
url,
|
|
446
|
+
id,
|
|
447
|
+
input
|
|
448
|
+
]))));
|
|
449
|
+
return `sub_${Array.from(digest.slice(0, 8), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Tell if an IP address is globally routable: not in a special-purpose range of the IANA IPv4 and IPv6 registries that
|
|
453
|
+
* is not global, for example loopback, private, link-local, shared, documentation or multicast addresses.
|
|
454
|
+
*/
|
|
455
|
+
function isGloballyRoutable(address) {
|
|
456
|
+
const ipv4 = ipv4Bytes(address);
|
|
457
|
+
if (ipv4 !== void 0) return isGlobalIpv4(ipv4);
|
|
458
|
+
const ipv6 = ipv6Words(address);
|
|
459
|
+
if (ipv6 === void 0) return false;
|
|
460
|
+
const [a = 0, b = 0, c = 0, d = 0, e = 0, f = 0, g = 0, h = 0] = ipv6;
|
|
461
|
+
if (a === 0 && b === 0 && c === 0 && d === 0 && e === 0 && f === 65535) return isGlobalIpv4([
|
|
462
|
+
g >> 8,
|
|
463
|
+
g & 255,
|
|
464
|
+
h >> 8,
|
|
465
|
+
h & 255
|
|
466
|
+
]);
|
|
467
|
+
if (ipv6.every((word) => word === 0)) return false;
|
|
468
|
+
if (a === 0 && b === 0 && c === 0 && d === 0 && e === 0 && f === 0 && g === 0 && h === 1) return false;
|
|
469
|
+
if (a === 100 && b === 65435) return false;
|
|
470
|
+
if (a === 256 && b === 0 && c === 0 && d === 0) return false;
|
|
471
|
+
if (a === 8193 && b < 512) return false;
|
|
472
|
+
if (a === 8193 && b === 3512) return false;
|
|
473
|
+
if (a === 8194) return false;
|
|
474
|
+
if (a === 16383 && b < 4096) return false;
|
|
475
|
+
if (a === 24320) return false;
|
|
476
|
+
if ((a & 65024) === 64512) return false;
|
|
477
|
+
if ((a & 65472) === 65152) return false;
|
|
478
|
+
if ((a & 65280) === 65280) return false;
|
|
479
|
+
return (a & 57344) === 8192;
|
|
480
|
+
}
|
|
481
|
+
function isGlobalIpv4([a = 0, b = 0, c = 0]) {
|
|
482
|
+
if (a === 0 || a === 10 || a === 127) return false;
|
|
483
|
+
if (a === 100 && b >= 64 && b <= 127) return false;
|
|
484
|
+
if (a === 169 && b === 254) return false;
|
|
485
|
+
if (a === 172 && b >= 16 && b <= 31) return false;
|
|
486
|
+
if (a === 192 && b === 0 && (c === 0 || c === 2)) return false;
|
|
487
|
+
if (a === 192 && b === 88 && c === 99) return false;
|
|
488
|
+
if (a === 192 && b === 168) return false;
|
|
489
|
+
if (a === 198 && (b === 18 || b === 19)) return false;
|
|
490
|
+
if (a === 198 && b === 51 && c === 100) return false;
|
|
491
|
+
if (a === 203 && b === 0 && c === 113) return false;
|
|
492
|
+
return a < 224;
|
|
493
|
+
}
|
|
494
|
+
function ipv4Bytes(address) {
|
|
495
|
+
const parts = address.split(".");
|
|
496
|
+
if (parts.length !== 4 || !parts.every((part) => /^(0|[1-9][0-9]{0,2})$/.test(part))) return void 0;
|
|
497
|
+
const bytes = parts.map(Number);
|
|
498
|
+
return bytes.every((byte) => byte <= 255) ? bytes : void 0;
|
|
499
|
+
}
|
|
500
|
+
function ipv6Words(input) {
|
|
501
|
+
let address = input.startsWith("[") && input.endsWith("]") ? input.slice(1, -1) : input;
|
|
502
|
+
const zone = address.indexOf("%");
|
|
503
|
+
if (zone !== -1) address = address.slice(0, zone);
|
|
504
|
+
const tail = [];
|
|
505
|
+
const lastColon = address.lastIndexOf(":");
|
|
506
|
+
const ipv4 = lastColon === -1 ? void 0 : ipv4Bytes(address.slice(lastColon + 1));
|
|
507
|
+
if (ipv4 !== void 0) {
|
|
508
|
+
const [w = 0, x = 0, y = 0, z = 0] = ipv4;
|
|
509
|
+
tail.push(w << 8 | x, y << 8 | z);
|
|
510
|
+
address = address.slice(0, lastColon + 1);
|
|
511
|
+
if (!address.endsWith("::")) address = address.slice(0, -1);
|
|
512
|
+
}
|
|
513
|
+
const halves = address.split("::");
|
|
514
|
+
if (halves.length > 2) return void 0;
|
|
515
|
+
const words = (text) => text === "" ? [] : text.split(":");
|
|
516
|
+
const head = words(halves[0] ?? "");
|
|
517
|
+
const rest = halves.length === 2 ? words(halves[1] ?? "") : [];
|
|
518
|
+
if (![...head, ...rest].every((word) => /^[0-9a-fA-F]{1,4}$/.test(word))) return void 0;
|
|
519
|
+
const count = head.length + rest.length + tail.length;
|
|
520
|
+
if (halves.length === 1 ? count !== 8 : count > 7) return void 0;
|
|
521
|
+
const zeros = halves.length === 2 ? Array.from({ length: 8 - count }, () => 0) : [];
|
|
522
|
+
return [
|
|
523
|
+
...head.map((word) => parseInt(word, 16)),
|
|
524
|
+
...zeros,
|
|
525
|
+
...rest.map((word) => parseInt(word, 16)),
|
|
526
|
+
...tail
|
|
527
|
+
];
|
|
528
|
+
}
|
|
190
529
|
/** The MCP tool result for an outcome that is not `unknown-capability`. */
|
|
191
530
|
function mcpToolResult(outcome, hasOutputSchema) {
|
|
192
531
|
const text = (value) => [{
|
|
@@ -228,6 +567,10 @@ function schemasOf(capability) {
|
|
|
228
567
|
...Object.entries(capability.failures ?? {}).map(([tag, schema]) => [`failure ${tag}`, schema])
|
|
229
568
|
].filter((entry) => entry[1] !== void 0);
|
|
230
569
|
}
|
|
570
|
+
/** The schemas of an event, each with the member that holds it. */
|
|
571
|
+
function eventSchemasOf(event) {
|
|
572
|
+
return [["input", event.input], ["payload", event.payload]].filter((entry) => entry[1] !== void 0);
|
|
573
|
+
}
|
|
231
574
|
/** Every object in a JSON value, at all levels. */
|
|
232
575
|
function visitObjects(value, onObject) {
|
|
233
576
|
if (Array.isArray(value)) value.forEach((item) => visitObjects(item, onObject));
|
|
@@ -288,16 +631,22 @@ function viewOf(catalog, target) {
|
|
|
288
631
|
...capability,
|
|
289
632
|
audience: [target]
|
|
290
633
|
}]));
|
|
634
|
+
const events = Object.fromEntries(Object.entries(catalog.events ?? {}).filter(([, event]) => (event.audience ?? []).includes(target)).map(([id, event]) => [id, {
|
|
635
|
+
...event,
|
|
636
|
+
audience: [target]
|
|
637
|
+
}]));
|
|
291
638
|
const used = /* @__PURE__ */ new Map();
|
|
292
|
-
|
|
293
|
-
const
|
|
639
|
+
const schemas = [...Object.values(capabilities).flatMap(schemasOf), ...Object.values(events).flatMap(eventSchemasOf)];
|
|
640
|
+
for (const [, schema] of schemas) for (const [name, type] of typesUsedBy(schema, types)) used.set(name, type);
|
|
641
|
+
const { $defs: _, events: __, ...rest } = catalog;
|
|
294
642
|
const declared = catalog.targets[target];
|
|
295
643
|
return {
|
|
296
644
|
...rest,
|
|
297
645
|
targets: declared === void 0 ? {} : { [target]: declared },
|
|
298
646
|
...used.size > 0 ? { $defs: Object.fromEntries(used) } : {},
|
|
299
|
-
capabilities
|
|
647
|
+
capabilities,
|
|
648
|
+
...Object.keys(events).length > 0 ? { events } : {}
|
|
300
649
|
};
|
|
301
650
|
}
|
|
302
651
|
//#endregion
|
|
303
|
-
export { AVAILABLE, CALL_FAILURE_KINDS, CLI_EXIT_CODES, CallFailureError, HTTP_STATUS, ID_PATTERN, MCP_PROTOCOL_VERSION, MCP_REJECTIONS, MCP_UNKNOWN_TOOL, REFERENCE, bundle, callFailure, cliExitCode, cliFlags, cliJsonArray, cliRepeatedItems, cliResolve, cliUsage, commandPath, fail, httpStatus, isCallFailure, isId, isJsonObject, mcpToolResult, objectInput, ok, readBounded, schemasOf, screenRequest, typesUsedBy, unavailable, viewOf, visitObjects };
|
|
652
|
+
export { AVAILABLE, CALL_FAILURE_KINDS, CLI_EXIT_CODES, CLI_RESUME_FLAGS, CLI_WATCH_EXIT_CODES, CallFailureError, HTTP_STATUS, ID_PATTERN, KEEPALIVE_MS, MCP_EVENTS_EXTENSION, MCP_EVENT_ERRORS, MCP_POLL_DEFAULTS, MCP_PROTOCOL_VERSION, MCP_REJECTIONS, MCP_SUBSCRIPTION_ID, MCP_UNKNOWN_TOOL, MCP_WEBHOOK_DEFAULTS, REFERENCE, SSE_MEDIA_TYPE, SUBSCRIBE_FAILURE_KINDS, WEBHOOK_ERRORS, bundle, callFailure, canonicalJson, cliEventUsage, cliExitCode, cliFlags, cliJsonArray, cliRepeatedItems, cliResolve, cliUsage, commandPath, decodePollCursor, encodePollCursor, eventSchemasOf, fail, httpEventMessage, httpStatus, isCallFailure, isGloballyRoutable, isId, isJsonObject, isOccurrence, mcpEventEntry, mcpEventParams, mcpSubscribeError, mcpToolResult, objectInput, occurrenceOf, ok, readBounded, schemasOf, screenRequest, sseMessage, typesUsedBy, unavailable, viewOf, visitObjects, webhookGrantMs, webhookSecretBytes, webhookSignature, webhookSignedText, webhookSubscriptionId };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acid-cat/spec-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "The fixed details of the AcidCat specification as TypeScript data and pure functions, with no dependencies. Part of the TypeScript reference implementation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"acidcat",
|