ngx-json-render 0.9.4 → 0.9.6
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 +52 -1
- package/fesm2022/ngx-json-render-mcp-server.mjs +424 -0
- package/fesm2022/ngx-json-render-mcp-server.mjs.map +1 -0
- package/package.json +6 -1
- package/schematics/builders.json +10 -0
- package/schematics/mcp-app-builder/index.cjs +98 -0
- package/schematics/mcp-app-builder/inline.cjs +117 -0
- package/schematics/mcp-app-builder/schema.json +31 -0
- package/types/ngx-json-render-mcp-server.d.ts +107 -0
package/README.md
CHANGED
|
@@ -734,7 +734,54 @@ export class App {
|
|
|
734
734
|
|
|
735
735
|
The spec renders while the model is still writing the tool call, from the host's `toolinputpartial` notifications; pass `streamPartialInput: false` to wait for the result. `mcp.sendMessage(text, data)` posts a user message to the chat, so a button can continue the conversation, and `mcp.callServerTool(name, args)` replaces the spec with another tool's result. The connection closes with the injector that created it.
|
|
736
736
|
|
|
737
|
-
The server
|
|
737
|
+
### The server
|
|
738
|
+
|
|
739
|
+
`ngx-json-render/mcp/server` is the other half: an MCP server with one `render-ui` tool for your catalog, and the view as its `ui://` resource. It needs no Angular, so it runs in plain Node or on an edge function, and it takes the same two optional peers.
|
|
740
|
+
|
|
741
|
+
```ts
|
|
742
|
+
import { readFileSync } from 'node:fs';
|
|
743
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
744
|
+
import { createRenderUiServer } from 'ngx-json-render/mcp/server';
|
|
745
|
+
import { catalog } from './catalog'; // defineCatalog(schema, …) from ngx-json-render/schema
|
|
746
|
+
|
|
747
|
+
const server = createRenderUiServer({
|
|
748
|
+
catalog,
|
|
749
|
+
html: readFileSync(new URL('./view.html', import.meta.url), 'utf8'), // the view above, built into one page
|
|
750
|
+
name: 'my-app',
|
|
751
|
+
version: '1.0.0',
|
|
752
|
+
});
|
|
753
|
+
await server.connect(new StdioServerTransport());
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
Over HTTP, `handleRenderUiRequest(options, request)` takes a web `Request` and returns a `Response`. It serves each request statelessly with a fresh server, as a serverless or edge function needs, and answers CORS for browser-based clients such as MCP Inspector.
|
|
757
|
+
|
|
758
|
+
What it does on top of upstream's `createMcpApp` from `@json-render/mcp`, whose tool and resource it registers the same way:
|
|
759
|
+
|
|
760
|
+
- **A description Claude reads whole.** `createMcpApp` describes the tool with `catalog.prompt()`, about 26 000 characters, and Claude cuts a tool description off after roughly 2 000, before the first component. `renderUiDescription(catalog)` stays under that limit (`DESCRIPTION_LIMIT`) and leaves the components to the input schema.
|
|
761
|
+
- **An input schema that keeps the spec whole.** `catalog.zodSchema()` has no `state`, `on` or `watch`, so the MCP SDK strips them from the arguments before the tool sees them, and it types no props. `specInputSchema(catalog)` declares them and gives each component its own props, with dynamic values allowed and unknown props rejected, and checks each action's params.
|
|
762
|
+
- **Errors the model can fix.** A spec with a missing child or root, or a `$template` that reads a repeat item as `${$item/field}`, goes back as a tool error that says what to write instead (`specProblems`).
|
|
763
|
+
- **The structured result ChatGPT asks for:** the spec as `structuredContent`, with an output schema (`specOutputSchema`).
|
|
764
|
+
|
|
765
|
+
Options: `name`, `version`, `toolName` (default `render-ui`), `title`, `description`, `csp` (the origins the view may load from and connect to; left out, the host allows none) and `widgetDomain` (the view's origin for ChatGPT's plugin directory). If your catalog has a `sendMessage` action, the description tells the model how to use it; the view's handler for it calls `mcp.sendMessage()`.
|
|
766
|
+
|
|
767
|
+
### Building it
|
|
768
|
+
|
|
769
|
+
A host loads the view as one HTML document with nowhere to fetch chunks from, so the Angular build has to be folded into a single page. The `ngx-json-render:mcp-app` builder does that, and bundles the server next to it. Give the view its own application project (`ng generate application mcp-view`), then add a target to it in `angular.json`:
|
|
770
|
+
|
|
771
|
+
```json
|
|
772
|
+
"mcp": {
|
|
773
|
+
"builder": "ngx-json-render:mcp-app",
|
|
774
|
+
"options": {
|
|
775
|
+
"buildTarget": "mcp-view:build:production",
|
|
776
|
+
"outputPath": "dist/mcp-app",
|
|
777
|
+
"server": "projects/mcp-view/server.ts"
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
`ng run mcp-view:mcp` runs the application build, then writes `dist/mcp-app/view.html` with every script and stylesheet inlined, and the fonts and images the stylesheets use as data URLs, and `dist/mcp-app/server.mjs` with its dependencies bundled in, so `node dist/mcp-app/server.mjs` runs anywhere without `node_modules`. Files the page cannot reach, such as an image referenced only from a template, are listed as a warning: point at them from a stylesheet or from an `https:` origin in `csp`. The server bundle is not type-checked; run `tsc --noEmit` on it first if you want that. Options: `server` (leave it out to build the view only), `tsConfig` for the server, and `externalPackages: true` to keep `node_modules` packages out of `server.mjs`.
|
|
783
|
+
|
|
784
|
+
[`projects/mcp-app`](https://github.com/shteynu/ngx-json-render/tree/main/projects/mcp-app) is a complete example with the Material catalog: the view, built with this builder, and this server, the one behind `https://ngx-json-render.vercel.app/mcp`.
|
|
738
785
|
|
|
739
786
|
## Devtools
|
|
740
787
|
|
|
@@ -1176,6 +1223,10 @@ Schema alone (`ngx-json-render/schema`): `schema`, `AngularSchema`, `AngularSpec
|
|
|
1176
1223
|
|
|
1177
1224
|
MCP Apps (`ngx-json-render/mcp`): `injectJsonRenderApp`, `parseSpecFromToolResult`, `messageText`, and the types `JsonRenderApp`, `JsonRenderAppOptions`. Needs the optional peers `@modelcontextprotocol/ext-apps` and `@modelcontextprotocol/sdk`.
|
|
1178
1225
|
|
|
1226
|
+
MCP App server (`ngx-json-render/mcp/server`): `createRenderUiServer`, `handleRenderUiRequest`, `renderUiDescription`, `specInputSchema`, `specOutputSchema`, `specProblems`, `DESCRIPTION_LIMIT`, and the types `RenderUiServerOptions`, `RenderUiDescriptionOptions`. No Angular; the same optional peers.
|
|
1227
|
+
|
|
1228
|
+
MCP App builder (`ngx-json-render:mcp-app`): the view's application build as one `view.html`, and the server bundled into `server.mjs`.
|
|
1229
|
+
|
|
1179
1230
|
Router (`ngx-json-render/router`): `injectRouterNavigate`, and the type `RouterNavigateOptions`. Needs the optional peer `@angular/router`, which an Angular app on the router already has.
|
|
1180
1231
|
|
|
1181
1232
|
AG-UI (`ngx-json-render/ag-ui`): `injectAgentUI`, `applyAgUiEvent`, `surfacesFromMessages`, `isJsonRenderSpec`, `JsonRenderActivity` (`<json-render-activity>`), `jsonRenderActivityRenderer`, `JSON_RENDER_ACTIVITY_TYPE`, and the types `AgUiAgent`, `AgUiEvent`, `AgUiMessage`, `AgUiSubscriber`, `AgUiSurface`, `AgentUIOptions`, `AgentUIReturn`, `JsonRenderActivityRendererConfig`, `JsonRenderActivityRendererOptions`. No peer: any agent and event of the right shape will do.
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
|
|
4
|
+
import { validateSpec } from '@json-render/core';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
// What the model sees of the `render-ui` tool: a short description and an
|
|
8
|
+
// input schema that carries the catalog.
|
|
9
|
+
//
|
|
10
|
+
// `catalog.prompt()` is written to be a system prompt, and it is about 26 000
|
|
11
|
+
// characters long. Claude shows a tool's description to the model cut off
|
|
12
|
+
// after roughly 2 000 characters, which `catalog.prompt()` spends on its
|
|
13
|
+
// JSON Patch streaming instructions: the model never saw a component, a prop
|
|
14
|
+
// or an action. So the description here stays short, and each component's
|
|
15
|
+
// props and each action's params go into the input schema, where they are
|
|
16
|
+
// also enforced.
|
|
17
|
+
/** Claude cuts a tool description off after roughly this many characters. */
|
|
18
|
+
const DESCRIPTION_LIMIT = 2000;
|
|
19
|
+
/**
|
|
20
|
+
* The `render-ui` tool description for `catalog`: how a spec is put
|
|
21
|
+
* together, in under {@link DESCRIPTION_LIMIT} characters. The components,
|
|
22
|
+
* props and actions are left to the input schema. The paragraph on
|
|
23
|
+
* `sendMessage` is included only when the catalog defines that action, since
|
|
24
|
+
* the view has to handle it.
|
|
25
|
+
*/
|
|
26
|
+
function renderUiDescription(catalog, { ui = 'UI' } = {}) {
|
|
27
|
+
const actions = catalog.data.actions ?? {};
|
|
28
|
+
const lines = [
|
|
29
|
+
`Render an interactive ${ui} inline in the chat. Pass \`spec\`: one complete json-render spec object (not JSON Patch lines). The input schema lists every component, its props and allowed values, and every action; use only those.`,
|
|
30
|
+
'',
|
|
31
|
+
'- `root` is the key of the top element. `elements` is a flat map of key → { type, props, children: [child keys] }; every child key must exist in `elements`.',
|
|
32
|
+
'- `state` holds the data. Read it with {"$state":"/path"} rather than repeating values in props. Data-backed UI always needs `state`; use realistic sample data.',
|
|
33
|
+
'- A prop can be dynamic: {"$state":"/p"} reads, {"$bindState":"/p"} binds a form field both ways, {"$template":"Hi ${/name}"} interpolates, {"$cond":{"$state":"/p"},"$then":a,"$else":b} picks.',
|
|
34
|
+
'- Lists: on a container, "repeat":{"statePath":"/items","key":"id"}; its children render once per item and read {"$item":"field"}, or ${field} inside a $template.',
|
|
35
|
+
'- `visible` and `on` sit next to `props`, never inside: "visible":{"$state":"/tab","eq":"home"}, "on":{"press":{"action":"setState","params":{"statePath":"/tab","value":"home"}}}.',
|
|
36
|
+
'- Forms: give inputs props.validation {"checks":[{"type":"required","message":"..."}]} and submit with submitForm, which runs its inner action only when every field is valid.',
|
|
37
|
+
];
|
|
38
|
+
if ('sendMessage' in actions) {
|
|
39
|
+
lines.push('- To continue the conversation from a button, use sendMessage: {"action":"sendMessage","params":{"text":"Approve release 2.4.0","data":{"$state":"/release"}}}. The host puts `text` (and `data` as JSON) in the chat as the user\'s message, so write it in their voice. For a form: {"action":"submitForm","params":{"action":"sendMessage","params":{"text":"Sign me up","data":{"$state":"/form"}}}}.');
|
|
40
|
+
}
|
|
41
|
+
return lines.join('\n');
|
|
42
|
+
}
|
|
43
|
+
/** A prop value computed from state, such as `{ "$state": "/path" }`. */
|
|
44
|
+
const dynamic = z
|
|
45
|
+
.looseObject({})
|
|
46
|
+
.refine((value) => Object.keys(value).some((key) => key.startsWith('$')), {
|
|
47
|
+
message: 'Expected a dynamic value such as {"$state": "/path"} or {"$bindState": "/path"}',
|
|
48
|
+
})
|
|
49
|
+
.meta({
|
|
50
|
+
// An id makes the JSON Schema define it once and refer to it.
|
|
51
|
+
id: 'DynamicValue',
|
|
52
|
+
description: 'Dynamic value: {"$state"}, {"$bindState"}, {"$bindItem"}, {"$item"}, {"$index"}, {"$template"} or {"$cond","$then","$else"}.',
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* A union's error that says why its first branch failed (say, the allowed
|
|
56
|
+
* enum values), rather than just that no branch matched.
|
|
57
|
+
*/
|
|
58
|
+
function firstBranchError(suffix = '') {
|
|
59
|
+
return (issue) => {
|
|
60
|
+
if (issue.code !== 'invalid_union')
|
|
61
|
+
return undefined;
|
|
62
|
+
const reasons = issue.errors[0]?.map((error) => error.message);
|
|
63
|
+
return reasons?.length ? reasons.join('; ') + suffix : undefined;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** `shape`'s fields, each of which may also be a dynamic value; nothing else. */
|
|
67
|
+
function allowDynamic(schema) {
|
|
68
|
+
const shape = schema.shape ?? {};
|
|
69
|
+
return z.strictObject(Object.fromEntries(Object.entries(shape).map(([key, field]) => {
|
|
70
|
+
const optional = field instanceof z.ZodOptional;
|
|
71
|
+
const value = z.union([optional ? field.unwrap() : field, dynamic], {
|
|
72
|
+
error: firstBranchError(', or a dynamic value such as {"$state": "/path"}'),
|
|
73
|
+
});
|
|
74
|
+
return [key, optional ? value.optional() : value];
|
|
75
|
+
})));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* An action binding, or several, with one branch per action so the params
|
|
79
|
+
* of the catalog's own actions are checked.
|
|
80
|
+
*/
|
|
81
|
+
function actionBinding(catalog) {
|
|
82
|
+
const common = {
|
|
83
|
+
confirm: z.unknown().optional(),
|
|
84
|
+
onSuccess: z.unknown().optional(),
|
|
85
|
+
onError: z.unknown().optional(),
|
|
86
|
+
preventDefault: z.boolean().optional(),
|
|
87
|
+
};
|
|
88
|
+
const builtIn = catalog.schema.builtInActions ?? [];
|
|
89
|
+
const actions = catalog.data.actions ?? {};
|
|
90
|
+
const bindings = Object.entries(actions).map(([name, action]) => z
|
|
91
|
+
.strictObject({
|
|
92
|
+
action: z.literal(name),
|
|
93
|
+
params: allowDynamic(action.params ?? z.object({})),
|
|
94
|
+
...common,
|
|
95
|
+
})
|
|
96
|
+
.describe(`${name}: ${action.description}`));
|
|
97
|
+
if (builtIn.length > 0) {
|
|
98
|
+
const names = builtIn.map((action) => action.name);
|
|
99
|
+
bindings.push(z
|
|
100
|
+
.strictObject({
|
|
101
|
+
action: z.enum(names),
|
|
102
|
+
params: z.record(z.string(), z.unknown()).optional(),
|
|
103
|
+
...common,
|
|
104
|
+
})
|
|
105
|
+
.describe(builtIn
|
|
106
|
+
.map((action) => `${action.name}: ${action.description}`)
|
|
107
|
+
.join('\n')));
|
|
108
|
+
}
|
|
109
|
+
const binding = z
|
|
110
|
+
.discriminatedUnion('action', bindings, {
|
|
111
|
+
error: `Unknown action. Use one of: ${[...Object.keys(actions), ...builtIn.map((action) => action.name)].join(', ')}.`,
|
|
112
|
+
})
|
|
113
|
+
.meta({ id: 'ActionBinding' });
|
|
114
|
+
return z.union([binding, z.array(binding)], { error: firstBranchError() });
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The tool's input schema: a spec whose elements are typed per component.
|
|
118
|
+
*
|
|
119
|
+
* `catalog.zodSchema()` types an element's `type` but not its props, and it
|
|
120
|
+
* has no `state`, `on` or `watch`, so the MCP SDK, which parses tool
|
|
121
|
+
* arguments with the input schema, stripped those and let any prop through.
|
|
122
|
+
* Here each component's props are its catalog schema (with dynamic values
|
|
123
|
+
* allowed and unknown props rejected), and each action's params are checked.
|
|
124
|
+
*/
|
|
125
|
+
function specInputSchema(catalog) {
|
|
126
|
+
let schema = inputSchemas.get(catalog);
|
|
127
|
+
if (!schema) {
|
|
128
|
+
schema = buildSpecInputSchema(catalog);
|
|
129
|
+
inputSchemas.set(catalog, schema);
|
|
130
|
+
}
|
|
131
|
+
return schema;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The sentences every validatable component's description ends with, or ""
|
|
135
|
+
* when there are none worth sharing.
|
|
136
|
+
*
|
|
137
|
+
* The catalog repeats its validation hint on each form field because
|
|
138
|
+
* `catalog.prompt()` lists components one by one and shows no prop-level
|
|
139
|
+
* descriptions. In the input schema the same ~600 characters would arrive
|
|
140
|
+
* once per field component, so the schema says them once, on `elements`, and
|
|
141
|
+
* each field points there. Found as the descriptions' common tail, cut back
|
|
142
|
+
* to a sentence start, so the catalog does not have to export it.
|
|
143
|
+
*/
|
|
144
|
+
function sharedValidationHint(components) {
|
|
145
|
+
const descriptions = Object.values(components)
|
|
146
|
+
.filter((component) => component.props.shape?.['validation'] !==
|
|
147
|
+
undefined)
|
|
148
|
+
.map((component) => component.description);
|
|
149
|
+
if (descriptions.length < 2)
|
|
150
|
+
return '';
|
|
151
|
+
let tail = descriptions[0];
|
|
152
|
+
for (const description of descriptions.slice(1)) {
|
|
153
|
+
let n = 0;
|
|
154
|
+
while (n < tail.length &&
|
|
155
|
+
n < description.length &&
|
|
156
|
+
tail[tail.length - 1 - n] === description[description.length - 1 - n]) {
|
|
157
|
+
n++;
|
|
158
|
+
}
|
|
159
|
+
tail = tail.slice(tail.length - n);
|
|
160
|
+
}
|
|
161
|
+
// The shared tail usually starts part-way through the sentence before it.
|
|
162
|
+
const before = descriptions[0].slice(0, descriptions[0].length - tail.length);
|
|
163
|
+
if (before !== '' && !/[.!?] $/.test(before)) {
|
|
164
|
+
const next = tail.indexOf('. ');
|
|
165
|
+
tail = next === -1 ? '' : tail.slice(next + 2);
|
|
166
|
+
}
|
|
167
|
+
return tail.length >= 200 ? tail : '';
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* What `render-ui` returns as `structuredContent`: the spec it was given,
|
|
171
|
+
* after the input schema filled in missing `children`. Only its outline,
|
|
172
|
+
* since the input schema already carries the catalog and a second copy would
|
|
173
|
+
* double what every `tools/list` sends the model. ChatGPT flags a tool
|
|
174
|
+
* without an output schema ("Output schema recommended").
|
|
175
|
+
*/
|
|
176
|
+
const specOutputSchema = z
|
|
177
|
+
.looseObject({
|
|
178
|
+
root: z.string(),
|
|
179
|
+
elements: z.record(z.string(), z.looseObject({
|
|
180
|
+
type: z.string(),
|
|
181
|
+
props: z.record(z.string(), z.unknown()),
|
|
182
|
+
children: z.array(z.string()),
|
|
183
|
+
})),
|
|
184
|
+
state: z.record(z.string(), z.unknown()).optional(),
|
|
185
|
+
})
|
|
186
|
+
.describe('The json-render spec that was rendered.');
|
|
187
|
+
// The server is stateless, so it is created for every request; build each
|
|
188
|
+
// catalog's schema once.
|
|
189
|
+
const inputSchemas = new WeakMap();
|
|
190
|
+
function buildSpecInputSchema(catalog) {
|
|
191
|
+
const bindings = actionBinding(catalog);
|
|
192
|
+
// The fields every element shares, defined once in the JSON Schema.
|
|
193
|
+
const shared = {
|
|
194
|
+
visible: z
|
|
195
|
+
.unknown()
|
|
196
|
+
.meta({
|
|
197
|
+
id: 'Visibility',
|
|
198
|
+
description: 'Show the element only when this holds: {"$state":"/p","eq":v} (or neq, gt, gte, lt, lte, not), an array of conditions (all), {"$or":[...]}, true or false.',
|
|
199
|
+
})
|
|
200
|
+
.optional(),
|
|
201
|
+
repeat: z
|
|
202
|
+
.strictObject({
|
|
203
|
+
statePath: z.union([z.string(), dynamic]),
|
|
204
|
+
key: z.string().optional(),
|
|
205
|
+
})
|
|
206
|
+
.meta({
|
|
207
|
+
id: 'Repeat',
|
|
208
|
+
description: 'Render the children once per item of the state array at statePath; they read {"$item":"field"} and {"$index":true}.',
|
|
209
|
+
})
|
|
210
|
+
.optional(),
|
|
211
|
+
on: z
|
|
212
|
+
.record(z.string(), bindings)
|
|
213
|
+
.meta({
|
|
214
|
+
id: 'Events',
|
|
215
|
+
description: 'Event name (such as "press", "change" or "submit") → action binding.',
|
|
216
|
+
})
|
|
217
|
+
.optional(),
|
|
218
|
+
watch: z
|
|
219
|
+
.record(z.string(), bindings)
|
|
220
|
+
.meta({
|
|
221
|
+
id: 'Watchers',
|
|
222
|
+
description: 'State path → action binding, run when the value at that path changes.',
|
|
223
|
+
})
|
|
224
|
+
.optional(),
|
|
225
|
+
};
|
|
226
|
+
const { components } = catalog.data;
|
|
227
|
+
const hint = sharedValidationHint(components);
|
|
228
|
+
const elements = Object.entries(components).map(([type, component]) => {
|
|
229
|
+
const description = hint && component.description.endsWith(hint)
|
|
230
|
+
? `${component.description.slice(0, -hint.length)}Validation: see \`elements\`.`
|
|
231
|
+
: component.description;
|
|
232
|
+
return z
|
|
233
|
+
.strictObject({
|
|
234
|
+
type: z.literal(type),
|
|
235
|
+
props: allowDynamic(component.props),
|
|
236
|
+
children: z.array(z.string()).default([]),
|
|
237
|
+
...shared,
|
|
238
|
+
})
|
|
239
|
+
.describe(`${type}: ${description}`);
|
|
240
|
+
});
|
|
241
|
+
return z.strictObject({
|
|
242
|
+
root: z.string().describe('Key of the top element in `elements`.'),
|
|
243
|
+
elements: z
|
|
244
|
+
.record(z.string(), z.discriminatedUnion('type', elements))
|
|
245
|
+
.describe(hint
|
|
246
|
+
? `Elements by key. On every component that takes \`validation\`: ${hint}`
|
|
247
|
+
: 'Elements by key.'),
|
|
248
|
+
state: z
|
|
249
|
+
.record(z.string(), z.unknown())
|
|
250
|
+
.optional()
|
|
251
|
+
.describe('Initial state model; bindings read it with {"$state": "/path"}.'),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* What the input schema cannot express: a root and children that exist,
|
|
256
|
+
* repeat with a template child, valid visibility conditions, and `$template`
|
|
257
|
+
* placeholders that read a repeat item. Returns the problems as text for the
|
|
258
|
+
* model, or `undefined` when there are none.
|
|
259
|
+
*/
|
|
260
|
+
function specProblems(spec) {
|
|
261
|
+
// The input schema has already checked the shape `Spec` describes.
|
|
262
|
+
const { issues } = validateSpec(spec);
|
|
263
|
+
const errors = issues.filter((issue) => issue.severity === 'error');
|
|
264
|
+
const messages = [
|
|
265
|
+
...errors.map((issue) => issue.message),
|
|
266
|
+
...itemTemplateProblems(spec),
|
|
267
|
+
];
|
|
268
|
+
return messages.length > 0
|
|
269
|
+
? [
|
|
270
|
+
'The generated UI spec has the following errors:',
|
|
271
|
+
...messages.map((message) => `- ${message}`),
|
|
272
|
+
].join('\n')
|
|
273
|
+
: undefined;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* `${$item/field}` placeholders in a `$template`. A `$template` reads an
|
|
277
|
+
* absolute `${/path}` from state and a bare `${field}` from the repeat item
|
|
278
|
+
* first, so `$item/field` is looked up as the state path `/$item/field`,
|
|
279
|
+
* finds nothing and renders as an empty string, without an error anywhere.
|
|
280
|
+
*/
|
|
281
|
+
function itemTemplateProblems(spec) {
|
|
282
|
+
const problems = [];
|
|
283
|
+
for (const [key, { props, on, watch }] of Object.entries(spec.elements)) {
|
|
284
|
+
walk({ props, on, watch }, '', (template, path) => {
|
|
285
|
+
for (const [, placeholder, field] of template.matchAll(ITEM_PLACEHOLDER)) {
|
|
286
|
+
problems.push(`Element "${key}" ${path}: inside $template write \${${field.replaceAll('.', '/')}}, not \${${placeholder}}`);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return problems;
|
|
291
|
+
}
|
|
292
|
+
/** `${$item/field}` or `${$item.field}`; the groups are the placeholder and the field. */
|
|
293
|
+
const ITEM_PLACEHOLDER = /\$\{(\$item[/.]([^}]+))\}/g;
|
|
294
|
+
/** Calls `visit` with every `$template` string under `value` and where it sits. */
|
|
295
|
+
function walk(value, path, visit) {
|
|
296
|
+
if (Array.isArray(value)) {
|
|
297
|
+
value.forEach((item, index) => walk(item, `${path}[${index}]`, visit));
|
|
298
|
+
}
|
|
299
|
+
else if (value !== null && typeof value === 'object') {
|
|
300
|
+
for (const [key, child] of Object.entries(value)) {
|
|
301
|
+
if (key === '$template' && typeof child === 'string') {
|
|
302
|
+
visit(child, path);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
walk(child, path ? `${path}.${key}` : key, visit);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* An MCP server with a `render-ui` tool for `catalog` and its `ui://` view.
|
|
313
|
+
* Connect it to any transport: `StdioServerTransport` for a local host,
|
|
314
|
+
* or {@link handleRenderUiRequest} for Streamable HTTP.
|
|
315
|
+
*
|
|
316
|
+
* The tool checks a spec against the catalog before it echoes it back. A
|
|
317
|
+
* rejected spec goes back to the model as an error it can fix.
|
|
318
|
+
*/
|
|
319
|
+
function createRenderUiServer(options) {
|
|
320
|
+
const { catalog, html, name = 'ngx-json-render', version = '0.0.0', toolName = 'render-ui', title = 'Render UI', description = renderUiDescription(catalog), csp, widgetDomain, } = options;
|
|
321
|
+
const resourceUri = `ui://${toolName}/view.html`;
|
|
322
|
+
const server = new McpServer({ name, version });
|
|
323
|
+
registerAppTool(server, toolName, {
|
|
324
|
+
title,
|
|
325
|
+
description,
|
|
326
|
+
inputSchema: { spec: specInputSchema(catalog) },
|
|
327
|
+
outputSchema: { spec: specOutputSchema },
|
|
328
|
+
// It only echoes the spec back for the view to draw: nothing is read
|
|
329
|
+
// from or written to any system.
|
|
330
|
+
annotations: {
|
|
331
|
+
title,
|
|
332
|
+
readOnlyHint: true,
|
|
333
|
+
destructiveHint: false,
|
|
334
|
+
idempotentHint: true,
|
|
335
|
+
openWorldHint: false,
|
|
336
|
+
},
|
|
337
|
+
_meta: { ui: { resourceUri } },
|
|
338
|
+
},
|
|
339
|
+
// The SDK has already parsed `spec` against the input schema; what is
|
|
340
|
+
// left is the structure between elements. The text keeps upstream's
|
|
341
|
+
// shape, the bare spec, which is what views built on
|
|
342
|
+
// `@json-render/mcp/app` read.
|
|
343
|
+
async ({ spec }) => {
|
|
344
|
+
const problems = specProblems(spec);
|
|
345
|
+
return problems
|
|
346
|
+
? { isError: true, content: [{ type: 'text', text: problems }] }
|
|
347
|
+
: {
|
|
348
|
+
content: [{ type: 'text', text: JSON.stringify(spec) }],
|
|
349
|
+
structuredContent: { spec },
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
const meta = {};
|
|
353
|
+
if (csp)
|
|
354
|
+
meta['ui'] = { csp };
|
|
355
|
+
if (widgetDomain)
|
|
356
|
+
meta['openai/widgetDomain'] = widgetDomain;
|
|
357
|
+
registerAppResource(server, resourceUri, resourceUri, { mimeType: RESOURCE_MIME_TYPE }, async () => ({
|
|
358
|
+
contents: [
|
|
359
|
+
{
|
|
360
|
+
uri: resourceUri,
|
|
361
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
362
|
+
text: html,
|
|
363
|
+
...(Object.keys(meta).length > 0 && { _meta: meta }),
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
}));
|
|
367
|
+
return server;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Serve one Streamable HTTP request, statelessly: a fresh server and
|
|
371
|
+
* transport per request, so it runs unchanged on a serverless or edge
|
|
372
|
+
* function (anything with web `Request` and `Response`). Answers CORS,
|
|
373
|
+
* because browser-based clients such as MCP Inspector call cross-origin.
|
|
374
|
+
*/
|
|
375
|
+
async function handleRenderUiRequest(options, request) {
|
|
376
|
+
if (request.method === 'OPTIONS') {
|
|
377
|
+
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
378
|
+
}
|
|
379
|
+
const server = createRenderUiServer(options);
|
|
380
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
381
|
+
sessionIdGenerator: undefined,
|
|
382
|
+
// One JSON body per request: nothing here streams, and a serverless
|
|
383
|
+
// function returns as soon as the response is complete.
|
|
384
|
+
enableJsonResponse: true,
|
|
385
|
+
});
|
|
386
|
+
await server.connect(transport);
|
|
387
|
+
try {
|
|
388
|
+
const response = await transport.handleRequest(request);
|
|
389
|
+
const headers = new Headers(response.headers);
|
|
390
|
+
for (const [key, value] of Object.entries(CORS_HEADERS)) {
|
|
391
|
+
headers.set(key, value);
|
|
392
|
+
}
|
|
393
|
+
return new Response(response.body, {
|
|
394
|
+
status: response.status,
|
|
395
|
+
statusText: response.statusText,
|
|
396
|
+
headers,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
await transport.close();
|
|
401
|
+
await server.close();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const CORS_HEADERS = {
|
|
405
|
+
'Access-Control-Allow-Origin': '*',
|
|
406
|
+
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
407
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID',
|
|
408
|
+
'Access-Control-Expose-Headers': 'Mcp-Session-Id',
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
/*
|
|
412
|
+
* Public API Surface of ngx-json-render/mcp/server
|
|
413
|
+
*
|
|
414
|
+
* The server half of an MCP App for a json-render catalog: the `render-ui`
|
|
415
|
+
* tool and the `ui://` view resource. Angular-free, so it runs in Node or on
|
|
416
|
+
* an edge function; the view it serves is built with `ngx-json-render/mcp`.
|
|
417
|
+
*/
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Generated bundle index. Do not edit.
|
|
421
|
+
*/
|
|
422
|
+
|
|
423
|
+
export { DESCRIPTION_LIMIT, createRenderUiServer, handleRenderUiRequest, renderUiDescription, specInputSchema, specOutputSchema, specProblems };
|
|
424
|
+
//# sourceMappingURL=ngx-json-render-mcp-server.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngx-json-render-mcp-server.mjs","sources":["../../../projects/ngx-json-render/mcp/server/src/tool.ts","../../../projects/ngx-json-render/mcp/server/src/server.ts","../../../projects/ngx-json-render/mcp/server/src/public-api.ts","../../../projects/ngx-json-render/mcp/server/src/ngx-json-render-mcp-server.ts"],"sourcesContent":["// What the model sees of the `render-ui` tool: a short description and an\n// input schema that carries the catalog.\n//\n// `catalog.prompt()` is written to be a system prompt, and it is about 26 000\n// characters long. Claude shows a tool's description to the model cut off\n// after roughly 2 000 characters, which `catalog.prompt()` spends on its\n// JSON Patch streaming instructions: the model never saw a component, a prop\n// or an action. So the description here stays short, and each component's\n// props and each action's params go into the input schema, where they are\n// also enforced.\nimport { type Catalog, type Spec, validateSpec } from '@json-render/core';\nimport { z } from 'zod';\n\n/** Claude cuts a tool description off after roughly this many characters. */\nexport const DESCRIPTION_LIMIT = 2000;\n\n/** Options for {@link renderUiDescription}. */\nexport interface RenderUiDescriptionOptions {\n /** What the tool draws, as the first sentence names it. Default `'UI'`. */\n ui?: string;\n}\n\n/**\n * The `render-ui` tool description for `catalog`: how a spec is put\n * together, in under {@link DESCRIPTION_LIMIT} characters. The components,\n * props and actions are left to the input schema. The paragraph on\n * `sendMessage` is included only when the catalog defines that action, since\n * the view has to handle it.\n */\nexport function renderUiDescription(\n catalog: Catalog,\n { ui = 'UI' }: RenderUiDescriptionOptions = {},\n): string {\n const actions = (catalog.data as CatalogData).actions ?? {};\n const lines = [\n `Render an interactive ${ui} inline in the chat. Pass \\`spec\\`: one complete json-render spec object (not JSON Patch lines). The input schema lists every component, its props and allowed values, and every action; use only those.`,\n '',\n '- `root` is the key of the top element. `elements` is a flat map of key → { type, props, children: [child keys] }; every child key must exist in `elements`.',\n '- `state` holds the data. Read it with {\"$state\":\"/path\"} rather than repeating values in props. Data-backed UI always needs `state`; use realistic sample data.',\n '- A prop can be dynamic: {\"$state\":\"/p\"} reads, {\"$bindState\":\"/p\"} binds a form field both ways, {\"$template\":\"Hi ${/name}\"} interpolates, {\"$cond\":{\"$state\":\"/p\"},\"$then\":a,\"$else\":b} picks.',\n '- Lists: on a container, \"repeat\":{\"statePath\":\"/items\",\"key\":\"id\"}; its children render once per item and read {\"$item\":\"field\"}, or ${field} inside a $template.',\n '- `visible` and `on` sit next to `props`, never inside: \"visible\":{\"$state\":\"/tab\",\"eq\":\"home\"}, \"on\":{\"press\":{\"action\":\"setState\",\"params\":{\"statePath\":\"/tab\",\"value\":\"home\"}}}.',\n '- Forms: give inputs props.validation {\"checks\":[{\"type\":\"required\",\"message\":\"...\"}]} and submit with submitForm, which runs its inner action only when every field is valid.',\n ];\n if ('sendMessage' in actions) {\n lines.push(\n '- To continue the conversation from a button, use sendMessage: {\"action\":\"sendMessage\",\"params\":{\"text\":\"Approve release 2.4.0\",\"data\":{\"$state\":\"/release\"}}}. The host puts `text` (and `data` as JSON) in the chat as the user\\'s message, so write it in their voice. For a form: {\"action\":\"submitForm\",\"params\":{\"action\":\"sendMessage\",\"params\":{\"text\":\"Sign me up\",\"data\":{\"$state\":\"/form\"}}}}.',\n );\n }\n return lines.join('\\n');\n}\n\n/** The parts of a catalog's data this file reads. */\ninterface CatalogData {\n components: Record<string, { props: z.ZodType; description: string }>;\n actions?: Record<string, { params?: z.ZodType; description: string }>;\n}\n\n/** A prop value computed from state, such as `{ \"$state\": \"/path\" }`. */\nconst dynamic = z\n .looseObject({})\n .refine((value) => Object.keys(value).some((key) => key.startsWith('$')), {\n message:\n 'Expected a dynamic value such as {\"$state\": \"/path\"} or {\"$bindState\": \"/path\"}',\n })\n .meta({\n // An id makes the JSON Schema define it once and refer to it.\n id: 'DynamicValue',\n description:\n 'Dynamic value: {\"$state\"}, {\"$bindState\"}, {\"$bindItem\"}, {\"$item\"}, {\"$index\"}, {\"$template\"} or {\"$cond\",\"$then\",\"$else\"}.',\n });\n\n/**\n * A union's error that says why its first branch failed (say, the allowed\n * enum values), rather than just that no branch matched.\n */\nfunction firstBranchError(suffix = '') {\n return (issue: z.core.$ZodRawIssue) => {\n if (issue.code !== 'invalid_union') return undefined;\n const reasons = issue.errors[0]?.map((error) => error.message);\n return reasons?.length ? reasons.join('; ') + suffix : undefined;\n };\n}\n\n/** `shape`'s fields, each of which may also be a dynamic value; nothing else. */\nfunction allowDynamic(schema: z.ZodType) {\n const shape = (schema as z.ZodObject).shape ?? {};\n return z.strictObject(\n Object.fromEntries(\n Object.entries(shape).map(([key, field]) => {\n const optional = field instanceof z.ZodOptional;\n const value = z.union([optional ? field.unwrap() : field, dynamic], {\n error: firstBranchError(\n ', or a dynamic value such as {\"$state\": \"/path\"}',\n ),\n });\n return [key, optional ? value.optional() : value];\n }),\n ),\n );\n}\n\n/**\n * An action binding, or several, with one branch per action so the params\n * of the catalog's own actions are checked.\n */\nfunction actionBinding(catalog: Catalog) {\n const common = {\n confirm: z.unknown().optional(),\n onSuccess: z.unknown().optional(),\n onError: z.unknown().optional(),\n preventDefault: z.boolean().optional(),\n };\n const builtIn = catalog.schema.builtInActions ?? [];\n const actions = (catalog.data as CatalogData).actions ?? {};\n const bindings: z.ZodObject[] = Object.entries(actions).map(\n ([name, action]) =>\n z\n .strictObject({\n action: z.literal(name),\n params: allowDynamic(action.params ?? z.object({})),\n ...common,\n })\n .describe(`${name}: ${action.description}`),\n );\n if (builtIn.length > 0) {\n const names = builtIn.map((action) => action.name) as [string, ...string[]];\n bindings.push(\n z\n .strictObject({\n action: z.enum(names),\n params: z.record(z.string(), z.unknown()).optional(),\n ...common,\n })\n .describe(\n builtIn\n .map((action) => `${action.name}: ${action.description}`)\n .join('\\n'),\n ),\n );\n }\n const binding = z\n .discriminatedUnion('action', bindings as [z.ZodObject, ...z.ZodObject[]], {\n error: `Unknown action. Use one of: ${[...Object.keys(actions), ...builtIn.map((action) => action.name)].join(', ')}.`,\n })\n .meta({ id: 'ActionBinding' });\n return z.union([binding, z.array(binding)], { error: firstBranchError() });\n}\n\n/**\n * The tool's input schema: a spec whose elements are typed per component.\n *\n * `catalog.zodSchema()` types an element's `type` but not its props, and it\n * has no `state`, `on` or `watch`, so the MCP SDK, which parses tool\n * arguments with the input schema, stripped those and let any prop through.\n * Here each component's props are its catalog schema (with dynamic values\n * allowed and unknown props rejected), and each action's params are checked.\n */\nexport function specInputSchema(catalog: Catalog) {\n let schema = inputSchemas.get(catalog);\n if (!schema) {\n schema = buildSpecInputSchema(catalog);\n inputSchemas.set(catalog, schema);\n }\n return schema;\n}\n\n/**\n * The sentences every validatable component's description ends with, or \"\"\n * when there are none worth sharing.\n *\n * The catalog repeats its validation hint on each form field because\n * `catalog.prompt()` lists components one by one and shows no prop-level\n * descriptions. In the input schema the same ~600 characters would arrive\n * once per field component, so the schema says them once, on `elements`, and\n * each field points there. Found as the descriptions' common tail, cut back\n * to a sentence start, so the catalog does not have to export it.\n */\nfunction sharedValidationHint(components: CatalogData['components']): string {\n const descriptions = Object.values(components)\n .filter(\n (component) =>\n (component.props as Partial<z.ZodObject>).shape?.['validation'] !==\n undefined,\n )\n .map((component) => component.description);\n if (descriptions.length < 2) return '';\n\n let tail = descriptions[0];\n for (const description of descriptions.slice(1)) {\n let n = 0;\n while (\n n < tail.length &&\n n < description.length &&\n tail[tail.length - 1 - n] === description[description.length - 1 - n]\n ) {\n n++;\n }\n tail = tail.slice(tail.length - n);\n }\n // The shared tail usually starts part-way through the sentence before it.\n const before = descriptions[0].slice(0, descriptions[0].length - tail.length);\n if (before !== '' && !/[.!?] $/.test(before)) {\n const next = tail.indexOf('. ');\n tail = next === -1 ? '' : tail.slice(next + 2);\n }\n return tail.length >= 200 ? tail : '';\n}\n\n/**\n * What `render-ui` returns as `structuredContent`: the spec it was given,\n * after the input schema filled in missing `children`. Only its outline,\n * since the input schema already carries the catalog and a second copy would\n * double what every `tools/list` sends the model. ChatGPT flags a tool\n * without an output schema (\"Output schema recommended\").\n */\nexport const specOutputSchema = z\n .looseObject({\n root: z.string(),\n elements: z.record(\n z.string(),\n z.looseObject({\n type: z.string(),\n props: z.record(z.string(), z.unknown()),\n children: z.array(z.string()),\n }),\n ),\n state: z.record(z.string(), z.unknown()).optional(),\n })\n .describe('The json-render spec that was rendered.');\n\n// The server is stateless, so it is created for every request; build each\n// catalog's schema once.\nconst inputSchemas = new WeakMap<\n Catalog,\n ReturnType<typeof buildSpecInputSchema>\n>();\n\nfunction buildSpecInputSchema(catalog: Catalog) {\n const bindings = actionBinding(catalog);\n // The fields every element shares, defined once in the JSON Schema.\n const shared = {\n visible: z\n .unknown()\n .meta({\n id: 'Visibility',\n description:\n 'Show the element only when this holds: {\"$state\":\"/p\",\"eq\":v} (or neq, gt, gte, lt, lte, not), an array of conditions (all), {\"$or\":[...]}, true or false.',\n })\n .optional(),\n repeat: z\n .strictObject({\n statePath: z.union([z.string(), dynamic]),\n key: z.string().optional(),\n })\n .meta({\n id: 'Repeat',\n description:\n 'Render the children once per item of the state array at statePath; they read {\"$item\":\"field\"} and {\"$index\":true}.',\n })\n .optional(),\n on: z\n .record(z.string(), bindings)\n .meta({\n id: 'Events',\n description:\n 'Event name (such as \"press\", \"change\" or \"submit\") → action binding.',\n })\n .optional(),\n watch: z\n .record(z.string(), bindings)\n .meta({\n id: 'Watchers',\n description:\n 'State path → action binding, run when the value at that path changes.',\n })\n .optional(),\n };\n const { components } = catalog.data as CatalogData;\n const hint = sharedValidationHint(components);\n const elements = Object.entries(components).map(([type, component]) => {\n const description =\n hint && component.description.endsWith(hint)\n ? `${component.description.slice(0, -hint.length)}Validation: see \\`elements\\`.`\n : component.description;\n return z\n .strictObject({\n type: z.literal(type),\n props: allowDynamic(component.props),\n children: z.array(z.string()).default([]),\n ...shared,\n })\n .describe(`${type}: ${description}`);\n });\n return z.strictObject({\n root: z.string().describe('Key of the top element in `elements`.'),\n elements: z\n .record(\n z.string(),\n z.discriminatedUnion(\n 'type',\n elements as unknown as [z.ZodObject, ...z.ZodObject[]],\n ),\n )\n .describe(\n hint\n ? `Elements by key. On every component that takes \\`validation\\`: ${hint}`\n : 'Elements by key.',\n ),\n state: z\n .record(z.string(), z.unknown())\n .optional()\n .describe(\n 'Initial state model; bindings read it with {\"$state\": \"/path\"}.',\n ),\n });\n}\n\n/**\n * What the input schema cannot express: a root and children that exist,\n * repeat with a template child, valid visibility conditions, and `$template`\n * placeholders that read a repeat item. Returns the problems as text for the\n * model, or `undefined` when there are none.\n */\nexport function specProblems(spec: object): string | undefined {\n // The input schema has already checked the shape `Spec` describes.\n const { issues } = validateSpec(spec as Spec);\n const errors = issues.filter((issue) => issue.severity === 'error');\n const messages = [\n ...errors.map((issue) => issue.message),\n ...itemTemplateProblems(spec as Spec),\n ];\n return messages.length > 0\n ? [\n 'The generated UI spec has the following errors:',\n ...messages.map((message) => `- ${message}`),\n ].join('\\n')\n : undefined;\n}\n\n/**\n * `${$item/field}` placeholders in a `$template`. A `$template` reads an\n * absolute `${/path}` from state and a bare `${field}` from the repeat item\n * first, so `$item/field` is looked up as the state path `/$item/field`,\n * finds nothing and renders as an empty string, without an error anywhere.\n */\nfunction itemTemplateProblems(spec: Spec): string[] {\n const problems: string[] = [];\n for (const [key, { props, on, watch }] of Object.entries(spec.elements)) {\n walk({ props, on, watch }, '', (template, path) => {\n for (const [, placeholder, field] of template.matchAll(\n ITEM_PLACEHOLDER,\n )) {\n problems.push(\n `Element \"${key}\" ${path}: inside $template write \\${${field.replaceAll('.', '/')}}, not \\${${placeholder}}`,\n );\n }\n });\n }\n return problems;\n}\n\n/** `${$item/field}` or `${$item.field}`; the groups are the placeholder and the field. */\nconst ITEM_PLACEHOLDER = /\\$\\{(\\$item[/.]([^}]+))\\}/g;\n\n/** Calls `visit` with every `$template` string under `value` and where it sits. */\nfunction walk(\n value: unknown,\n path: string,\n visit: (template: string, path: string) => void,\n): void {\n if (Array.isArray(value)) {\n value.forEach((item, index) => walk(item, `${path}[${index}]`, visit));\n } else if (value !== null && typeof value === 'object') {\n for (const [key, child] of Object.entries(value)) {\n if (key === '$template' && typeof child === 'string') {\n visit(child, path);\n } else {\n walk(child, path ? `${path}.${key}` : key, visit);\n }\n }\n }\n}\n","// The MCP server half of an MCP App: one `render-ui` tool that takes a\n// json-render spec, and the `ui://` view a host renders it in. Registered the\n// way upstream's `createMcpApp` (`@json-render/mcp`) does it, same tool name\n// and same view resource, with a short description and a typed input schema\n// (`tool.ts`) in place of `catalog.prompt()` and `catalog.zodSchema()`.\n//\n// Nothing here is Angular-specific: the Angular part is the view, passed in\n// as one HTML document.\nimport type { Catalog } from '@json-render/core';\nimport {\n RESOURCE_MIME_TYPE,\n registerAppResource,\n registerAppTool,\n} from '@modelcontextprotocol/ext-apps/server';\nimport type { McpUiResourceCsp } from '@modelcontextprotocol/ext-apps';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';\nimport {\n renderUiDescription,\n specInputSchema,\n specOutputSchema,\n specProblems,\n} from './tool';\n\n/** Options for {@link createRenderUiServer}. */\nexport interface RenderUiServerOptions {\n /** The components and actions the model may use. */\n catalog: Catalog;\n /** The view: one self-contained HTML document that renders the spec. */\n html: string;\n /** The server's name, as hosts list it. Default `'ngx-json-render'`. */\n name?: string;\n /** The server's version. Default `'0.0.0'`. */\n version?: string;\n /** The tool's name; the view resource is `ui://<toolName>/view.html`. Default `'render-ui'`. */\n toolName?: string;\n /** The tool's title, as hosts show it. Default `'Render UI'`. */\n title?: string;\n /** The tool's description. Default: {@link renderUiDescription} for the catalog. */\n description?: string;\n /**\n * The origins the view may load from (`resourceDomains`) and connect to\n * (`connectDomains`). Left out, the host's default applies, which the MCP\n * Apps spec defines as none.\n */\n csp?: McpUiResourceCsp;\n /**\n * The origin ChatGPT derives the view's sandbox origin from, which its\n * plugin directory requires, unique per plugin. Set through the OpenAI\n * alias rather than `_meta.ui.domain`, because that key's format is up to\n * each host and Claude expects a `{hash}.claudemcpcontent.com` value there.\n */\n widgetDomain?: string;\n}\n\n/**\n * An MCP server with a `render-ui` tool for `catalog` and its `ui://` view.\n * Connect it to any transport: `StdioServerTransport` for a local host,\n * or {@link handleRenderUiRequest} for Streamable HTTP.\n *\n * The tool checks a spec against the catalog before it echoes it back. A\n * rejected spec goes back to the model as an error it can fix.\n */\nexport function createRenderUiServer(options: RenderUiServerOptions) {\n const {\n catalog,\n html,\n name = 'ngx-json-render',\n version = '0.0.0',\n toolName = 'render-ui',\n title = 'Render UI',\n description = renderUiDescription(catalog),\n csp,\n widgetDomain,\n } = options;\n const resourceUri = `ui://${toolName}/view.html`;\n const server = new McpServer({ name, version });\n registerAppTool(\n server,\n toolName,\n {\n title,\n description,\n inputSchema: { spec: specInputSchema(catalog) },\n outputSchema: { spec: specOutputSchema },\n // It only echoes the spec back for the view to draw: nothing is read\n // from or written to any system.\n annotations: {\n title,\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n _meta: { ui: { resourceUri } },\n },\n // The SDK has already parsed `spec` against the input schema; what is\n // left is the structure between elements. The text keeps upstream's\n // shape, the bare spec, which is what views built on\n // `@json-render/mcp/app` read.\n async ({ spec }) => {\n const problems = specProblems(spec);\n return problems\n ? { isError: true, content: [{ type: 'text', text: problems }] }\n : {\n content: [{ type: 'text', text: JSON.stringify(spec) }],\n structuredContent: { spec },\n };\n },\n );\n const meta: Record<string, unknown> = {};\n if (csp) meta['ui'] = { csp };\n if (widgetDomain) meta['openai/widgetDomain'] = widgetDomain;\n registerAppResource(\n server,\n resourceUri,\n resourceUri,\n { mimeType: RESOURCE_MIME_TYPE },\n async () => ({\n contents: [\n {\n uri: resourceUri,\n mimeType: RESOURCE_MIME_TYPE,\n text: html,\n ...(Object.keys(meta).length > 0 && { _meta: meta }),\n },\n ],\n }),\n );\n return server;\n}\n\n/**\n * Serve one Streamable HTTP request, statelessly: a fresh server and\n * transport per request, so it runs unchanged on a serverless or edge\n * function (anything with web `Request` and `Response`). Answers CORS,\n * because browser-based clients such as MCP Inspector call cross-origin.\n */\nexport async function handleRenderUiRequest(\n options: RenderUiServerOptions,\n request: Request,\n): Promise<Response> {\n if (request.method === 'OPTIONS') {\n return new Response(null, { status: 204, headers: CORS_HEADERS });\n }\n const server = createRenderUiServer(options);\n const transport = new WebStandardStreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n // One JSON body per request: nothing here streams, and a serverless\n // function returns as soon as the response is complete.\n enableJsonResponse: true,\n });\n await server.connect(transport);\n try {\n const response = await transport.handleRequest(request);\n const headers = new Headers(response.headers);\n for (const [key, value] of Object.entries(CORS_HEADERS)) {\n headers.set(key, value);\n }\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n } finally {\n await transport.close();\n await server.close();\n }\n}\n\nconst CORS_HEADERS: Record<string, string> = {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',\n 'Access-Control-Allow-Headers':\n 'Content-Type, Accept, Authorization, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID',\n 'Access-Control-Expose-Headers': 'Mcp-Session-Id',\n};\n","/*\n * Public API Surface of ngx-json-render/mcp/server\n *\n * The server half of an MCP App for a json-render catalog: the `render-ui`\n * tool and the `ui://` view resource. Angular-free, so it runs in Node or on\n * an edge function; the view it serves is built with `ngx-json-render/mcp`.\n */\n\nexport {\n createRenderUiServer,\n handleRenderUiRequest,\n type RenderUiServerOptions,\n} from './server';\nexport {\n DESCRIPTION_LIMIT,\n renderUiDescription,\n specInputSchema,\n specOutputSchema,\n specProblems,\n type RenderUiDescriptionOptions,\n} from './tool';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ngx-json-render",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "Generative UI for Angular: a json-render renderer that streams AI-generated JSON specs into real Angular components with signals.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"angular",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"sideEffects": false,
|
|
54
54
|
"schematics": "./schematics/collection.json",
|
|
55
|
+
"builders": "./schematics/builders.json",
|
|
55
56
|
"ng-add": {
|
|
56
57
|
"save": "dependencies"
|
|
57
58
|
},
|
|
@@ -77,6 +78,10 @@
|
|
|
77
78
|
"types": "./types/ngx-json-render-mcp.d.ts",
|
|
78
79
|
"default": "./fesm2022/ngx-json-render-mcp.mjs"
|
|
79
80
|
},
|
|
81
|
+
"./mcp/server": {
|
|
82
|
+
"types": "./types/ngx-json-render-mcp-server.d.ts",
|
|
83
|
+
"default": "./fesm2022/ngx-json-render-mcp-server.mjs"
|
|
84
|
+
},
|
|
80
85
|
"./router": {
|
|
81
86
|
"types": "./types/ngx-json-render-router.d.ts",
|
|
82
87
|
"default": "./fesm2022/ngx-json-render-router.mjs"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../node_modules/@angular-devkit/architect/src/builders-schema.json",
|
|
3
|
+
"builders": {
|
|
4
|
+
"mcp-app": {
|
|
5
|
+
"implementation": "./mcp-app-builder/index.cjs",
|
|
6
|
+
"schema": "./mcp-app-builder/schema.json",
|
|
7
|
+
"description": "Builds an MCP App: the view as one view.html, the server as server.mjs."
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildMcpApp = buildMcpApp;
|
|
4
|
+
const architect_1 = require("@angular-devkit/architect");
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_module_1 = require("node:module");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const inline_cjs_1 = require("./inline.cjs");
|
|
9
|
+
/**
|
|
10
|
+
* `ngx-json-render:mcp-app`: runs the view's application build, folds its
|
|
11
|
+
* browser output into one `view.html` (an MCP Apps host loads the view as a
|
|
12
|
+
* single document), and bundles the server entry, if there is one, into
|
|
13
|
+
* `server.mjs` next to it.
|
|
14
|
+
*/
|
|
15
|
+
async function buildMcpApp(options, context) {
|
|
16
|
+
const root = context.workspaceRoot;
|
|
17
|
+
const target = (0, architect_1.targetFromTargetString)(options.buildTarget, context.target?.project);
|
|
18
|
+
const run = await context.scheduleTarget(target);
|
|
19
|
+
const result = await run.result;
|
|
20
|
+
await run.stop();
|
|
21
|
+
if (!result.success)
|
|
22
|
+
return result;
|
|
23
|
+
const esbuild = loadEsbuild(root);
|
|
24
|
+
const browserDir = await browserOutput(context, target);
|
|
25
|
+
const view = await (0, inline_cjs_1.inlineView)(browserDir, async (entry) => {
|
|
26
|
+
const { outputFiles } = await esbuild.build({
|
|
27
|
+
entryPoints: [entry],
|
|
28
|
+
bundle: true,
|
|
29
|
+
format: 'esm',
|
|
30
|
+
minify: true,
|
|
31
|
+
write: false,
|
|
32
|
+
logLevel: 'warning',
|
|
33
|
+
});
|
|
34
|
+
return outputFiles[0].text;
|
|
35
|
+
});
|
|
36
|
+
const out = (0, node_path_1.resolve)(root, options.outputPath);
|
|
37
|
+
(0, node_fs_1.mkdirSync)(out, { recursive: true });
|
|
38
|
+
const viewPath = (0, node_path_1.join)(out, 'view.html');
|
|
39
|
+
(0, node_fs_1.writeFileSync)(viewPath, view.html);
|
|
40
|
+
context.logger.info(`Built ${(0, node_path_1.relative)(root, viewPath)} (${Math.round(view.html.length / 1024)} kB)`);
|
|
41
|
+
if (view.leftOut.length > 0) {
|
|
42
|
+
context.logger.warn(`Not in view.html, so the host cannot load them: ${view.leftOut.join(', ')}. ` +
|
|
43
|
+
'Reference assets from a stylesheet (they are inlined) or from an https: URL the view CSP allows.');
|
|
44
|
+
}
|
|
45
|
+
if (options.server) {
|
|
46
|
+
const serverPath = (0, node_path_1.join)(out, 'server.mjs');
|
|
47
|
+
await esbuild.build({
|
|
48
|
+
entryPoints: [(0, node_path_1.resolve)(root, options.server)],
|
|
49
|
+
outfile: serverPath,
|
|
50
|
+
bundle: true,
|
|
51
|
+
platform: 'node',
|
|
52
|
+
format: 'esm',
|
|
53
|
+
target: 'node20',
|
|
54
|
+
tsconfig: options.tsConfig ? (0, node_path_1.resolve)(root, options.tsConfig) : undefined,
|
|
55
|
+
packages: options.externalPackages ? 'external' : undefined,
|
|
56
|
+
// The view as text, for a server that imports it rather than reading
|
|
57
|
+
// the file next to it.
|
|
58
|
+
loader: { '.html': 'text' },
|
|
59
|
+
// Bundled CommonJS dependencies may call require(); give the ES module
|
|
60
|
+
// one.
|
|
61
|
+
banner: options.externalPackages
|
|
62
|
+
? undefined
|
|
63
|
+
: {
|
|
64
|
+
js: "import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);",
|
|
65
|
+
},
|
|
66
|
+
logLevel: 'warning',
|
|
67
|
+
});
|
|
68
|
+
context.logger.info(`Built ${(0, node_path_1.relative)(root, serverPath)}`);
|
|
69
|
+
}
|
|
70
|
+
return { success: true, outputPath: out };
|
|
71
|
+
}
|
|
72
|
+
/** Where the application build wrote its browser files. */
|
|
73
|
+
async function browserOutput(context, target) {
|
|
74
|
+
const options = await context.getTargetOptions(target);
|
|
75
|
+
const outputPath = options['outputPath'];
|
|
76
|
+
const base = typeof outputPath === 'string'
|
|
77
|
+
? outputPath
|
|
78
|
+
: (outputPath?.base ?? (0, node_path_1.join)('dist', target.project));
|
|
79
|
+
const browser = typeof outputPath === 'object' && outputPath.browser !== undefined
|
|
80
|
+
? outputPath.browser
|
|
81
|
+
: 'browser';
|
|
82
|
+
return (0, node_path_1.resolve)(context.workspaceRoot, base, browser);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* esbuild, from `@angular/build` (which every application build already
|
|
86
|
+
* installs at a version it was tested with), or else from the workspace.
|
|
87
|
+
*/
|
|
88
|
+
function loadEsbuild(root) {
|
|
89
|
+
const workspace = (0, node_module_1.createRequire)((0, node_path_1.join)(root, 'package.json'));
|
|
90
|
+
try {
|
|
91
|
+
const build = (0, node_path_1.dirname)(workspace.resolve('@angular/build/package.json'));
|
|
92
|
+
return (0, node_module_1.createRequire)((0, node_path_1.join)(build, 'package.json'))('esbuild');
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return workspace('esbuild');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.default = (0, architect_1.createBuilder)(buildMcpApp);
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inlineView = inlineView;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const MIME = {
|
|
7
|
+
'.woff2': 'font/woff2',
|
|
8
|
+
'.woff': 'font/woff',
|
|
9
|
+
'.ttf': 'font/ttf',
|
|
10
|
+
'.otf': 'font/otf',
|
|
11
|
+
'.png': 'image/png',
|
|
12
|
+
'.jpg': 'image/jpeg',
|
|
13
|
+
'.jpeg': 'image/jpeg',
|
|
14
|
+
'.gif': 'image/gif',
|
|
15
|
+
'.webp': 'image/webp',
|
|
16
|
+
'.avif': 'image/avif',
|
|
17
|
+
'.svg': 'image/svg+xml',
|
|
18
|
+
'.ico': 'image/x-icon',
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The browser output of an Angular application build as one HTML document.
|
|
22
|
+
* MCP Apps hosts load a view's `ui://` resource as a single document with no
|
|
23
|
+
* origin to fetch from, so every module script (main, polyfills, and the
|
|
24
|
+
* chunks they import) is bundled and inlined, every local stylesheet is
|
|
25
|
+
* inlined, and fonts and images the stylesheets point at become data URLs.
|
|
26
|
+
* External URLs stay as they are; the view's CSP has to allow them.
|
|
27
|
+
*/
|
|
28
|
+
async function inlineView(browserDir, bundle) {
|
|
29
|
+
const indexPath = (0, node_path_1.join)(browserDir, 'index.html');
|
|
30
|
+
if (!(0, node_fs_1.existsSync)(indexPath)) {
|
|
31
|
+
throw new Error(`No index.html in ${browserDir}. Is the build target an @angular/build:application build?`);
|
|
32
|
+
}
|
|
33
|
+
let html = (0, node_fs_1.readFileSync)(indexPath, 'utf8');
|
|
34
|
+
const used = new Set(['index.html']);
|
|
35
|
+
// Preloads point at chunks that are about to be inlined.
|
|
36
|
+
html = html.replace(/<link\b[^>]*\brel="modulepreload"[^>]*>/g, '');
|
|
37
|
+
// A view lives in an iframe, which shows no icon; `ng new` links one.
|
|
38
|
+
html = html.replace(/<link\b[^>]*\brel="(?:shortcut )?icon"[^>]*>\s*/g, (tag) => {
|
|
39
|
+
const href = attribute(tag, 'href');
|
|
40
|
+
const file = href && local(browserDir, href);
|
|
41
|
+
if (!file)
|
|
42
|
+
return tag;
|
|
43
|
+
used.add((0, node_path_1.relative)(browserDir, file));
|
|
44
|
+
return '';
|
|
45
|
+
});
|
|
46
|
+
for (const tag of html.match(/<script\b[^>]*\bsrc="[^"]*"[^>]*><\/script>/g) ?? []) {
|
|
47
|
+
const src = attribute(tag, 'src');
|
|
48
|
+
const file = src && local(browserDir, src);
|
|
49
|
+
if (!file)
|
|
50
|
+
continue;
|
|
51
|
+
used.add((0, node_path_1.relative)(browserDir, file));
|
|
52
|
+
const isModule = attribute(tag, 'type') === 'module';
|
|
53
|
+
const code = isModule ? await bundle(file) : (0, node_fs_1.readFileSync)(file, 'utf8');
|
|
54
|
+
html = html.replace(tag, () => `<script${isModule ? ' type="module"' : ''}>${escapeTag(code, 'script')}</script>`);
|
|
55
|
+
}
|
|
56
|
+
// Angular may load the stylesheet as `media="print" onload=…` with a
|
|
57
|
+
// <noscript> fallback, so it does not block the first paint.
|
|
58
|
+
const stylesheet = /<link\b[^>]*\brel="stylesheet"[^>]*>(\s*<noscript>[\s\S]*?<\/noscript>)?/g;
|
|
59
|
+
for (const [match] of html.matchAll(stylesheet)) {
|
|
60
|
+
const href = attribute(match, 'href');
|
|
61
|
+
const file = href && local(browserDir, href);
|
|
62
|
+
if (!file)
|
|
63
|
+
continue;
|
|
64
|
+
used.add((0, node_path_1.relative)(browserDir, file));
|
|
65
|
+
const css = inlineUrls((0, node_fs_1.readFileSync)(file, 'utf8'), file, browserDir, used);
|
|
66
|
+
html = html.replace(match, () => `<style>${escapeTag(css, 'style')}</style>`);
|
|
67
|
+
}
|
|
68
|
+
// Inline <style> blocks (critical CSS) can point at media files too.
|
|
69
|
+
html = html.replace(/(<style\b[^>]*>)([\s\S]*?)(<\/style>)/g, (_, open, css, close) => open + inlineUrls(css, indexPath, browserDir, used) + close);
|
|
70
|
+
const leftOut = files(browserDir)
|
|
71
|
+
.map((file) => (0, node_path_1.relative)(browserDir, file))
|
|
72
|
+
.filter((file) => !used.has(file) && !isBuildArtifact(file));
|
|
73
|
+
return { html, leftOut };
|
|
74
|
+
}
|
|
75
|
+
/** `url(...)` references to files of the build, as data URLs. */
|
|
76
|
+
function inlineUrls(css, from, browserDir, used) {
|
|
77
|
+
return css.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/g, (match, quote, url) => {
|
|
78
|
+
const base = (0, node_path_1.join)(from, '..');
|
|
79
|
+
const file = local(base, url) ?? local(browserDir, url);
|
|
80
|
+
const mime = file && MIME[(0, node_path_1.extname)(file).toLowerCase()];
|
|
81
|
+
if (!file || !mime)
|
|
82
|
+
return match;
|
|
83
|
+
used.add((0, node_path_1.relative)(browserDir, file));
|
|
84
|
+
const data = (0, node_fs_1.readFileSync)(file).toString('base64');
|
|
85
|
+
return `url(${quote}data:${mime};base64,${data}${quote})`;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** The file `url` names under `dir`, or undefined for anything external. */
|
|
89
|
+
function local(dir, url) {
|
|
90
|
+
if (/^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(url))
|
|
91
|
+
return undefined;
|
|
92
|
+
const path = decodeURIComponent(url.split(/[?#]/)[0]).replace(/^\/+/, '');
|
|
93
|
+
const file = (0, node_path_1.resolve)(dir, path);
|
|
94
|
+
if (file !== (0, node_path_1.resolve)(dir) && !file.startsWith((0, node_path_1.resolve)(dir) + node_path_1.sep)) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return (0, node_fs_1.existsSync)(file) && (0, node_fs_1.statSync)(file).isFile() ? file : undefined;
|
|
98
|
+
}
|
|
99
|
+
function attribute(tag, name) {
|
|
100
|
+
return new RegExp(`\\b${name}="([^"]*)"`).exec(tag)?.[1];
|
|
101
|
+
}
|
|
102
|
+
/** Keeps `</script>` or `</style>` inside the code from closing the tag. */
|
|
103
|
+
function escapeTag(code, tag) {
|
|
104
|
+
return code.replace(new RegExp(`</${tag}`, 'gi'), `<\\/${tag}`);
|
|
105
|
+
}
|
|
106
|
+
/** Chunks and maps the inlined scripts already carry, and build metadata. */
|
|
107
|
+
function isBuildArtifact(file) {
|
|
108
|
+
return (/\.(?:js|mjs|map)$/.test(file) ||
|
|
109
|
+
file === '3rdpartylicenses.txt' ||
|
|
110
|
+
file === 'prerendered-routes.json');
|
|
111
|
+
}
|
|
112
|
+
function files(dir) {
|
|
113
|
+
return (0, node_fs_1.readdirSync)(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
114
|
+
const path = (0, node_path_1.join)(dir, entry.name);
|
|
115
|
+
return entry.isDirectory() ? files(path) : [path];
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema",
|
|
3
|
+
"title": "ngx-json-render MCP App builder",
|
|
4
|
+
"description": "Builds an MCP App view into one view.html and bundles its server into server.mjs.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"buildTarget": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "The view's @angular/build:application target, such as \"my-app:build:production\" or \":build:production\" for this project."
|
|
10
|
+
},
|
|
11
|
+
"outputPath": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"description": "Directory for view.html and server.mjs, relative to the workspace root."
|
|
14
|
+
},
|
|
15
|
+
"server": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "The server's entry file, bundled into server.mjs. Leave it out to build the view only."
|
|
18
|
+
},
|
|
19
|
+
"tsConfig": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "The tsconfig esbuild reads for the server (paths, decorators). By default, the nearest one to each file."
|
|
22
|
+
},
|
|
23
|
+
"externalPackages": {
|
|
24
|
+
"type": "boolean",
|
|
25
|
+
"default": false,
|
|
26
|
+
"description": "Leave node_modules packages out of server.mjs, to be installed next to it. By default the server is one self-contained file."
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"required": ["buildTarget", "outputPath"],
|
|
30
|
+
"additionalProperties": false
|
|
31
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Catalog } from '@json-render/core';
|
|
2
|
+
import { McpUiResourceCsp } from '@modelcontextprotocol/ext-apps';
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
/** Options for {@link createRenderUiServer}. */
|
|
7
|
+
interface RenderUiServerOptions {
|
|
8
|
+
/** The components and actions the model may use. */
|
|
9
|
+
catalog: Catalog;
|
|
10
|
+
/** The view: one self-contained HTML document that renders the spec. */
|
|
11
|
+
html: string;
|
|
12
|
+
/** The server's name, as hosts list it. Default `'ngx-json-render'`. */
|
|
13
|
+
name?: string;
|
|
14
|
+
/** The server's version. Default `'0.0.0'`. */
|
|
15
|
+
version?: string;
|
|
16
|
+
/** The tool's name; the view resource is `ui://<toolName>/view.html`. Default `'render-ui'`. */
|
|
17
|
+
toolName?: string;
|
|
18
|
+
/** The tool's title, as hosts show it. Default `'Render UI'`. */
|
|
19
|
+
title?: string;
|
|
20
|
+
/** The tool's description. Default: {@link renderUiDescription} for the catalog. */
|
|
21
|
+
description?: string;
|
|
22
|
+
/**
|
|
23
|
+
* The origins the view may load from (`resourceDomains`) and connect to
|
|
24
|
+
* (`connectDomains`). Left out, the host's default applies, which the MCP
|
|
25
|
+
* Apps spec defines as none.
|
|
26
|
+
*/
|
|
27
|
+
csp?: McpUiResourceCsp;
|
|
28
|
+
/**
|
|
29
|
+
* The origin ChatGPT derives the view's sandbox origin from, which its
|
|
30
|
+
* plugin directory requires, unique per plugin. Set through the OpenAI
|
|
31
|
+
* alias rather than `_meta.ui.domain`, because that key's format is up to
|
|
32
|
+
* each host and Claude expects a `{hash}.claudemcpcontent.com` value there.
|
|
33
|
+
*/
|
|
34
|
+
widgetDomain?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* An MCP server with a `render-ui` tool for `catalog` and its `ui://` view.
|
|
38
|
+
* Connect it to any transport: `StdioServerTransport` for a local host,
|
|
39
|
+
* or {@link handleRenderUiRequest} for Streamable HTTP.
|
|
40
|
+
*
|
|
41
|
+
* The tool checks a spec against the catalog before it echoes it back. A
|
|
42
|
+
* rejected spec goes back to the model as an error it can fix.
|
|
43
|
+
*/
|
|
44
|
+
declare function createRenderUiServer(options: RenderUiServerOptions): McpServer;
|
|
45
|
+
/**
|
|
46
|
+
* Serve one Streamable HTTP request, statelessly: a fresh server and
|
|
47
|
+
* transport per request, so it runs unchanged on a serverless or edge
|
|
48
|
+
* function (anything with web `Request` and `Response`). Answers CORS,
|
|
49
|
+
* because browser-based clients such as MCP Inspector call cross-origin.
|
|
50
|
+
*/
|
|
51
|
+
declare function handleRenderUiRequest(options: RenderUiServerOptions, request: Request): Promise<Response>;
|
|
52
|
+
|
|
53
|
+
/** Claude cuts a tool description off after roughly this many characters. */
|
|
54
|
+
declare const DESCRIPTION_LIMIT = 2000;
|
|
55
|
+
/** Options for {@link renderUiDescription}. */
|
|
56
|
+
interface RenderUiDescriptionOptions {
|
|
57
|
+
/** What the tool draws, as the first sentence names it. Default `'UI'`. */
|
|
58
|
+
ui?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The `render-ui` tool description for `catalog`: how a spec is put
|
|
62
|
+
* together, in under {@link DESCRIPTION_LIMIT} characters. The components,
|
|
63
|
+
* props and actions are left to the input schema. The paragraph on
|
|
64
|
+
* `sendMessage` is included only when the catalog defines that action, since
|
|
65
|
+
* the view has to handle it.
|
|
66
|
+
*/
|
|
67
|
+
declare function renderUiDescription(catalog: Catalog, { ui }?: RenderUiDescriptionOptions): string;
|
|
68
|
+
/**
|
|
69
|
+
* The tool's input schema: a spec whose elements are typed per component.
|
|
70
|
+
*
|
|
71
|
+
* `catalog.zodSchema()` types an element's `type` but not its props, and it
|
|
72
|
+
* has no `state`, `on` or `watch`, so the MCP SDK, which parses tool
|
|
73
|
+
* arguments with the input schema, stripped those and let any prop through.
|
|
74
|
+
* Here each component's props are its catalog schema (with dynamic values
|
|
75
|
+
* allowed and unknown props rejected), and each action's params are checked.
|
|
76
|
+
*/
|
|
77
|
+
declare function specInputSchema(catalog: Catalog): z.ZodObject<{
|
|
78
|
+
root: z.ZodString;
|
|
79
|
+
elements: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>, ...z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>[]], "type">>;
|
|
80
|
+
state: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
81
|
+
}, z.core.$strict>;
|
|
82
|
+
/**
|
|
83
|
+
* What `render-ui` returns as `structuredContent`: the spec it was given,
|
|
84
|
+
* after the input schema filled in missing `children`. Only its outline,
|
|
85
|
+
* since the input schema already carries the catalog and a second copy would
|
|
86
|
+
* double what every `tools/list` sends the model. ChatGPT flags a tool
|
|
87
|
+
* without an output schema ("Output schema recommended").
|
|
88
|
+
*/
|
|
89
|
+
declare const specOutputSchema: z.ZodObject<{
|
|
90
|
+
root: z.ZodString;
|
|
91
|
+
elements: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
92
|
+
type: z.ZodString;
|
|
93
|
+
props: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
94
|
+
children: z.ZodArray<z.ZodString>;
|
|
95
|
+
}, z.core.$loose>>;
|
|
96
|
+
state: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
97
|
+
}, z.core.$loose>;
|
|
98
|
+
/**
|
|
99
|
+
* What the input schema cannot express: a root and children that exist,
|
|
100
|
+
* repeat with a template child, valid visibility conditions, and `$template`
|
|
101
|
+
* placeholders that read a repeat item. Returns the problems as text for the
|
|
102
|
+
* model, or `undefined` when there are none.
|
|
103
|
+
*/
|
|
104
|
+
declare function specProblems(spec: object): string | undefined;
|
|
105
|
+
|
|
106
|
+
export { DESCRIPTION_LIMIT, createRenderUiServer, handleRenderUiRequest, renderUiDescription, specInputSchema, specOutputSchema, specProblems };
|
|
107
|
+
export type { RenderUiDescriptionOptions, RenderUiServerOptions };
|