holon-sh 0.0.1-dev.7 → 0.0.1-dev.9

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.
Files changed (52) hide show
  1. package/README.md +96 -67
  2. package/dist/context.d.ts +55 -0
  3. package/dist/context.d.ts.map +1 -0
  4. package/dist/context.js +9 -0
  5. package/dist/context.js.map +1 -0
  6. package/dist/ext/events.d.ts +268 -0
  7. package/dist/ext/events.d.ts.map +1 -0
  8. package/dist/ext/events.js +619 -0
  9. package/dist/ext/events.js.map +1 -0
  10. package/dist/ext/files.client.d.ts +50 -0
  11. package/dist/ext/files.client.d.ts.map +1 -0
  12. package/dist/ext/files.client.js +359 -0
  13. package/dist/ext/files.client.js.map +1 -0
  14. package/dist/ext/files.d.ts +21 -0
  15. package/dist/ext/files.d.ts.map +1 -0
  16. package/dist/ext/files.js +3 -0
  17. package/dist/ext/files.js.map +1 -0
  18. package/dist/ext/platform.browser.d.ts +4 -0
  19. package/dist/ext/platform.browser.d.ts.map +1 -0
  20. package/dist/ext/platform.browser.js +15 -0
  21. package/dist/ext/platform.browser.js.map +1 -0
  22. package/dist/ext/platform.node.d.ts +4 -0
  23. package/dist/ext/platform.node.d.ts.map +1 -0
  24. package/dist/ext/platform.node.js +45 -0
  25. package/dist/ext/platform.node.js.map +1 -0
  26. package/dist/ext/webhooks.d.ts +18 -0
  27. package/dist/ext/webhooks.d.ts.map +1 -0
  28. package/dist/ext/webhooks.js +527 -0
  29. package/dist/ext/webhooks.js.map +1 -0
  30. package/dist/protocol.d.ts +134 -0
  31. package/dist/protocol.d.ts.map +1 -0
  32. package/dist/protocol.js +32 -0
  33. package/dist/protocol.js.map +1 -0
  34. package/dist/runtime.d.ts +72 -0
  35. package/dist/runtime.d.ts.map +1 -0
  36. package/dist/runtime.js +453 -0
  37. package/dist/runtime.js.map +1 -0
  38. package/package.json +56 -19
  39. package/{context.d.ts → src/context.ts} +11 -1
  40. package/src/ext/events.ts +812 -0
  41. package/src/ext/files.client.ts +329 -0
  42. package/src/ext/files.ts +16 -0
  43. package/src/ext/platform.browser.ts +15 -0
  44. package/src/ext/platform.node.ts +43 -0
  45. package/src/ext/webhooks.ts +512 -0
  46. package/src/protocol.ts +119 -0
  47. package/src/runtime.ts +467 -0
  48. package/context.mjs +0 -6
  49. package/protocol.d.ts +0 -51
  50. package/runtime.d.ts +0 -47
  51. package/runtime.mjs +0 -207
  52. package/validate.mjs +0 -12
