holon-ui 0.0.0-stage → 0.0.1-dev.1

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,139 @@
1
- # Temporary Holding Version
1
+ # holon-ui
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
+ Typed MCP clients for browser pages: views that run inside an MCP Apps host, or standalone pages that talk to MCP servers directly. The [`holon` CLI](https://github.com/holon-sh/cli) generates the clients into this package's `generated/` directory, so `import { createApps } from "holon-ui"` resolves to your app's own types. Clients validate arguments before each call and results after it, and can expose tools to the page's WebMCP agent.
4
+
5
+ `holon-ui` contains only browser code. Server code uses `holon-sh` ([Node.js](https://github.com/holon-sh/sdk-ts) or [Python](https://github.com/holon-sh/sdk-py)). Start with the [CLI README](https://github.com/holon-sh/cli) for the walkthrough.
6
+
7
+ ## Install
8
+
9
+ In the app's root directory, next to its `server.json`:
10
+
11
+ ```sh
12
+ npm install holon-ui@dev
13
+ ```
14
+
15
+ Declare `holon-ui` in the app root's `package.json` even when the view's sources live in a subdirectory: `holon types generate` writes browser clients into `node_modules/holon-ui/generated/` whenever that `package.json` declares `holon-ui` (and into `holon-sh` as well when it declares both).
16
+
17
+ ## The generated module
18
+
19
+ ```typescript
20
+ import { App, ToolCallError, createApps, exposeTools, registerPageTools, type Inputs, type Outputs, type Resources } from "holon-ui";
21
+ ```
22
+
23
+ | Export | What it is |
24
+ |---|---|
25
+ | `App` | The `App` from `@modelcontextprotocol/ext-apps` with your own server's `tools`, `resources` and `callTool` typed. Calls go through the host that embeds the page. |
26
+ | `createApps(bindings, options?)` | Clients for servers you reach by endpoint, keyed like `bindings`. |
27
+ | `ToolCallError` | Thrown by `tools.<tool>` when a tool reports an error or asks for more input. |
28
+ | `exposeTools(client, options?)` | Registers a client's tools with the page's WebMCP agent. |
29
+ | `registerPageTools(tools, options?)` | Registers tools implemented by the page itself. |
30
+ | `Inputs`, `Outputs`, `Resources` | Argument, result and resource value types, as `Inputs["dep"]["tool"]`. |
31
+
32
+ Your own server appears under the last segment of its `server.json` name, e.g. `example.com/checkout` → `checkout`: `Inputs["checkout"]`, `Outputs["checkout"]`, and `createApps({ checkout: { endpoint } })` for a standalone page. Every other key is a dependency declared in `server.json`.
33
+
34
+ ## Bind a client
35
+
36
+ ### Inside an MCP Apps host
37
+
38
+ A view embedded in an MCP Apps host calls its own server through the host, which owns the connection and the credentials. Construct and connect `App` exactly as you would the one from `@modelcontextprotocol/ext-apps`; it is that class with typed members added:
39
+
40
+ ```typescript
41
+ import { App } from "holon-ui";
42
+
43
+ const view = new App({ name: "checkout-view", version: "1.0.0" });
44
+ view.ontoolresult = ({ structuredContent }) => render(structuredContent);
45
+ await view.connect();
46
+
47
+ const { total } = await view.tools.total({ id: "A-1" });
48
+ ```
49
+
50
+ The host owns the tool catalog, so `listTools`, `exposeTools` and `watch` need an endpoint binding instead.
51
+
52
+ ### Directly to an endpoint
53
+
54
+ ```typescript
55
+ const apps = createApps({
56
+ checkout: { endpoint: new URL("/mcp", location.origin) },
57
+ orders: { endpoint: "https://orders.example.com/mcp" },
58
+ });
59
+
60
+ const order = await apps.orders.tools.lookup({ id: "A-1" }, { timeout: 5000 });
61
+ ```
62
+
63
+ An endpoint binding takes `endpoint`, and optionally `authProvider`, `fetch`, and `scoped: true` to close the session after every call. Otherwise a client keeps one session open; call `close()` when the page no longer needs it.
64
+
65
+ ## Read and watch resources
66
+
67
+ A server types a JSON resource by registering it with `mimeType: "application/json"` and an `"sh.holon/outputSchema"` JSON Schema in its `_meta`:
68
+
69
+ ```typescript
70
+ const summary = await apps.orders.resources["orders://summary"].read();
71
+
72
+ const lifetime = new AbortController();
73
+ await apps.orders.resources["orders://summary"].watch(value => render(value.open), { signal: lifetime.signal });
74
+ ```
75
+
76
+ `watch` calls back with the current value and then on every update, until the signal aborts; the server must support resource subscriptions.
77
+
78
+ ## Give tools to the page's agent
79
+
80
+ `exposeTools` registers every tool of an endpoint-bound client with the page's WebMCP context (`navigator.modelContext`). Calls from the agent go through the client, so they are validated. It returns `false` when the browser has no WebMCP context.
81
+
82
+ ```typescript
83
+ await exposeTools(apps.orders, { signal: lifetime.signal });
84
+ ```
85
+
86
+ `registerPageTools` registers tools the page implements itself. Input schemas are Standard Schema validators that can produce JSON Schema, such as Zod 4. Pass `view` to register with the MCP Apps host; without it they go to WebMCP. Aborting the signal unregisters them.
87
+
88
+ ```typescript
89
+ import { z } from "zod";
90
+
91
+ await registerPageTools(
92
+ {
93
+ highlight: {
94
+ description: "Highlight an order on the page",
95
+ inputSchema: z.object({ id: z.string() }),
96
+ execute: ({ id }) => {
97
+ select(id);
98
+ return { content: [{ type: "text", text: `Highlighted ${id}` }] };
99
+ },
100
+ },
101
+ },
102
+ { view, signal: lifetime.signal },
103
+ );
104
+ ```
105
+
106
+ ## Build and serve a view
107
+
108
+ The server serves the view as one HTML file, so bundle it into a single file. With Vite, in the app root:
109
+
110
+ ```typescript
111
+ // vite.config.ts
112
+ import { defineConfig } from "vite";
113
+ import { viteSingleFile } from "vite-plugin-singlefile";
114
+
115
+ export default defineConfig({ root: "ui", plugins: [viteSingleFile()], build: { outDir: "../dist", emptyOutDir: true } });
116
+ ```
117
+
118
+ ```sh
119
+ npm install --save-dev vite vite-plugin-singlefile
120
+ npm install @modelcontextprotocol/ext-apps
121
+ npx vite build
122
+ ```
123
+
124
+ Then serve `dist/index.html` from the server with `appView` from `holon-sh/view` (Node.js) or `app_view` from `holon.view` (Python); both are documented in their runtime READMEs.
125
+
126
+ ## Low-level runtime
127
+
128
+ `holon-ui/runtime` exports what the generated module calls: `createApps(identity, contracts, bindings?, options?)`, `bindApps`, `appClass(identity, contracts, own)`, `exposeTools`, `registerPageTools`, `ToolCallError`, and the `EndpointBinding`, `View` and page-tool types. Apps use the generated module instead.
129
+
130
+ ## Develop
131
+
132
+ ```sh
133
+ npm ci
134
+ npm test
135
+ ```
136
+
137
+ ## License
138
+
139
+ MIT
@@ -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 "holon-sh/protocol";
9
+ export type { EndpointBinding, CreateOptions } from "../runtime.js";
10
+ export { exposeTools, registerPageTools } from "../runtime.js";
11
+ import type { CreateOptions } from "../runtime.js";
12
+ export declare function createApps(bindings: Record<string, never>, options?: CreateOptions): Apps;
@@ -0,0 +1,4 @@
1
+ // holon-generated-apps-v1 placeholder
2
+ import {bindApps} from "../runtime.mjs";
3
+ export {ToolCallError,exposeTools,registerPageTools} from "../runtime.mjs";
4
+ export function createApps(bindings, options) { return bindApps({name:"unconfigured",version:"0",requires:{}}, {}, bindings, options); }
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "holon-ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1-dev.1",
4
+ "description": "Typed MCP clients for browser views: MCP Apps host bindings and WebMCP tool exposure.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/holon-sh/sdk-ui.git"
8
+ },
9
+ "license": "MIT",
10
+ "type": "module",
11
+ "sideEffects": false,
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "holon": {
16
+ "slot": "generated",
17
+ "contractVersion": 1
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "types": "./generated/index.d.ts",
22
+ "import": "./generated/index.js"
23
+ },
24
+ "./runtime": {
25
+ "types": "./runtime.d.ts",
26
+ "import": "./runtime.mjs"
27
+ }
28
+ },
29
+ "files": [
30
+ "runtime.mjs",
31
+ "runtime.d.ts",
32
+ "generated/",
33
+ "README.md"
34
+ ],
35
+ "scripts": {
36
+ "build": "node scripts/build.mjs",
37
+ "prepack": "npm run build",
38
+ "test": "npm run build && node --test tests/*.test.mjs",
39
+ "fixtures": "node scripts/fixtures.mjs"
40
+ },
41
+ "dependencies": {
42
+ "holon-sh": "^0.0.1-dev.2"
43
+ },
44
+ "peerDependencies": {
45
+ "@modelcontextprotocol/ext-apps": ">=1.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@modelcontextprotocol/ext-apps": "^1.7.5",
49
+ "@modelcontextprotocol/node": "2.1.1",
50
+ "@modelcontextprotocol/server": "2.3.1",
51
+ "@types/node": "^22.0.0",
52
+ "typescript": "~5.9.3",
53
+ "zod": "4.6.6"
54
+ }
55
+ }
package/runtime.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { App } from "@modelcontextprotocol/ext-apps";
2
+ import type { Binding as EndpointBinding, Identity, Contract, RuntimeClient } from "holon-sh/runtime";
3
+ export type { Binding as EndpointBinding, Identity, Contract, RuntimeClient, AuthProvider, CallOptions, CallMetadata, ToolResult, ToolClient } from "holon-sh/runtime";
4
+ export { ToolCallError } from "holon-sh/runtime";
5
+
6
+ export interface CreateOptions { validator?: (schema: object) => (value: unknown) => boolean }
7
+ export declare function createApps(identity: Identity, contracts: Record<string, Contract | null>, bindings?: Record<string, EndpointBinding>, options?: CreateOptions): Readonly<Record<string, RuntimeClient>>;
8
+ export declare function bindApps(identity: Identity, contracts: Record<string, Contract | null>, bindings: Record<string, EndpointBinding>, options?: CreateOptions): Readonly<Record<string, RuntimeClient>>;
9
+ /** The ext-apps App with the app's own tools and resources typed; calls go through the host that embeds the page. */
10
+ export type View = App & Pick<RuntimeClient, "tools" | "resources" | "callTool">;
11
+ export declare function appClass(identity: Identity, contracts: Record<string, Contract | null>, own: string): new (...args: ConstructorParameters<typeof App>) => View;
12
+ export interface ExposeOptions { signal?: AbortSignal; execute?: (name: string, args: Record<string, unknown>) => Promise<unknown> }
13
+ export declare function exposeTools(app: Pick<RuntimeClient, "callTool" | "listTools">, options?: ExposeOptions): Promise<boolean>;
14
+ export type PageResult<I> = { readonly value: I; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string }> };
15
+ export interface PageSchema<I = unknown> { readonly "~standard": { readonly version: 1; readonly vendor: string; readonly types?: { readonly input: unknown; readonly output: I }; readonly validate: (value: unknown) => PageResult<I> | Promise<PageResult<I>>; readonly jsonSchema: { readonly input: (options: { target: "draft-2020-12" }) => object } } }
16
+ export type PageInput<S> = S extends PageSchema<infer I> ? I : never;
17
+ export interface PageAnnotations { title?: string; readOnlyHint?: boolean; destructiveHint?: boolean; idempotentHint?: boolean; openWorldHint?: boolean }
18
+ export interface PageTool<S extends PageSchema = PageSchema> { description: string; inputSchema: S; annotations?: PageAnnotations; execute: (args: PageInput<S>) => unknown }
19
+ export interface PageHost { registerTool(name: string, config: { description: string; inputSchema: PageSchema; annotations?: PageAnnotations }, callback: (args: any, extra: any) => any): { remove(): void } }
20
+ export declare function registerPageTools<S extends { [name: string]: PageSchema }>(tools: { [K in keyof S]: PageTool<S[K]> }, options?: { view?: PageHost; signal?: AbortSignal }): Promise<boolean>;
package/runtime.mjs ADDED
@@ -0,0 +1,63 @@
1
+ import { App } from "@modelcontextprotocol/ext-apps";
2
+ import { createApps as create, bindApps, ToolCallError } from "holon-sh/runtime";
3
+
4
+ export { bindApps, ToolCallError };
5
+
6
+ // Browser clients have no process environment or host-wide default binding.
7
+ export function createApps(identity, contracts, bindings, options) {
8
+ return create(identity, contracts, bindings, { ...options, resolve: () => undefined });
9
+ }
10
+
11
+ // appClass returns the ext-apps App with the app's own tools and resources (contracts[own]) typed.
12
+ // Calls go through the host that embeds the page, so connect the App first, as with any App.
13
+ export function appClass(identity, contracts, own) {
14
+ return class extends App {
15
+ #client = bindApps(identity, { [own]: contracts[own] }, { [own]: {} }, {
16
+ session: () => ({
17
+ callTool: (request, { signal, timeout } = {}) => this.callServerTool(request, { signal, timeout }),
18
+ readResource: (request, { signal, timeout } = {}) => this.readServerResource(request, { signal, timeout }),
19
+ listTools: () => { throw new Error("The host owns the tool catalog; bind an endpoint to list tools."); },
20
+ }),
21
+ })[own];
22
+ get tools() { return this.#client.tools; }
23
+ get resources() { return this.#client.resources; }
24
+ callTool(request, options) { return this.#client.callTool(request, options); }
25
+ };
26
+ }
27
+
28
+ export async function exposeTools(app, options = {}) {
29
+ const context = globalThis.document?.modelContext ?? globalThis.navigator?.modelContext;
30
+ if (!context) return false;
31
+ const { signal } = options;
32
+ const execute = options.execute ?? ((name, args) => app.callTool({ name, arguments: args }, { signal }));
33
+ const { tools } = await app.listTools({ signal });
34
+ for (const tool of tools) {
35
+ await context.registerTool({
36
+ name: tool.name, title: tool.title, description: tool.description ?? "", inputSchema: tool.inputSchema, annotations: tool.annotations,
37
+ execute: args => execute(tool.name, args ?? {}),
38
+ }, { signal });
39
+ }
40
+ return true;
41
+ }
42
+
43
+ export async function registerPageTools(tools, { view, signal } = {}) {
44
+ const context = globalThis.document?.modelContext ?? globalThis.navigator?.modelContext;
45
+ const removals = [];
46
+ for (const [name, { description, inputSchema, annotations, execute }] of Object.entries(tools)) {
47
+ if (view) {
48
+ removals.push(view.registerTool(name, { description, inputSchema, annotations }, args => execute(args)).remove);
49
+ } else if (context) {
50
+ const standard = inputSchema["~standard"];
51
+ await context.registerTool({
52
+ name, description, annotations, inputSchema: standard.jsonSchema.input({ target: "draft-2020-12" }),
53
+ execute: async args => {
54
+ const parsed = await standard.validate(args ?? {});
55
+ if (parsed.issues) throw new Error(`Invalid arguments for ${name}: ${JSON.stringify(parsed.issues)}`);
56
+ return execute(parsed.value);
57
+ },
58
+ }, { signal });
59
+ }
60
+ }
61
+ signal?.addEventListener("abort", () => { for (const remove of removals) remove(); }, { once: true });
62
+ return Boolean(view || context);
63
+ }