holon-sh 0.0.1-dev.12 → 0.0.1-dev.14
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 +33 -154
- package/dist/ext/events.d.ts.map +1 -1
- package/dist/ext/events.js +146 -224
- 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 +17 -12
- package/dist/runtime.js.map +1 -1
- package/package.json +1 -2
- package/src/ext/events.ts +201 -331
- package/src/ext/files.client.ts +2 -2
- package/src/protocol.ts +1 -1
- package/src/runtime.ts +17 -13
- 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) {
|
|
@@ -394,7 +398,7 @@ export function createApps<B = Binding>(identity: Identity, contracts: Record<st
|
|
|
394
398
|
// The MCP client keeps waiting for the answer of a request whose stream has ended, so the end is watched for here.
|
|
395
399
|
// Only the stream's own response is watched: a 2025 server answers its initialization as a stream too.
|
|
396
400
|
const watched: typeof fetch = async (input, init) => {
|
|
397
|
-
const response = await (
|
|
401
|
+
const response = await (resolved.fetch ?? fetch)(input, init);
|
|
398
402
|
const streaming = typeof init?.body === "string" && init.body.includes('"method":"events/stream"');
|
|
399
403
|
if (!streaming || !response.body || !response.headers.get("content-type")?.startsWith("text/event-stream")) return response;
|
|
400
404
|
const reader = response.body.getReader();
|
|
@@ -410,7 +414,7 @@ export function createApps<B = Binding>(identity: Identity, contracts: Record<st
|
|
|
410
414
|
return new Response(body, response);
|
|
411
415
|
};
|
|
412
416
|
// Each stream has its own connection, so every event notification on it is this subscription's.
|
|
413
|
-
const connection = connect({ ...
|
|
417
|
+
const connection = connect({ ...resolved, fetch: watched });
|
|
414
418
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
415
419
|
const heard = () => {
|
|
416
420
|
clearTimeout(timer);
|
|
@@ -420,7 +424,7 @@ export function createApps<B = Binding>(identity: Identity, contracts: Record<st
|
|
|
420
424
|
try {
|
|
421
425
|
await untilAborted(connection.ready, ended);
|
|
422
426
|
const on = (method: string, handle: (params: any) => void) => connection.client.setNotificationHandler(`notifications/events/${method}`, { params: anything }, params => { heard(); handle(params); });
|
|
423
|
-
on("active", ({ cursor: next, truncated }) => { reach(next, truncated);
|
|
427
|
+
on("active", ({ cursor: next, truncated }) => { reach(next, truncated); confirm(); });
|
|
424
428
|
on("event", occurrence => {
|
|
425
429
|
wait = 0;
|
|
426
430
|
try { deliver(occurrence); } catch (error) { failure = error; return cancelled.abort(); }
|
|
@@ -445,7 +449,7 @@ export function createApps<B = Binding>(identity: Identity, contracts: Record<st
|
|
|
445
449
|
void (delivery === "push" ? stream() : poll())
|
|
446
450
|
.catch(error => { failure ??= error; })
|
|
447
451
|
.finally(() => { done = true; wake(); established.reject(failure ?? ended.reason); });
|
|
448
|
-
await
|
|
452
|
+
await established.promise;
|
|
449
453
|
return {
|
|
450
454
|
delivery,
|
|
451
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"}
|