ngx-json-render 0.9.6 → 0.9.7

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 CHANGED
@@ -732,7 +732,7 @@ export class App {
732
732
  }
733
733
  ```
734
734
 
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.
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. `mcp.handlers` holds a ready `sendMessage` action handler for `<json-render [handlers]>`, the one the server's `sendMessage: true` describes to the model (below), and `mcp.lastMessage` is a signal with how its last call went, so the view can say when a host declined the message. The connection closes with the injector that created it.
736
736
 
737
737
  ### The server
738
738
 
@@ -762,11 +762,21 @@ What it does on top of upstream's `createMcpApp` from `@json-render/mcp`, whose
762
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
763
  - **The structured result ChatGPT asks for:** the spec as `structuredContent`, with an output schema (`specOutputSchema`).
764
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()`.
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) `widgetDomain` (the view's origin for ChatGPT's plugin directory) and `sendMessage: true`, which adds a `sendMessage` action to the catalog the model sees, so a button in the view can post a message to the chat as the user; hand the view `mcp.handlers` to handle it. Your catalog stays host-neutral; `withSendMessage(catalog)` gives you the extended catalog if you need it elsewhere.
766
+
767
+ ### In one command
768
+
769
+ ```bash
770
+ ng generate ngx-json-render:mcp-app --catalog src/app/catalog.ts#catalog --registry src/app/registry.ts#registry
771
+ ng run mcp-app:mcp
772
+ node dist/mcp-app/server.mjs
773
+ ```
774
+
775
+ The schematic adds an application project for the view (`--name`, default `mcp-app`): `src/app/app.ts` renders the model's spec with your registry, handles `sendMessage` with `mcp.handlers`, says under the UI whether the host took the message, and follows the host's light or dark theme. Next to it, `server.ts` serves your catalog with `createRenderUiServer({ sendMessage: true })` over stdio, or over HTTP on port 3001 with `--http`, and an `mcp` target builds both with the builder below. It also adds the MCP peers to `package.json`. The server loads the catalog file in Node, so that file must not import Angular; the schematic warns when it does. Without `--catalog` and `--registry` it uses the Angular Material catalog when `ngx-json-render-material` is installed, with its theme, fonts and CSP, and otherwise writes a starter catalog and registry into the new project.
766
776
 
767
777
  ### Building it
768
778
 
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`:
779
+ 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. The schematic above sets it up; by hand, give the view its own application project (`ng generate application mcp-view`), then add a target to it in `angular.json`:
770
780
 
771
781
  ```json
