@bleedingdev/modern-js-mcp-apps 0.0.0-bootstrap → 3.11.3-ultramodern.1

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,360 @@
1
+ import promises from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import node_path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { createRsbuild } from "@rsbuild/core";
6
+ import { createHash } from "node:crypto";
7
+ import { validateUiMetadata, validateViewsConfig } from "./3~0.mjs";
8
+ function assetPrefixOrigin(prefix) {
9
+ if ('string' != typeof prefix) return;
10
+ if (prefix.startsWith('//')) return new URL(`https:${prefix}`).origin;
11
+ if (/^https?:\/\//.test(prefix)) return new URL(prefix).origin;
12
+ }
13
+ function mergeCsp(...sources) {
14
+ const merged = {};
15
+ for (const source of sources)for (const [key, domains] of Object.entries(source ?? {}))merged[key] = [
16
+ ...new Set([
17
+ ...merged[key] ?? [],
18
+ ...domains ?? []
19
+ ])
20
+ ];
21
+ return Object.keys(merged).length ? merged : void 0;
22
+ }
23
+ function viewEntryName(name) {
24
+ const label = name.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').slice(0, 48).replace(/^-+|-+$/g, '') || 'view';
25
+ const hash = createHash('sha256').update(name).digest('hex').slice(0, 12);
26
+ return `mcp-ui-${label}-${hash}`;
27
+ }
28
+ function findMfManifest(assets, manifestFile) {
29
+ const matches = assets.filter((asset)=>node_path.posix.basename(asset.name) === manifestFile);
30
+ if (1 !== matches.length) throw new Error(`Expected one MF manifest named ${manifestFile} in the web compilation, found ${matches.length}${matches.length ? `: ${matches.map((asset)=>asset.name).join(', ')}` : ''}. Set mf.manifestFile to the manifest filename`);
31
+ const [asset] = matches;
32
+ const manifest = JSON.parse(String(asset.source.source()));
33
+ if (!manifest || 'string' != typeof manifest.name || !Array.isArray(manifest.exposes)) throw new Error(`Invalid MF manifest: ${asset.name}`);
34
+ return {
35
+ filename: asset.name,
36
+ manifest: manifest
37
+ };
38
+ }
39
+ function createUiManifest(config, prefix, mf, csp) {
40
+ validateViewsConfig(config);
41
+ if (void 0 !== csp) validateUiMetadata({
42
+ csp
43
+ });
44
+ const { views, remote: defaultRemote, ...metadata } = config;
45
+ const assetOrigin = assetPrefixOrigin(prefix);
46
+ const mergedCsp = mergeCsp(metadata.csp, csp, assetOrigin ? {
47
+ resourceDomains: [
48
+ assetOrigin
49
+ ],
50
+ connectDomains: [
51
+ assetOrigin
52
+ ]
53
+ } : void 0);
54
+ const ui = {
55
+ prefersBorder: true,
56
+ ...metadata,
57
+ ...mergedCsp ? {
58
+ csp: mergedCsp
59
+ } : {}
60
+ };
61
+ const manifest = {
62
+ version: 1,
63
+ assetBase: 'string' == typeof prefix && /^https?:\/\//.test(prefix) ? `${prefix.replace(/\/+$/, '')}/` : 'request',
64
+ remotes: Object.create(null),
65
+ views: Object.create(null)
66
+ };
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 = [
86
+ assetPath,
87
+ mf.filename
88
+ ].filter(Boolean).join('/');
89
+ remote = mf.manifest.name;
90
+ }
91
+ manifest.remotes[remote] = {
92
+ entry,
93
+ shell: './ui/mcp-shell.html'
94
+ };
95
+ manifest.views[name] = {
96
+ remote,
97
+ expose: source.expose,
98
+ ui
99
+ };
100
+ }
101
+ return manifest;
102
+ }
103
+ async function writeArtifact(filename, contents) {
104
+ await promises.mkdir(node_path.dirname(filename), {
105
+ recursive: true
106
+ });
107
+ await promises.writeFile(`${filename}.tmp`, contents);
108
+ await promises.rename(`${filename}.tmp`, filename);
109
+ }
110
+ const packageFile = (filename)=>node_path.resolve(node_path.dirname(fileURLToPath(import.meta.url)), '../..', 'dist/esm', filename);
111
+ const mcpAppsPlugin = (options = {})=>({
112
+ name: '@modern-js/mcp-apps',
113
+ post: [
114
+ '@modern-js/runtime'
115
+ ],
116
+ setup (api) {
117
+ api._internalServerPlugins(({ plugins })=>{
118
+ const ctx = api.getAppContext();
119
+ plugins.push({
120
+ name: '@modern-js/mcp-apps/server-plugin',
121
+ options: [
122
+ 'dev',
123
+ 'start'
124
+ ].includes(ctx.command) ? {
125
+ developmentManifest: node_path.join(ctx.internalDirectory, 'mcp-apps/ui-manifest.json')
126
+ } : {}
127
+ });
128
+ return {
129
+ plugins
130
+ };
131
+ });
132
+ let definition;
133
+ const attachedCompilers = new WeakSet();
134
+ const shellHtml = new Map();
135
+ const shellAssets = new Map();
136
+ let buildShell;
137
+ let shellDirty = true;
138
+ let shellBuild;
139
+ const localEntries = new Set();
140
+ const configPath = ()=>node_path.resolve(api.getAppContext().appDirectory, options.views ?? 'mcp-views.json');
141
+ api.addWatchFiles(()=>[
142
+ configPath()
143
+ ]);
144
+ api.modifyEntrypoints(async ({ entrypoints: current })=>{
145
+ const ctx = api.getAppContext();
146
+ const source = JSON.parse(await promises.readFile(configPath(), 'utf8'));
147
+ validateViewsConfig(source);
148
+ definition = source;
149
+ const appRequire = createRequire(node_path.join(ctx.appDirectory, 'package.json'));
150
+ const needsMf = Object.values(definition.views).some((view)=>'string' != typeof view);
151
+ localEntries.clear();
152
+ shellHtml.clear();
153
+ shellAssets.clear();
154
+ buildShell = void 0;
155
+ shellDirty = true;
156
+ const shellEntries = {};
157
+ const entryDir = node_path.join(ctx.internalDirectory, 'mcp-apps/entries');
158
+ const entrypoints = current.filter((entry)=>entry.absoluteEntryDir !== entryDir);
159
+ const { enableAsyncEntry } = api.getNormalizedConfig().source;
160
+ for (const [name, source] of Object.entries(definition.views)){
161
+ if ('string' != typeof source) continue;
162
+ const entryName = viewEntryName(name);
163
+ if (entrypoints.some((entry)=>entry.entryName === entryName)) throw new Error(`MCP entry conflicts with application entry: ${entryName}`);
164
+ const entry = node_path.join(entryDir, `${entryName}.jsx`);
165
+ const mountEntry = enableAsyncEntry ? node_path.join(entryDir, `${entryName}-mount.jsx`) : entry;
166
+ await writeArtifact(mountEntry, `import React from ${JSON.stringify(appRequire.resolve('react'))};\nimport { createRoot } from ${JSON.stringify(appRequire.resolve('react-dom/client'))};\nimport { createMcpView } from ${JSON.stringify(packageFile('react.mjs'))};\nimport View, * as viewModule from ${JSON.stringify(node_path.resolve(node_path.dirname(configPath()), source))};\ncreateRoot(document.getElementById('root')).render(React.createElement(createMcpView(View, ${JSON.stringify({
167
+ name,
168
+ version: '1.0.0'
169
+ })}, Reflect.get(viewModule, 'mcpCapabilities'))));\n`);
170
+ if (enableAsyncEntry) await writeArtifact(entry, `import(${JSON.stringify(mountEntry)});\n`);
171
+ localEntries.add(entryName);
172
+ entrypoints.push({
173
+ entryName,
174
+ entry,
175
+ isMainEntry: false,
176
+ isAutoMount: false,
177
+ absoluteEntryDir: entryDir
178
+ });
179
+ }
180
+ if (needsMf) shellEntries['mcp-shell'] = packageFile('mf-entry.mjs');
181
+ if (Object.keys(shellEntries).length) {
182
+ const shellOutput = node_path.join(ctx.internalDirectory, 'mcp-apps/shell');
183
+ const config = api.getNormalizedConfig();
184
+ const development = [
185
+ 'dev',
186
+ 'start'
187
+ ].includes(ctx.command);
188
+ const assetPrefix = development ? config.dev.assetPrefix : config.output.assetPrefix;
189
+ const shell = await createRsbuild({
190
+ cwd: ctx.appDirectory,
191
+ rsbuildConfig: {
192
+ mode: 'production',
193
+ html: {
194
+ inject: 'body',
195
+ title: 'MCP App'
196
+ },
197
+ source: {
198
+ entry: shellEntries,
199
+ define: {
200
+ ...config.source.define,
201
+ 'process.env.NODE_ENV': JSON.stringify(development ? 'development' : 'production')
202
+ }
203
+ },
204
+ resolve: {
205
+ alias: {
206
+ ...config.source.alias,
207
+ ...Object.fromEntries([
208
+ 'react',
209
+ 'react-dom',
210
+ 'react-dom/client',
211
+ 'react/jsx-runtime',
212
+ 'react/jsx-dev-runtime'
213
+ ].map((name)=>[
214
+ name + '$',
215
+ appRequire.resolve(name)
216
+ ])),
217
+ ...needsMf ? {
218
+ '@module-federation/enhanced/runtime$': appRequire.resolve(options.mf?.runtime ?? '@module-federation/enhanced/runtime')
219
+ } : {}
220
+ }
221
+ },
222
+ output: {
223
+ distPath: {
224
+ root: shellOutput
225
+ },
226
+ assetPrefix: 'string' == typeof assetPrefix ? assetPrefix : void 0,
227
+ inlineScripts: true,
228
+ inlineStyles: true,
229
+ legalComments: 'inline'
230
+ },
231
+ tools: {
232
+ swc: {
233
+ jsc: {
234
+ transform: {
235
+ react: {
236
+ runtime: 'automatic'
237
+ }
238
+ }
239
+ }
240
+ },
241
+ rspack: {
242
+ optimization: {
243
+ splitChunks: false,
244
+ nodeEnv: false
245
+ },
246
+ module: {
247
+ parser: {
248
+ javascript: {
249
+ dynamicImportMode: 'eager'
250
+ }
251
+ }
252
+ }
253
+ }
254
+ }
255
+ }
256
+ });
257
+ const compileShell = async ()=>{
258
+ const result = await shell.build();
259
+ await result.close();
260
+ shellAssets.clear();
261
+ for (const filename of (await promises.readdir(shellOutput, {
262
+ recursive: true
263
+ }))){
264
+ const absolute = node_path.join(shellOutput, filename);
265
+ if (!filename.endsWith('.html') && (await promises.stat(absolute)).isFile()) shellAssets.set(filename.replaceAll(node_path.sep, '/'), await promises.readFile(absolute));
266
+ }
267
+ for (const name of Object.keys(shellEntries))shellHtml.set(name, await promises.readFile(node_path.join(shellOutput, `${name}.html`), 'utf8'));
268
+ };
269
+ buildShell = ()=>{
270
+ if (shellBuild) return shellBuild;
271
+ if (!shellDirty) return Promise.resolve();
272
+ shellDirty = false;
273
+ shellBuild = compileShell().catch((error)=>{
274
+ shellDirty = true;
275
+ throw error;
276
+ }).finally(()=>{
277
+ shellBuild = void 0;
278
+ });
279
+ return shellBuild;
280
+ };
281
+ }
282
+ return {
283
+ entrypoints
284
+ };
285
+ });
286
+ api.modifyRsbuildConfig((config)=>{
287
+ const lazy = config.dev?.lazyCompilation;
288
+ if (!lazy) return config;
289
+ const current = 'object' == typeof lazy ? lazy : {};
290
+ const test = current.test;
291
+ const internal = api.getAppContext().internalDirectory;
292
+ const eager = [
293
+ node_path.join(internal, 'mcp-apps/entries') + node_path.sep
294
+ ];
295
+ config.dev = {
296
+ ...config.dev,
297
+ lazyCompilation: {
298
+ ...current,
299
+ test (module) {
300
+ const resource = module.resource;
301
+ if (resource && eager.some((directory)=>resource.startsWith(directory))) return false;
302
+ return 'function' == typeof test ? test(module) : test instanceof RegExp ? test.test(resource ?? '') : true;
303
+ }
304
+ }
305
+ };
306
+ return config;
307
+ });
308
+ api.onAfterCreateCompiler(({ compiler, environments })=>{
309
+ api.onBeforeRestart(()=>new Promise((resolve, reject)=>compiler.close((error)=>error ? reject(error) : resolve())));
310
+ const compilers = 'compilers' in compiler ? compiler.compilers : [
311
+ compiler
312
+ ];
313
+ for (const child of compilers){
314
+ if (attachedCompilers.has(child)) continue;
315
+ const environment = environments[child.name ?? 'client'];
316
+ if (environment && 'web' === environment.config.output.target) {
317
+ attachedCompilers.add(child);
318
+ child.hooks.invalid.tap('modern-mcp-card-invalidate', ()=>{
319
+ shellDirty = true;
320
+ });
321
+ child.hooks.beforeCompile.tapPromise('modern-mcp-card-build', async ()=>{
322
+ await buildShell?.();
323
+ });
324
+ child.hooks.thisCompilation.tap('modern-mcp-card-html', (compilation)=>{
325
+ compilation.hooks.processAssets.tap({
326
+ name: 'modern-mcp-card-html',
327
+ stage: child.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT
328
+ }, ()=>{
329
+ for (const [filename, contents] of shellAssets)if (!compilation.getAsset(filename)) compilation.emitAsset(filename, new child.webpack.sources.RawSource(contents));
330
+ });
331
+ });
332
+ child.hooks.afterEmit.tapPromise('modern-mcp-ui-manifest', async (compilation)=>{
333
+ if (compilation.errors.length) return;
334
+ const ctx = api.getAppContext();
335
+ const development = [
336
+ 'dev',
337
+ 'start'
338
+ ].includes(ctx.command);
339
+ const output = node_path.join(development ? ctx.internalDirectory : ctx.distDirectory, 'mcp-apps');
340
+ let mf;
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');
342
+ const config = api.getNormalizedConfig();
343
+ const manifest = createUiManifest(definition, development ? config.dev.assetPrefix : config.output.assetPrefix, mf, options.csp);
344
+ for (const name of localEntries){
345
+ const htmlPath = environment.htmlPaths[name];
346
+ const html = htmlPath && compilation.getAsset(htmlPath);
347
+ if (!html) throw new Error(`Missing MCP HTML output: ${name}`);
348
+ await writeArtifact(node_path.join(output, 'ui', `${name}.html`), html.source.source());
349
+ }
350
+ for (const [name, html] of shellHtml)await writeArtifact(node_path.join(output, 'ui', `${name}.html`), html);
351
+ await writeArtifact(node_path.join(output, 'ui-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
352
+ });
353
+ }
354
+ }
355
+ });
356
+ }
357
+ });
358
+ const cli = mcpAppsPlugin;
359
+ export default cli;
360
+ export { mcpAppsPlugin };
@@ -0,0 +1,31 @@
1
+ import node_path from "node:path";
2
+ const server_plugin = (options = {})=>({
3
+ name: '@modern-js/mcp-apps/server-plugin',
4
+ setup (api) {
5
+ api.onPrepare(()=>{
6
+ const { middlewares, distDirectory } = api.getServerContext();
7
+ const uiOptions = options.developmentManifest ? {
8
+ manifest: options.developmentManifest,
9
+ mode: 'development'
10
+ } : {
11
+ manifest: node_path.join(distDirectory, 'mcp-apps/ui-manifest.json'),
12
+ mode: 'production'
13
+ };
14
+ middlewares.push({
15
+ name: 'mcp-apps-context',
16
+ before: [
17
+ 'bind-bff',
18
+ 'hono-bff-api'
19
+ ],
20
+ handler: async (context, next)=>{
21
+ context.set('locals', {
22
+ ...context.get('locals'),
23
+ modernMcpApps: uiOptions
24
+ });
25
+ await next();
26
+ }
27
+ });
28
+ });
29
+ }
30
+ });
31
+ export default server_plugin;
@@ -0,0 +1,216 @@
1
+ import { createMcpHandler } from "@modelcontextprotocol/server";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import node_path from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { RESOURCE_MIME_TYPE, registerAppResource } from "@modelcontextprotocol/ext-apps/server";
6
+ import { validateUiManifest, assertHttpUrl } from "./3~0.mjs";
7
+ const requestUiOptions = new WeakMap();
8
+ const manifestCache = new Map();
9
+ const remoteManifestTtl = 60000;
10
+ async function loadUiManifest(source, mode) {
11
+ const url = source instanceof URL ? new URL(source.href) : /^https?:\/\//.test(source) ? new URL(source) : pathToFileURL(node_path.resolve(source));
12
+ const production = 'production' === mode;
13
+ const key = `${production ? 'production' : 'development'}:${url.href}`;
14
+ let version = 'process';
15
+ if (!production && 'file:' === url.protocol) {
16
+ const info = await stat(fileURLToPath(url), {
17
+ bigint: true
18
+ });
19
+ version = `${info.ino}:${info.size}:${info.mtimeNs}:${info.ctimeNs}`;
20
+ }
21
+ const cached = manifestCache.get(key);
22
+ if (cached?.version === version && cached.expiresAt > Date.now()) return cached.value;
23
+ const entry = {
24
+ version,
25
+ expiresAt: 1 / 0,
26
+ value: readSource(url).then((text)=>{
27
+ const manifest = JSON.parse(text);
28
+ validateUiManifest(manifest);
29
+ if (!production && 'file:' !== url.protocol) entry.expiresAt = Date.now() + remoteManifestTtl;
30
+ return {
31
+ manifest,
32
+ location: url
33
+ };
34
+ }).catch((error)=>{
35
+ if (manifestCache.get(key) === entry) manifestCache.delete(key);
36
+ throw error;
37
+ })
38
+ };
39
+ manifestCache.set(key, entry);
40
+ return entry.value;
41
+ }
42
+ async function readSource(url, signal) {
43
+ if ('file:' === url.protocol) return readFile(fileURLToPath(url), {
44
+ encoding: 'utf8',
45
+ signal
46
+ });
47
+ assertHttpUrl(url.href);
48
+ const response = await fetch(url, {
49
+ signal: signal ? AbortSignal.any([
50
+ signal,
51
+ AbortSignal.timeout(30000)
52
+ ]) : AbortSignal.timeout(30000)
53
+ });
54
+ if (!response.ok) throw new Error(`Failed to read UI resource: HTTP ${response.status}`);
55
+ return response.text();
56
+ }
57
+ function publicBase(manifest, options) {
58
+ const configured = options.assetBase ?? manifest.assetBase;
59
+ if ('request' !== configured) {
60
+ const base = assertHttpUrl(configured);
61
+ if (!base.pathname.endsWith('/')) base.pathname += '/';
62
+ return base;
63
+ }
64
+ if (!options.request) throw new Error('Request-relative UI assets require a request or an explicit assetBase');
65
+ return new URL('/', requestOrigin(options.request));
66
+ }
67
+ function requestOrigin(request) {
68
+ const url = new URL(request.url);
69
+ const proto = request.headers.get('x-forwarded-proto')?.split(',')[0].trim();
70
+ if ('https' === proto || 'http' === proto) url.protocol = `${proto}:`;
71
+ return url.origin;
72
+ }
73
+ function escapeAttribute(value) {
74
+ return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
75
+ }
76
+ async function registerUiResources(server, options = {}) {
77
+ const injected = options.request && requestUiOptions.get(options.request);
78
+ options = {
79
+ ...options,
80
+ manifest: options.manifest ?? injected?.manifest,
81
+ mode: options.mode ?? injected?.mode
82
+ };
83
+ if (!options.manifest) throw new Error('MCP UI manifest is required. Use the Modern.js MCP Apps server plugin with a request or pass an explicit manifest path or URL');
84
+ const { manifest, location } = await loadUiManifest(options.manifest, options.mode ?? 'production');
85
+ const base = publicBase(manifest, options);
86
+ const devSocket = 'development' === options.mode && options.request ? new URL(requestOrigin(options.request)) : void 0;
87
+ if (devSocket) devSocket.protocol = 'https:' === devSocket.protocol ? 'wss:' : 'ws:';
88
+ const uris = Object.create(null);
89
+ for (const [name, view] of Object.entries(manifest.views)){
90
+ const uri = `ui://app/${encodeURIComponent(name)}`;
91
+ uris[name] = uri;
92
+ const remote = 'remote' in view ? manifest.remotes[view.remote] : void 0;
93
+ const entry = remote ? new URL(remote.entry, base).href : void 0;
94
+ const ui = {
95
+ ...view.ui,
96
+ csp: {
97
+ ...view.ui.csp,
98
+ resourceDomains: [
99
+ ...new Set([
100
+ ...view.ui.csp?.resourceDomains ?? [],
101
+ base.origin,
102
+ ...entry ? [
103
+ new URL(entry).origin
104
+ ] : []
105
+ ])
106
+ ],
107
+ baseUriDomains: [
108
+ ...new Set([
109
+ ...view.ui.csp?.baseUriDomains ?? [],
110
+ base.origin
111
+ ])
112
+ ],
113
+ connectDomains: [
114
+ ...new Set([
115
+ ...view.ui.csp?.connectDomains ?? [],
116
+ base.origin,
117
+ ...entry ? [
118
+ new URL(entry).origin
119
+ ] : [],
120
+ ...devSocket ? [
121
+ devSocket.origin
122
+ ] : []
123
+ ])
124
+ ]
125
+ }
126
+ };
127
+ registerAppResource(server, name, uri, {
128
+ _meta: {
129
+ ui
130
+ }
131
+ }, async (_uri, extra)=>{
132
+ const source = 'html' in view ? view.html : remote.shell;
133
+ const sourceUrl = /^https?:\/\//.test(source) ? new URL(source) : new URL(source, location);
134
+ let html = await readSource(sourceUrl, options.request ? AbortSignal.any([
135
+ options.request.signal,
136
+ extra.mcpReq.signal
137
+ ]) : extra.mcpReq.signal);
138
+ if (!/<head\b[^>]*>/i.test(html)) throw new Error('UI HTML requires a head element');
139
+ if (/<base\b/i.test(html)) throw new Error('UI HTML must not contain a base element');
140
+ const config = remote && 'expose' in view ? `<script type="application/json" id="modern-mcp-config">${JSON.stringify({
141
+ name: view.remote,
142
+ entry,
143
+ expose: view.expose,
144
+ view: name
145
+ }).replace(/</g, '\\u003c')}</script>` : '';
146
+ html = html.replace(/<head\b[^>]*>/i, (head)=>`${head}<base href="${escapeAttribute(base.href)}">${config}`);
147
+ return {
148
+ contents: [
149
+ {
150
+ uri,
151
+ mimeType: RESOURCE_MIME_TYPE,
152
+ text: html,
153
+ _meta: {
154
+ ui
155
+ }
156
+ }
157
+ ]
158
+ };
159
+ });
160
+ }
161
+ return uris;
162
+ }
163
+ async function currentContext(useContext) {
164
+ if (useContext) return useContext();
165
+ const { useHonoContext } = await import("@modern-js/server-runtime");
166
+ return useHonoContext();
167
+ }
168
+ function createFetchHandler(createServer, options = {}) {
169
+ const handler = createMcpHandler(({ requestInfo })=>{
170
+ if (!requestInfo) throw new Error('MCP HTTP request is required');
171
+ return createServer(requestInfo);
172
+ }, {
173
+ legacy: 'stateless',
174
+ onerror: options.onError
175
+ });
176
+ return async (request)=>{
177
+ const origin = request.headers.get('origin');
178
+ if (origin && ![
179
+ new URL(request.url).origin,
180
+ requestOrigin(request),
181
+ ...options.allowedOrigins ?? []
182
+ ].includes(origin)) return new Response('Forbidden origin', {
183
+ status: 403
184
+ });
185
+ return handler.fetch(request);
186
+ };
187
+ }
188
+ function createBffHandlers(createServer, options = {}) {
189
+ const { useContext, ...fetchOptions } = options;
190
+ const handle = createFetchHandler(async (request)=>{
191
+ const uiOptions = (await currentContext(useContext)).get('locals')?.modernMcpApps;
192
+ if (uiOptions) requestUiOptions.set(request, uiOptions);
193
+ return createServer(request);
194
+ }, fetchOptions);
195
+ const route = async (input)=>{
196
+ const context = await currentContext(useContext);
197
+ const raw = context.req.raw;
198
+ const headers = new Headers(raw.headers);
199
+ headers.delete('content-length');
200
+ const request = new Request(raw.url, {
201
+ method: raw.method,
202
+ headers,
203
+ ...'GET' === raw.method || 'HEAD' === raw.method ? {} : {
204
+ body: void 0 === input.data ? '' : JSON.stringify(input.data)
205
+ },
206
+ signal: raw.signal
207
+ });
208
+ return handle(request);
209
+ };
210
+ return {
211
+ POST: route,
212
+ GET: route,
213
+ DELETE: route
214
+ };
215
+ }
216
+ export { createBffHandlers, createFetchHandler, registerUiResources };
@@ -0,0 +1,23 @@
1
+ import { type McpViewsConfig, type UiManifest, type UiMetadata } from '../manifest.js';
2
+ export type UiCsp = NonNullable<UiMetadata['csp']>;
3
+ export declare function viewEntryName(name: string): string;
4
+ export interface MfManifest {
5
+ name: string;
6
+ exposes: Array<{
7
+ name: string;
8
+ }>;
9
+ }
10
+ /** Identify the MF manifest by filename. Stats files share its shape. */
11
+ export declare function findMfManifest(assets: ReadonlyArray<{
12
+ name: string;
13
+ source: {
14
+ source(): string | Buffer;
15
+ };
16
+ }>, manifestFile: string): {
17
+ filename: string;
18
+ manifest: MfManifest;
19
+ };
20
+ export declare function createUiManifest(config: McpViewsConfig, prefix: unknown, mf?: {
21
+ manifest: MfManifest;
22
+ filename: string;
23
+ }, csp?: UiCsp): UiManifest;
@@ -0,0 +1,16 @@
1
+ import type { AppTools, CliPlugin } from '@modern-js/app-tools';
2
+ import { type UiCsp } from './build.js';
3
+ export interface McpAppsPluginOptions {
4
+ /** JSON UI configuration, relative to the application root. */
5
+ views?: string;
6
+ /** CSP domains added to every view, merged with the csp in the views file. */
7
+ csp?: UiCsp;
8
+ mf?: {
9
+ /** MF manifest filename in the build output. Defaults to 'mf-manifest.json'. */
10
+ manifestFile?: string;
11
+ /** Module providing the shell's MF `createInstance`. Defaults to '@module-federation/enhanced/runtime'. */
12
+ runtime?: string;
13
+ };
14
+ }
15
+ export declare const mcpAppsPlugin: (options?: McpAppsPluginOptions) => CliPlugin<AppTools>;
16
+ export default mcpAppsPlugin;
@@ -0,0 +1,6 @@
1
+ import type { ServerPlugin } from '@modern-js/server-runtime';
2
+ export interface McpAppsServerOptions {
3
+ developmentManifest?: string;
4
+ }
5
+ export default _default;
6
+ declare function _default(options?: McpAppsServerOptions): ServerPlugin;