ngx-json-render 0.9.5 → 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.
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inlineView = inlineView;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const MIME = {
7
+ '.woff2': 'font/woff2',
8
+ '.woff': 'font/woff',
9
+ '.ttf': 'font/ttf',
10
+ '.otf': 'font/otf',
11
+ '.png': 'image/png',
12
+ '.jpg': 'image/jpeg',
13
+ '.jpeg': 'image/jpeg',
14
+ '.gif': 'image/gif',
15
+ '.webp': 'image/webp',
16
+ '.avif': 'image/avif',
17
+ '.svg': 'image/svg+xml',
18
+ '.ico': 'image/x-icon',
19
+ };
20
+ /**
21
+ * The browser output of an Angular application build as one HTML document.
22
+ * MCP Apps hosts load a view's `ui://` resource as a single document with no
23
+ * origin to fetch from, so every module script (main, polyfills, and the
24
+ * chunks they import) is bundled and inlined, every local stylesheet is
25
+ * inlined, and fonts and images the stylesheets point at become data URLs.
26
+ * External URLs stay as they are; the view's CSP has to allow them.
27
+ */
28
+ async function inlineView(browserDir, bundle) {
29
+ const indexPath = (0, node_path_1.join)(browserDir, 'index.html');
30
+ if (!(0, node_fs_1.existsSync)(indexPath)) {
31
+ throw new Error(`No index.html in ${browserDir}. Is the build target an @angular/build:application build?`);
32
+ }
33
+ let html = (0, node_fs_1.readFileSync)(indexPath, 'utf8');
34
+ const used = new Set(['index.html']);
35
+ // Preloads point at chunks that are about to be inlined.
36
+ html = html.replace(/<link\b[^>]*\brel="modulepreload"[^>]*>/g, '');
37
+ // A view lives in an iframe, which shows no icon; `ng new` links one.
38
+ html = html.replace(/<link\b[^>]*\brel="(?:shortcut )?icon"[^>]*>\s*/g, (tag) => {
39
+ const href = attribute(tag, 'href');
40
+ const file = href && local(browserDir, href);
41
+ if (!file)
42
+ return tag;
43
+ used.add((0, node_path_1.relative)(browserDir, file));
44
+ return '';
45
+ });
46
+ for (const tag of html.match(/<script\b[^>]*\bsrc="[^"]*"[^>]*><\/script>/g) ?? []) {
47
+ const src = attribute(tag, 'src');
48
+ const file = src && local(browserDir, src);
49
+ if (!file)
50
+ continue;
51
+ used.add((0, node_path_1.relative)(browserDir, file));
52
+ const isModule = attribute(tag, 'type') === 'module';
53
+ const code = isModule ? await bundle(file) : (0, node_fs_1.readFileSync)(file, 'utf8');
54
+ html = html.replace(tag, () => `<script${isModule ? ' type="module"' : ''}>${escapeTag(code, 'script')}</script>`);
55
+ }
56
+ // Angular may load the stylesheet as `media="print" onload=…` with a
57
+ // <noscript> fallback, so it does not block the first paint.
58
+ const stylesheet = /<link\b[^>]*\brel="stylesheet"[^>]*>(\s*<noscript>[\s\S]*?<\/noscript>)?/g;
59
+ for (const [match] of html.matchAll(stylesheet)) {
60
+ const href = attribute(match, 'href');
61
+ const file = href && local(browserDir, href);
62
+ if (!file)
63
+ continue;
64
+ used.add((0, node_path_1.relative)(browserDir, file));
65
+ const css = inlineUrls((0, node_fs_1.readFileSync)(file, 'utf8'), file, browserDir, used);
66
+ html = html.replace(match, () => `<style>${escapeTag(css, 'style')}</style>`);
67
+ }
68
+ // Inline <style> blocks (critical CSS) can point at media files too.
69
+ html = html.replace(/(<style\b[^>]*>)([\s\S]*?)(<\/style>)/g, (_, open, css, close) => open + inlineUrls(css, indexPath, browserDir, used) + close);
70
+ const leftOut = files(browserDir)
71
+ .map((file) => (0, node_path_1.relative)(browserDir, file))
72
+ .filter((file) => !used.has(file) && !isBuildArtifact(file));
73
+ return { html, leftOut };
74
+ }
75
+ /** `url(...)` references to files of the build, as data URLs. */
76
+ function inlineUrls(css, from, browserDir, used) {
77
+ return css.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/g, (match, quote, url) => {
78
+ const base = (0, node_path_1.join)(from, '..');
79
+ const file = local(base, url) ?? local(browserDir, url);
80
+ const mime = file && MIME[(0, node_path_1.extname)(file).toLowerCase()];
81
+ if (!file || !mime)
82
+ return match;
83
+ used.add((0, node_path_1.relative)(browserDir, file));
84
+ const data = (0, node_fs_1.readFileSync)(file).toString('base64');
85
+ return `url(${quote}data:${mime};base64,${data}${quote})`;
86
+ });
87
+ }
88
+ /** The file `url` names under `dir`, or undefined for anything external. */
89
+ function local(dir, url) {
90
+ if (/^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(url))
91
+ return undefined;
92
+ const path = decodeURIComponent(url.split(/[?#]/)[0]).replace(/^\/+/, '');
93
+ const file = (0, node_path_1.resolve)(dir, path);
94
+ if (file !== (0, node_path_1.resolve)(dir) && !file.startsWith((0, node_path_1.resolve)(dir) + node_path_1.sep)) {
95
+ return undefined;
96
+ }
97
+ return (0, node_fs_1.existsSync)(file) && (0, node_fs_1.statSync)(file).isFile() ? file : undefined;
98
+ }
99
+ function attribute(tag, name) {
100
+ return new RegExp(`\\b${name}="([^"]*)"`).exec(tag)?.[1];
101
+ }
102
+ /** Keeps `</script>` or `</style>` inside the code from closing the tag. */
103
+ function escapeTag(code, tag) {
104
+ return code.replace(new RegExp(`</${tag}`, 'gi'), `<\\/${tag}`);
105
+ }
106
+ /** Chunks and maps the inlined scripts already carry, and build metadata. */
107
+ function isBuildArtifact(file) {
108
+ return (/\.(?:js|mjs|map)$/.test(file) ||
109
+ file === '3rdpartylicenses.txt' ||
110
+ file === 'prerendered-routes.json');
111
+ }
112
+ function files(dir) {
113
+ return (0, node_fs_1.readdirSync)(dir, { withFileTypes: true }).flatMap((entry) => {
114
+ const path = (0, node_path_1.join)(dir, entry.name);
115
+ return entry.isDirectory() ? files(path) : [path];
116
+ });
117
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "title": "ngx-json-render MCP App builder",
4
+ "description": "Builds an MCP App view into one view.html and bundles its server into server.mjs.",
5
+ "type": "object",
6
+ "properties": {
7
+ "buildTarget": {
8
+ "type": "string",
9
+ "description": "The view's @angular/build:application target, such as \"my-app:build:production\" or \":build:production\" for this project."
10
+ },
11
+ "outputPath": {
12
+ "type": "string",
13
+ "description": "Directory for view.html and server.mjs, relative to the workspace root."
14
+ },
15
+ "server": {
16
+ "type": "string",
17
+ "description": "The server's entry file, bundled into server.mjs. Leave it out to build the view only."
18
+ },
19
+ "tsConfig": {
20
+ "type": "string",
21
+ "description": "The tsconfig esbuild reads for the server (paths, decorators). By default, the nearest one to each file."
22
+ },
23
+ "externalPackages": {
24
+ "type": "boolean",
25
+ "default": false,
26
+ "description": "Leave node_modules packages out of server.mjs, to be installed next to it. By default the server is one self-contained file."
27
+ }
28
+ },
29
+ "required": ["buildTarget", "outputPath"],
30
+ "additionalProperties": false
31
+ }
@@ -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 };