holon-sh 0.0.0-stage → 0.0.1-dev.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Holon Systems, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,109 @@
1
- # Temporary Holding Version
1
+ # Holon TypeScript SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/holon-sh/sdk-ts/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/holon-sh/sdk-ts/actions/workflows/ci.yml)
4
+ [![Node.js](https://img.shields.io/badge/Node.js-22%2B-5FA04E?logo=node.js&logoColor=white)](https://nodejs.org/en/download)
5
+ [![MIT License](https://img.shields.io/badge/license-MIT-green)](https://github.com/holon-sh/sdk-ts/blob/main/LICENSE)
6
+
7
+ This repository contains the Node.js runtime for [`holon`](https://github.com/holon-sh/cli).
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install holon-sh@dev
13
+ npm install -D holon-types
14
+ ```
15
+
16
+ `holon-types` is the `holon` CLI. `holon types check` brings its own TypeScript compiler and uses the project's `typescript` when one is installed.
17
+
18
+ ## Invoking MCP Servers
19
+
20
+ The Holon TypeScript SDK allows generation of type-safe MCP clients. See [Example](https://github.com/holon-sh/cli#example) for an explanation, or [Dependencies](https://github.com/holon-sh/cli#dependencies) to understand dependency resolution.
21
+
22
+ ### Type-checking MCP invocations
23
+
24
+ ```sh
25
+ npx holon types check
26
+ ```
27
+
28
+ <details open>
29
+ <summary><h3>Call a tool</h3></summary>
30
+
31
+ ```ts
32
+ import { apps } from "holon-sh";
33
+
34
+ export async function greet(name: string): Promise<string> {
35
+ const greeting = await apps.bar.tools.greet({ name }, { timeout: 5000 });
36
+ return greeting.result;
37
+ }
38
+ ```
39
+
40
+ Arguments are validated before sending; the result is validated against the output schema and returned.
41
+
42
+ </details>
43
+
44
+ <details open>
45
+ <summary><h3>Handle errors</h3></summary>
46
+
47
+ ```ts
48
+ import { ToolCallError, apps } from "holon-sh";
49
+
50
+ try {
51
+ await apps.bar.tools.greet({ name: "world" });
52
+ } catch (error) {
53
+ if (!(error instanceof ToolCallError)) throw error;
54
+ error.code; // "tool_error" | "input_required"
55
+ error.result; // the original MCP result
56
+ }
57
+ ```
58
+
59
+ </details>
60
+
61
+ <details open>
62
+ <summary><h3>Read the whole result</h3></summary>
63
+
64
+ `callTool` returns the full MCP result instead of throwing:
65
+
66
+ ```ts
67
+ const result = await apps.bar.callTool({ name: "greet", arguments: { name: "world" } });
68
+ if (result.resultType === "input_required") {
69
+ // call again with inputResponses and requestState
70
+ } else if (result.isError) {
71
+ // inspect result.content
72
+ } else {
73
+ result.structuredContent; // typed as Outputs["bar"]["greet"]
74
+ }
75
+ ```
76
+
77
+ </details>
78
+
79
+ <details open>
80
+ <summary><h3>Read a resource</h3></summary>
81
+
82
+ A provider types a JSON resource by registering it with `mimeType: "application/json"` and an `"sh.holon/outputSchema"` JSON Schema in its `_meta`, as the `orders` server in `tests/fixtures` does. The client then offers `read` and `watch`:
83
+
84
+ ```ts
85
+ const summary = await apps.orders.resources["orders://summary"].read();
86
+
87
+ const controller = new AbortController();
88
+ await apps.orders.resources["orders://summary"].watch(value => console.log(value.total), {
89
+ signal: controller.signal,
90
+ });
91
+ ```
92
+
93
+ `watch` calls back with the current value, then on every update, until the signal aborts. It needs an endpoint binding and a server that supports resource subscriptions.
94
+
95
+ </details>
96
+
97
+ ## Serve an [MCP App](https://modelcontextprotocol.io/extensions/apps/overview) view
98
+
99
+ ```ts
100
+ import { appView } from "holon-sh/view";
101
+
102
+ server.registerResource(
103
+ ...appView("Foo", "ui://foo/index.html", new URL("./dist/index.html", import.meta.url), {
104
+ csp: { connectDomains: ["https://bar.example.com"] },
105
+ }),
106
+ );
107
+ ```
108
+
109
+ The page itself uses [`holon-ui`](https://github.com/holon-sh/sdk-ui).
@@ -0,0 +1,4 @@
1
+ {
2
+ "marker": "holon-generated-apps-v1",
3
+ "placeholder": true
4
+ }
@@ -0,0 +1,12 @@
1
+ // holon-generated-apps-v1 placeholder
2
+ export type Apps = Record<never, never>;
3
+ export type Inputs = Record<never, never>;
4
+ export type Outputs = Record<never, never>;
5
+ export type InputTypes = Inputs;
6
+ export type OutputTypes = Outputs;
7
+ export type Resources = Record<never, never>;
8
+ export { ToolCallError } from "../protocol.js";
9
+ export type { Binding, EndpointBinding, CreateOptions } from "../runtime.js";
10
+ import type { CreateOptions } from "../runtime.js";
11
+ export declare function createApps(bindings: Record<string, never>, options?: CreateOptions): Apps;
12
+ export declare const apps: Apps;
@@ -0,0 +1,5 @@
1
+ // holon-generated-apps-v1 placeholder
2
+ import {createApps as create} from "../runtime.mjs";
3
+ export {ToolCallError} from "../runtime.mjs";
4
+ export const apps = create({name:"unconfigured",version:"0",requires:{}}, {});
5
+ export function createApps(bindings, options) { if (Object.keys(bindings).length) throw new Error("Run holon types generate to configure app bindings."); return apps; }
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "holon-sh",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1-dev.3",
4
+ "description": "Typed MCP clients and app view resources for Node.js.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/holon-sh/sdk-ts.git",
8
+ "directory": "packages/node"
9
+ },
10
+ "license": "MIT",
11
+ "type": "module",
12
+ "engines": {
13
+ "node": ">=22"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "holon": {
19
+ "slot": "generated",
20
+ "contractVersion": 1
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "types": "./generated/index.d.ts",
25
+ "import": "./generated/index.js"
26
+ },
27
+ "./runtime": {
28
+ "types": "./runtime.d.ts",
29
+ "import": "./runtime.mjs"
30
+ },
31
+ "./view": {
32
+ "types": "./view.d.ts",
33
+ "import": "./view.js"
34
+ },
35
+ "./protocol": {
36
+ "types": "./protocol.d.ts"
37
+ }
38
+ },
39
+ "files": [
40
+ "runtime.mjs",
41
+ "runtime.d.ts",
42
+ "validate.mjs",
43
+ "protocol.d.ts",
44
+ "view.js",
45
+ "view.d.ts",
46
+ "generated/",
47
+ "README.md"
48
+ ],
49
+ "scripts": {
50
+ "build": "node scripts/build.mjs",
51
+ "prepack": "npm run build",
52
+ "docs:build": "node scripts/build-reference.mjs"
53
+ },
54
+ "dependencies": {
55
+ "@modelcontextprotocol/client": "^2.3.1",
56
+ "ajv": "^8.20.0",
57
+ "ajv-formats": "^3.0.1"
58
+ }
59
+ }
package/protocol.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ import type { CallToolRequest, CallToolResult, InputRequiredResult, InputResponses, ListToolsResult, RequestOptions } from "@modelcontextprotocol/client";
2
+
3
+ /** Bearer-token or OAuth provider accepted by the MCP transport. */
4
+ export type AuthProvider =
5
+ | { token(): Promise<string | undefined>; onUnauthorized?(context: unknown): Promise<void> }
6
+ | { tokens(): unknown; redirectUrl: unknown; clientMetadata: unknown };
7
+
8
+ /** MCP request options, including AbortSignal and timeout in milliseconds. Input-required responses are enabled by the runtime. */
9
+ export type CallOptions = Omit<RequestOptions, "allowInputRequired">;
10
+ /** MCP request metadata and input continuation fields. Pass inputResponses and requestState when continuing an input-required result. */
11
+ export type CallMetadata = Omit<CallToolRequest["params"], "name" | "arguments"> & { inputResponses?: InputResponses; requestState?: string };
12
+ /** @internal */
13
+ type Fields<T> = { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] };
14
+ /** Full tool result: successful typed output, an application error, or an input-required continuation. */
15
+ export type ToolResult<Output> =
16
+ | (Omit<Fields<CallToolResult>, "structuredContent" | "isError"> & { resultType: "complete"; isError?: false; structuredContent: Output })
17
+ | (Omit<Fields<CallToolResult>, "structuredContent" | "isError"> & { resultType: "complete"; isError: true; structuredContent?: unknown })
18
+ | InputRequiredResult;
19
+
20
+ /** Error thrown by the tools convenience methods for tool_error or input_required results. Inspect app, tool, code, and the original result; use callTool for continuation workflows. */
21
+ export declare class ToolCallError extends Error {
22
+ readonly code: "tool_error" | "input_required";
23
+ readonly app: string;
24
+ readonly tool: string;
25
+ readonly result: Extract<ToolResult<unknown>, { isError: true } | { resultType: "input_required" }>;
26
+ constructor(app: string, tool: string, result: ToolCallError["result"]);
27
+ }
28
+
29
+ /** Client operations shared by generated app clients. Transport, validation, and protocol errors reject the returned promises. */
30
+ export interface ToolClient<I, O, R> {
31
+ /** Close the cached endpoint session. Scoped calls close their own session; a host-owned session is closed by its host. */
32
+ readonly close: () => Promise<void>;
33
+ /** Fetch the live tool catalog. */
34
+ readonly listTools: (options?: CallOptions) => Promise<ListToolsResult>;
35
+ /** Contracted JSON resources. read validates the first text content as JSON. watch requires an endpoint binding to a server that supports subscriptions; it reads the initial value, then listens for updates until aborted. */
36
+ readonly resources: { readonly [U in keyof R]: {
37
+ readonly read: (options?: CallOptions) => Promise<R[U]>;
38
+ readonly watch: (onChange: (value: R[U]) => void | Promise<void>, options?: { signal?: AbortSignal }) => Promise<void>;
39
+ } };
40
+ /** Convenience methods returning structured output. Error or input-required results throw ToolCallError. Reserved object method names remain callable through callTool. */
41
+ readonly tools: { readonly [K in Exclude<keyof I, "then" | "toJSON" | "toString" | "valueOf" | "toLocaleString">]: (input: I[K], options?: CallOptions) => Promise<K extends keyof O ? O[K] : unknown> };
42
+ /** Return the full MCP result, preserving error and input-required results. Arguments and successful output are contract-validated. */
43
+ readonly callTool: <K extends keyof I & string>(request: CallMetadata & { name: K; arguments: I[K] }, options?: CallOptions) => Promise<ToolResult<K extends keyof O ? O[K] : unknown>>;
44
+ }
package/runtime.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ import type { AuthProvider, CallOptions, ToolClient } from "./protocol.js";
2
+ export { ToolCallError } from "./protocol.js";
3
+ export type { AuthProvider, CallOptions, CallMetadata, ToolResult, ToolClient } from "./protocol.js";
4
+
5
+ /** Connection settings for an MCP endpoint. */
6
+ export interface Binding {
7
+ /** Streamable HTTP MCP endpoint. */
8
+ endpoint: URL | string;
9
+ /** Authentication provider; use application-owned credentials. */
10
+ authProvider?: AuthProvider;
11
+ /** Custom fetch implementation, for example for an in-process transport. */
12
+ fetch?: typeof fetch;
13
+ /** Optional X-Holon-Installation-UID routing header. */
14
+ installationUID?: string;
15
+ /** Close the session after every call instead of keeping one connection open. */
16
+ scoped?: boolean;
17
+ }
18
+ /** Alias for an endpoint binding. */
19
+ export type EndpointBinding = Binding;
20
+ /** Calling application identity and its declared dependencies. */
21
+ export interface Identity { name: string; version: string; requires?: Record<string, { name?: string; version?: string }> }
22
+ /** Generation-time tool and JSON resource schemas. A null contract permits unvalidated callTool requests. */
23
+ export interface Contract {
24
+ tools: Array<{ name: string; inputSchema: Record<string, unknown>; outputSchema?: Record<string, unknown> }>;
25
+ resources?: Array<{ uri: string; outputSchema: Record<string, unknown> }>;
26
+ }
27
+ /** Untyped client returned by the low-level runtime. Generated clients specialize its inputs, outputs, and resources. */
28
+ export type RuntimeClient = ToolClient<Record<string, Record<string, unknown>>, Record<string, unknown>, Record<string, unknown>>;
29
+ /** The subset of an MCP client a session must provide. */
30
+ export interface ToolSession {
31
+ callTool(request: { name: string; arguments?: Record<string, unknown> }, options?: Record<string, unknown>): Promise<any>;
32
+ readResource(request: { uri: string }, options?: Record<string, unknown>): Promise<any>;
33
+ listTools(params?: undefined, options?: Record<string, unknown>): Promise<any>;
34
+ }
35
+ /** Optional validation, binding resolution, and host-session hooks. */
36
+ export interface CreateOptions<B = Binding> {
37
+ /** JSON Schema validator factory. The default uses Ajv. */
38
+ validator?: (schema: object) => (value: unknown) => boolean;
39
+ /** Binding of an app created without explicit bindings; consulted on every call. */
40
+ resolve?: (app: string) => Binding | undefined;
41
+ /** Session for a binding the runtime does not connect to itself (anything without an endpoint). */
42
+ session?: (app: string, binding: B) => ToolSession | undefined;
43
+ }
44
+ /** Construct runtime clients from identity and contracts. Connections open lazily. Explicit and resolved endpoint bindings reuse one session unless scoped is true; <NAME>_MCP_URL bindings use one session per call. Close persistent clients on shutdown. Most callers use the generated package root. */
45
+ export declare function createApps<B = Binding>(identity: Identity, contracts: Record<string, Contract | null>, bindings?: Record<string, B>, options?: CreateOptions<B>): Readonly<Record<string, RuntimeClient>>;
46
+ /** Construct only the explicitly bound dependencies. Unknown names throw; an empty binding map selects none. Used by generated createApps factories. */
47
+ export declare function bindApps<B = Binding>(identity: Identity, contracts: Record<string, Contract | null>, bindings: Record<string, B>, options?: CreateOptions<B>): Readonly<Record<string, RuntimeClient>>;
48
+ /** For hosts: register a process-wide resolver for clients created without explicit bindings. Explicit bindings take precedence; return undefined to fall back to the <NAME>_MCP_URL environment variable. Register during startup. */
49
+ export declare function setDefaultBinding(resolve: (app: string) => Binding | undefined): void;
package/runtime.mjs ADDED
@@ -0,0 +1,180 @@
1
+ import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
2
+ import { validator as defaultValidator } from "./validate.mjs";
3
+
4
+ // Shared with the private @holon-sh/sdk and existing hosts.
5
+ const DEFAULT_BINDING = Symbol.for("@holon-sh/sdk/default-binding");
6
+
7
+ // Unbound clients fall back to <NAME>_MCP_URL when the resolver returns undefined.
8
+ export function setDefaultBinding(resolve) {
9
+ if (typeof resolve !== "function") throw new TypeError("Binding resolver must be a function.");
10
+ globalThis[DEFAULT_BINDING] = resolve;
11
+ }
12
+
13
+ function environment(identity, name) {
14
+ if (!Object.hasOwn(identity.requires ?? {}, name)) return undefined;
15
+ const variable = `${name.toUpperCase().replaceAll("-", "_")}_MCP_URL`;
16
+ const endpoint = process.env[variable];
17
+ // Environment-bound dependencies connect for one call at a time.
18
+ if (endpoint) return { endpoint, scoped: true };
19
+ throw new Error(`Dependency ${name} is unavailable (${variable}). Provide a binding or set the environment variable.`);
20
+ }
21
+
22
+ export class ToolCallError extends Error {
23
+ constructor(app, tool, result) {
24
+ const [code, message] = result.resultType === "input_required"
25
+ ? ["input_required", "additional input required"] : ["tool_error", "tool failed"];
26
+ super(`${app}.${tool}: ${message}.`);
27
+ this.name = "ToolCallError";
28
+ this.code = code;
29
+ this.app = app;
30
+ this.tool = tool;
31
+ this.result = result;
32
+ }
33
+ }
34
+
35
+ function validate(check, value, label) {
36
+ if (!check(value)) throw new Error(`Invalid ${label}: ${JSON.stringify(check.errors)}`);
37
+ }
38
+
39
+ function untilAborted(promise, signal) {
40
+ if (!signal) return promise;
41
+ return new Promise((resolve, reject) => {
42
+ const aborted = () => reject(signal.reason);
43
+ signal.addEventListener("abort", aborted, { once: true });
44
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", aborted));
45
+ });
46
+ }
47
+
48
+ function delay(milliseconds, signal) {
49
+ return new Promise(resolve => {
50
+ const timer = setTimeout(resolve, milliseconds);
51
+ signal?.addEventListener("abort", () => { clearTimeout(timer); resolve(); }, { once: true });
52
+ });
53
+ }
54
+
55
+ export function bindApps(identity, contracts, bindings, options) {
56
+ const selected = Object.fromEntries(Object.keys(bindings).map(name => {
57
+ if (!Object.hasOwn(contracts, name) && !Object.hasOwn(identity.requires ?? {}, name)) throw new Error("Unknown app binding: " + name);
58
+ return [name, contracts[name] ?? null];
59
+ }));
60
+ return createApps(identity, selected, bindings, options);
61
+ }
62
+
63
+ export function createApps(identity, contracts, bindings, options) {
64
+ const { validator = defaultValidator, resolve = name => globalThis[DEFAULT_BINDING]?.(name) ?? environment(identity, name), session: hosted } = options ?? {};
65
+ const apps = Object.create(null);
66
+ // Without explicit bindings, every declared dependency gets a client; those without a contract are untyped.
67
+ const members = { ...(bindings ? {} : Object.fromEntries(Object.keys(identity.requires ?? {}).map(name => [name, null]))), ...contracts };
68
+ for (const [name, contract] of Object.entries(members)) {
69
+ const binding = bindings?.[name];
70
+ // Without explicit bindings, the resolve hook supplies the binding on every call.
71
+ const current = () => binding ?? (bindings ? undefined : resolve(name));
72
+ let connection;
73
+ const connect = resolved => {
74
+ const client = new Client({ name: identity.name, version: identity.version }, { versionNegotiation: { mode: "auto" } });
75
+ const transport = new StreamableHTTPClientTransport(new URL(resolved.endpoint), { authProvider: resolved.authProvider, fetch: resolved.fetch, requestInit: { credentials: "include", redirect: "error", headers: resolved.installationUID ? { "X-Holon-Installation-UID": resolved.installationUID } : {} } });
76
+ const current = { client, close: async () => {
77
+ await transport.close();
78
+ await current.ready.catch(() => {});
79
+ } };
80
+ transport.onclose = () => { if (connection === current) connection = undefined; };
81
+ current.ready = client.connect(transport).catch(async error => {
82
+ await transport.close();
83
+ throw error;
84
+ });
85
+ return current;
86
+ };
87
+ // The session hook serves bindings the runtime does not connect to itself.
88
+ const host = binding && hosted?.(name, binding);
89
+ const session = async (options, run) => {
90
+ options?.signal?.throwIfAborted();
91
+ if (host) return run(host);
92
+ const resolved = current();
93
+ if (!resolved) throw new Error(Object.hasOwn(identity.requires ?? {}, name)
94
+ ? `Dependency ${name} is unavailable. Provide a binding.`
95
+ : `Dependency ${name} is not declared by this application.`);
96
+ if (!resolved.endpoint) throw new Error(`Dependency ${name} binding has no endpoint.`);
97
+ // Bindings marked scoped connect for one call at a time.
98
+ const scoped = resolved.scoped === true;
99
+ const active = scoped ? connect(resolved) : (connection ??= connect(resolved));
100
+ try {
101
+ await untilAborted(active.ready, options?.signal);
102
+ return await run(active.client);
103
+ } finally { if (scoped) await active.close(); }
104
+ };
105
+ const tools = Object.create(null);
106
+ const definitions = new Map((contract?.tools ?? []).map(definition => [definition.name, { definition }]));
107
+ const callTool = async (request, options) => {
108
+ const tool = definitions.get(request.name);
109
+ if (!tool && contract) throw new Error(`Unknown tool ${name}.${request.name}.`);
110
+ let validation;
111
+ if (tool) {
112
+ const { definition } = tool;
113
+ validation = tool.validation ??= {
114
+ input: validator(definition.inputSchema),
115
+ output: definition.outputSchema ? validator(definition.outputSchema) : undefined,
116
+ };
117
+ validate(validation.input, request.arguments, `arguments for ${name}.${request.name}`);
118
+ }
119
+ const result = await session(options, client => client.callTool(request, {
120
+ ...options, allowInputRequired: true,
121
+ ...(tool && { toolDefinition: { ...tool.definition, outputSchema: undefined } }),
122
+ }));
123
+ if (result.resultType === "input_required") return result;
124
+ if (!result.isError && validation?.output) {
125
+ validate(validation.output, result.structuredContent, `structured output from ${name}.${request.name}`);
126
+ }
127
+ return { ...result, resultType: "complete" };
128
+ };
129
+ const listTools = options => session(options, client => client.listTools(undefined, options));
130
+ for (const tool of contract?.tools ?? []) if (!["then", "toJSON", "toString", "valueOf", "toLocaleString"].includes(tool.name)) {
131
+ tools[tool.name] = async (input, options) => {
132
+ const result = await callTool({ name: tool.name, arguments: input }, options);
133
+ if (result.resultType === "input_required" || result.isError) throw new ToolCallError(name, tool.name, result);
134
+ return result.structuredContent;
135
+ };
136
+ }
137
+ const resources = Object.create(null);
138
+ for (const resource of contract?.resources ?? []) {
139
+ let valid;
140
+ const parse = result => {
141
+ const value = JSON.parse(result.contents[0].text);
142
+ valid ??= validator(resource.outputSchema);
143
+ validate(valid, value, `contents from ${name} ${resource.uri}`);
144
+ return value;
145
+ };
146
+ const read = async options => parse(await session(options, client => client.readResource({ uri: resource.uri }, options)));
147
+ const watch = async (onChange, { signal } = {}) => {
148
+ const resolved = current();
149
+ if (!resolved?.endpoint) throw new Error(`Watching ${name} ${resource.uri} requires an endpoint binding.`);
150
+ for (let established = false; !signal?.aborted; await delay(1000, signal)) {
151
+ const current = connect(resolved);
152
+ let failure;
153
+ try {
154
+ await untilAborted(current.ready, signal);
155
+ const { client } = current;
156
+ if (!client.getServerCapabilities()?.resources?.subscribe) throw new Error(`${name} does not advertise resource subscriptions.`);
157
+ let reads = Promise.resolve();
158
+ const refresh = () => (reads = reads.then(async () => onChange(parse(await client.readResource({ uri: resource.uri }, { signal, cacheMode: "bypass" }))))
159
+ .catch(error => { failure ??= error; return current.close(); }));
160
+ client.setNotificationHandler("notifications/resources/updated", ({ params }) => { if (params.uri === resource.uri) void refresh(); });
161
+ const subscription = await client.listen({ resourceSubscriptions: [resource.uri] }, { signal });
162
+ if (!subscription.honoredFilter.resourceSubscriptions?.includes(resource.uri)) throw new Error(`${name} did not accept a subscription to ${resource.uri}.`);
163
+ established = true;
164
+ await refresh();
165
+ await subscription.closed;
166
+ } catch (error) {
167
+ failure ??= error;
168
+ } finally {
169
+ await current.close();
170
+ }
171
+ if (signal?.aborted) return;
172
+ if (failure && !established) throw failure;
173
+ }
174
+ };
175
+ resources[resource.uri] = Object.freeze({ read, watch });
176
+ }
177
+ apps[name] = Object.freeze({ callTool, listTools, resources: Object.freeze(resources), close: async () => { await connection?.close(); }, tools: Object.freeze(tools) });
178
+ }
179
+ return Object.freeze(apps);
180
+ }
package/validate.mjs ADDED
@@ -0,0 +1,10 @@
1
+ import Ajv from "ajv";
2
+ import { Ajv2020 } from "ajv/dist/2020.js";
3
+ import addFormats from "ajv-formats";
4
+
5
+ export function validator(schema) {
6
+ const constructor = schema.$schema?.includes("draft-07") ? Ajv : Ajv2020;
7
+ const ajv = new constructor({ strict: false, ownProperties: true, allErrors: true });
8
+ addFormats(ajv);
9
+ return ajv.compile(schema);
10
+ }
package/view.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ /** MIME type of an MCP App HTML resource. */
2
+ export declare const VIEW_MIME_TYPE = "text/html;profile=mcp-app";
3
+ /** Allowed network and asset domains for the view content security policy. */
4
+ export interface ViewOptions {
5
+ csp?: {
6
+ connectDomains?: string[];
7
+ resourceDomains?: string[];
8
+ };
9
+ }
10
+ type ViewRegistration = [
11
+ name: string,
12
+ uri: string,
13
+ metadata: {
14
+ mimeType: string;
15
+ _meta: {
16
+ ui: {
17
+ csp: {
18
+ connectDomains: string[];
19
+ resourceDomains: string[];
20
+ };
21
+ };
22
+ };
23
+ },
24
+ read: () => Promise<{
25
+ ttlMs: number;
26
+ cacheScope: "private";
27
+ contents: {
28
+ uri: string;
29
+ mimeType: string;
30
+ text: string;
31
+ }[];
32
+ }>
33
+ ];
34
+ /**
35
+ * Create a registration tuple for server.registerResource(...appView(...)).
36
+ * Node only: the HTML file is read once when this helper is called.
37
+ * Resource reads reuse that promise; file read failures reject resource requests.
38
+ * @param name Human-readable resource name.
39
+ * @param uri Resource URI, usually starting with ui://.
40
+ * @param file Built HTML file path or file URL.
41
+ * @param options View content security policy.
42
+ * @returns Name, URI, metadata, and resource-read callback.
43
+ * @example
44
+ * server.registerResource(...appView("App", "ui://app/index.html", new URL("./index.html", import.meta.url)));
45
+ */
46
+ export declare function appView(name: string, uri: string, file: URL | string, options?: ViewOptions): ViewRegistration;
47
+ export {};
package/view.js ADDED
@@ -0,0 +1,30 @@
1
+ import { readFile } from "node:fs/promises";
2
+ /** MIME type of an MCP App HTML resource. */
3
+ export const VIEW_MIME_TYPE = "text/html;profile=mcp-app";
4
+ /**
5
+ * Create a registration tuple for server.registerResource(...appView(...)).
6
+ * Node only: the HTML file is read once when this helper is called.
7
+ * Resource reads reuse that promise; file read failures reject resource requests.
8
+ * @param name Human-readable resource name.
9
+ * @param uri Resource URI, usually starting with ui://.
10
+ * @param file Built HTML file path or file URL.
11
+ * @param options View content security policy.
12
+ * @returns Name, URI, metadata, and resource-read callback.
13
+ * @example
14
+ * server.registerResource(...appView("App", "ui://app/index.html", new URL("./index.html", import.meta.url)));
15
+ */
16
+ export function appView(name, uri, file, options = {}) {
17
+ const csp = { connectDomains: [], resourceDomains: [], ...options.csp };
18
+ const page = readFile(file, "utf8");
19
+ page.catch(() => { });
20
+ return [
21
+ name,
22
+ uri,
23
+ { mimeType: VIEW_MIME_TYPE, _meta: { ui: { csp } } },
24
+ async () => ({
25
+ ttlMs: 0,
26
+ cacheScope: "private",
27
+ contents: [{ uri, mimeType: VIEW_MIME_TYPE, text: await page }],
28
+ }),
29
+ ];
30
+ }