@modern-js/mcp-apps 3.11.1 → 3.11.2
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 +1 -1
- package/dist/cjs/cli.cjs +51 -15
- package/dist/cjs/server.cjs +2 -1
- package/dist/esm/mf-entry.mjs +1 -1
- package/dist/esm-node/3~0.mjs +16 -1
- package/dist/esm-node/cli.mjs +27 -14
- package/dist/esm-node/server.mjs +2 -1
- package/dist/types/manifest.d.ts +10 -0
- package/dist/types/ui/mf.d.ts +2 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Declare React cards in `mcp-views.json` and enable `mcpAppsPlugin({ views: './mc
|
|
|
25
25
|
}
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Component paths resolve relative to the JSON file. Components must have a default export. MF exposes refer to this application's MF manifest output and must also have a default component export. Local cards are added as application entries and reuse the framework's multi-entry compilation, including its loaders, styles and build plugins. Only the MF shell uses a separate Rsbuild compilation with the application's React dependencies, with its startup scripts and styles inlined. The plugin emits `dist/mcp-apps/ui-manifest.json` and never registers HTTP routes.
|
|
28
|
+
Component paths resolve relative to the JSON file. Components must have a default export. MF exposes refer to this application's MF manifest output and must also have a default component export. `{ "remote": "https://host/mf-manifest.json", "expose": "./Name" }` refers to a deployed remote instead. Set `remote` at the top level alongside `csp` to share it across all expose views, for example `{ "remote": "https://host/mf-manifest.json", "views": { "todos": { "expose": "./Todos" }, "stats": { "expose": "./Stats" } } }`. A view-level `remote` overrides this default. Component paths remain local. Without either remote setting, exposes refer to this application. With a top-level `remote`, expose views in the same config cannot refer to this application's exposes, so set `remote` per view to mix both. The URL must point to the remote's manifest, with a path ending in `.json`, and the runtime reads the remote's name from it. The shell loads the remote manifest when the card opens, so republishing the remote needs no MCP server rebuild. Its exposes are not checked at build time, and the application needs no MF plugin. The manifest origin is added to CSP. Declare other origins serving the remote's chunks in `csp`. Local cards are added as application entries and reuse the framework's multi-entry compilation, including its loaders, styles and build plugins. Only the MF shell uses a separate Rsbuild compilation with the application's React dependencies, with its startup scripts and styles inlined. The plugin emits `dist/mcp-apps/ui-manifest.json` and never registers HTTP routes.
|
|
29
29
|
|
|
30
30
|
- `mf.manifestFile` is the MF manifest filename in the web build output. It defaults to `mf-manifest.json` and must match exactly one emitted file.
|
|
31
31
|
- `mf.runtime` is the module, resolved from the application, that the MF shell imports `createInstance` from. It defaults to `@module-federation/enhanced/runtime`.
|
package/dist/cjs/cli.cjs
CHANGED
|
@@ -71,8 +71,13 @@ function validateUiMetadata(value) {
|
|
|
71
71
|
function validateViewsConfig(value) {
|
|
72
72
|
if (!isRecord(value) || !isRecord(value.views)) throw new Error('mcp-views.json requires a views object');
|
|
73
73
|
validateUiMetadata(value);
|
|
74
|
+
if (void 0 !== value.remote) {
|
|
75
|
+
if ('string' != typeof value.remote) throw new Error('remote must be a string');
|
|
76
|
+
assertRemoteManifest(value.remote);
|
|
77
|
+
}
|
|
74
78
|
for (const key of Object.keys(value))if (![
|
|
75
79
|
'views',
|
|
80
|
+
'remote',
|
|
76
81
|
'csp',
|
|
77
82
|
'prefersBorder',
|
|
78
83
|
'permissions',
|
|
@@ -81,10 +86,28 @@ function validateViewsConfig(value) {
|
|
|
81
86
|
for (const [name, view] of Object.entries(value.views)){
|
|
82
87
|
if (!name.trim()) throw new Error('View names must be non-empty');
|
|
83
88
|
if (!('string' == typeof view && view.trim())) {
|
|
84
|
-
if (
|
|
89
|
+
if (isRecord(view) && 'string' == typeof view.expose && view.expose.startsWith('./') && Object.keys(view).every((key)=>'expose' === key || 'remote' === key)) {
|
|
90
|
+
if (!('remote' in view)) continue;
|
|
91
|
+
if ('string' == typeof view.remote) {
|
|
92
|
+
assertRemoteManifest(view.remote);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Invalid view: ${name}. Use a component path, { expose: './Name' } or { remote: 'https://host/mf-manifest.json', expose: './Name' }`);
|
|
85
97
|
}
|
|
86
98
|
}
|
|
87
99
|
}
|
|
100
|
+
function assertRemoteManifest(remote) {
|
|
101
|
+
if (!/^https?:\/\//.test(remote) || !assertHttpUrl(remote).pathname.endsWith('.json')) throw new Error(`Invalid MF remote: ${remote}. Use https://host/mf-manifest.json`);
|
|
102
|
+
}
|
|
103
|
+
function assertHttpUrl(source) {
|
|
104
|
+
const url = new URL(source);
|
|
105
|
+
if (![
|
|
106
|
+
'http:',
|
|
107
|
+
'https:'
|
|
108
|
+
].includes(url.protocol) || url.username || url.password) throw new Error(`Invalid UI URL: ${source}`);
|
|
109
|
+
return url;
|
|
110
|
+
}
|
|
88
111
|
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
89
112
|
function assetPrefixOrigin(prefix) {
|
|
90
113
|
if ('string' != typeof prefix) return;
|
|
@@ -122,7 +145,7 @@ function createUiManifest(config, prefix, mf, csp) {
|
|
|
122
145
|
if (void 0 !== csp) validateUiMetadata({
|
|
123
146
|
csp
|
|
124
147
|
});
|
|
125
|
-
const { views, ...metadata } = config;
|
|
148
|
+
const { views, remote: defaultRemote, ...metadata } = config;
|
|
126
149
|
const assetOrigin = assetPrefixOrigin(prefix);
|
|
127
150
|
const mergedCsp = mergeCsp(metadata.csp, csp, assetOrigin ? {
|
|
128
151
|
resourceDomains: [
|
|
@@ -145,23 +168,36 @@ function createUiManifest(config, prefix, mf, csp) {
|
|
|
145
168
|
remotes: Object.create(null),
|
|
146
169
|
views: Object.create(null)
|
|
147
170
|
};
|
|
148
|
-
for (const [name, source] of Object.entries(views))
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
171
|
+
for (const [name, source] of Object.entries(views)){
|
|
172
|
+
if ('string' == typeof source) {
|
|
173
|
+
manifest.views[name] = {
|
|
174
|
+
html: `./ui/${viewEntryName(name)}.html`,
|
|
175
|
+
ui
|
|
176
|
+
};
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
let remote;
|
|
180
|
+
let entry;
|
|
181
|
+
const deployedRemote = source.remote ?? defaultRemote;
|
|
182
|
+
if (void 0 !== deployedRemote) {
|
|
183
|
+
entry = deployedRemote;
|
|
184
|
+
remote = `mcp-remote-${(0, external_node_crypto_namespaceObject.createHash)('sha256').update(entry).digest('hex').slice(0, 12)}`;
|
|
185
|
+
} else {
|
|
186
|
+
if (!mf) throw new Error(`View ${name} requires a Module Federation manifest. Enable the MF plugin and manifest output`);
|
|
187
|
+
if (!mf.manifest.exposes.some((item)=>item.name.replace(/^\.\//, '') === source.expose.slice(2))) throw new Error(`View ${name} references missing MF expose: ${source.expose}`);
|
|
188
|
+
const assetPath = 'string' == typeof prefix && prefix.startsWith('/') && !prefix.startsWith('//') ? prefix.replace(/^\/+|\/+$/g, '') : '';
|
|
189
|
+
entry = [
|
|
158
190
|
assetPath,
|
|
159
191
|
mf.filename
|
|
160
|
-
].filter(Boolean).join('/')
|
|
192
|
+
].filter(Boolean).join('/');
|
|
193
|
+
remote = mf.manifest.name;
|
|
194
|
+
}
|
|
195
|
+
manifest.remotes[remote] = {
|
|
196
|
+
entry,
|
|
161
197
|
shell: './ui/mcp-shell.html'
|
|
162
198
|
};
|
|
163
199
|
manifest.views[name] = {
|
|
164
|
-
remote
|
|
200
|
+
remote,
|
|
165
201
|
expose: source.expose,
|
|
166
202
|
ui
|
|
167
203
|
};
|
|
@@ -406,7 +442,7 @@ const mcpAppsPlugin = (options = {})=>({
|
|
|
406
442
|
].includes(ctx.command);
|
|
407
443
|
const output = external_node_path_default().join(development ? ctx.internalDirectory : ctx.distDirectory, 'mcp-apps');
|
|
408
444
|
let mf;
|
|
409
|
-
if (Object.values(definition.views).some((view)=>'string' != typeof view)) mf = findMfManifest(compilation.getAssets(), options.mf?.manifestFile ?? 'mf-manifest.json');
|
|
445
|
+
if (Object.values(definition.views).some((view)=>'string' != typeof view && void 0 === view.remote && void 0 === definition.remote)) mf = findMfManifest(compilation.getAssets(), options.mf?.manifestFile ?? 'mf-manifest.json');
|
|
410
446
|
const config = api.getNormalizedConfig();
|
|
411
447
|
const manifest = createUiManifest(definition, development ? config.dev.assetPrefix : config.output.assetPrefix, mf, options.csp);
|
|
412
448
|
for (const name of localEntries){
|
package/dist/cjs/server.cjs
CHANGED
|
@@ -231,7 +231,8 @@ async function registerUiResources(server, options = {}) {
|
|
|
231
231
|
const config = remote && 'expose' in view ? `<script type="application/json" id="modern-mcp-config">${JSON.stringify({
|
|
232
232
|
name: view.remote,
|
|
233
233
|
entry,
|
|
234
|
-
expose: view.expose
|
|
234
|
+
expose: view.expose,
|
|
235
|
+
view: name
|
|
235
236
|
}).replace(/</g, '\\u003c')}</script>` : '';
|
|
236
237
|
html = html.replace(/<head\b[^>]*>/i, (head)=>`${head}<base href="${escapeAttribute(base.href)}">${config}`);
|
|
237
238
|
return {
|
package/dist/esm/mf-entry.mjs
CHANGED
|
@@ -41,7 +41,7 @@ function createMfView(config) {
|
|
|
41
41
|
if (!mod?.default) throw new Error(`Missing default export for ${config.expose}`);
|
|
42
42
|
return {
|
|
43
43
|
default: createMcpView(mod.default, {
|
|
44
|
-
name: config.
|
|
44
|
+
name: config.view,
|
|
45
45
|
version: '1.0.0'
|
|
46
46
|
}, mod.mcpCapabilities)
|
|
47
47
|
};
|
package/dist/esm-node/3~0.mjs
CHANGED
|
@@ -19,8 +19,13 @@ function validateUiMetadata(value) {
|
|
|
19
19
|
function validateViewsConfig(value) {
|
|
20
20
|
if (!isRecord(value) || !isRecord(value.views)) throw new Error('mcp-views.json requires a views object');
|
|
21
21
|
validateUiMetadata(value);
|
|
22
|
+
if (void 0 !== value.remote) {
|
|
23
|
+
if ('string' != typeof value.remote) throw new Error('remote must be a string');
|
|
24
|
+
assertRemoteManifest(value.remote);
|
|
25
|
+
}
|
|
22
26
|
for (const key of Object.keys(value))if (![
|
|
23
27
|
'views',
|
|
28
|
+
'remote',
|
|
24
29
|
'csp',
|
|
25
30
|
'prefersBorder',
|
|
26
31
|
'permissions',
|
|
@@ -29,10 +34,20 @@ function validateViewsConfig(value) {
|
|
|
29
34
|
for (const [name, view] of Object.entries(value.views)){
|
|
30
35
|
if (!name.trim()) throw new Error('View names must be non-empty');
|
|
31
36
|
if (!('string' == typeof view && view.trim())) {
|
|
32
|
-
if (
|
|
37
|
+
if (isRecord(view) && 'string' == typeof view.expose && view.expose.startsWith('./') && Object.keys(view).every((key)=>'expose' === key || 'remote' === key)) {
|
|
38
|
+
if (!('remote' in view)) continue;
|
|
39
|
+
if ('string' == typeof view.remote) {
|
|
40
|
+
assertRemoteManifest(view.remote);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`Invalid view: ${name}. Use a component path, { expose: './Name' } or { remote: 'https://host/mf-manifest.json', expose: './Name' }`);
|
|
33
45
|
}
|
|
34
46
|
}
|
|
35
47
|
}
|
|
48
|
+
function assertRemoteManifest(remote) {
|
|
49
|
+
if (!/^https?:\/\//.test(remote) || !assertHttpUrl(remote).pathname.endsWith('.json')) throw new Error(`Invalid MF remote: ${remote}. Use https://host/mf-manifest.json`);
|
|
50
|
+
}
|
|
36
51
|
function assertHttpUrl(source) {
|
|
37
52
|
const url = new URL(source);
|
|
38
53
|
if (![
|
package/dist/esm-node/cli.mjs
CHANGED
|
@@ -41,7 +41,7 @@ function createUiManifest(config, prefix, mf, csp) {
|
|
|
41
41
|
if (void 0 !== csp) validateUiMetadata({
|
|
42
42
|
csp
|
|
43
43
|
});
|
|
44
|
-
const { views, ...metadata } = config;
|
|
44
|
+
const { views, remote: defaultRemote, ...metadata } = config;
|
|
45
45
|
const assetOrigin = assetPrefixOrigin(prefix);
|
|
46
46
|
const mergedCsp = mergeCsp(metadata.csp, csp, assetOrigin ? {
|
|
47
47
|
resourceDomains: [
|
|
@@ -64,23 +64,36 @@ function createUiManifest(config, prefix, mf, csp) {
|
|
|
64
64
|
remotes: Object.create(null),
|
|
65
65
|
views: Object.create(null)
|
|
66
66
|
};
|
|
67
|
-
for (const [name, source] of Object.entries(views))
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
for (const [name, source] of Object.entries(views)){
|
|
68
|
+
if ('string' == typeof source) {
|
|
69
|
+
manifest.views[name] = {
|
|
70
|
+
html: `./ui/${viewEntryName(name)}.html`,
|
|
71
|
+
ui
|
|
72
|
+
};
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
let remote;
|
|
76
|
+
let entry;
|
|
77
|
+
const deployedRemote = source.remote ?? defaultRemote;
|
|
78
|
+
if (void 0 !== deployedRemote) {
|
|
79
|
+
entry = deployedRemote;
|
|
80
|
+
remote = `mcp-remote-${createHash('sha256').update(entry).digest('hex').slice(0, 12)}`;
|
|
81
|
+
} else {
|
|
82
|
+
if (!mf) throw new Error(`View ${name} requires a Module Federation manifest. Enable the MF plugin and manifest output`);
|
|
83
|
+
if (!mf.manifest.exposes.some((item)=>item.name.replace(/^\.\//, '') === source.expose.slice(2))) throw new Error(`View ${name} references missing MF expose: ${source.expose}`);
|
|
84
|
+
const assetPath = 'string' == typeof prefix && prefix.startsWith('/') && !prefix.startsWith('//') ? prefix.replace(/^\/+|\/+$/g, '') : '';
|
|
85
|
+
entry = [
|
|
77
86
|
assetPath,
|
|
78
87
|
mf.filename
|
|
79
|
-
].filter(Boolean).join('/')
|
|
88
|
+
].filter(Boolean).join('/');
|
|
89
|
+
remote = mf.manifest.name;
|
|
90
|
+
}
|
|
91
|
+
manifest.remotes[remote] = {
|
|
92
|
+
entry,
|
|
80
93
|
shell: './ui/mcp-shell.html'
|
|
81
94
|
};
|
|
82
95
|
manifest.views[name] = {
|
|
83
|
-
remote
|
|
96
|
+
remote,
|
|
84
97
|
expose: source.expose,
|
|
85
98
|
ui
|
|
86
99
|
};
|
|
@@ -325,7 +338,7 @@ const mcpAppsPlugin = (options = {})=>({
|
|
|
325
338
|
].includes(ctx.command);
|
|
326
339
|
const output = node_path.join(development ? ctx.internalDirectory : ctx.distDirectory, 'mcp-apps');
|
|
327
340
|
let mf;
|
|
328
|
-
if (Object.values(definition.views).some((view)=>'string' != typeof view)) mf = findMfManifest(compilation.getAssets(), options.mf?.manifestFile ?? 'mf-manifest.json');
|
|
341
|
+
if (Object.values(definition.views).some((view)=>'string' != typeof view && void 0 === view.remote && void 0 === definition.remote)) mf = findMfManifest(compilation.getAssets(), options.mf?.manifestFile ?? 'mf-manifest.json');
|
|
329
342
|
const config = api.getNormalizedConfig();
|
|
330
343
|
const manifest = createUiManifest(definition, development ? config.dev.assetPrefix : config.output.assetPrefix, mf, options.csp);
|
|
331
344
|
for (const name of localEntries){
|
package/dist/esm-node/server.mjs
CHANGED
|
@@ -140,7 +140,8 @@ async function registerUiResources(server, options = {}) {
|
|
|
140
140
|
const config = remote && 'expose' in view ? `<script type="application/json" id="modern-mcp-config">${JSON.stringify({
|
|
141
141
|
name: view.remote,
|
|
142
142
|
entry,
|
|
143
|
-
expose: view.expose
|
|
143
|
+
expose: view.expose,
|
|
144
|
+
view: name
|
|
144
145
|
}).replace(/</g, '\\u003c')}</script>` : '';
|
|
145
146
|
html = html.replace(/<head\b[^>]*>/i, (head)=>`${head}<base href="${escapeAttribute(base.href)}">${config}`);
|
|
146
147
|
return {
|
package/dist/types/manifest.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { McpUiResourceMeta } from '@modelcontextprotocol/ext-apps';
|
|
2
2
|
export type UiMetadata = McpUiResourceMeta;
|
|
3
3
|
export interface McpViewsConfig extends UiMetadata {
|
|
4
|
+
/** Default deployed MF remote for expose views. Individual views may override it. */
|
|
5
|
+
remote?: string;
|
|
6
|
+
/** A component path, an expose of this application, or an expose of a deployed remote. */
|
|
4
7
|
views: Record<string, string | {
|
|
5
8
|
expose: string;
|
|
9
|
+
remote?: string;
|
|
6
10
|
}>;
|
|
7
11
|
}
|
|
8
12
|
export interface UiManifest {
|
|
@@ -24,5 +28,11 @@ export interface UiManifest {
|
|
|
24
28
|
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
25
29
|
export declare function validateUiMetadata(value: unknown): asserts value is UiMetadata;
|
|
26
30
|
export declare function validateViewsConfig(value: unknown): asserts value is McpViewsConfig;
|
|
31
|
+
/**
|
|
32
|
+
* A deployed MF remote must be a manifest URL. The MF runtime finds the
|
|
33
|
+
* container through the manifest only for `.json` entries. Any other entry
|
|
34
|
+
* would need the remote's real MF name.
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertRemoteManifest(remote: string): void;
|
|
27
37
|
export declare function assertHttpUrl(source: string): URL;
|
|
28
38
|
export declare function validateUiManifest(value: unknown): asserts value is UiManifest;
|
package/dist/types/ui/mf.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export interface MfViewConfig {
|
|
|
3
3
|
name: string;
|
|
4
4
|
entry: string;
|
|
5
5
|
expose: string;
|
|
6
|
+
/** The view name, reported to the host as the app name like local cards. */
|
|
7
|
+
view: string;
|
|
6
8
|
}
|
|
7
9
|
/** Executed in the application build, so the shell and exposes share its React. */
|
|
8
10
|
export declare function createMfView(config: MfViewConfig): () => JsxRuntime.JSX.Element;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modern-js/mcp-apps",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.2",
|
|
4
4
|
"description": "Build, serve and render MCP Apps in Modern.js applications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://modernjs.dev",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@module-federation/enhanced": "^2.0.0",
|
|
52
52
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
53
53
|
"@modelcontextprotocol/ext-apps": "^2.0.4",
|
|
54
|
-
"@modern-js/server-runtime": "^3.11.
|
|
54
|
+
"@modern-js/server-runtime": "^3.11.2"
|
|
55
55
|
},
|
|
56
56
|
"peerDependenciesMeta": {
|
|
57
57
|
"@module-federation/enhanced": {
|
|
@@ -74,9 +74,9 @@
|
|
|
74
74
|
"zod": "^4.2.0",
|
|
75
75
|
"@modelcontextprotocol/server": "2.3.1",
|
|
76
76
|
"@modelcontextprotocol/ext-apps": "2.0.4",
|
|
77
|
-
"@scripts/rstest-config": "2.66.
|
|
78
|
-
"@modern-js/
|
|
79
|
-
"@modern-js/
|
|
77
|
+
"@scripts/rstest-config": "2.66.6",
|
|
78
|
+
"@modern-js/server-runtime": "3.11.2",
|
|
79
|
+
"@modern-js/app-tools": "3.11.2"
|
|
80
80
|
},
|
|
81
81
|
"publishConfig": {
|
|
82
82
|
"access": "public",
|