holon-sh 0.0.1-dev.11 → 0.0.1-dev.13
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 +2 -30
- package/dist/ext/events.d.ts +15 -155
- package/dist/ext/events.d.ts.map +1 -1
- package/dist/ext/events.js +103 -218
- package/dist/ext/events.js.map +1 -1
- package/dist/ext/files.client.d.ts +1 -1
- package/dist/ext/files.client.d.ts.map +1 -1
- package/dist/ext/files.client.js +2 -2
- package/dist/ext/files.client.js.map +1 -1
- package/dist/protocol.js +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +20 -13
- package/dist/runtime.js.map +1 -1
- package/package.json +1 -2
- package/src/ext/events.ts +140 -328
- package/src/ext/files.client.ts +2 -2
- package/src/protocol.ts +1 -1
- package/src/runtime.ts +20 -14
- package/dist/ext/webhooks.d.ts +0 -18
- package/dist/ext/webhooks.d.ts.map +0 -1
- package/dist/ext/webhooks.js +0 -527
- package/dist/ext/webhooks.js.map +0 -1
- package/src/ext/webhooks.ts +0 -512
package/src/ext/files.client.ts
CHANGED
|
@@ -321,9 +321,9 @@ function download(raw: unknown, connection: FileConnection): Download {
|
|
|
321
321
|
return Object.freeze({ ...Object.fromEntries(Object.entries({ name, mimeType, size, digest, expiresAt }).filter(([, value]) => value !== undefined)), blob }) as Download;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
-
export function downloads<R extends { resultType?: string; _meta?: object }>(result: R, connection: FileConnection
|
|
324
|
+
export function downloads<R extends { resultType?: string; _meta?: object }>(result: R, connection: FileConnection): R & { downloads?: Download[] } {
|
|
325
325
|
if (result.resultType === "input_required" || !isObject(result._meta) || !(EXTENSION in result._meta)) return result;
|
|
326
326
|
const { [EXTENSION]: meta, ...rest } = result._meta;
|
|
327
|
-
const handles =
|
|
327
|
+
const handles = settings(connection) ? list((meta as { downloads?: unknown } | null | undefined)?.downloads).map(target => download(target, connection)) : [];
|
|
328
328
|
return { ...result, _meta: rest, ...(handles.length > 0 && { downloads: handles }) };
|
|
329
329
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -38,7 +38,7 @@ export class ToolCallError extends Error {
|
|
|
38
38
|
readonly data?: unknown;
|
|
39
39
|
constructor(app: string, tool: string, result: ToolCallError["result"]) {
|
|
40
40
|
const asking = result.resultType === "input_required";
|
|
41
|
-
const text = (asking ? undefined : result.content
|
|
41
|
+
const text = (asking ? undefined : result.content.filter(part => part.type === "text").map(part => part.text).join("\n")) || undefined;
|
|
42
42
|
// A question says what it asks, and a URL question where to answer it.
|
|
43
43
|
const detail = asking
|
|
44
44
|
? Object.values(result.inputRequests ?? {}).flatMap(question => question.method === "elicitation/create"
|
package/src/runtime.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Client, isInputRequiredResult, SdkHttpError, StreamableHTTPClientTransp
|
|
|
2
2
|
import type { CacheableRequestOptions, CallToolRequest, CallToolRequestOptions, CallToolResult, InputRequiredResult, InputResponses, ListToolsRequest, ListToolsResult, PriorDiscovery, ReadResourceRequest, ReadResourceResult, Tool } from "@modelcontextprotocol/client";
|
|
3
3
|
import { AjvJsonSchemaValidator } from "@modelcontextprotocol/client/validators/ajv";
|
|
4
4
|
import { EXTENSION } from "./ext/files.js";
|
|
5
|
-
import { acceptUploadRequests, downloads, isUploadRequest, prepare,
|
|
5
|
+
import { acceptUploadRequests, downloads, isUploadRequest, prepare, upload } from "./ext/files.client.js";
|
|
6
6
|
import { ToolCallError } from "./protocol.js";
|
|
7
7
|
import type { AuthProvider, CallMetadata, CallOptions, EventOccurrence, EventType, SubscribeOptions, Subscription, ToolCallOptions, ToolClient, ToolResult } from "./protocol.js";
|
|
8
8
|
|
|
@@ -253,7 +253,7 @@ export function createApps<B = Binding>(identity: Identity, contracts: Record<st
|
|
|
253
253
|
throw error;
|
|
254
254
|
}
|
|
255
255
|
// A server may keep asking, so answering stops after ten rounds, the MCP clients' own limit, and the last question reaches the caller.
|
|
256
|
-
if (!elicit || !isInputRequiredResult(result) || round++ === 10) return connection ? downloads(result, connection
|
|
256
|
+
if (!elicit || !isInputRequiredResult(result) || round++ === 10) return connection ? downloads(result, connection) : result;
|
|
257
257
|
const inputResponses: InputResponses = {};
|
|
258
258
|
for (const [key, question] of Object.entries(result.inputRequests ?? {})) {
|
|
259
259
|
// Every client declares elicitation alone, so it answers nothing else and such a question reaches the caller.
|
|
@@ -324,10 +324,15 @@ export function createApps<B = Binding>(identity: Identity, contracts: Record<st
|
|
|
324
324
|
resources[resource.uri] = Object.freeze({ read, watch });
|
|
325
325
|
}
|
|
326
326
|
// MCP Events, the draft extension: https://github.com/modelcontextprotocol/experimental-ext-triggers-events/blob/main/docs/design-sketch-proposal.md
|
|
327
|
-
|
|
327
|
+
// Its requests go over the runtime's own connection, which a hosted session does not have.
|
|
328
|
+
const connected = <T>(options: CallOptions | undefined, run: (client: Client) => Promise<T>) => session(options, (_, connection) => {
|
|
329
|
+
if (!connection) throw new Error(`MCP Events of ${name} require an endpoint binding.`);
|
|
330
|
+
return run(connection.client);
|
|
331
|
+
});
|
|
332
|
+
const list = (options?: CallOptions) => connected(options, async client => {
|
|
328
333
|
const events: EventType[] = [];
|
|
329
334
|
for (let cursor: string | undefined; ;) {
|
|
330
|
-
const page = await
|
|
335
|
+
const page = await client.request({ method: "events/list", params: { ...(cursor && { cursor }) } }, anything, patient(options)) as Listed;
|
|
331
336
|
events.push(...page.events ?? []);
|
|
332
337
|
if (!(cursor = page.nextCursor)) return events;
|
|
333
338
|
}
|
|
@@ -335,7 +340,6 @@ export function createApps<B = Binding>(identity: Identity, contracts: Record<st
|
|
|
335
340
|
const subscribe = async (event: string, args: unknown = {}, { signal, onGap, maxAgeMs, ...options }: SubscribeOptions = {}): Promise<Subscription> => {
|
|
336
341
|
const resolved = current();
|
|
337
342
|
if (!resolved?.endpoint) throw new Error(`Subscribing to ${name} ${event} requires an endpoint binding.`);
|
|
338
|
-
const endpoint = resolved;
|
|
339
343
|
const offered = (await list(signal && { signal })).find(type => type.name === event)?.delivery;
|
|
340
344
|
if (!offered) throw new Error(`${name} has no event ${event}.`);
|
|
341
345
|
// Push hears of an event at once, and poll works wherever a request does. Webhook delivery is not supported.
|
|
@@ -368,16 +372,16 @@ export function createApps<B = Binding>(identity: Identity, contracts: Record<st
|
|
|
368
372
|
};
|
|
369
373
|
const request = { name: event, arguments: args, ...(maxAgeMs !== undefined && { maxAgeMs }) };
|
|
370
374
|
let confirmed = false;
|
|
371
|
-
|
|
372
|
-
const
|
|
375
|
+
const established = Promise.withResolvers<void>();
|
|
376
|
+
const confirm = () => { confirmed = true; established.resolve(); };
|
|
373
377
|
const poll = async () => {
|
|
374
378
|
while (!ended.aborted) {
|
|
375
379
|
let wait = 1000;
|
|
376
380
|
try {
|
|
377
|
-
const result = await
|
|
381
|
+
const result = await connected({ signal: ended }, client => client.request({ method: "events/poll", params: { ...request, cursor } }, anything, patient({ signal: ended })) as Promise<Polled>);
|
|
378
382
|
try { for (const occurrence of result.events ?? []) deliver(occurrence); } catch (error) { failure = error; return cancelled.abort(); }
|
|
379
383
|
reach(result.cursor, result.truncated);
|
|
380
|
-
|
|
384
|
+
confirm();
|
|
381
385
|
// A server that asks for a tight loop is not obeyed.
|
|
382
386
|
wait = result.hasMore ? 0 : Math.max(1000, result.nextPollMs ?? 0);
|
|
383
387
|
} catch (error) {
|
|
@@ -389,11 +393,12 @@ export function createApps<B = Binding>(identity: Identity, contracts: Record<st
|
|
|
389
393
|
};
|
|
390
394
|
const stream = async () => {
|
|
391
395
|
while (!ended.aborted) {
|
|
396
|
+
let wait = 1000;
|
|
392
397
|
const quiet = new AbortController();
|
|
393
398
|
// The MCP client keeps waiting for the answer of a request whose stream has ended, so the end is watched for here.
|
|
394
399
|
// Only the stream's own response is watched: a 2025 server answers its initialization as a stream too.
|
|
395
400
|
const watched: typeof fetch = async (input, init) => {
|
|
396
|
-
const response = await (
|
|
401
|
+
const response = await (resolved.fetch ?? fetch)(input, init);
|
|
397
402
|
const streaming = typeof init?.body === "string" && init.body.includes('"method":"events/stream"');
|
|
398
403
|
if (!streaming || !response.body || !response.headers.get("content-type")?.startsWith("text/event-stream")) return response;
|
|
399
404
|
const reader = response.body.getReader();
|
|
@@ -409,7 +414,7 @@ export function createApps<B = Binding>(identity: Identity, contracts: Record<st
|
|
|
409
414
|
return new Response(body, response);
|
|
410
415
|
};
|
|
411
416
|
// Each stream has its own connection, so every event notification on it is this subscription's.
|
|
412
|
-
const connection = connect({ ...
|
|
417
|
+
const connection = connect({ ...resolved, fetch: watched });
|
|
413
418
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
414
419
|
const heard = () => {
|
|
415
420
|
clearTimeout(timer);
|
|
@@ -419,8 +424,9 @@ export function createApps<B = Binding>(identity: Identity, contracts: Record<st
|
|
|
419
424
|
try {
|
|
420
425
|
await untilAborted(connection.ready, ended);
|
|
421
426
|
const on = (method: string, handle: (params: any) => void) => connection.client.setNotificationHandler(`notifications/events/${method}`, { params: anything }, params => { heard(); handle(params); });
|
|
422
|
-
on("active", ({ cursor: next, truncated }) => { reach(next, truncated);
|
|
427
|
+
on("active", ({ cursor: next, truncated }) => { reach(next, truncated); confirm(); });
|
|
423
428
|
on("event", occurrence => {
|
|
429
|
+
wait = 0;
|
|
424
430
|
try { deliver(occurrence); } catch (error) { failure = error; return cancelled.abort(); }
|
|
425
431
|
reach(occurrence.cursor);
|
|
426
432
|
});
|
|
@@ -437,13 +443,13 @@ export function createApps<B = Binding>(identity: Identity, contracts: Record<st
|
|
|
437
443
|
await connection.close();
|
|
438
444
|
}
|
|
439
445
|
if (terminated) throw Object.assign(new Error(`${name} ended the subscription to ${event}: ${terminated.message}`), { code: terminated.code, data: terminated.data });
|
|
440
|
-
await sleep(
|
|
446
|
+
await sleep(wait, ended);
|
|
441
447
|
}
|
|
442
448
|
};
|
|
443
449
|
void (delivery === "push" ? stream() : poll())
|
|
444
450
|
.catch(error => { failure ??= error; })
|
|
445
451
|
.finally(() => { done = true; wake(); established.reject(failure ?? ended.reason); });
|
|
446
|
-
await
|
|
452
|
+
await established.promise;
|
|
447
453
|
return {
|
|
448
454
|
delivery,
|
|
449
455
|
get cursor() { return cursor; },
|
package/dist/ext/webhooks.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { DeliveryMode } from "./events.js";
|
|
2
|
-
/** How webhook delivery runs: createEvents' `webhook` option. Its `webhookTtl` turns it on and caps grants. */
|
|
3
|
-
export interface WebhookOptions {
|
|
4
|
-
/** For tests only: accepts http callbacks and callbacks at private addresses, such as a receiver on localhost. */
|
|
5
|
-
allowInsecureLocalCallbacksForTesting?: boolean;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* The webhook delivery mode, which createEvents adds when given a webhookTtl, the `ttl` here. Access is checked again
|
|
9
|
-
* every `heartbeat`, as push streams check it. It serves
|
|
10
|
-
* events/subscribe and events/unsubscribe to authenticated callers, verifies each callback by a challenge before
|
|
11
|
-
* delivering to it, signs each delivery per Standard Webhooks with the client's secret, retries failed ones, and lets
|
|
12
|
-
* subscriptions lapse when not refreshed.
|
|
13
|
-
*/
|
|
14
|
-
export declare function webhooks({ ttl, heartbeat, allowInsecureLocalCallbacksForTesting }: WebhookOptions & {
|
|
15
|
-
ttl: number;
|
|
16
|
-
heartbeat: number;
|
|
17
|
-
}): DeliveryMode;
|
|
18
|
-
//# sourceMappingURL=webhooks.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../src/ext/webhooks.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAqF,MAAM,aAAa,CAAC;AAiBnI,+GAA+G;AAC/G,MAAM,WAAW,cAAc;IAC7B,kHAAkH;IAClH,qCAAqC,CAAC,EAAE,OAAO,CAAC;CACjD;AAuID;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,qCAA6C,EAAE,EAAE,cAAc,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,YAAY,CAkV7J"}
|