package/runtime.mjs DELETED
@@ -1,207 +0,0 @@
1
- import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
2
- import { validator as defaultValidator } from "./validate.mjs";
3
-
4
- // Environment-bound calls reuse the last server/discover result for the same endpoint and token instead of discovering
5
- // the server on every call.
6
- const DISCOVERY = Symbol("discovery");
7
- const discovered = new Map();
8
-
9
- function environment(identity, name) {
10
- if (!Object.hasOwn(identity.requires ?? {}, name)) return undefined;
11
- const prefix = `${name.toUpperCase().replaceAll("-", "_")}_MCP_`;
12
- const endpoint = process.env[`${prefix}URL`];
13
- // Environment-bound dependencies connect for one call at a time, sending <NAME>_MCP_TOKEN as the bearer token when it is set.
14
- const token = process.env[`${prefix}TOKEN`];
15
- if (endpoint) return { endpoint, scoped: true, [DISCOVERY]: `${endpoint} ${token ?? ""}`, ...(token && { authProvider: { token: async () => token } }) };
16
- throw new Error(`Dependency ${name} is unavailable (${prefix}URL). Provide a binding or set the environment variable.`);
17
- }
18
-
19
- function json(text) {
20
- try { return JSON.parse(text); } catch { return undefined; }
21
- }
22
-
23
- export class ToolCallError extends Error {
24
- constructor(app, tool, result) {
25
- const asking = result.resultType === "input_required";
26
- const text = result.content?.filter(part => part.type === "text").map(part => part.text).join("\n") || undefined;
27
- const detail = asking
28
- ? Object.values(result.inputRequests ?? {}).flatMap(({ params }) => [params?.message, params?.url]).filter(Boolean).join(" ")
29
- : text;
30
- super(`${app}.${tool}: ${asking ? "additional input required" : "tool failed"}${detail ? `: ${detail}` : ""}`);
31
- this.name = "ToolCallError";
32
- this.result = result;
33
- this.text = text;
34
- this.data = result.structuredContent ?? json(text);
35
- }
36
- }
37
-
38
- function validate(check, value, label) {
39
- if (!check(value)) throw new Error(`Invalid ${label}: ${JSON.stringify(check.errors)}`);
40
- }
41
-
42
- function untilAborted(promise, signal) {
43
- if (!signal) return promise;
44
- return new Promise((resolve, reject) => {
45
- const aborted = () => reject(signal.reason);
46
- signal.addEventListener("abort", aborted, { once: true });
47
- promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", aborted));
48
- });
49
- }
50
-
51
- // The MCP client ends a request after 60 s unless given a timeout. A request without one waits as long as a timer can
52
- // (2^31 - 1 ms, about 24.8 days). A request with one asks for progress, and progress restarts its timeout.
53
- function patient(options) {
54
- return options?.timeout === undefined
55
- ? { ...options, timeout: 2 ** 31 - 1 }
56
- : { onprogress: () => {}, resetTimeoutOnProgress: true, ...options };
57
- }
58
-
59
- function delay(milliseconds, signal) {
60
- return new Promise(resolve => {
61
- const timer = setTimeout(resolve, milliseconds);
62
- signal?.addEventListener("abort", () => { clearTimeout(timer); resolve(); }, { once: true });
63
- });
64
- }
65
-
66
- export function bindApps(identity, contracts, bindings, options) {
67
- const selected = Object.fromEntries(Object.keys(bindings).map(name => {
68
- if (!Object.hasOwn(contracts, name) && !Object.hasOwn(identity.requires ?? {}, name)) throw new Error("Unknown app binding: " + name);
69
- return [name, contracts[name] ?? null];
70
- }));
71
- return createApps(identity, selected, bindings, options);
72
- }
73
-
74
- export function createApps(identity, contracts, bindings, options) {
75
- const { validator = defaultValidator, resolve = name => environment(identity, name), session: hosted } = options ?? {};
76
- const apps = Object.create(null);
77
- // Without explicit bindings, every declared dependency gets a client; those without a contract are untyped.
78
- const members = { ...(bindings ? {} : Object.fromEntries(Object.keys(identity.requires ?? {}).map(name => [name, null]))), ...contracts };
79
- for (const [name, contract] of Object.entries(members)) {
80
- const binding = bindings?.[name];
81
- // Without explicit bindings, the resolve hook supplies the binding on every call.
82
- const current = () => binding ?? (bindings ? undefined : resolve(name));
83
- let connection;
84
- const connect = (resolved, discover) => {
85
- // 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
86
- // 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
87
- const client = new Client({ name: identity.name, version: identity.version }, { capabilities: { elicitation: { form: {}, url: {} } }, versionNegotiation: { mode: "auto" } });
88
- const transport = new StreamableHTTPClientTransport(new URL(resolved.endpoint), { authProvider: resolved.authProvider, fetch: async (input, init) => {
89
- const response = await (resolved.fetch ?? fetch)(input, init);
90
- current.answered ||= response.ok;
91
- return response;
92
- }, requestInit: { credentials: "include", redirect: "error", headers: resolved.installationUID ? { "X-Holon-Installation-UID": resolved.installationUID } : {} } });
93
- const current = { client, close: async () => {
94
- await transport.close();
95
- await current.ready.catch(() => {});
96
- } };
97
- transport.onclose = () => { if (connection === current) connection = undefined; };
98
- current.ready = client.connect(transport, discover && { prior: { kind: "modern", discover } }).catch(async error => {
99
- await transport.close();
100
- throw error;
101
- });
102
- return current;
103
- };
104
- // The session hook serves bindings the runtime does not connect to itself.
105
- const host = binding && hosted?.(name, binding);
106
- const session = async (options, run) => {
107
- options?.signal?.throwIfAborted();
108
- if (host) return run(host);
109
- const resolved = current();
110
- if (!resolved) throw new Error(Object.hasOwn(identity.requires ?? {}, name)
111
- ? `Dependency ${name} is unavailable. Provide a binding.`
112
- : `Dependency ${name} is not declared by this application.`);
113
- if (!resolved.endpoint) throw new Error(`Dependency ${name} binding has no endpoint.`);
114
- // Bindings marked scoped connect for one call at a time.
115
- const scoped = resolved.scoped === true;
116
- const key = resolved[DISCOVERY];
117
- const prior = discovered.get(key);
118
- const active = scoped ? connect(resolved, prior) : (connection ??= connect(resolved));
119
- try {
120
- await untilAborted(active.ready, options?.signal);
121
- if (key) discovered.set(key, active.client.getDiscoverResult());
122
- return await run(active.client);
123
- } catch (error) {
124
- // A server that no longer speaks the discovered protocol rejects the first request without running it.
125
- if (!prior || active.answered || error?.status !== 400) throw error;
126
- discovered.delete(key);
127
- return session(options, run);
128
- } finally { if (scoped) await active.close(); }
129
- };
130
- const tools = Object.create(null);
131
- const definitions = new Map((contract?.tools ?? []).map(definition => [definition.name, { definition }]));
132
- const callTool = async (request, { elicit, ...options } = {}) => {
133
- const tool = definitions.get(request.name);
134
- if (!tool && contract) throw new Error(`Unknown tool ${name}.${request.name}.`);
135
- const result = await session(options, async client => {
136
- const send = request => client.callTool(request, {
137
- ...patient(options), allowInputRequired: true,
138
- ...(tool && { toolDefinition: { ...tool.definition, outputSchema: undefined } }),
139
- });
140
- let result = await send(request);
141
- // A server may keep asking, so answering stops after ten rounds, the MCP clients' own limit, and the last question reaches the caller.
142
- for (let round = 0; elicit && result.resultType === "input_required" && round < 10; round++) {
143
- const inputResponses = {};
144
- for (const [key, { params }] of Object.entries(result.inputRequests ?? {})) inputResponses[key] = await elicit(params);
145
- result = await send({ ...request, inputResponses, requestState: result.requestState });
146
- }
147
- return result;
148
- });
149
- if (result.resultType === "input_required") return result;
150
- // The server validates arguments; the client checks the output against the contract it was compiled against.
151
- if (!result.isError && tool?.definition.outputSchema) {
152
- validate(tool.output ??= validator(tool.definition.outputSchema), result.structuredContent, `structured output from ${name}.${request.name}`);
153
- }
154
- return { ...result, resultType: "complete" };
155
- };
156
- const listTools = options => session(options, client => client.listTools(undefined, patient(options)));
157
- for (const tool of contract?.tools ?? []) if (!["then", "toJSON", "toString", "valueOf", "toLocaleString"].includes(tool.name)) {
158
- tools[tool.name] = async (input, options) => {
159
- const result = await callTool({ name: tool.name, arguments: input }, options);
160
- if (result.resultType === "input_required" || result.isError) throw new ToolCallError(name, tool.name, result);
161
- return tool.outputSchema ? result.structuredContent : result;
162
- };
163
- }
164
- const resources = Object.create(null);
165
- for (const resource of contract?.resources ?? []) {
166
- let valid;
167
- const parse = result => {
168
- const value = JSON.parse(result.contents[0].text);
169
- valid ??= validator(resource.outputSchema);
170
- validate(valid, value, `contents from ${name} ${resource.uri}`);
171
- return value;
172
- };
173
- const read = async options => parse(await session(options, client => client.readResource({ uri: resource.uri }, patient(options))));
174
- const watch = async (onChange, { signal } = {}) => {
175
- const resolved = current();
176
- if (!resolved?.endpoint) throw new Error(`Watching ${name} ${resource.uri} requires an endpoint binding.`);
177
- for (let established = false; !signal?.aborted; await delay(1000, signal)) {
178
- const current = connect(resolved);
179
- let failure;
180
- try {
181
- await untilAborted(current.ready, signal);
182
- const { client } = current;
183
- if (!client.getServerCapabilities()?.resources?.subscribe) throw new Error(`${name} does not advertise resource subscriptions.`);
184
- let reads = Promise.resolve();
185
- const refresh = () => (reads = reads.then(async () => onChange(parse(await client.readResource({ uri: resource.uri }, patient({ signal, cacheMode: "bypass" })))))
186
- .catch(error => { failure ??= error; return current.close(); }));
187
- client.setNotificationHandler("notifications/resources/updated", ({ params }) => { if (params.uri === resource.uri) void refresh(); });
188
- const subscription = await client.listen({ resourceSubscriptions: [resource.uri] }, { signal });
189
- if (!subscription.honoredFilter.resourceSubscriptions?.includes(resource.uri)) throw new Error(`${name} did not accept a subscription to ${resource.uri}.`);
190
- established = true;
191
- await refresh();
192
- await subscription.closed;
193
- } catch (error) {
194
- failure ??= error;
195
- } finally {
196
- await current.close();
197
- }
198
- if (signal?.aborted) return;
199
- if (failure && !established) throw failure;
200
- }
201
- };
202
- resources[resource.uri] = Object.freeze({ read, watch });
203
- }
204
- apps[name] = Object.freeze({ callTool, listTools, resources: Object.freeze(resources), close: async () => { await connection?.close(); }, tools: Object.freeze(tools) });
205
- }
206
- return Object.freeze(apps);
207
- }
package/validate.mjs DELETED
@@ -1,12 +0,0 @@
1
- import { AjvJsonSchemaValidator } from "@modelcontextprotocol/client/validators/ajv";
2
-
3
- // The MCP client's own Ajv, one engine per schema so contracts that reuse an $id stay apart.
4
- export function validator(schema) {
5
- const validate = new AjvJsonSchemaValidator().getValidator(schema);
6
- const check = value => {
7
- const { valid, errorMessage } = validate(value);
8
- check.errors = errorMessage;
9
- return valid;
10
- };
11
- return check;
12
- }