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.
- package/README.md +35 -6
- package/fesm2022/ngx-json-render-mcp-server.mjs +46 -4
- package/fesm2022/ngx-json-render-mcp-server.mjs.map +1 -1
- package/fesm2022/ngx-json-render-mcp.mjs +35 -12
- package/fesm2022/ngx-json-render-mcp.mjs.map +1 -1
- package/package.json +2 -1
- package/schematics/builders.json +10 -0
- package/schematics/collection.json +5 -0
- package/schematics/mcp-app/index.cjs +552 -0
- package/schematics/mcp-app/schema.json +27 -0
- package/schematics/mcp-app-builder/index.cjs +98 -0
- package/schematics/mcp-app-builder/inline.cjs +117 -0
- package/schematics/mcp-app-builder/schema.json +31 -0
- package/types/ngx-json-render-mcp-server.d.ts +25 -2
- package/types/ngx-json-render-mcp.d.ts +25 -3
|
@@ -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
|
-
|
|
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 `
|
|
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 };
|