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.
@@ -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, enabled: boolean): R & { downloads?: Download[] } {
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 = enabled ? list((meta as { downloads?: unknown } | null | undefined)?.downloads).map(target => download(target, connection)) : [];
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?.filter(part => part.type === "text").map(part => part.text).join("\n")) || undefined;
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, settings, upload } from "./ext/files.client.js";
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, settings(connection) !== undefined) : result;
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
- const list = (options?: CallOptions) => session(options, async client => {
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 (client as Client).request({ method: "events/list", params: { ...(cursor && { cursor }) } }, anything, patient(options)) as Listed;
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
- let established!: { resolve: () => void; reject: (reason: unknown) => void };
372
- const active = new Promise<void>((resolve, reject) => { established = { resolve: () => { confirmed = true; resolve(); }, reject }; });
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 session({ signal: ended }, client => (client as Client).request({ method: "events/poll", params: { ...request, cursor } }, anything, patient({ signal: ended })) as Promise<Polled>);
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
- established.resolve();
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 (endpoint.fetch ?? fetch)(input, init);
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({ ...endpoint, fetch: watched });
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); established.resolve(); });
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 active;
452
+ await established.promise;
449
453
  return {
450
454
  delivery,
451
455
  get cursor() { return cursor; },
@@ -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"}