772
782
  "mcp": {
@@ -1221,11 +1231,11 @@ Registry & schema: `defineRegistry`, `createStoreSetState`, `schema`, and the ca
1221
1231
 
1222
1232
  Schema alone (`ngx-json-render/schema`): `schema`, `AngularSchema`, `AngularSpec`, with no Angular behind them, for a server that defines a catalog.
1223
1233
 
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`.
1234
+ MCP Apps (`ngx-json-render/mcp`): `injectJsonRenderApp`, `parseSpecFromToolResult`, `messageText`, and the types `JsonRenderApp`, `JsonRenderAppOptions`, `MessageOutcome`. Needs the optional peers `@modelcontextprotocol/ext-apps` and `@modelcontextprotocol/sdk`.
1225
1235
 
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.
1236
+ MCP App server (`ngx-json-render/mcp/server`): `createRenderUiServer`, `handleRenderUiRequest`, `renderUiDescription`, `specInputSchema`, `specOutputSchema`, `specProblems`, `DESCRIPTION_LIMIT`, `withSendMessage`, `sendMessageAction`, and the types `RenderUiServerOptions`, `RenderUiDescriptionOptions`. No Angular; the same optional peers.
1227
1237
 
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`.
1238
+ MCP App builder (`ngx-json-render:mcp-app`): the view's application build as one `view.html`, and the server bundled into `server.mjs`. Schematic `ng generate ngx-json-render:mcp-app` (`--name`, `--catalog`, `--registry`): the view project, its server and the `mcp` target.
1229
1239
 
1230
1240
  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.
1231
1241
 
@@ -1,8 +1,47 @@
1
+ import { defineCatalog, validateSpec } from '@json-render/core';
2
+ import { z } from 'zod';
1
3
  import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
2
4
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
5
  import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
4
- import { validateSpec } from '@json-render/core';
5
- import { z } from 'zod';
6
+
7
+ // Actions that only make sense inside an MCP Apps host. A catalog stays
8
+ // host-neutral; the server adds these to the catalog it describes to the
9
+ // model, and the view handles them (`injectJsonRenderApp().handlers` in
10
+ // `ngx-json-render/mcp`).
11
+ /**
12
+ * `sendMessage`: post a message to the chat as the user, so the conversation
13
+ * carries on from what was clicked in the view.
14
+ */
15
+ const sendMessageAction = {
16
+ params: z.object({
17
+ text: z.string(),
18
+ data: z.record(z.string(), z.unknown()).optional(),
19
+ }),
20
+ description: 'Send a message to you, the assistant, as the user, so the conversation continues from the UI. ' +
21
+ 'Params: { "text": "what the user chose or asks for", "data"?: { "$state": "/path" } }. `data` is optional and is attached as JSON; ' +
22
+ 'make it a single reference to a state object, because references nested inside `data` are not resolved. ' +
23
+ 'Use it for choices and follow-ups ("Approve", "Show more", a picked option). ' +
24
+ 'To send a form, gate it with submitForm: ' +
25
+ '{ "action": "submitForm", "params": { "action": "sendMessage", "params": { "text": "Submit the sign-up form", "data": { "$state": "/form" } } } }. ' +
26
+ "The host puts the message in the chat as the user's (Claude lets the user review it and send it), so write `text` in their voice.",
27
+ };
28
+ const withSendMessageCache = new WeakMap();
29
+ /**
30
+ * `catalog` plus the {@link sendMessageAction}, the same object for the same
31
+ * catalog, so the input schema built from it is built once.
32
+ */
33
+ function withSendMessage(catalog) {
34
+ let extended = withSendMessageCache.get(catalog);
35
+ if (!extended) {
36
+ const data = catalog.data;
37
+ extended = defineCatalog(catalog.schema, {
38
+ ...data,
39
+ actions: { ...data.actions, sendMessage: sendMessageAction },
40
+ });
41
+ withSendMessageCache.set(catalog, extended);
42
+ }
43
+ return extended;
44
+ }
6
45
 
7
46
  // What the model sees of the `render-ui` tool: a short description and an
8
47
  // input schema that carries the catalog.
@@ -317,7 +356,10 @@ function walk(value, path, visit) {
317
356
  * rejected spec goes back to the model as an error it can fix.
318
357
  */
319
358
  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;
359
+ const catalog = options.sendMessage
360
+ ? withSendMessage(options.catalog)
361
+ : options.catalog;
362
+ const { html, name = 'ngx-json-render', version = '0.0.0', toolName = 'render-ui', title = 'Render UI', description = renderUiDescription(catalog), csp, widgetDomain, } = options;
321
363
  const resourceUri = `ui://${toolName}/view.html`;
322
364
  const server = new McpServer({ name, version });
323
365
  registerAppTool(server, toolName, {
@@ -420,5 +462,5 @@ const CORS_HEADERS = {
420
462
  * Generated bundle index. Do not edit.
421
463
  */
422
464
 
423
- export { DESCRIPTION_LIMIT, createRenderUiServer, handleRenderUiRequest, renderUiDescription, specInputSchema, specOutputSchema, specProblems };
465
+ export { DESCRIPTION_LIMIT, createRenderUiServer, handleRenderUiRequest, renderUiDescription, sendMessageAction, specInputSchema, specOutputSchema, specProblems, withSendMessage };
424
466
  //# sourceMappingURL=ngx-json-render-mcp-server.mjs.map
@@ -1 +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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"}
1
+ {"version":3,"file":"ngx-json-render-mcp-server.mjs","sources":["../../../projects/ngx-json-render/mcp/server/src/actions.ts","../../../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":["// Actions that only make sense inside an MCP Apps host. A catalog stays\n// host-neutral; the server adds these to the catalog it describes to the\n// model, and the view handles them (`injectJsonRenderApp().handlers` in\n// `ngx-json-render/mcp`).\nimport { type Catalog, defineCatalog } from '@json-render/core';\nimport { z } from 'zod';\n\n/**\n * `sendMessage`: post a message to the chat as the user, so the conversation\n * carries on from what was clicked in the view.\n */\nexport const sendMessageAction = {\n params: z.object({\n text: z.string(),\n data: z.record(z.string(), z.unknown()).optional(),\n }),\n description:\n 'Send a message to you, the assistant, as the user, so the conversation continues from the UI. ' +\n 'Params: { \"text\": \"what the user chose or asks for\", \"data\"?: { \"$state\": \"/path\" } }. `data` is optional and is attached as JSON; ' +\n 'make it a single reference to a state object, because references nested inside `data` are not resolved. ' +\n 'Use it for choices and follow-ups (\"Approve\", \"Show more\", a picked option). ' +\n 'To send a form, gate it with submitForm: ' +\n '{ \"action\": \"submitForm\", \"params\": { \"action\": \"sendMessage\", \"params\": { \"text\": \"Submit the sign-up form\", \"data\": { \"$state\": \"/form\" } } } }. ' +\n \"The host puts the message in the chat as the user's (Claude lets the user review it and send it), so write `text` in their voice.\",\n};\n\nconst withSendMessageCache = new WeakMap<Catalog, Catalog>();\n\n/**\n * `catalog` plus the {@link sendMessageAction}, the same object for the same\n * catalog, so the input schema built from it is built once.\n */\nexport function withSendMessage<C extends Catalog>(catalog: C): C {\n let extended = withSendMessageCache.get(catalog);\n if (!extended) {\n const data = catalog.data as {\n actions?: Record<string, unknown>;\n } & Record<string, unknown>;\n extended = defineCatalog(catalog.schema, {\n ...data,\n actions: { ...data.actions, sendMessage: sendMessageAction },\n } as Parameters<typeof defineCatalog>[1]);\n withSendMessageCache.set(catalog, extended);\n }\n return extended as C;\n}\n","// 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 { withSendMessage } from './actions';\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 * Add the `sendMessage` action to the catalog the model sees, so a button\n * in the view can post a message to the chat as the user. The view handles\n * it with `injectJsonRenderApp().handlers` from `ngx-json-render/mcp`.\n */\n sendMessage?: boolean;\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 catalog = options.sendMessage\n ? withSendMessage(options.catalog)\n : options.catalog;\n const {\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 { sendMessageAction, withSendMessage } from './actions';\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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"}
@@ -35,7 +35,8 @@ function specFrom(value) {
35
35
  * Connect an Angular app running inside an MCP Apps iframe to its host and
36
36
  * keep the json-render spec the model sent in a signal. The Angular
37
37
  * counterpart of `useJsonRenderApp` from `@json-render/mcp/app`; the server
38
- * side is `createMcpApp` from `@json-render/mcp`, unchanged.
38
+ * side is `createRenderUiServer` from `ngx-json-render/mcp/server`, or
39
+ * `createMcpApp` from `@json-render/mcp`.
39
40
  *
40
41
  * Call it in an injection context. The connection closes when the injector
41
42
  * that created it is destroyed.
@@ -51,6 +52,7 @@ function injectJsonRenderApp(options = {}) {
51
52
  const loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
52
53
  const connected = signal(false, ...(ngDevMode ? [{ debugName: "connected" }] : /* istanbul ignore next */ []));
53
54
  const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
55
+ const lastMessage = signal(null, ...(ngDevMode ? [{ debugName: "lastMessage" }] : /* istanbul ignore next */ []));
54
56
  const app = new App({ name, version }, {}, { autoResize });
55
57
  // Listeners go on before connect(), so a notification the host sends right
56
58
  // after the handshake is not lost.
@@ -80,6 +82,35 @@ function injectJsonRenderApp(options = {}) {
80
82
  inject(DestroyRef).onDestroy(() => {
81
83
  app.close().catch(() => undefined);
82
84
  });
85
+ async function sendMessage(text, data) {
86
+ if (!app.getHostCapabilities()?.message) {
87
+ throw new Error('The host does not accept messages from the view.');
88
+ }
89
+ const result = await app.sendMessage({
90
+ role: 'user',
91
+ content: [{ type: 'text', text: messageText(text, data) }],
92
+ });
93
+ if (result.isError)
94
+ throw new Error('The host declined the message.');
95
+ }
96
+ const handlers = {
97
+ sendMessage: async ({ text, data }) => {
98
+ try {
99
+ if (typeof text !== 'string' || !text.trim()) {
100
+ throw new Error('sendMessage needs a non-empty "text" param.');
101
+ }
102
+ const isObject = data !== null && typeof data === 'object' && !Array.isArray(data);
103
+ await sendMessage(text, isObject ? data : undefined);
104
+ lastMessage.set({ ok: true });
105
+ }
106
+ catch (err) {
107
+ const reason = err instanceof Error ? err : new Error(String(err));
108
+ lastMessage.set({ ok: false, error: reason });
109
+ // Rethrown, so a binding's `onError` still runs.
110
+ throw reason;
111
+ }
112
+ },
113
+ };
83
114
  return {
84
115
  spec: spec.asReadonly(),
85
116
  connecting: computed(() => !connected() && !error()),
@@ -102,17 +133,9 @@ function injectJsonRenderApp(options = {}) {
102
133
  loading.set(false);
103
134
  }
104
135
  },
105
- async sendMessage(text, data) {
106
- if (!app.getHostCapabilities()?.message) {
107
- throw new Error('The host does not accept messages from the view.');
108
- }
109
- const result = await app.sendMessage({
110
- role: 'user',
111
- content: [{ type: 'text', text: messageText(text, data) }],
112
- });
113
- if (result.isError)
114
- throw new Error('The host declined the message.');
115
- },
136
+ sendMessage,
137
+ handlers,
138
+ lastMessage: lastMessage.asReadonly(),
116
139
  };
117
140
  }
118
141
 
@@ -1 +1 @@
1
- {"version":3,"file":"ngx-json-render-mcp.mjs","sources":["../../../projects/ngx-json-render/mcp/src/json-render-app.ts","../../../projects/ngx-json-render/mcp/src/public-api.ts","../../../projects/ngx-json-render/mcp/src/ngx-json-render-mcp.ts"],"sourcesContent":["import {\n DestroyRef,\n type Signal,\n computed,\n inject,\n signal,\n} from '@angular/core';\nimport type { Spec } from '@json-render/core';\nimport { App } from '@modelcontextprotocol/ext-apps';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\n\n/** Options for {@link injectJsonRenderApp}. */\nexport interface JsonRenderAppOptions {\n /** App name sent to the host during initialization. Defaults to `\"json-render\"`. */\n name?: string;\n /** App version sent to the host. Defaults to `\"1.0.0\"`. */\n version?: string;\n /**\n * Render the spec while the model is still writing the tool call, from the\n * host's `toolinputpartial` notifications. The host \"heals\" the truncated\n * JSON, so each partial spec is valid but may end in a half-written\n * element; the renderer skips children that do not exist yet. Defaults to\n * `true`. `useJsonRenderApp` in `@json-render/mcp/app` has no equivalent —\n * it waits for the tool result.\n */\n streamPartialInput?: boolean;\n /**\n * Transport to the host. Omit it inside a real MCP Apps iframe: `App`\n * then talks to `window.parent` over `postMessage`. Tests pass one end of\n * an in-memory pair here and an `AppBridge` on the other.\n */\n transport?: Transport;\n /**\n * Report the page's size to the host as it changes, so the iframe grows\n * with the rendered UI. Defaults to `true`; needs `ResizeObserver`.\n */\n autoResize?: boolean;\n}\n\n/**\n * What {@link injectJsonRenderApp} returns — the fields of\n * `UseJsonRenderAppReturn` from `@json-render/mcp/app`, as signals.\n */\nexport interface JsonRenderApp {\n /** The current spec; `null` until the first tool input or result. */\n readonly spec: Signal<Spec | null>;\n /** Whether the app is still connecting to the host. */\n readonly connecting: Signal<boolean>;\n /** Whether the app is connected to the host. */\n readonly connected: Signal<boolean>;\n /** Connection error, if any. */\n readonly error: Signal<Error | null>;\n /** Whether the spec is still arriving: pass it to `<json-render [loading]>`. */\n readonly loading: Signal<boolean>;\n /** The underlying MCP Apps `App`. */\n readonly app: App;\n /**\n * Call a tool on the MCP server and replace the spec with its result —\n * for refresh and drill-down interactions.\n */\n callServerTool(name: string, args?: Record<string, unknown>): Promise<void>;\n /**\n * Post a user message to the host's chat, so the model answers what the\n * user did in the view. `data`, if given, follows the text as a JSON block.\n * Rejects when the host does not accept messages from views or turns this\n * one down, so an action's `onError` runs.\n */\n sendMessage(text: string, data?: Record<string, unknown>): Promise<void>;\n}\n\n/** The text of a {@link JsonRenderApp.sendMessage} message. */\nexport function messageText(\n text: string,\n data?: Record<string, unknown>,\n): string {\n if (!data) return text;\n return `${text}\\n\\n\\`\\`\\`json\\n${JSON.stringify(data, null, 2)}\\n\\`\\`\\``;\n}\n\ninterface ToolResultLike {\n content?: { type: string; text?: string }[];\n}\n\n/**\n * Read a spec out of a tool result. Same rule as `@json-render/mcp/app`: the\n * first text block, parsed as JSON, either a bare spec or `{ spec }`.\n */\nexport function parseSpecFromToolResult(result: ToolResultLike): Spec | null {\n const text = result.content?.find((c) => c.type === 'text')?.text;\n if (!text) return null;\n try {\n return specFrom(JSON.parse(text));\n } catch {\n return null;\n }\n}\n\n/** A bare spec, or the `spec` field of a tool's arguments or result. */\nfunction specFrom(value: unknown): Spec | null {\n if (!value || typeof value !== 'object') return null;\n const spec = 'spec' in value ? (value as { spec: unknown }).spec : value;\n if (!spec || typeof spec !== 'object' || !('root' in spec)) return null;\n return spec as Spec;\n}\n\n/**\n * Connect an Angular app running inside an MCP Apps iframe to its host and\n * keep the json-render spec the model sent in a signal. The Angular\n * counterpart of `useJsonRenderApp` from `@json-render/mcp/app`; the server\n * side is `createMcpApp` from `@json-render/mcp`, unchanged.\n *\n * Call it in an injection context. The connection closes when the injector\n * that created it is destroyed.\n *\n * ```ts\n * readonly mcp = injectJsonRenderApp();\n * // <json-render [spec]=\"mcp.spec()\" [loading]=\"mcp.loading()\" [registry]=\"registry\" />\n * ```\n */\nexport function injectJsonRenderApp(\n options: JsonRenderAppOptions = {},\n): JsonRenderApp {\n const {\n name = 'json-render',\n version = '1.0.0',\n streamPartialInput = true,\n transport,\n autoResize = true,\n } = options;\n\n const spec = signal<Spec | null>(null);\n const loading = signal(true);\n const connected = signal(false);\n const error = signal<Error | null>(null);\n\n const app = new App({ name, version }, {}, { autoResize });\n\n // Listeners go on before connect(), so a notification the host sends right\n // after the handshake is not lost.\n if (streamPartialInput) {\n app.addEventListener('toolinputpartial', ({ arguments: args }) => {\n const partial = specFrom(args);\n if (partial) spec.set(partial);\n });\n }\n app.addEventListener('toolinput', ({ arguments: args }) => {\n const input = specFrom(args);\n if (input) spec.set(input);\n });\n app.addEventListener('toolresult', (result) => {\n const parsed = parseSpecFromToolResult(result);\n if (parsed) spec.set(parsed);\n loading.set(false);\n });\n app.addEventListener('toolcancelled', () => loading.set(false));\n\n app\n .connect(transport)\n .then(() => connected.set(true))\n .catch((err: unknown) =>\n error.set(err instanceof Error ? err : new Error(String(err))),\n );\n\n inject(DestroyRef).onDestroy(() => {\n app.close().catch(() => undefined);\n });\n\n return {\n spec: spec.asReadonly(),\n connecting: computed(() => !connected() && !error()),\n connected: connected.asReadonly(),\n error: error.asReadonly(),\n loading: loading.asReadonly(),\n app,\n async callServerTool(toolName, args = {}) {\n loading.set(true);\n try {\n const result = await app.callServerTool({\n name: toolName,\n arguments: args,\n });\n const parsed = parseSpecFromToolResult(result);\n if (parsed) spec.set(parsed);\n } finally {\n loading.set(false);\n }\n },\n async sendMessage(text, data) {\n if (!app.getHostCapabilities()?.message) {\n throw new Error('The host does not accept messages from the view.');\n }\n const result = await app.sendMessage({\n role: 'user',\n content: [{ type: 'text', text: messageText(text, data) }],\n });\n if (result.isError) throw new Error('The host declined the message.');\n },\n };\n}\n","/*\n * Public API Surface of ngx-json-render/mcp\n *\n * Render json-render specs inside an MCP Apps host (Claude, ChatGPT, VS Code).\n * A separate entry point so `@modelcontextprotocol/ext-apps`, an optional\n * peer, is only needed by apps that import it.\n */\n\nexport {\n injectJsonRenderApp,\n messageText,\n parseSpecFromToolResult,\n type JsonRenderApp,\n type JsonRenderAppOptions,\n} from './json-render-app';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ngx-json-render-mcp.mjs","sources":["../../../projects/ngx-json-render/mcp/src/json-render-app.ts","../../../projects/ngx-json-render/mcp/src/public-api.ts","../../../projects/ngx-json-render/mcp/src/ngx-json-render-mcp.ts"],"sourcesContent":["import {\n DestroyRef,\n type Signal,\n computed,\n inject,\n signal,\n} from '@angular/core';\nimport type { ActionHandler, Spec } from '@json-render/core';\nimport { App } from '@modelcontextprotocol/ext-apps';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\n\n/** Options for {@link injectJsonRenderApp}. */\nexport interface JsonRenderAppOptions {\n /** App name sent to the host during initialization. Defaults to `\"json-render\"`. */\n name?: string;\n /** App version sent to the host. Defaults to `\"1.0.0\"`. */\n version?: string;\n /**\n * Render the spec while the model is still writing the tool call, from the\n * host's `toolinputpartial` notifications. The host \"heals\" the truncated\n * JSON, so each partial spec is valid but may end in a half-written\n * element; the renderer skips children that do not exist yet. Defaults to\n * `true`. `useJsonRenderApp` in `@json-render/mcp/app` has no equivalent —\n * it waits for the tool result.\n */\n streamPartialInput?: boolean;\n /**\n * Transport to the host. Omit it inside a real MCP Apps iframe: `App`\n * then talks to `window.parent` over `postMessage`. Tests pass one end of\n * an in-memory pair here and an `AppBridge` on the other.\n */\n transport?: Transport;\n /**\n * Report the page's size to the host as it changes, so the iframe grows\n * with the rendered UI. Defaults to `true`; needs `ResizeObserver`.\n */\n autoResize?: boolean;\n}\n\n/** How the last {@link JsonRenderApp.sendMessage} from an action went. */\nexport type MessageOutcome = { ok: true } | { ok: false; error: Error };\n\n/**\n * What {@link injectJsonRenderApp} returns — the fields of\n * `UseJsonRenderAppReturn` from `@json-render/mcp/app`, as signals.\n */\nexport interface JsonRenderApp {\n /** The current spec; `null` until the first tool input or result. */\n readonly spec: Signal<Spec | null>;\n /** Whether the app is still connecting to the host. */\n readonly connecting: Signal<boolean>;\n /** Whether the app is connected to the host. */\n readonly connected: Signal<boolean>;\n /** Connection error, if any. */\n readonly error: Signal<Error | null>;\n /** Whether the spec is still arriving: pass it to `<json-render [loading]>`. */\n readonly loading: Signal<boolean>;\n /** The underlying MCP Apps `App`. */\n readonly app: App;\n /**\n * Call a tool on the MCP server and replace the spec with its result —\n * for refresh and drill-down interactions.\n */\n callServerTool(name: string, args?: Record<string, unknown>): Promise<void>;\n /**\n * Post a user message to the host's chat, so the model answers what the\n * user did in the view. `data`, if given, follows the text as a JSON block.\n * Rejects when the host does not accept messages from views or turns this\n * one down, so an action's `onError` runs.\n */\n sendMessage(text: string, data?: Record<string, unknown>): Promise<void>;\n /**\n * Handlers for the actions the server adds for MCP Apps hosts\n * (`createRenderUiServer({ sendMessage: true })` in\n * `ngx-json-render/mcp/server`): pass them to `<json-render [handlers]>`.\n * `sendMessage` takes `{ text, data? }` and posts it with\n * {@link JsonRenderApp.sendMessage}.\n */\n readonly handlers: Record<string, ActionHandler>;\n /**\n * How the last `sendMessage` action went, `null` before the first. A host\n * may decline a message or take none at all; show this so a press does not\n * look like it did nothing.\n */\n readonly lastMessage: Signal<MessageOutcome | null>;\n}\n\n/** The text of a {@link JsonRenderApp.sendMessage} message. */\nexport function messageText(\n text: string,\n data?: Record<string, unknown>,\n): string {\n if (!data) return text;\n return `${text}\\n\\n\\`\\`\\`json\\n${JSON.stringify(data, null, 2)}\\n\\`\\`\\``;\n}\n\ninterface ToolResultLike {\n content?: { type: string; text?: string }[];\n}\n\n/**\n * Read a spec out of a tool result. Same rule as `@json-render/mcp/app`: the\n * first text block, parsed as JSON, either a bare spec or `{ spec }`.\n */\nexport function parseSpecFromToolResult(result: ToolResultLike): Spec | null {\n const text = result.content?.find((c) => c.type === 'text')?.text;\n if (!text) return null;\n try {\n return specFrom(JSON.parse(text));\n } catch {\n return null;\n }\n}\n\n/** A bare spec, or the `spec` field of a tool's arguments or result. */\nfunction specFrom(value: unknown): Spec | null {\n if (!value || typeof value !== 'object') return null;\n const spec = 'spec' in value ? (value as { spec: unknown }).spec : value;\n if (!spec || typeof spec !== 'object' || !('root' in spec)) return null;\n return spec as Spec;\n}\n\n/**\n * Connect an Angular app running inside an MCP Apps iframe to its host and\n * keep the json-render spec the model sent in a signal. The Angular\n * counterpart of `useJsonRenderApp` from `@json-render/mcp/app`; the server\n * side is `createRenderUiServer` from `ngx-json-render/mcp/server`, or\n * `createMcpApp` from `@json-render/mcp`.\n *\n * Call it in an injection context. The connection closes when the injector\n * that created it is destroyed.\n *\n * ```ts\n * readonly mcp = injectJsonRenderApp();\n * // <json-render [spec]=\"mcp.spec()\" [loading]=\"mcp.loading()\" [registry]=\"registry\" />\n * ```\n */\nexport function injectJsonRenderApp(\n options: JsonRenderAppOptions = {},\n): JsonRenderApp {\n const {\n name = 'json-render',\n version = '1.0.0',\n streamPartialInput = true,\n transport,\n autoResize = true,\n } = options;\n\n const spec = signal<Spec | null>(null);\n const loading = signal(true);\n const connected = signal(false);\n const error = signal<Error | null>(null);\n const lastMessage = signal<MessageOutcome | null>(null);\n\n const app = new App({ name, version }, {}, { autoResize });\n\n // Listeners go on before connect(), so a notification the host sends right\n // after the handshake is not lost.\n if (streamPartialInput) {\n app.addEventListener('toolinputpartial', ({ arguments: args }) => {\n const partial = specFrom(args);\n if (partial) spec.set(partial);\n });\n }\n app.addEventListener('toolinput', ({ arguments: args }) => {\n const input = specFrom(args);\n if (input) spec.set(input);\n });\n app.addEventListener('toolresult', (result) => {\n const parsed = parseSpecFromToolResult(result);\n if (parsed) spec.set(parsed);\n loading.set(false);\n });\n app.addEventListener('toolcancelled', () => loading.set(false));\n\n app\n .connect(transport)\n .then(() => connected.set(true))\n .catch((err: unknown) =>\n error.set(err instanceof Error ? err : new Error(String(err))),\n );\n\n inject(DestroyRef).onDestroy(() => {\n app.close().catch(() => undefined);\n });\n\n async function sendMessage(text: string, data?: Record<string, unknown>) {\n if (!app.getHostCapabilities()?.message) {\n throw new Error('The host does not accept messages from the view.');\n }\n const result = await app.sendMessage({\n role: 'user',\n content: [{ type: 'text', text: messageText(text, data) }],\n });\n if (result.isError) throw new Error('The host declined the message.');\n }\n\n const handlers: Record<string, ActionHandler> = {\n sendMessage: async ({ text, data }) => {\n try {\n if (typeof text !== 'string' || !text.trim()) {\n throw new Error('sendMessage needs a non-empty \"text\" param.');\n }\n const isObject =\n data !== null && typeof data === 'object' && !Array.isArray(data);\n await sendMessage(\n text,\n isObject ? (data as Record<string, unknown>) : undefined,\n );\n lastMessage.set({ ok: true });\n } catch (err) {\n const reason = err instanceof Error ? err : new Error(String(err));\n lastMessage.set({ ok: false, error: reason });\n // Rethrown, so a binding's `onError` still runs.\n throw reason;\n }\n },\n };\n\n return {\n spec: spec.asReadonly(),\n connecting: computed(() => !connected() && !error()),\n connected: connected.asReadonly(),\n error: error.asReadonly(),\n loading: loading.asReadonly(),\n app,\n async callServerTool(toolName, args = {}) {\n loading.set(true);\n try {\n const result = await app.callServerTool({\n name: toolName,\n arguments: args,\n });\n const parsed = parseSpecFromToolResult(result);\n if (parsed) spec.set(parsed);\n } finally {\n loading.set(false);\n }\n },\n sendMessage,\n handlers,\n lastMessage: lastMessage.asReadonly(),\n };\n}\n","/*\n * Public API Surface of ngx-json-render/mcp\n *\n * Render json-render specs inside an MCP Apps host (Claude, ChatGPT, VS Code).\n * A separate entry point so `@modelcontextprotocol/ext-apps`, an optional\n * peer, is only needed by apps that import it.\n */\n\nexport {\n injectJsonRenderApp,\n messageText,\n parseSpecFromToolResult,\n type JsonRenderApp,\n type JsonRenderAppOptions,\n type MessageOutcome,\n} from './json-render-app';\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.6",
3
+ "version": "0.9.7",
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",
@@ -4,6 +4,11 @@
4
4
  "ng-add": {
5
5
  "description": "Adds ngx-json-render's peer dependencies (@json-render/core and zod) to the workspace.",
6
6
  "factory": "./ng-add/index.cjs#ngAdd"
7
+ },
8
+ "mcp-app": {
9
+ "description": "Generates an MCP App for a catalog: an Angular view project, its MCP server, and an mcp target that builds both into one view.html and server.mjs.",
10
+ "factory": "./mcp-app/index.cjs#mcpApp",
11
+ "schema": "./mcp-app/schema.json"
7
12
  }
8
13
  }
9
14
  }
@@ -0,0 +1,552 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mcpApp = mcpApp;
4
+ const schematics_1 = require("@angular-devkit/schematics");
5
+ const utility_1 = require("@schematics/angular/utility");
6
+ const node_path_1 = require("node:path");
7
+ const self = require('../../package.json');
8
+ /** Peers the view and the server need. */
9
+ const PEERS = [
10
+ '@json-render/core',
11
+ 'zod',
12
+ '@modelcontextprotocol/ext-apps',
13
+ '@modelcontextprotocol/sdk',
14
+ ];
15
+ /** Origins the Material view loads its fonts from. */
16
+ const MATERIAL_FONTS = [
17
+ 'https://fonts.googleapis.com',
18
+ 'https://fonts.gstatic.com',
19
+ ];
20
+ /**
21
+ * `ng generate ngx-json-render:mcp-app`: an MCP App for a catalog in one go.
22
+ * Adds an application project for the view, a `server.ts` with the
23
+ * `render-ui` tool next to it, and an `mcp` target that builds both with the
24
+ * `ngx-json-render:mcp-app` builder.
25
+ */
26
+ function mcpApp(options) {
27
+ return async (tree, context) => {
28
+ const name = options.name || 'mcp-app';
29
+ const manifest = readManifest(tree);
30
+ const workspace = await (0, utility_1.readWorkspace)(tree);
31
+ if (workspace.projects.has(name)) {
32
+ throw new schematics_1.SchematicsException(`The workspace already has a project named "${name}". Pass another --name.`);
33
+ }
34
+ const material = declares(manifest, 'ngx-json-render-material');
35
+ const source = resolveSource(tree, options, material);
36
+ for (const warning of nodeUnsafeImports(tree, source)) {
37
+ context.logger.warn(warning);
38
+ }
39
+ const rules = [
40
+ (0, schematics_1.externalSchematic)('@schematics/angular', 'application', {
41
+ name,
42
+ routing: false,
43
+ style: source.kind === 'material' ? 'scss' : 'css',
44
+ skipTests: true,
45
+ ssr: false,
46
+ inlineStyle: true,
47
+ inlineTemplate: true,
48
+ }),
49
+ writeFiles(name, source),
50
+ (0, utility_1.updateWorkspace)((workspace) => {
51
+ const project = workspace.projects.get(name);
52
+ for (const target of ['build', 'test']) {
53
+ dropZone(project.targets.get(target)?.options);
54
+ }
55
+ widenBudget(project.targets.get('build')?.configurations?.['production']);
56
+ project.targets.set('mcp', {
57
+ builder: 'ngx-json-render:mcp-app',
58
+ options: {
59
+ buildTarget: `${name}:build:production`,
60
+ outputPath: `dist/${name}`,
61
+ server: node_path_1.posix.join(project.root, 'server.ts'),
62
+ },
63
+ });
64
+ }),
65
+ ];
66
+ for (const peer of PEERS) {
67
+ if (declares(manifest, peer))
68
+ continue;
69
+ const range = self.peerDependencies?.[peer];
70
+ if (!range) {
71
+ throw new schematics_1.SchematicsException(`ngx-json-render declares no peer range for ${peer}.`);
72
+ }
73
+ rules.push((0, utility_1.addDependency)(peer, range));
74
+ context.logger.info(`Adding ${peer}@${range}`);
75
+ }
76
+ rules.push(() => {
77
+ context.logger.info([
78
+ '',
79
+ `The MCP App is in project "${name}". Build it, then run its server:`,
80
+ ` ng run ${name}:mcp`,
81
+ ` node dist/${name}/server.mjs # stdio, for Claude Desktop or Claude Code`,
82
+ ` node dist/${name}/server.mjs --http # http://localhost:3001/mcp`,
83
+ ].join('\n'));
84
+ });
85
+ return (0, schematics_1.chain)(rules);
86
+ };
87
+ }
88
+ function resolveSource(tree, options, material) {
89
+ if (!options.catalog !== !options.registry) {
90
+ throw new schematics_1.SchematicsException('Pass --catalog and --registry together: the server describes the catalog to the model, and the view renders it with the registry.');
91
+ }
92
+ if (options.catalog && options.registry) {
93
+ const catalog = reference(tree, options.catalog, 'catalog');
94
+ const registry = reference(tree, options.registry, 'registry');
95
+ return {
96
+ kind: 'own',
97
+ catalogFile: catalog.file,
98
+ catalogExport: catalog.name,
99
+ registryFile: registry.file,
100
+ registryExport: registry.name,
101
+ };
102
+ }
103
+ if (material) {
104
+ return {
105
+ kind: 'material',
106
+ catalogFile: 'ngx-json-render-material/catalog',
107
+ catalogExport: 'materialCatalog',
108
+ registryFile: 'ngx-json-render-material',
109
+ registryExport: 'materialRegistry',
110
+ };
111
+ }
112
+ return {
113
+ kind: 'starter',
114
+ catalogFile: '',
115
+ catalogExport: 'catalog',
116
+ registryFile: '',
117
+ registryExport: 'registry',
118
+ };
119
+ }
120
+ /** `<file>#<export>`, checked against the tree. */
121
+ function reference(tree, value, fallback) {
122
+ const [path, name = fallback] = value.split('#');
123
+ const file = '/' + node_path_1.posix.normalize(path).replace(/^\/+/, '');
124
+ if (!tree.exists(file)) {
125
+ throw new schematics_1.SchematicsException(`No file at ${path} (from "${value}").`);
126
+ }
127
+ const content = tree.readText(file);
128
+ const exported = new RegExp(`export\\s+(?:const|let|var|function)\\s+(?:\\{[^}]*\\b${name}\\b[^}]*\\}|${name}\\b)|export\\s*\\{[^}]*\\b${name}\\b`);
129
+ if (!exported.test(content)) {
130
+ throw new schematics_1.SchematicsException(`${path} exports no "${name}".`);
131
+ }
132
+ return { file, name };
133
+ }
134
+ /**
135
+ * The server imports the catalog in Node, where Angular components cannot
136
+ * load. Flags the imports that would pull them in.
137
+ */
138
+ function nodeUnsafeImports(tree, source) {
139
+ if (source.kind !== 'own')
140
+ return [];
141
+ const content = tree.readText(source.catalogFile);
142
+ const unsafe = [...content.matchAll(/from\s+['"]([^'"]+)['"]/g)]
143
+ .map((match) => match[1])
144
+ .filter((specifier) => specifier.startsWith('@angular/') ||
145
+ specifier === 'ngx-json-render' ||
146
+ specifier === 'ngx-json-render-material');
147
+ return unsafe.map((specifier) => `${source.catalogFile.slice(1)} imports ${specifier}, which needs Angular, and the MCP server loads the catalog in Node. ` +
148
+ 'Keep the catalog in a file that imports only ngx-json-render/schema (or ngx-json-render-material/catalog), zod and @json-render/core.');
149
+ }
150
+ /** Replaces the application's generated sources with the view and adds the server. */
151
+ function writeFiles(name, source) {
152
+ return async (tree) => {
153
+ const workspace = await (0, utility_1.readWorkspace)(tree);
154
+ const project = workspace.projects.get(name);
155
+ if (!project) {
156
+ throw new schematics_1.SchematicsException(`The project "${name}" was not created.`);
157
+ }
158
+ const root = '/' + project.root.replace(/^\/+/, '');
159
+ const src = '/' + (project.sourceRoot ?? node_path_1.posix.join(project.root, 'src'));
160
+ const appDir = node_path_1.posix.join(src, 'app');
161
+ tree.getDir(appDir).visit((file) => tree.delete(file));
162
+ // A view lives in an iframe, which shows no icon.
163
+ const favicon = node_path_1.posix.join(root, 'public', 'favicon.ico');
164
+ if (tree.exists(favicon))
165
+ tree.delete(favicon);
166
+ for (const style of ['styles.css', 'styles.scss']) {
167
+ const file = node_path_1.posix.join(src, style);
168
+ if (tree.exists(file))
169
+ tree.delete(file);
170
+ }
171
+ let catalog = source.catalogFile;
172
+ let registry = source.registryFile;
173
+ if (source.kind === 'starter') {
174
+ catalog = node_path_1.posix.join(appDir, 'catalog.ts');
175
+ registry = node_path_1.posix.join(appDir, 'registry.ts');
176
+ tree.create(catalog, STARTER_CATALOG);
177
+ tree.create(registry, STARTER_REGISTRY);
178
+ }
179
+ const material = source.kind === 'material';
180
+ const from = (file, specifier) => source.kind === 'material' ? specifier : relativeImport(file, specifier);
181
+ const appFile = node_path_1.posix.join(appDir, 'app.ts');
182
+ tree.create(appFile, viewComponent(name, source.registryExport, from(appFile, registry)));
183
+ overwrite(tree, node_path_1.posix.join(src, 'main.ts'), mainFile(material));
184
+ overwrite(tree, node_path_1.posix.join(src, 'index.html'), indexFile(name, material));
185
+ tree.create(node_path_1.posix.join(src, material ? 'styles.scss' : 'styles.css'), material ? MATERIAL_STYLES : PLAIN_STYLES);
186
+ const serverFile = node_path_1.posix.join(root, 'server.ts');
187
+ tree.create(serverFile, serverModule(name, source.catalogExport, from(serverFile, catalog), material));
188
+ };
189
+ }
190
+ function overwrite(tree, file, content) {
191
+ if (tree.exists(file))
192
+ tree.overwrite(file, content);
193
+ else
194
+ tree.create(file, content);
195
+ }
196
+ /** An import specifier for `target` (a tree path) from `file`, without `.ts`. */
197
+ function relativeImport(file, target) {
198
+ const path = node_path_1.posix
199
+ .relative(node_path_1.posix.dirname(file), target)
200
+ .replace(/\.(?:m|c)?ts$/, '');
201
+ return path.startsWith('.') ? path : `./${path}`;
202
+ }
203
+ /** The view is zoneless; `ng generate application` may still add zone.js. */
204
+ function dropZone(options) {
205
+ const polyfills = options?.['polyfills'];
206
+ if (!options || !Array.isArray(polyfills))
207
+ return;
208
+ const kept = polyfills.filter((entry) => entry !== 'zone.js' && entry !== 'zone.js/testing');
209
+ if (kept.length)
210
+ options['polyfills'] = kept;
211
+ else
212
+ delete options['polyfills'];
213
+ }
214
+ /**
215
+ * The view carries the MCP Apps SDK and zod (about 750 kB, 1.6 MB with
216
+ * Material) and loads from the host as one inlined page, so the 500 kB
217
+ * initial budget of a web app would fail the build.
218
+ */
219
+ function widenBudget(configuration) {
220
+ const budgets = configuration?.['budgets'];
221
+ if (!configuration || !Array.isArray(budgets))
222
+ return;
223
+ // A new array: the workspace writer cannot follow edits inside one.
224
+ configuration['budgets'] = budgets.map((budget) => budget['type'] === 'initial'
225
+ ? { ...budget, maximumWarning: '3MB', maximumError: '5MB' }
226
+ : budget);
227
+ }
228
+ function readManifest(tree) {
229
+ if (!tree.exists('/package.json')) {
230
+ throw new schematics_1.SchematicsException('No package.json at the workspace root.');
231
+ }
232
+ return tree.readJson('/package.json');
233
+ }
234
+ function declares(manifest, name) {
235
+ return !!(manifest.dependencies?.[name] ?? manifest.devDependencies?.[name]);
236
+ }
237
+ function viewComponent(name, registryExport, registryModule) {
238
+ const registry = registryExport === 'registry'
239
+ ? `import { registry } from '${registryModule}';`
240
+ : `import { ${registryExport} as registry } from '${registryModule}';`;
241
+ return `import { Component, effect } from '@angular/core';
242
+ import type { McpUiTheme } from '@modelcontextprotocol/ext-apps';
243
+ import { JsonRenderer } from 'ngx-json-render';
244
+ import { injectJsonRenderApp } from 'ngx-json-render/mcp';
245
+ ${registry}
246
+
247
+ /**
248
+ * The view an MCP host shows in its iframe: the spec the model passed to the
249
+ * \`render-ui\` tool, rendered with the registry. \`mcp.handlers\` handles the
250
+ * \`sendMessage\` action the server describes to the model.
251
+ */
252
+ @Component({
253
+ selector: 'app-root',
254
+ imports: [JsonRenderer],
255
+ template: \`
256
+ @if (mcp.error(); as error) {
257
+ <p class="status error">Could not connect to the host: {{ error.message }}</p>
258
+ } @else if (mcp.spec(); as spec) {
259
+ <json-render
260
+ [spec]="spec"
261
+ [loading]="mcp.loading()"
262
+ [registry]="registry"
263
+ [handlers]="mcp.handlers"
264
+ />
265
+ @if (mcp.lastMessage(); as outcome) {
266
+ @if (outcome.ok) {
267
+ <p class="status" role="status">Message passed to the chat.</p>
268
+ } @else {
269
+ <p class="status error" role="alert">
270
+ Could not send the message: {{ outcome.error.message }}
271
+ </p>
272
+ }
273
+ }
274
+ } @else {
275
+ <p class="status">Waiting for the model's spec…</p>
276
+ }
277
+ \`,
278
+ styles: \`
279
+ :host {
280
+ display: block;
281
+ padding: 16px;
282
+ }
283
+ json-render + .status {
284
+ margin-top: 12px;
285
+ }
286
+ .status {
287
+ margin: 0;
288
+ opacity: 0.7;
289
+ }
290
+ .error {
291
+ color: #b3261e;
292
+ opacity: 1;
293
+ }
294
+ \`,
295
+ })
296
+ export class App {
297
+ readonly mcp = injectJsonRenderApp({ name: '${name}', version: '0.0.0' });
298
+ readonly registry = registry;
299
+
300
+ constructor() {
301
+ // Follow the host's light or dark theme.
302
+ const applyTheme = (theme: McpUiTheme | undefined) => {
303
+ if (theme) document.documentElement.style.colorScheme = theme;
304
+ };
305
+ this.mcp.app.addEventListener('hostcontextchanged', (context) =>
306
+ applyTheme(context.theme),
307
+ );
308
+ effect(() => {
309
+ if (this.mcp.connected()) applyTheme(this.mcp.app.getHostContext()?.theme);
310
+ });
311
+ }
312
+ }
313
+ `;
314
+ }
315
+ function mainFile(material) {
316
+ if (!material) {
317
+ return `import { provideZonelessChangeDetection } from '@angular/core';
318
+ import { bootstrapApplication } from '@angular/platform-browser';
319
+ import { App } from './app/app';
320
+
321
+ bootstrapApplication(App, {
322
+ providers: [provideZonelessChangeDetection()],
323
+ }).catch((err) => console.error(err));
324
+ `;
325
+ }
326
+ return `import {
327
+ inject,
328
+ provideEnvironmentInitializer,
329
+ provideZonelessChangeDetection,
330
+ } from '@angular/core';
331
+ import { MatIconRegistry } from '@angular/material/icon';
332
+ import { bootstrapApplication } from '@angular/platform-browser';
333
+ import { App } from './app/app';
334
+
335
+ bootstrapApplication(App, {
336
+ providers: [
337
+ provideZonelessChangeDetection(),
338
+ // The Material catalog names icons as Material Symbols ligatures.
339
+ provideEnvironmentInitializer(() =>
340
+ inject(MatIconRegistry).setDefaultFontSetClass('material-symbols-outlined'),
341
+ ),
342
+ ],
343
+ }).catch((err) => console.error(err));
344
+ `;
345
+ }
346
+ function indexFile(name, material) {
347
+ const fonts = material
348
+ ? `
349
+ <link
350
+ href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined&family=Roboto:wght@400;500&display=swap"
351
+ rel="stylesheet"
352
+ />`
353
+ : '';
354
+ return `<!doctype html>
355
+ <html lang="en">
356
+ <head>
357
+ <meta charset="utf-8" />
358
+ <title>${name}</title>
359
+ <meta name="viewport" content="width=device-width, initial-scale=1" />${fonts}
360
+ </head>
361
+ <body>
362
+ <app-root></app-root>
363
+ </body>
364
+ </html>
365
+ `;
366
+ }
367
+ const PLAIN_STYLES = `html {
368
+ color-scheme: light dark;
369
+ }
370
+
371
+ body {
372
+ margin: 0;
373
+ background: transparent;
374
+ font: 14px/1.5 system-ui, sans-serif;
375
+ }
376
+ `;
377
+ const MATERIAL_STYLES = `@use '@angular/material' as mat;
378
+
379
+ // Both palettes in one pass; the view sets \`color-scheme\` from the host's
380
+ // theme, which picks one.
381
+ html {
382
+ color-scheme: light dark;
383
+
384
+ @include mat.theme(
385
+ (
386
+ color: mat.$azure-palette,
387
+ typography: Roboto,
388
+ density: 0,
389
+ )
390
+ );
391
+ }
392
+
393
+ body {
394
+ margin: 0;
395
+ background: transparent;
396
+ color: var(--mat-sys-on-surface);
397
+ font-family: Roboto, system-ui, sans-serif;
398
+ }
399
+ `;
400
+ function serverModule(name, catalogExport, catalogModule, material) {
401
+ const catalog = catalogExport === 'catalog'
402
+ ? `import { catalog } from '${catalogModule}';`
403
+ : `import { ${catalogExport} as catalog } from '${catalogModule}';`;
404
+ const csp = material
405
+ ? `
406
+ // The Material view loads its fonts from Google Fonts.
407
+ csp: {
408
+ resourceDomains: [${MATERIAL_FONTS.map((origin) => `'${origin}'`).join(', ')}],
409
+ },`
410
+ : `
411
+ // Origins the view may load from or connect to; the host allows none
412
+ // without them: csp: { resourceDomains: [...], connectDomains: [...] }.`;
413
+ return `// The MCP server for the view: one \`render-ui\` tool for the catalog, and
414
+ // the view as its \`ui://\` resource. \`ng run ${name}:mcp\` bundles it into
415
+ // dist/${name}/server.mjs, next to view.html.
416
+ //
417
+ // node dist/${name}/server.mjs stdio, for Claude Desktop or Claude Code
418
+ // node dist/${name}/server.mjs --http http://localhost:3001/mcp (PORT to change)
419
+ import { readFileSync } from 'node:fs';
420
+ import { createServer } from 'node:http';
421
+ import { Readable } from 'node:stream';
422
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
423
+ import {
424
+ type RenderUiServerOptions,
425
+ createRenderUiServer,
426
+ handleRenderUiRequest,
427
+ } from 'ngx-json-render/mcp/server';
428
+ ${catalog}
429
+
430
+ const options: RenderUiServerOptions = {
431
+ catalog,
432
+ html: readFileSync(new URL('./view.html', import.meta.url), 'utf8'),
433
+ name: '${name}',
434
+ version: '0.0.0',
435
+ // Lets a button in the view post a message to the chat as the user.
436
+ sendMessage: true,${csp}
437
+ };
438
+
439
+ if (process.argv.includes('--http')) {
440
+ const port = Number(process.env['PORT'] ?? 3001);
441
+ createServer(async (req, res) => {
442
+ const hasBody = req.method !== 'GET' && req.method !== 'HEAD';
443
+ const request = new Request(\`http://localhost\${req.url}\`, {
444
+ method: req.method,
445
+ headers: req.headers as Record<string, string>,
446
+ body: hasBody ? (Readable.toWeb(req) as ReadableStream) : undefined,
447
+ duplex: 'half',
448
+ } as RequestInit);
449
+ const response = await handleRenderUiRequest(options, request);
450
+ res.writeHead(response.status, Object.fromEntries(response.headers));
451
+ res.end(Buffer.from(await response.arrayBuffer()));
452
+ }).listen(port, () => console.error(\`MCP on http://localhost:\${port}/mcp\`));
453
+ } else {
454
+ await createRenderUiServer(options).connect(new StdioServerTransport());
455
+ }
456
+ `;
457
+ }
458
+ const STARTER_CATALOG = `// The components the model may use in the view. The MCP server imports this
459
+ // file in Node, so it imports no Angular: components live in registry.ts.
460
+ import { schema } from 'ngx-json-render/schema';
461
+ import { z } from 'zod';
462
+
463
+ export const catalog = schema.createCatalog({
464
+ components: {
465
+ Card: {
466
+ props: z.object({ title: z.string().optional() }),
467
+ slots: ['default'],
468
+ description: 'A card that groups its children under an optional title.',
469
+ },
470
+ Text: {
471
+ props: z.object({ content: z.string() }),
472
+ slots: [],
473
+ description: 'A paragraph of text.',
474
+ },
475
+ Button: {
476
+ props: z.object({ label: z.string() }),
477
+ slots: [],
478
+ description: "A button that emits 'press'.",
479
+ },
480
+ },
481
+ actions: {},
482
+ });
483
+ `;
484
+ const STARTER_REGISTRY = `import { Component } from '@angular/core';
485
+ import {
486
+ type InferComponentProps,
487
+ JrChildren,
488
+ defineRegistry,
489
+ injectRenderContext,
490
+ } from 'ngx-json-render';
491
+ import { catalog } from './catalog';
492
+
493
+ @Component({
494
+ selector: 'app-card',
495
+ imports: [JrChildren],
496
+ template: \`
497
+ <section class="card">
498
+ @if (ctx.props().title; as title) {
499
+ <h3>{{ title }}</h3>
500
+ }
501
+ <jr-children />
502
+ </section>
503
+ \`,
504
+ styles: \`
505
+ .card {
506
+ display: grid;
507
+ gap: 8px;
508
+ padding: 16px;
509
+ border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
510
+ border-radius: 12px;
511
+ }
512
+ h3 {
513
+ margin: 0;
514
+ }
515
+ \`,
516
+ })
517
+ export class CardComponent {
518
+ readonly ctx =
519
+ injectRenderContext<InferComponentProps<typeof catalog, 'Card'>>();
520
+ }
521
+
522
+ @Component({
523
+ selector: 'app-text',
524
+ template: \`<p>{{ ctx.props().content }}</p>\`,
525
+ styles: \`
526
+ p {
527
+ margin: 0;
528
+ }
529
+ \`,
530
+ })
531
+ export class TextComponent {
532
+ readonly ctx =
533
+ injectRenderContext<InferComponentProps<typeof catalog, 'Text'>>();
534
+ }
535
+
536
+ @Component({
537
+ selector: 'app-button',
538
+ template: \`<button (click)="ctx.emit('press')">{{ ctx.props().label }}</button>\`,
539
+ })
540
+ export class ButtonComponent {
541
+ readonly ctx =
542
+ injectRenderContext<InferComponentProps<typeof catalog, 'Button'>>();
543
+ }
544
+
545
+ export const { registry } = defineRegistry(catalog, {
546
+ components: {
547
+ Card: CardComponent,
548
+ Text: TextComponent,
549
+ Button: ButtonComponent,
550
+ },
551
+ });
552
+ `;
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "$id": "ngx-json-render-mcp-app",
4
+ "title": "ngx-json-render MCP App schematic",
5
+ "description": "Generates an MCP App for a catalog: an Angular view project, its MCP server, and an mcp target that builds both.",
6
+ "type": "object",
7
+ "properties": {
8
+ "name": {
9
+ "type": "string",
10
+ "description": "The name of the view's application project; the build lands in dist/<name>.",
11
+ "default": "mcp-app",
12
+ "$default": {
13
+ "$source": "argv",
14
+ "index": 0
15
+ }
16
+ },
17
+ "catalog": {
18
+ "type": "string",
19
+ "description": "The catalog the model may use, as <file>#<export> relative to the workspace root, such as \"src/app/catalog.ts#catalog\". The server imports it in Node, so the file must not import Angular. Leave it out for the Angular Material catalog when ngx-json-render-material is installed, or a starter catalog otherwise."
20
+ },
21
+ "registry": {
22
+ "type": "string",
23
+ "description": "The registry that renders the catalog, as <file>#<export>, such as \"src/app/registry.ts#registry\". Required with --catalog."
24
+ }
25
+ },
26
+ "required": []
27
+ }
@@ -1,7 +1,24 @@
1
1
  import { Catalog } from '@json-render/core';
2
+ import { z } from 'zod';
2
3
  import { McpUiResourceCsp } from '@modelcontextprotocol/ext-apps';
3
4
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
- import { z } from 'zod';
5
+
6
+ /**
7
+ * `sendMessage`: post a message to the chat as the user, so the conversation
8
+ * carries on from what was clicked in the view.
9
+ */
10
+ declare const sendMessageAction: {
11
+ params: z.ZodObject<{
12
+ text: z.ZodString;
13
+ data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
14
+ }, z.core.$strip>;
15
+ description: string;
16
+ };
17
+ /**
18
+ * `catalog` plus the {@link sendMessageAction}, the same object for the same
19
+ * catalog, so the input schema built from it is built once.
20
+ */
21
+ declare function withSendMessage<C extends Catalog>(catalog: C): C;
5
22
 
6
23
  /** Options for {@link createRenderUiServer}. */
7
24
  interface RenderUiServerOptions {
@@ -19,6 +36,12 @@ interface RenderUiServerOptions {
19
36
  title?: string;
20
37
  /** The tool's description. Default: {@link renderUiDescription} for the catalog. */
21
38
  description?: string;
39
+ /**
40
+ * Add the `sendMessage` action to the catalog the model sees, so a button
41
+ * in the view can post a message to the chat as the user. The view handles
42
+ * it with `injectJsonRenderApp().handlers` from `ngx-json-render/mcp`.
43
+ */
44
+ sendMessage?: boolean;
22
45
  /**
23
46
  * The origins the view may load from (`resourceDomains`) and connect to
24
47
  * (`connectDomains`). Left out, the host's default applies, which the MCP
@@ -103,5 +126,5 @@ declare const specOutputSchema: z.ZodObject<{
103
126
  */
104
127
  declare function specProblems(spec: object): string | undefined;
105
128
 
106
- export { DESCRIPTION_LIMIT, createRenderUiServer, handleRenderUiRequest, renderUiDescription, specInputSchema, specOutputSchema, specProblems };
129
+ export { DESCRIPTION_LIMIT, createRenderUiServer, handleRenderUiRequest, renderUiDescription, sendMessageAction, specInputSchema, specOutputSchema, specProblems, withSendMessage };
107
130
  export type { RenderUiDescriptionOptions, RenderUiServerOptions };
@@ -1,5 +1,5 @@
1
1
  import { Signal } from '@angular/core';
2
- import { Spec } from '@json-render/core';
2
+ import { Spec, ActionHandler } from '@json-render/core';
3
3
  import { App } from '@modelcontextprotocol/ext-apps';
4
4
  import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
5
5
 
@@ -30,6 +30,13 @@ interface JsonRenderAppOptions {
30
30
  */
31
31
  autoResize?: boolean;
32
32
  }
33
+ /** How the last {@link JsonRenderApp.sendMessage} from an action went. */
34
+ type MessageOutcome = {
35
+ ok: true;
36
+ } | {
37
+ ok: false;
38
+ error: Error;
39
+ };
33
40
  /**
34
41
  * What {@link injectJsonRenderApp} returns — the fields of
35
42
  * `UseJsonRenderAppReturn` from `@json-render/mcp/app`, as signals.
@@ -59,6 +66,20 @@ interface JsonRenderApp {
59
66
  * one down, so an action's `onError` runs.
60
67
  */
61
68
  sendMessage(text: string, data?: Record<string, unknown>): Promise<void>;
69
+ /**
70
+ * Handlers for the actions the server adds for MCP Apps hosts
71
+ * (`createRenderUiServer({ sendMessage: true })` in
72
+ * `ngx-json-render/mcp/server`): pass them to `<json-render [handlers]>`.
73
+ * `sendMessage` takes `{ text, data? }` and posts it with
74
+ * {@link JsonRenderApp.sendMessage}.
75
+ */
76
+ readonly handlers: Record<string, ActionHandler>;
77
+ /**
78
+ * How the last `sendMessage` action went, `null` before the first. A host
79
+ * may decline a message or take none at all; show this so a press does not
80
+ * look like it did nothing.
81
+ */
82
+ readonly lastMessage: Signal<MessageOutcome | null>;
62
83
  }
63
84
  /** The text of a {@link JsonRenderApp.sendMessage} message. */
64
85
  declare function messageText(text: string, data?: Record<string, unknown>): string;
@@ -77,7 +98,8 @@ declare function parseSpecFromToolResult(result: ToolResultLike): Spec | null;
77
98
  * Connect an Angular app running inside an MCP Apps iframe to its host and
78
99
  * keep the json-render spec the model sent in a signal. The Angular
79
100
  * counterpart of `useJsonRenderApp` from `@json-render/mcp/app`; the server
80
- * side is `createMcpApp` from `@json-render/mcp`, unchanged.
101
+ * side is `createRenderUiServer` from `ngx-json-render/mcp/server`, or
102
+ * `createMcpApp` from `@json-render/mcp`.
81
103
  *
82
104
  * Call it in an injection context. The connection closes when the injector
83
105
  * that created it is destroyed.
@@ -90,4 +112,4 @@ declare function parseSpecFromToolResult(result: ToolResultLike): Spec | null;
90
112
  declare function injectJsonRenderApp(options?: JsonRenderAppOptions): JsonRenderApp;
91
113
 
92
114
  export { injectJsonRenderApp, messageText, parseSpecFromToolResult };
93
- export type { JsonRenderApp, JsonRenderAppOptions };
115
+ export type { JsonRenderApp, JsonRenderAppOptions, MessageOutcome };