@frontmcp/uipack 1.0.0-beta.9 → 1.0.0
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/adapters/index.js +156 -46
- package/adapters/template-renderer.d.ts +8 -5
- package/adapters/template-renderer.d.ts.map +1 -1
- package/bridge-runtime/iife-generator.d.ts.map +1 -1
- package/bridge-runtime/index.js +76 -15
- package/component/index.js +136 -26
- package/component/transpiler.d.ts.map +1 -1
- package/esm/adapters/index.mjs +156 -46
- package/esm/bridge-runtime/index.mjs +76 -15
- package/esm/component/index.mjs +136 -26
- package/esm/index.mjs +156 -46
- package/esm/package.json +2 -2
- package/esm/shell/index.mjs +77 -15
- package/index.js +156 -46
- package/package.json +2 -2
- package/shell/data-injector.d.ts.map +1 -1
- package/shell/index.js +77 -15
package/esm/component/index.mjs
CHANGED
|
@@ -463,22 +463,71 @@ function bundleFileSource(source, filename, resolveDir, componentName) {
|
|
|
463
463
|
const esbuild = __require("esbuild");
|
|
464
464
|
const mountCode = `
|
|
465
465
|
// --- Auto-generated mount ---
|
|
466
|
+
import { createElement as __h } from 'react';
|
|
466
467
|
import { createRoot } from 'react-dom/client';
|
|
467
468
|
import { McpBridgeProvider } from '@frontmcp/ui/react';
|
|
468
|
-
|
|
469
|
-
const __root = document.getElementById('root');
|
|
469
|
+
var __root = document.getElementById('root');
|
|
470
470
|
if (__root) {
|
|
471
|
-
createRoot(__root)
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
471
|
+
var __reactRoot = createRoot(__root);
|
|
472
|
+
function __hasData(v) { return v !== undefined; }
|
|
473
|
+
function __render(output) {
|
|
474
|
+
__reactRoot.render(
|
|
475
|
+
__h(McpBridgeProvider, null,
|
|
476
|
+
__h(${componentName}, { output: output !== undefined ? output : null, input: window.__mcpToolInput, loading: !__hasData(output) })
|
|
477
|
+
)
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
// Render immediately (component shows loading state until data arrives)
|
|
481
|
+
__render(undefined);
|
|
482
|
+
// 1. Try OpenAI SDK (toolOutput set synchronously or after load)
|
|
483
|
+
if (typeof window !== 'undefined') {
|
|
484
|
+
if (!window.openai) window.openai = {};
|
|
485
|
+
var __cur = window.openai.toolOutput;
|
|
486
|
+
if (__hasData(__cur)) { __render(__cur); }
|
|
487
|
+
Object.defineProperty(window.openai, 'toolOutput', {
|
|
488
|
+
get: function() { return __cur; },
|
|
489
|
+
set: function(v) { __cur = v; __render(v); },
|
|
490
|
+
configurable: true, enumerable: true
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
// 2. Try injected data globals
|
|
494
|
+
if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
|
|
495
|
+
// 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
|
|
496
|
+
var __bridge = window.FrontMcpBridge;
|
|
497
|
+
if (__bridge && typeof __bridge.onToolResult === 'function') {
|
|
498
|
+
__bridge.onToolResult(function(data) { __render(data); });
|
|
499
|
+
} else {
|
|
500
|
+
// 4. Fallback: listen for tool:result CustomEvent (standalone / MCP Inspector)
|
|
501
|
+
window.addEventListener('tool:result', function(e) {
|
|
502
|
+
var d = e.detail;
|
|
503
|
+
if (d) __render(d.structuredContent !== undefined ? d.structuredContent : d.content !== undefined ? d.content : d);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
476
506
|
}`;
|
|
477
507
|
const loader = {
|
|
478
508
|
".tsx": "tsx",
|
|
479
509
|
".jsx": "jsx"
|
|
480
510
|
};
|
|
481
511
|
const stdinLoader = filename.endsWith(".tsx") ? "tsx" : "jsx";
|
|
512
|
+
const alias = {};
|
|
513
|
+
try {
|
|
514
|
+
const nodePath = __require("path");
|
|
515
|
+
const nodeFs = __require("fs");
|
|
516
|
+
const candidates = [
|
|
517
|
+
nodePath.join(process.cwd(), "node_modules", "@frontmcp", "ui", "dist", "esm"),
|
|
518
|
+
nodePath.join(resolveDir, "node_modules", "@frontmcp", "ui", "dist", "esm")
|
|
519
|
+
];
|
|
520
|
+
for (const uiEsmBase of candidates) {
|
|
521
|
+
if (!nodeFs.existsSync(uiEsmBase)) continue;
|
|
522
|
+
const subpaths = ["components", "react", "theme", "bridge", "runtime"];
|
|
523
|
+
for (const sub of subpaths) {
|
|
524
|
+
const mjs = nodePath.join(uiEsmBase, sub, "index.mjs");
|
|
525
|
+
if (nodeFs.existsSync(mjs)) alias[`@frontmcp/ui/${sub}`] = mjs;
|
|
526
|
+
}
|
|
527
|
+
if (Object.keys(alias).length > 0) break;
|
|
528
|
+
}
|
|
529
|
+
} catch {
|
|
530
|
+
}
|
|
482
531
|
try {
|
|
483
532
|
const result = esbuild.buildSync({
|
|
484
533
|
stdin: {
|
|
@@ -491,10 +540,9 @@ if (__root) {
|
|
|
491
540
|
write: false,
|
|
492
541
|
format: "esm",
|
|
493
542
|
target: "es2020",
|
|
494
|
-
jsx: "
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
external: ["react", "react-dom"],
|
|
543
|
+
jsx: "automatic",
|
|
544
|
+
external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
|
|
545
|
+
alias,
|
|
498
546
|
define: { "process.env.NODE_ENV": '"production"' },
|
|
499
547
|
platform: "browser",
|
|
500
548
|
treeShaking: true,
|
|
@@ -718,6 +766,7 @@ function escapeAttribute(str) {
|
|
|
718
766
|
function buildDataInjectionScript(options) {
|
|
719
767
|
const { toolName, input, output, structuredContent } = options;
|
|
720
768
|
const lines = [
|
|
769
|
+
`window.__mcpAppsEnabled = true;`,
|
|
721
770
|
`window.__mcpToolName = ${safeJsonForScript(toolName)};`,
|
|
722
771
|
`window.__mcpToolInput = ${safeJsonForScript(input ?? null)};`,
|
|
723
772
|
`window.__mcpToolOutput = ${safeJsonForScript(output ?? null)};`,
|
|
@@ -782,6 +831,19 @@ function generateBridgeIIFE(options = {}) {
|
|
|
782
831
|
parts.push("});");
|
|
783
832
|
parts.push("");
|
|
784
833
|
parts.push("window.FrontMcpBridge = bridge;");
|
|
834
|
+
parts.push("function __showLoading() {");
|
|
835
|
+
parts.push(' var root = document.getElementById("root");');
|
|
836
|
+
parts.push(" if (root && !root.hasChildNodes()) {");
|
|
837
|
+
parts.push(
|
|
838
|
+
` root.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;padding:2rem;color:#6b7280;font-family:system-ui,sans-serif"><div style="text-align:center"><div style="width:24px;height:24px;border:2px solid #e5e7eb;border-top-color:#3b82f6;border-radius:50%;animation:__spin 0.6s linear infinite;margin:0 auto 12px"></div><div style="font-size:0.875rem">Loading widget...</div></div></div><style>@keyframes __spin{to{transform:rotate(360deg)}}</style>';`
|
|
839
|
+
);
|
|
840
|
+
parts.push(" }");
|
|
841
|
+
parts.push("}");
|
|
842
|
+
parts.push('if (document.readyState === "loading") {');
|
|
843
|
+
parts.push(' document.addEventListener("DOMContentLoaded", __showLoading);');
|
|
844
|
+
parts.push("} else {");
|
|
845
|
+
parts.push(" __showLoading();");
|
|
846
|
+
parts.push("}");
|
|
785
847
|
parts.push("})();");
|
|
786
848
|
const code = parts.join("\n");
|
|
787
849
|
if (minify) {
|
|
@@ -958,7 +1020,20 @@ var ExtAppsAdapter = {
|
|
|
958
1020
|
self.handleMessage(context, event);
|
|
959
1021
|
});
|
|
960
1022
|
|
|
961
|
-
|
|
1023
|
+
// Defer handshake until after the document is fully loaded.
|
|
1024
|
+
// During document.write() (used by the sandbox proxy), postMessage
|
|
1025
|
+
// from the inner iframe may not reach the parent. Waiting for DOMContentLoaded
|
|
1026
|
+
// or using setTimeout ensures the iframe is fully attached.
|
|
1027
|
+
return new Promise(function(resolve) {
|
|
1028
|
+
function doHandshake() {
|
|
1029
|
+
self.sendHandshake(context).then(resolve, resolve);
|
|
1030
|
+
}
|
|
1031
|
+
if (document.readyState === 'loading') {
|
|
1032
|
+
document.addEventListener('DOMContentLoaded', doHandshake);
|
|
1033
|
+
} else {
|
|
1034
|
+
setTimeout(doHandshake, 0);
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
962
1037
|
},
|
|
963
1038
|
handleMessage: function(context, event) {
|
|
964
1039
|
if (!this.isOriginTrusted(event.origin)) return;
|
|
@@ -1052,32 +1127,67 @@ var ExtAppsAdapter = {
|
|
|
1052
1127
|
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
|
|
1053
1128
|
});
|
|
1054
1129
|
},
|
|
1055
|
-
|
|
1130
|
+
sendHandshake: function(context) {
|
|
1131
|
+
// Send ui/initialize using '*' as target origin since TOFU hasn't
|
|
1132
|
+
// been established yet. The response from the host will establish
|
|
1133
|
+
// TOFU trust via handleMessage \u2192 isOriginTrusted.
|
|
1056
1134
|
var self = this;
|
|
1135
|
+
var id = ++this.requestId;
|
|
1057
1136
|
var params = {
|
|
1058
1137
|
appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
|
|
1059
1138
|
appCapabilities: { tools: { listChanged: false } },
|
|
1060
1139
|
protocolVersion: '2024-11-05'
|
|
1061
1140
|
};
|
|
1062
1141
|
|
|
1063
|
-
return
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1142
|
+
return new Promise(function(resolve, reject) {
|
|
1143
|
+
var timeout = setTimeout(function() {
|
|
1144
|
+
delete self.pendingRequests[id];
|
|
1145
|
+
// Handshake timeout is non-fatal \u2014 notifications may still arrive
|
|
1146
|
+
resolve();
|
|
1147
|
+
}, 10000);
|
|
1148
|
+
|
|
1149
|
+
self.pendingRequests[id] = {
|
|
1150
|
+
resolve: function(result) {
|
|
1151
|
+
self.hostCapabilities = result.hostCapabilities || {};
|
|
1152
|
+
self.capabilities = Object.assign({}, self.capabilities, {
|
|
1153
|
+
canCallTools: Boolean(self.hostCapabilities.serverTools || self.hostCapabilities.serverToolProxy),
|
|
1154
|
+
canSendMessages: true,
|
|
1155
|
+
canOpenLinks: Boolean(self.hostCapabilities.openLinks || self.hostCapabilities.openLink),
|
|
1156
|
+
supportsDisplayModes: true
|
|
1157
|
+
});
|
|
1158
|
+
if (result.hostContext) {
|
|
1159
|
+
Object.assign(context.hostContext, result.hostContext);
|
|
1160
|
+
}
|
|
1161
|
+
// Send ui/notifications/initialized to tell the host the view is ready.
|
|
1162
|
+
// Per MCP Apps spec, the host waits for this before sending tool-result.
|
|
1163
|
+
var targetOrigin = self.trustedOrigin || '*';
|
|
1164
|
+
window.parent.postMessage({
|
|
1165
|
+
jsonrpc: '2.0',
|
|
1166
|
+
method: 'ui/notifications/initialized',
|
|
1167
|
+
params: {}
|
|
1168
|
+
}, targetOrigin);
|
|
1169
|
+
resolve();
|
|
1170
|
+
},
|
|
1171
|
+
reject: function(err) { resolve(); }, // Non-fatal
|
|
1172
|
+
timeout: timeout
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
// Use '*' for the initial handshake \u2014 we don't know the host origin yet.
|
|
1176
|
+
// The sandbox proxy will relay this to the host.
|
|
1177
|
+
window.parent.postMessage({
|
|
1178
|
+
jsonrpc: '2.0', id: id, method: 'ui/initialize', params: params
|
|
1179
|
+
}, '*');
|
|
1074
1180
|
});
|
|
1075
1181
|
},
|
|
1182
|
+
performHandshake: function(context) {
|
|
1183
|
+
return this.sendHandshake(context);
|
|
1184
|
+
},
|
|
1076
1185
|
callTool: function(context, name, args) {
|
|
1077
|
-
if (!this.hostCapabilities.serverToolProxy) {
|
|
1186
|
+
if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
|
|
1078
1187
|
return Promise.reject(new Error('Server tool proxy not supported'));
|
|
1079
1188
|
}
|
|
1080
|
-
|
|
1189
|
+
// Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
|
|
1190
|
+
return this.sendRequest('tools/call', { name: name, arguments: args || {} });
|
|
1081
1191
|
},
|
|
1082
1192
|
sendMessage: function(context, content) {
|
|
1083
1193
|
return this.sendRequest('ui/message', { content: content });
|
package/esm/index.mjs
CHANGED
|
@@ -181,6 +181,19 @@ function generateBridgeIIFE(options = {}) {
|
|
|
181
181
|
parts.push("});");
|
|
182
182
|
parts.push("");
|
|
183
183
|
parts.push("window.FrontMcpBridge = bridge;");
|
|
184
|
+
parts.push("function __showLoading() {");
|
|
185
|
+
parts.push(' var root = document.getElementById("root");');
|
|
186
|
+
parts.push(" if (root && !root.hasChildNodes()) {");
|
|
187
|
+
parts.push(
|
|
188
|
+
` root.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;padding:2rem;color:#6b7280;font-family:system-ui,sans-serif"><div style="text-align:center"><div style="width:24px;height:24px;border:2px solid #e5e7eb;border-top-color:#3b82f6;border-radius:50%;animation:__spin 0.6s linear infinite;margin:0 auto 12px"></div><div style="font-size:0.875rem">Loading widget...</div></div></div><style>@keyframes __spin{to{transform:rotate(360deg)}}</style>';`
|
|
189
|
+
);
|
|
190
|
+
parts.push(" }");
|
|
191
|
+
parts.push("}");
|
|
192
|
+
parts.push('if (document.readyState === "loading") {');
|
|
193
|
+
parts.push(' document.addEventListener("DOMContentLoaded", __showLoading);');
|
|
194
|
+
parts.push("} else {");
|
|
195
|
+
parts.push(" __showLoading();");
|
|
196
|
+
parts.push("}");
|
|
184
197
|
parts.push("})();");
|
|
185
198
|
const code = parts.join("\n");
|
|
186
199
|
if (minify) {
|
|
@@ -357,7 +370,20 @@ var ExtAppsAdapter = {
|
|
|
357
370
|
self.handleMessage(context, event);
|
|
358
371
|
});
|
|
359
372
|
|
|
360
|
-
|
|
373
|
+
// Defer handshake until after the document is fully loaded.
|
|
374
|
+
// During document.write() (used by the sandbox proxy), postMessage
|
|
375
|
+
// from the inner iframe may not reach the parent. Waiting for DOMContentLoaded
|
|
376
|
+
// or using setTimeout ensures the iframe is fully attached.
|
|
377
|
+
return new Promise(function(resolve) {
|
|
378
|
+
function doHandshake() {
|
|
379
|
+
self.sendHandshake(context).then(resolve, resolve);
|
|
380
|
+
}
|
|
381
|
+
if (document.readyState === 'loading') {
|
|
382
|
+
document.addEventListener('DOMContentLoaded', doHandshake);
|
|
383
|
+
} else {
|
|
384
|
+
setTimeout(doHandshake, 0);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
361
387
|
},
|
|
362
388
|
handleMessage: function(context, event) {
|
|
363
389
|
if (!this.isOriginTrusted(event.origin)) return;
|
|
@@ -451,32 +477,67 @@ var ExtAppsAdapter = {
|
|
|
451
477
|
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
|
|
452
478
|
});
|
|
453
479
|
},
|
|
454
|
-
|
|
480
|
+
sendHandshake: function(context) {
|
|
481
|
+
// Send ui/initialize using '*' as target origin since TOFU hasn't
|
|
482
|
+
// been established yet. The response from the host will establish
|
|
483
|
+
// TOFU trust via handleMessage \u2192 isOriginTrusted.
|
|
455
484
|
var self = this;
|
|
485
|
+
var id = ++this.requestId;
|
|
456
486
|
var params = {
|
|
457
487
|
appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
|
|
458
488
|
appCapabilities: { tools: { listChanged: false } },
|
|
459
489
|
protocolVersion: '2024-11-05'
|
|
460
490
|
};
|
|
461
491
|
|
|
462
|
-
return
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
492
|
+
return new Promise(function(resolve, reject) {
|
|
493
|
+
var timeout = setTimeout(function() {
|
|
494
|
+
delete self.pendingRequests[id];
|
|
495
|
+
// Handshake timeout is non-fatal \u2014 notifications may still arrive
|
|
496
|
+
resolve();
|
|
497
|
+
}, 10000);
|
|
498
|
+
|
|
499
|
+
self.pendingRequests[id] = {
|
|
500
|
+
resolve: function(result) {
|
|
501
|
+
self.hostCapabilities = result.hostCapabilities || {};
|
|
502
|
+
self.capabilities = Object.assign({}, self.capabilities, {
|
|
503
|
+
canCallTools: Boolean(self.hostCapabilities.serverTools || self.hostCapabilities.serverToolProxy),
|
|
504
|
+
canSendMessages: true,
|
|
505
|
+
canOpenLinks: Boolean(self.hostCapabilities.openLinks || self.hostCapabilities.openLink),
|
|
506
|
+
supportsDisplayModes: true
|
|
507
|
+
});
|
|
508
|
+
if (result.hostContext) {
|
|
509
|
+
Object.assign(context.hostContext, result.hostContext);
|
|
510
|
+
}
|
|
511
|
+
// Send ui/notifications/initialized to tell the host the view is ready.
|
|
512
|
+
// Per MCP Apps spec, the host waits for this before sending tool-result.
|
|
513
|
+
var targetOrigin = self.trustedOrigin || '*';
|
|
514
|
+
window.parent.postMessage({
|
|
515
|
+
jsonrpc: '2.0',
|
|
516
|
+
method: 'ui/notifications/initialized',
|
|
517
|
+
params: {}
|
|
518
|
+
}, targetOrigin);
|
|
519
|
+
resolve();
|
|
520
|
+
},
|
|
521
|
+
reject: function(err) { resolve(); }, // Non-fatal
|
|
522
|
+
timeout: timeout
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// Use '*' for the initial handshake \u2014 we don't know the host origin yet.
|
|
526
|
+
// The sandbox proxy will relay this to the host.
|
|
527
|
+
window.parent.postMessage({
|
|
528
|
+
jsonrpc: '2.0', id: id, method: 'ui/initialize', params: params
|
|
529
|
+
}, '*');
|
|
473
530
|
});
|
|
474
531
|
},
|
|
532
|
+
performHandshake: function(context) {
|
|
533
|
+
return this.sendHandshake(context);
|
|
534
|
+
},
|
|
475
535
|
callTool: function(context, name, args) {
|
|
476
|
-
if (!this.hostCapabilities.serverToolProxy) {
|
|
536
|
+
if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
|
|
477
537
|
return Promise.reject(new Error('Server tool proxy not supported'));
|
|
478
538
|
}
|
|
479
|
-
|
|
539
|
+
// Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
|
|
540
|
+
return this.sendRequest('tools/call', { name: name, arguments: args || {} });
|
|
480
541
|
},
|
|
481
542
|
sendMessage: function(context, content) {
|
|
482
543
|
return this.sendRequest('ui/message', { content: content });
|
|
@@ -2657,6 +2718,7 @@ function escapeAttribute(str) {
|
|
|
2657
2718
|
function buildDataInjectionScript(options) {
|
|
2658
2719
|
const { toolName, input, output, structuredContent } = options;
|
|
2659
2720
|
const lines = [
|
|
2721
|
+
`window.__mcpAppsEnabled = true;`,
|
|
2660
2722
|
`window.__mcpToolName = ${safeJsonForScript(toolName)};`,
|
|
2661
2723
|
`window.__mcpToolInput = ${safeJsonForScript(input ?? null)};`,
|
|
2662
2724
|
`window.__mcpToolOutput = ${safeJsonForScript(output ?? null)};`,
|
|
@@ -2987,22 +3049,71 @@ function bundleFileSource(source, filename, resolveDir, componentName) {
|
|
|
2987
3049
|
const esbuild = __require("esbuild");
|
|
2988
3050
|
const mountCode = `
|
|
2989
3051
|
// --- Auto-generated mount ---
|
|
3052
|
+
import { createElement as __h } from 'react';
|
|
2990
3053
|
import { createRoot } from 'react-dom/client';
|
|
2991
3054
|
import { McpBridgeProvider } from '@frontmcp/ui/react';
|
|
2992
|
-
|
|
2993
|
-
const __root = document.getElementById('root');
|
|
3055
|
+
var __root = document.getElementById('root');
|
|
2994
3056
|
if (__root) {
|
|
2995
|
-
createRoot(__root)
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3057
|
+
var __reactRoot = createRoot(__root);
|
|
3058
|
+
function __hasData(v) { return v !== undefined; }
|
|
3059
|
+
function __render(output) {
|
|
3060
|
+
__reactRoot.render(
|
|
3061
|
+
__h(McpBridgeProvider, null,
|
|
3062
|
+
__h(${componentName}, { output: output !== undefined ? output : null, input: window.__mcpToolInput, loading: !__hasData(output) })
|
|
3063
|
+
)
|
|
3064
|
+
);
|
|
3065
|
+
}
|
|
3066
|
+
// Render immediately (component shows loading state until data arrives)
|
|
3067
|
+
__render(undefined);
|
|
3068
|
+
// 1. Try OpenAI SDK (toolOutput set synchronously or after load)
|
|
3069
|
+
if (typeof window !== 'undefined') {
|
|
3070
|
+
if (!window.openai) window.openai = {};
|
|
3071
|
+
var __cur = window.openai.toolOutput;
|
|
3072
|
+
if (__hasData(__cur)) { __render(__cur); }
|
|
3073
|
+
Object.defineProperty(window.openai, 'toolOutput', {
|
|
3074
|
+
get: function() { return __cur; },
|
|
3075
|
+
set: function(v) { __cur = v; __render(v); },
|
|
3076
|
+
configurable: true, enumerable: true
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
// 2. Try injected data globals
|
|
3080
|
+
if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
|
|
3081
|
+
// 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
|
|
3082
|
+
var __bridge = window.FrontMcpBridge;
|
|
3083
|
+
if (__bridge && typeof __bridge.onToolResult === 'function') {
|
|
3084
|
+
__bridge.onToolResult(function(data) { __render(data); });
|
|
3085
|
+
} else {
|
|
3086
|
+
// 4. Fallback: listen for tool:result CustomEvent (standalone / MCP Inspector)
|
|
3087
|
+
window.addEventListener('tool:result', function(e) {
|
|
3088
|
+
var d = e.detail;
|
|
3089
|
+
if (d) __render(d.structuredContent !== undefined ? d.structuredContent : d.content !== undefined ? d.content : d);
|
|
3090
|
+
});
|
|
3091
|
+
}
|
|
3000
3092
|
}`;
|
|
3001
3093
|
const loader = {
|
|
3002
3094
|
".tsx": "tsx",
|
|
3003
3095
|
".jsx": "jsx"
|
|
3004
3096
|
};
|
|
3005
3097
|
const stdinLoader = filename.endsWith(".tsx") ? "tsx" : "jsx";
|
|
3098
|
+
const alias = {};
|
|
3099
|
+
try {
|
|
3100
|
+
const nodePath = __require("path");
|
|
3101
|
+
const nodeFs = __require("fs");
|
|
3102
|
+
const candidates = [
|
|
3103
|
+
nodePath.join(process.cwd(), "node_modules", "@frontmcp", "ui", "dist", "esm"),
|
|
3104
|
+
nodePath.join(resolveDir, "node_modules", "@frontmcp", "ui", "dist", "esm")
|
|
3105
|
+
];
|
|
3106
|
+
for (const uiEsmBase of candidates) {
|
|
3107
|
+
if (!nodeFs.existsSync(uiEsmBase)) continue;
|
|
3108
|
+
const subpaths = ["components", "react", "theme", "bridge", "runtime"];
|
|
3109
|
+
for (const sub of subpaths) {
|
|
3110
|
+
const mjs = nodePath.join(uiEsmBase, sub, "index.mjs");
|
|
3111
|
+
if (nodeFs.existsSync(mjs)) alias[`@frontmcp/ui/${sub}`] = mjs;
|
|
3112
|
+
}
|
|
3113
|
+
if (Object.keys(alias).length > 0) break;
|
|
3114
|
+
}
|
|
3115
|
+
} catch {
|
|
3116
|
+
}
|
|
3006
3117
|
try {
|
|
3007
3118
|
const result = esbuild.buildSync({
|
|
3008
3119
|
stdin: {
|
|
@@ -3015,10 +3126,9 @@ if (__root) {
|
|
|
3015
3126
|
write: false,
|
|
3016
3127
|
format: "esm",
|
|
3017
3128
|
target: "es2020",
|
|
3018
|
-
jsx: "
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
external: ["react", "react-dom"],
|
|
3129
|
+
jsx: "automatic",
|
|
3130
|
+
external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
|
|
3131
|
+
alias,
|
|
3022
3132
|
define: { "process.env.NODE_ENV": '"production"' },
|
|
3023
3133
|
platform: "browser",
|
|
3024
3134
|
treeShaking: true,
|
|
@@ -3541,8 +3651,26 @@ function wrapDetectedContent(value) {
|
|
|
3541
3651
|
}
|
|
3542
3652
|
|
|
3543
3653
|
// libs/uipack/src/adapters/template-renderer.ts
|
|
3654
|
+
function buildCspConfig(resolver) {
|
|
3655
|
+
const cspResourceDomains = ["https://esm.sh"];
|
|
3656
|
+
const cspConnectDomains = ["https://esm.sh"];
|
|
3657
|
+
if (resolver && "overrides" in resolver) {
|
|
3658
|
+
const overrides = resolver.overrides;
|
|
3659
|
+
if (overrides) {
|
|
3660
|
+
for (const url of Object.values(overrides)) {
|
|
3661
|
+
try {
|
|
3662
|
+
const origin = new URL(url).origin;
|
|
3663
|
+
if (!cspResourceDomains.includes(origin)) cspResourceDomains.push(origin);
|
|
3664
|
+
if (!cspConnectDomains.includes(origin)) cspConnectDomains.push(origin);
|
|
3665
|
+
} catch {
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
return { resourceDomains: cspResourceDomains, connectDomains: cspConnectDomains };
|
|
3671
|
+
}
|
|
3544
3672
|
function renderToolTemplate(options) {
|
|
3545
|
-
const { toolName, input, output, template,
|
|
3673
|
+
const { toolName, input, output, template, resolver } = options;
|
|
3546
3674
|
const uiType = detectUIType(template);
|
|
3547
3675
|
const shellConfig = {
|
|
3548
3676
|
toolName,
|
|
@@ -3555,25 +3683,7 @@ function renderToolTemplate(options) {
|
|
|
3555
3683
|
let hash = "";
|
|
3556
3684
|
let size = 0;
|
|
3557
3685
|
if (typeof template === "object" && template !== null && "file" in template) {
|
|
3558
|
-
const
|
|
3559
|
-
const cspConnectDomains = ["https://esm.sh"];
|
|
3560
|
-
if (resolver && "overrides" in resolver) {
|
|
3561
|
-
const overrides = resolver.overrides;
|
|
3562
|
-
if (overrides) {
|
|
3563
|
-
for (const url of Object.values(overrides)) {
|
|
3564
|
-
try {
|
|
3565
|
-
const origin = new URL(url).origin;
|
|
3566
|
-
if (!cspResourceDomains.includes(origin)) cspResourceDomains.push(origin);
|
|
3567
|
-
if (!cspConnectDomains.includes(origin)) cspConnectDomains.push(origin);
|
|
3568
|
-
} catch {
|
|
3569
|
-
}
|
|
3570
|
-
}
|
|
3571
|
-
}
|
|
3572
|
-
}
|
|
3573
|
-
const cspConfig = {
|
|
3574
|
-
resourceDomains: cspResourceDomains,
|
|
3575
|
-
connectDomains: cspConnectDomains
|
|
3576
|
-
};
|
|
3686
|
+
const cspConfig = buildCspConfig(resolver);
|
|
3577
3687
|
const result = renderComponent({ source: template }, { ...shellConfig, csp: cspConfig });
|
|
3578
3688
|
html = result.html;
|
|
3579
3689
|
hash = result.hash;
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/uipack",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "FrontMCP UIpack - HTML shell builder, pluggable import resolver, and NPM component loader for MCP UI (React-free core)",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"homepage": "https://docs.agentfront.dev",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"./esm": null
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
|
55
|
-
"node": ">=
|
|
55
|
+
"node": ">=24.0.0"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"zod": "^4.0.0"
|
package/esm/shell/index.mjs
CHANGED
|
@@ -116,6 +116,7 @@ function safeJsonForScript(value) {
|
|
|
116
116
|
function buildDataInjectionScript(options) {
|
|
117
117
|
const { toolName, input, output, structuredContent } = options;
|
|
118
118
|
const lines = [
|
|
119
|
+
`window.__mcpAppsEnabled = true;`,
|
|
119
120
|
`window.__mcpToolName = ${safeJsonForScript(toolName)};`,
|
|
120
121
|
`window.__mcpToolInput = ${safeJsonForScript(input ?? null)};`,
|
|
121
122
|
`window.__mcpToolOutput = ${safeJsonForScript(output ?? null)};`,
|
|
@@ -206,6 +207,19 @@ function generateBridgeIIFE(options = {}) {
|
|
|
206
207
|
parts.push("});");
|
|
207
208
|
parts.push("");
|
|
208
209
|
parts.push("window.FrontMcpBridge = bridge;");
|
|
210
|
+
parts.push("function __showLoading() {");
|
|
211
|
+
parts.push(' var root = document.getElementById("root");');
|
|
212
|
+
parts.push(" if (root && !root.hasChildNodes()) {");
|
|
213
|
+
parts.push(
|
|
214
|
+
` root.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;padding:2rem;color:#6b7280;font-family:system-ui,sans-serif"><div style="text-align:center"><div style="width:24px;height:24px;border:2px solid #e5e7eb;border-top-color:#3b82f6;border-radius:50%;animation:__spin 0.6s linear infinite;margin:0 auto 12px"></div><div style="font-size:0.875rem">Loading widget...</div></div></div><style>@keyframes __spin{to{transform:rotate(360deg)}}</style>';`
|
|
215
|
+
);
|
|
216
|
+
parts.push(" }");
|
|
217
|
+
parts.push("}");
|
|
218
|
+
parts.push('if (document.readyState === "loading") {');
|
|
219
|
+
parts.push(' document.addEventListener("DOMContentLoaded", __showLoading);');
|
|
220
|
+
parts.push("} else {");
|
|
221
|
+
parts.push(" __showLoading();");
|
|
222
|
+
parts.push("}");
|
|
209
223
|
parts.push("})();");
|
|
210
224
|
const code = parts.join("\n");
|
|
211
225
|
if (minify) {
|
|
@@ -382,7 +396,20 @@ var ExtAppsAdapter = {
|
|
|
382
396
|
self.handleMessage(context, event);
|
|
383
397
|
});
|
|
384
398
|
|
|
385
|
-
|
|
399
|
+
// Defer handshake until after the document is fully loaded.
|
|
400
|
+
// During document.write() (used by the sandbox proxy), postMessage
|
|
401
|
+
// from the inner iframe may not reach the parent. Waiting for DOMContentLoaded
|
|
402
|
+
// or using setTimeout ensures the iframe is fully attached.
|
|
403
|
+
return new Promise(function(resolve) {
|
|
404
|
+
function doHandshake() {
|
|
405
|
+
self.sendHandshake(context).then(resolve, resolve);
|
|
406
|
+
}
|
|
407
|
+
if (document.readyState === 'loading') {
|
|
408
|
+
document.addEventListener('DOMContentLoaded', doHandshake);
|
|
409
|
+
} else {
|
|
410
|
+
setTimeout(doHandshake, 0);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
386
413
|
},
|
|
387
414
|
handleMessage: function(context, event) {
|
|
388
415
|
if (!this.isOriginTrusted(event.origin)) return;
|
|
@@ -476,32 +503,67 @@ var ExtAppsAdapter = {
|
|
|
476
503
|
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
|
|
477
504
|
});
|
|
478
505
|
},
|
|
479
|
-
|
|
506
|
+
sendHandshake: function(context) {
|
|
507
|
+
// Send ui/initialize using '*' as target origin since TOFU hasn't
|
|
508
|
+
// been established yet. The response from the host will establish
|
|
509
|
+
// TOFU trust via handleMessage \u2192 isOriginTrusted.
|
|
480
510
|
var self = this;
|
|
511
|
+
var id = ++this.requestId;
|
|
481
512
|
var params = {
|
|
482
513
|
appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
|
|
483
514
|
appCapabilities: { tools: { listChanged: false } },
|
|
484
515
|
protocolVersion: '2024-11-05'
|
|
485
516
|
};
|
|
486
517
|
|
|
487
|
-
return
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
518
|
+
return new Promise(function(resolve, reject) {
|
|
519
|
+
var timeout = setTimeout(function() {
|
|
520
|
+
delete self.pendingRequests[id];
|
|
521
|
+
// Handshake timeout is non-fatal \u2014 notifications may still arrive
|
|
522
|
+
resolve();
|
|
523
|
+
}, 10000);
|
|
524
|
+
|
|
525
|
+
self.pendingRequests[id] = {
|
|
526
|
+
resolve: function(result) {
|
|
527
|
+
self.hostCapabilities = result.hostCapabilities || {};
|
|
528
|
+
self.capabilities = Object.assign({}, self.capabilities, {
|
|
529
|
+
canCallTools: Boolean(self.hostCapabilities.serverTools || self.hostCapabilities.serverToolProxy),
|
|
530
|
+
canSendMessages: true,
|
|
531
|
+
canOpenLinks: Boolean(self.hostCapabilities.openLinks || self.hostCapabilities.openLink),
|
|
532
|
+
supportsDisplayModes: true
|
|
533
|
+
});
|
|
534
|
+
if (result.hostContext) {
|
|
535
|
+
Object.assign(context.hostContext, result.hostContext);
|
|
536
|
+
}
|
|
537
|
+
// Send ui/notifications/initialized to tell the host the view is ready.
|
|
538
|
+
// Per MCP Apps spec, the host waits for this before sending tool-result.
|
|
539
|
+
var targetOrigin = self.trustedOrigin || '*';
|
|
540
|
+
window.parent.postMessage({
|
|
541
|
+
jsonrpc: '2.0',
|
|
542
|
+
method: 'ui/notifications/initialized',
|
|
543
|
+
params: {}
|
|
544
|
+
}, targetOrigin);
|
|
545
|
+
resolve();
|
|
546
|
+
},
|
|
547
|
+
reject: function(err) { resolve(); }, // Non-fatal
|
|
548
|
+
timeout: timeout
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// Use '*' for the initial handshake \u2014 we don't know the host origin yet.
|
|
552
|
+
// The sandbox proxy will relay this to the host.
|
|
553
|
+
window.parent.postMessage({
|
|
554
|
+
jsonrpc: '2.0', id: id, method: 'ui/initialize', params: params
|
|
555
|
+
}, '*');
|
|
498
556
|
});
|
|
499
557
|
},
|
|
558
|
+
performHandshake: function(context) {
|
|
559
|
+
return this.sendHandshake(context);
|
|
560
|
+
},
|
|
500
561
|
callTool: function(context, name, args) {
|
|
501
|
-
if (!this.hostCapabilities.serverToolProxy) {
|
|
562
|
+
if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
|
|
502
563
|
return Promise.reject(new Error('Server tool proxy not supported'));
|
|
503
564
|
}
|
|
504
|
-
|
|
565
|
+
// Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
|
|
566
|
+
return this.sendRequest('tools/call', { name: name, arguments: args || {} });
|
|
505
567
|
},
|
|
506
568
|
sendMessage: function(context, content) {
|
|
507
569
|
return this.sendRequest('ui/message', { content: content });
|