holon-sh 0.0.1-dev.16 → 0.0.1-dev.17
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/context.d.ts +2 -5
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js.map +1 -1
- package/dist/ext/events.client.d.ts +4 -0
- package/dist/ext/events.client.d.ts.map +1 -0
- package/dist/ext/events.client.js +180 -0
- package/dist/ext/events.client.js.map +1 -0
- package/dist/ext/events.d.ts +6 -12
- package/dist/ext/events.d.ts.map +1 -1
- package/dist/ext/events.js +30 -51
- package/dist/ext/events.js.map +1 -1
- package/dist/ext/files.client.d.ts +2 -3
- package/dist/ext/files.client.d.ts.map +1 -1
- package/dist/ext/files.client.js +20 -22
- package/dist/ext/files.client.js.map +1 -1
- package/dist/ext/files.d.ts +1 -1
- package/dist/ext/files.d.ts.map +1 -1
- package/dist/link.d.ts +35 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +118 -0
- package/dist/link.js.map +1 -0
- package/dist/protocol.d.ts +6 -7
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +0 -1
- package/dist/protocol.js.map +1 -1
- package/dist/runtime.d.ts +0 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +129 -409
- package/dist/runtime.js.map +1 -1
- package/package.json +1 -1
- package/src/context.ts +3 -1
- package/src/ext/events.client.ts +157 -0
- package/src/ext/events.ts +41 -83
- package/src/ext/files.client.ts +21 -20
- package/src/ext/files.ts +1 -1
- package/src/link.ts +136 -0
- package/src/protocol.ts +6 -9
- package/src/runtime.ts +120 -379
package/src/ext/files.client.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { EXTENSION, type Download } from "./files.js";
|
|
|
6
6
|
|
|
7
7
|
type Json = Record<string, unknown>;
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
interface FileConnection {
|
|
10
10
|
client: Pick<Client, "getServerCapabilities">;
|
|
11
11
|
resolved: { endpoint: URL | string; fileOrigins?: string[]; installationUID?: string };
|
|
12
12
|
authorization?: string;
|
|
@@ -14,7 +14,7 @@ export interface FileConnection {
|
|
|
14
14
|
|
|
15
15
|
interface FileInput { id: string; name?: string; mimeType: string; size: number; digest?: { algorithm: "sha-256"; value: string }; data?: string }
|
|
16
16
|
interface Attachment { blob: Blob; input: FileInput }
|
|
17
|
-
|
|
17
|
+
type Files = Map<string, Attachment>;
|
|
18
18
|
|
|
19
19
|
const list = (value: unknown): unknown[] => Array.isArray(value) ? value : [];
|
|
20
20
|
const isObject = (value: unknown): value is Json => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -26,7 +26,7 @@ function refuse(reason: string): never {
|
|
|
26
26
|
throw new Error(reason);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
function settings({ client }: FileConnection): Json | undefined {
|
|
30
30
|
const advertised = client.getServerCapabilities()?.extensions?.[EXTENSION];
|
|
31
31
|
return isObject(advertised) && advertised.revision == null ? advertised : undefined;
|
|
32
32
|
}
|
|
@@ -143,6 +143,7 @@ const PATH = /^(?:\/(?!\.\.?(?:\/|$))[A-Za-z0-9\-_~.]+)+$/;
|
|
|
143
143
|
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
|
|
144
144
|
const loopback = (host: string) => host === "localhost" || host.endsWith(".localhost") || /^127\.\d+\.\d+\.\d+$/.test(host) || host === "[::1]";
|
|
145
145
|
const expired = (target: { expiresAt?: string }) => target.expiresAt !== undefined && Date.parse(target.expiresAt) <= Date.now();
|
|
146
|
+
const timestamp = (value: unknown): value is string | undefined => value === undefined || typeof value === "string" && TIMESTAMP.test(value) && !Number.isNaN(Date.parse(value));
|
|
146
147
|
|
|
147
148
|
function parse(url: string): URL | undefined {
|
|
148
149
|
try { return new URL(url); } catch { return undefined; }
|
|
@@ -156,7 +157,7 @@ function check(raw: unknown, kind: "upload" | "download", { resolved }: FileConn
|
|
|
156
157
|
if (typeof target.url === "string" === (typeof target.path === "string") || [target.url, target.path].some(value => value !== undefined && typeof value !== "string")) refuse("needs exactly one of url and path");
|
|
157
158
|
if (target.credentials !== "omit" && target.credentials !== "include") refuse("credentials must be omit or include");
|
|
158
159
|
const { expiresAt } = target;
|
|
159
|
-
if (
|
|
160
|
+
if (!timestamp(expiresAt)) refuse("expiresAt is not an RFC 3339 timestamp");
|
|
160
161
|
if (target.headers !== undefined && !(isObject(target.headers) && Object.values(target.headers).every(value => typeof value === "string"))) refuse("headers must map names to strings");
|
|
161
162
|
const headers = (target.headers ?? {}) as Record<string, string>;
|
|
162
163
|
let form: Form | undefined;
|
|
@@ -175,12 +176,6 @@ function check(raw: unknown, kind: "upload" | "download", { resolved }: FileConn
|
|
|
175
176
|
}
|
|
176
177
|
form = { fileField: raw.fileField, fields: fields as Form["fields"] };
|
|
177
178
|
}
|
|
178
|
-
} else {
|
|
179
|
-
if (target.name !== undefined && typeof target.name !== "string") refuse("name is not a string");
|
|
180
|
-
if (typeof target.mimeType !== "string") refuse("mimeType is not a string");
|
|
181
|
-
if (!Number.isSafeInteger(target.size) || (target.size as number) < 0) refuse("size is not a non-negative integer");
|
|
182
|
-
const digest = target.digest === undefined ? undefined : present(target.digest);
|
|
183
|
-
if (target.digest !== undefined && (!digest || Object.keys(digest).some(key => key !== "algorithm" && key !== "value") || digest.algorithm !== "sha-256" || typeof digest.value !== "string")) refuse("digest is malformed");
|
|
184
179
|
}
|
|
185
180
|
const include = target.credentials === "include";
|
|
186
181
|
for (const [name, value] of Object.entries(headers)) {
|
|
@@ -283,14 +278,20 @@ export async function upload(request: Json, files: Files, connection: FileConnec
|
|
|
283
278
|
return requestState;
|
|
284
279
|
}
|
|
285
280
|
|
|
286
|
-
function download(raw: unknown, connection: FileConnection): Download {
|
|
281
|
+
function download(raw: unknown, connection: FileConnection): Download | undefined {
|
|
287
282
|
const { name, mimeType, size, digest, expiresAt }: Json = present(raw) ?? {};
|
|
288
|
-
|
|
283
|
+
if (name !== undefined && typeof name !== "string" || typeof mimeType !== "string" || typeof size !== "number" || !Number.isSafeInteger(size) || size < 0 || !timestamp(expiresAt)) return undefined;
|
|
284
|
+
let value: string | undefined;
|
|
285
|
+
if (digest !== undefined) {
|
|
286
|
+
const given = present(digest);
|
|
287
|
+
if (!given || Object.keys(given).some(key => key !== "algorithm" && key !== "value") || given.algorithm !== "sha-256" || typeof given.value !== "string") return undefined;
|
|
288
|
+
value = given.value;
|
|
289
|
+
}
|
|
290
|
+
const label = `Downloading ${quoted(name)}`;
|
|
289
291
|
const blob = async ({ signal }: { signal?: AbortSignal } = {}) => {
|
|
290
292
|
let target: Target;
|
|
291
293
|
try { target = check(raw, "download", connection); } catch (error) { throw new Error(`${label} was refused: its target was rejected (${(error as Error).message}).`); }
|
|
292
294
|
if (expired(target)) throw new Error(`${label} was refused: its target expired.`);
|
|
293
|
-
const declared = size as number;
|
|
294
295
|
const chunks: Uint8Array[] = [];
|
|
295
296
|
let received = 0;
|
|
296
297
|
try {
|
|
@@ -304,26 +305,26 @@ function download(raw: unknown, connection: FileConnection): Download {
|
|
|
304
305
|
const chunk = await reader.read();
|
|
305
306
|
if (chunk.done) break;
|
|
306
307
|
received += chunk.value.byteLength;
|
|
307
|
-
if (received >
|
|
308
|
+
if (received > size) {
|
|
308
309
|
await reader.cancel();
|
|
309
|
-
throw new Error(`more than the declared ${
|
|
310
|
+
throw new Error(`more than the declared ${size} bytes; discarded`);
|
|
310
311
|
}
|
|
311
312
|
chunks.push(chunk.value);
|
|
312
313
|
}
|
|
313
314
|
} catch (error) {
|
|
314
315
|
throw new Error(`${label} failed (${reason(error)}).`);
|
|
315
316
|
}
|
|
316
|
-
if (received !==
|
|
317
|
-
const file = new Blob(chunks, { type: mimeType
|
|
318
|
-
if (
|
|
317
|
+
if (received !== size) throw new Error(`${label} failed (${received} bytes, not the declared ${size}; discarded).`);
|
|
318
|
+
const file = new Blob(chunks, { type: mimeType });
|
|
319
|
+
if (value !== undefined && await sha256(file) !== value) throw new Error(`${label} failed (the bytes do not match the digest; discarded).`);
|
|
319
320
|
return file;
|
|
320
321
|
};
|
|
321
|
-
return Object.freeze({ ...
|
|
322
|
+
return Object.freeze({ ...(name !== undefined && { name }), mimeType, size, ...(value !== undefined && { digest: { algorithm: "sha-256" as const, value } }), ...(expiresAt !== undefined && { expiresAt }), blob });
|
|
322
323
|
}
|
|
323
324
|
|
|
324
325
|
export function downloads<R extends { resultType?: string; _meta?: object }>(result: R, connection: FileConnection): R & { downloads?: Download[] } {
|
|
325
326
|
if (result.resultType === "input_required" || !isObject(result._meta) || !(EXTENSION in result._meta)) return result;
|
|
326
327
|
const { [EXTENSION]: meta, ...rest } = result._meta;
|
|
327
|
-
const handles = settings(connection) ? list((meta as { downloads?: unknown } | null | undefined)?.downloads).
|
|
328
|
+
const handles = settings(connection) ? list((meta as { downloads?: unknown } | null | undefined)?.downloads).flatMap(target => download(target, connection) ?? []) : [];
|
|
328
329
|
return { ...result, _meta: rest, ...(handles.length > 0 && { downloads: handles }) };
|
|
329
330
|
}
|
package/src/ext/files.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Identifier of the MCP files extension (draft), the key under which a client and a server declare it in their capabilities. */
|
|
2
2
|
export const EXTENSION = "io.modelcontextprotocol/files";
|
|
3
3
|
|
|
4
|
-
/** A file a tool returned for the caller to fetch. Its fields come from the server
|
|
4
|
+
/** A file a tool returned for the caller to fetch. Its fields come from the server: they have the types below, but their values are untrusted. */
|
|
5
5
|
export interface Download {
|
|
6
6
|
readonly name?: string;
|
|
7
7
|
readonly mimeType: string;
|
package/src/link.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Client, SdkHttpError, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
2
|
+
import type { CacheableRequestOptions, PriorDiscovery, StandardSchemaV1 } from "@modelcontextprotocol/client";
|
|
3
|
+
import { EXTENSION } from "./ext/files.js";
|
|
4
|
+
import { acceptUploadRequests } from "./ext/files.client.js";
|
|
5
|
+
import type { CallOptions } from "./protocol.js";
|
|
6
|
+
import type { Binding, Identity, ToolSession } from "./runtime.js";
|
|
7
|
+
|
|
8
|
+
interface Connection {
|
|
9
|
+
client: Client;
|
|
10
|
+
resolved: Binding;
|
|
11
|
+
authorization?: string;
|
|
12
|
+
ready: Promise<void>;
|
|
13
|
+
answered: boolean;
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Link {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
session<T>(options: CallOptions | undefined, run: (client: ToolSession, connection?: Connection) => Promise<T>): Promise<T>;
|
|
20
|
+
endpoint(what: string): Binding;
|
|
21
|
+
connect(binding: Binding): Connection;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Environment-bound calls reuse the last server/discover result for the same endpoint and token instead of discovering
|
|
26
|
+
// the server on every call. A dependency keeps only its latest result, so a changed endpoint or token replaces it.
|
|
27
|
+
const DISCOVERY: unique symbol = Symbol("discovery");
|
|
28
|
+
const discovered = new Map<string, { key: string; prior: PriorDiscovery; expires: number }>();
|
|
29
|
+
const LEGACY_VERDICT_MS = 10 * 60 * 1000;
|
|
30
|
+
|
|
31
|
+
type Endpoint = Binding & { [DISCOVERY]?: string };
|
|
32
|
+
/** Without an endpoint, a binding is served by the session hook or not at all. */
|
|
33
|
+
type Resolved = Endpoint | { endpoint?: undefined };
|
|
34
|
+
|
|
35
|
+
export function environment(name: string): Endpoint {
|
|
36
|
+
const prefix = `${name.toUpperCase().replaceAll("-", "_")}_MCP_`;
|
|
37
|
+
const endpoint = process.env[`${prefix}URL`];
|
|
38
|
+
const token = process.env[`${prefix}TOKEN`];
|
|
39
|
+
if (endpoint) return { endpoint, scoped: true, [DISCOVERY]: `${endpoint} ${token ?? ""}`, ...(token && { authProvider: { token: async () => token } }) };
|
|
40
|
+
throw new Error(`Dependency ${name} is unavailable (${prefix}URL). Provide a binding or set the environment variable.`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function connect(identity: Identity, resolved: Binding, prior?: PriorDiscovery, onclose?: (connection: Connection) => void): Connection {
|
|
44
|
+
// Every client can be asked: a call answers with elicit, or the question reaches its caller. Servers must not assume a client answers: https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#server-requirements-basic-workflow
|
|
45
|
+
// On 2026-07-28 the client sends these capabilities in every request's _meta: https://github.com/modelcontextprotocol/typescript-sdk/blob/fcef852f6cdafea07f36cf6fae69b37c8adf85be/packages/client/src/client/client.ts#L700-L711
|
|
46
|
+
const client = new Client({ name: identity.name, version: identity.version }, { capabilities: { elicitation: { form: {}, url: {} }, extensions: { [EXTENSION]: { upload: {}, download: {} } } }, versionNegotiation: { mode: "auto" } });
|
|
47
|
+
const endpoint = new URL(resolved.endpoint);
|
|
48
|
+
const transport = acceptUploadRequests(new StreamableHTTPClientTransport(endpoint, { authProvider: resolved.authProvider, fetch: async (input, init) => {
|
|
49
|
+
if (String(input) === endpoint.href) connection.authorization = new Headers(init?.headers).get("authorization") ?? undefined;
|
|
50
|
+
const response = await (resolved.fetch ?? fetch)(input, init);
|
|
51
|
+
connection.answered ||= response.ok;
|
|
52
|
+
return response;
|
|
53
|
+
}, requestInit: { credentials: "include", redirect: "error", headers: resolved.installationUID ? { "X-Holon-Installation-UID": resolved.installationUID } : {} } }));
|
|
54
|
+
transport.onclose = () => onclose?.(connection);
|
|
55
|
+
const ready = client.connect(transport, { prior }).catch(async (error: unknown) => {
|
|
56
|
+
await transport.close();
|
|
57
|
+
throw error;
|
|
58
|
+
});
|
|
59
|
+
const connection: Connection = { client, resolved, ready, answered: false, close: async () => {
|
|
60
|
+
await transport.terminateSession().catch(() => {});
|
|
61
|
+
await transport.close();
|
|
62
|
+
await ready.catch(() => {});
|
|
63
|
+
} };
|
|
64
|
+
return connection;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createLink(identity: Identity, name: string, { binding, resolve, host }: { binding: unknown; resolve?: (app: string) => Binding | undefined; host?: ToolSession | undefined }): Link {
|
|
68
|
+
const current = (): Resolved | undefined => (binding as Resolved | undefined) ?? resolve?.(name);
|
|
69
|
+
let kept: Connection | undefined;
|
|
70
|
+
const session = async <T>(options: CallOptions | undefined, run: (client: ToolSession, connection?: Connection) => Promise<T>): Promise<T> => {
|
|
71
|
+
options?.signal?.throwIfAborted();
|
|
72
|
+
if (host) return run(host);
|
|
73
|
+
const resolved = current();
|
|
74
|
+
if (!resolved?.endpoint) throw new Error(`Dependency ${name} is unavailable. Provide a binding.`);
|
|
75
|
+
const scoped = resolved.scoped === true;
|
|
76
|
+
const key = resolved[DISCOVERY];
|
|
77
|
+
const cached = discovered.get(name);
|
|
78
|
+
const prior = key !== undefined && cached?.key === key && Date.now() < cached.expires ? cached.prior : undefined;
|
|
79
|
+
const active = scoped ? connect(identity, resolved, prior) : (kept ??= connect(identity, resolved, undefined, closed => { if (kept === closed) kept = undefined; }));
|
|
80
|
+
try {
|
|
81
|
+
await untilAborted(active.ready, options?.signal);
|
|
82
|
+
if (key !== undefined && !prior) {
|
|
83
|
+
const discover = active.client.getDiscoverResult();
|
|
84
|
+
discovered.set(name, { key, prior: discover ? { kind: "modern", discover } : { kind: "legacy" }, expires: discover ? Infinity : Date.now() + LEGACY_VERDICT_MS });
|
|
85
|
+
}
|
|
86
|
+
return await run(active.client, active);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// A server that no longer speaks the discovered protocol rejects the first request without running it.
|
|
89
|
+
if (!prior || active.answered || !(SdkHttpError.isInstance(error) && error.status === 400)) throw error;
|
|
90
|
+
discovered.delete(name);
|
|
91
|
+
return session(options, run);
|
|
92
|
+
} finally { if (scoped) await active.close(); }
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
name,
|
|
96
|
+
session,
|
|
97
|
+
endpoint(what) {
|
|
98
|
+
const resolved = current();
|
|
99
|
+
if (!resolved?.endpoint) throw new Error(`${what} requires an endpoint binding.`);
|
|
100
|
+
return resolved;
|
|
101
|
+
},
|
|
102
|
+
connect: resolved => connect(identity, resolved),
|
|
103
|
+
close: async () => { await kept?.close(); },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function sleep(milliseconds: number, signal?: AbortSignal): Promise<void> {
|
|
108
|
+
return new Promise(resolve => {
|
|
109
|
+
const done = () => {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
signal?.removeEventListener("abort", done);
|
|
112
|
+
resolve();
|
|
113
|
+
};
|
|
114
|
+
const timer = setTimeout(done, milliseconds);
|
|
115
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function untilAborted<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
120
|
+
if (!signal) return promise;
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const aborted = () => reject(signal.reason);
|
|
123
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
124
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", aborted));
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The MCP client ends a request after 60 s unless given a timeout. A request without one waits as long as a timer can
|
|
129
|
+
// (2^31 - 1 ms, about 24.8 days). A request with one asks for progress, and progress restarts its timeout.
|
|
130
|
+
export function patient(options?: CacheableRequestOptions): CacheableRequestOptions {
|
|
131
|
+
return options?.timeout === undefined
|
|
132
|
+
? { ...options, timeout: 2 ** 31 - 1 }
|
|
133
|
+
: { onprogress: () => {}, resetTimeoutOnProgress: true, ...options };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const unchecked = <T>(): StandardSchemaV1<unknown, T> => ({ "~standard": { version: 1, vendor: "holon-sh", validate: value => ({ value: (value ?? {}) as T }) } });
|
package/src/protocol.ts
CHANGED
|
@@ -13,17 +13,15 @@ export type ToolCallOptions = CallOptions & { elicit?: ElicitHandler };
|
|
|
13
13
|
/** MCP request metadata and input continuation fields. Pass inputResponses and requestState when continuing an input-required result. */
|
|
14
14
|
export type CallMetadata = Omit<CallToolRequest["params"], "name" | "arguments"> & { inputResponses?: InputResponses; requestState?: string };
|
|
15
15
|
type Fields<T> = { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] };
|
|
16
|
-
type
|
|
17
|
-
|
|
16
|
+
type Completed = Omit<Fields<CallToolResult>, "structuredContent" | "isError"> & {
|
|
17
|
+
resultType: "complete";
|
|
18
|
+
/** Files the tool returned, from a server with the files extension. Their targets are removed from _meta, and a file whose fields do not have their types is left out. */
|
|
18
19
|
downloads?: Download[];
|
|
19
20
|
};
|
|
20
21
|
/** Successful tool result with contract-typed structured content. */
|
|
21
|
-
export type CompleteResult<Output> =
|
|
22
|
+
export type CompleteResult<Output> = Completed & { isError?: false; structuredContent: Output };
|
|
22
23
|
/** Full tool result: successful typed output, an application error, or an input-required continuation. */
|
|
23
|
-
export type ToolResult<Output> =
|
|
24
|
-
| CompleteResult<Output>
|
|
25
|
-
| (Omit<Fields<CallToolResult>, "structuredContent" | "isError"> & Files & { resultType: "complete"; isError: true; structuredContent?: unknown })
|
|
26
|
-
| InputRequiredResult;
|
|
24
|
+
export type ToolResult<Output> = CompleteResult<Output> | (Completed & { isError: true; structuredContent?: unknown }) | InputRequiredResult;
|
|
27
25
|
|
|
28
26
|
function json(text: string | undefined): unknown {
|
|
29
27
|
try { return text === undefined ? undefined : JSON.parse(text); } catch { return undefined; }
|
|
@@ -39,7 +37,6 @@ export class ToolCallError extends Error {
|
|
|
39
37
|
constructor(app: string, tool: string, result: ToolCallError["result"]) {
|
|
40
38
|
const asking = result.resultType === "input_required";
|
|
41
39
|
const text = (asking ? undefined : result.content.filter(part => part.type === "text").map(part => part.text).join("\n")) || undefined;
|
|
42
|
-
// A question says what it asks, and a URL question where to answer it.
|
|
43
40
|
const detail = asking
|
|
44
41
|
? Object.values(result.inputRequests ?? {}).flatMap(question => question.method === "elicitation/create"
|
|
45
42
|
? [question.params.message, question.params.mode === "url" ? question.params.url : undefined]
|
|
@@ -66,7 +63,7 @@ export interface EventType {
|
|
|
66
63
|
_meta?: Record<string, unknown>;
|
|
67
64
|
}
|
|
68
65
|
/** One occurrence of an event. `eventId` is stable, so an occurrence delivered twice is yielded once. */
|
|
69
|
-
export interface EventOccurrence<Data = unknown> { eventId: string; name: string; timestamp: string; data: Data }
|
|
66
|
+
export interface EventOccurrence<Data = unknown> { eventId: string; name: string; /** When it happened, in ISO 8601. */ timestamp: string; data: Data }
|
|
70
67
|
export interface SubscribeOptions {
|
|
71
68
|
/** Ends the subscription. */
|
|
72
69
|
signal?: AbortSignal;
|