@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/component/index.js
CHANGED
|
@@ -492,22 +492,71 @@ function bundleFileSource(source, filename, resolveDir, componentName) {
|
|
|
492
492
|
const esbuild = require("esbuild");
|
|
493
493
|
const mountCode = `
|
|
494
494
|
// --- Auto-generated mount ---
|
|
495
|
+
import { createElement as __h } from 'react';
|
|
495
496
|
import { createRoot } from 'react-dom/client';
|
|
496
497
|
import { McpBridgeProvider } from '@frontmcp/ui/react';
|
|
497
|
-
|
|
498
|
-
const __root = document.getElementById('root');
|
|
498
|
+
var __root = document.getElementById('root');
|
|
499
499
|
if (__root) {
|
|
500
|
-
createRoot(__root)
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
500
|
+
var __reactRoot = createRoot(__root);
|
|
501
|
+
function __hasData(v) { return v !== undefined; }
|
|
502
|
+
function __render(output) {
|
|
503
|
+
__reactRoot.render(
|
|
504
|
+
__h(McpBridgeProvider, null,
|
|
505
|
+
__h(${componentName}, { output: output !== undefined ? output : null, input: window.__mcpToolInput, loading: !__hasData(output) })
|
|
506
|
+
)
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
// Render immediately (component shows loading state until data arrives)
|
|
510
|
+
__render(undefined);
|
|
511
|
+
// 1. Try OpenAI SDK (toolOutput set synchronously or after load)
|
|
512
|
+
if (typeof window !== 'undefined') {
|
|
513
|
+
if (!window.openai) window.openai = {};
|
|
514
|
+
var __cur = window.openai.toolOutput;
|
|
515
|
+
if (__hasData(__cur)) { __render(__cur); }
|
|
516
|
+
Object.defineProperty(window.openai, 'toolOutput', {
|
|
517
|
+
get: function() { return __cur; },
|
|
518
|
+
set: function(v) { __cur = v; __render(v); },
|
|
519
|
+
configurable: true, enumerable: true
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
// 2. Try injected data globals
|
|
523
|
+
if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
|
|
524
|
+
// 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
|
|
525
|
+
var __bridge = window.FrontMcpBridge;
|
|
526
|
+
if (__bridge && typeof __bridge.onToolResult === 'function') {
|
|
527
|
+
__bridge.onToolResult(function(data) { __render(data); });
|
|
528
|
+
} else {
|
|
529
|
+
// 4. Fallback: listen for tool:result CustomEvent (standalone / MCP Inspector)
|
|
530
|
+
window.addEventListener('tool:result', function(e) {
|
|
531
|
+
var d = e.detail;
|
|
532
|
+
if (d) __render(d.structuredContent !== undefined ? d.structuredContent : d.content !== undefined ? d.content : d);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
505
535
|
}`;
|
|
506
536
|
const loader = {
|
|
507
537
|
".tsx": "tsx",
|
|
508
538
|
".jsx": "jsx"
|
|
509
539
|
};
|
|
510
540
|
const stdinLoader = filename.endsWith(".tsx") ? "tsx" : "jsx";
|
|
541
|
+
const alias = {};
|
|
542
|
+
try {
|
|
543
|
+
const nodePath = require("path");
|
|
544
|
+
const nodeFs = require("fs");
|
|
545
|
+
const candidates = [
|
|
546
|
+
nodePath.join(process.cwd(), "node_modules", "@frontmcp", "ui", "dist", "esm"),
|
|
547
|
+
nodePath.join(resolveDir, "node_modules", "@frontmcp", "ui", "dist", "esm")
|
|
548
|
+
];
|
|
549
|
+
for (const uiEsmBase of candidates) {
|
|
550
|
+
if (!nodeFs.existsSync(uiEsmBase)) continue;
|
|
551
|
+
const subpaths = ["components", "react", "theme", "bridge", "runtime"];
|
|
552
|
+
for (const sub of subpaths) {
|
|
553
|
+
const mjs = nodePath.join(uiEsmBase, sub, "index.mjs");
|
|
554
|
+
if (nodeFs.existsSync(mjs)) alias[`@frontmcp/ui/${sub}`] = mjs;
|
|
555
|
+
}
|
|
556
|
+
if (Object.keys(alias).length > 0) break;
|
|
557
|
+
}
|
|
558
|
+
} catch {
|
|
559
|
+
}
|
|
511
560
|
try {
|
|
512
561
|
const result = esbuild.buildSync({
|
|
513
562
|
stdin: {
|
|
@@ -520,10 +569,9 @@ if (__root) {
|
|
|
520
569
|
write: false,
|
|
521
570
|
format: "esm",
|
|
522
571
|
target: "es2020",
|
|
523
|
-
jsx: "
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
external: ["react", "react-dom"],
|
|
572
|
+
jsx: "automatic",
|
|
573
|
+
external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
|
|
574
|
+
alias,
|
|
527
575
|
define: { "process.env.NODE_ENV": '"production"' },
|
|
528
576
|
platform: "browser",
|
|
529
577
|
treeShaking: true,
|
|
@@ -747,6 +795,7 @@ function escapeAttribute(str) {
|
|
|
747
795
|
function buildDataInjectionScript(options) {
|
|
748
796
|
const { toolName, input, output, structuredContent } = options;
|
|
749
797
|
const lines = [
|
|
798
|
+
`window.__mcpAppsEnabled = true;`,
|
|
750
799
|
`window.__mcpToolName = ${safeJsonForScript(toolName)};`,
|
|
751
800
|
`window.__mcpToolInput = ${safeJsonForScript(input ?? null)};`,
|
|
752
801
|
`window.__mcpToolOutput = ${safeJsonForScript(output ?? null)};`,
|
|
@@ -811,6 +860,19 @@ function generateBridgeIIFE(options = {}) {
|
|
|
811
860
|
parts.push("});");
|
|
812
861
|
parts.push("");
|
|
813
862
|
parts.push("window.FrontMcpBridge = bridge;");
|
|
863
|
+
parts.push("function __showLoading() {");
|
|
864
|
+
parts.push(' var root = document.getElementById("root");');
|
|
865
|
+
parts.push(" if (root && !root.hasChildNodes()) {");
|
|
866
|
+
parts.push(
|
|
867
|
+
` 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>';`
|
|
868
|
+
);
|
|
869
|
+
parts.push(" }");
|
|
870
|
+
parts.push("}");
|
|
871
|
+
parts.push('if (document.readyState === "loading") {');
|
|
872
|
+
parts.push(' document.addEventListener("DOMContentLoaded", __showLoading);');
|
|
873
|
+
parts.push("} else {");
|
|
874
|
+
parts.push(" __showLoading();");
|
|
875
|
+
parts.push("}");
|
|
814
876
|
parts.push("})();");
|
|
815
877
|
const code = parts.join("\n");
|
|
816
878
|
if (minify) {
|
|
@@ -987,7 +1049,20 @@ var ExtAppsAdapter = {
|
|
|
987
1049
|
self.handleMessage(context, event);
|
|
988
1050
|
});
|
|
989
1051
|
|
|
990
|
-
|
|
1052
|
+
// Defer handshake until after the document is fully loaded.
|
|
1053
|
+
// During document.write() (used by the sandbox proxy), postMessage
|
|
1054
|
+
// from the inner iframe may not reach the parent. Waiting for DOMContentLoaded
|
|
1055
|
+
// or using setTimeout ensures the iframe is fully attached.
|
|
1056
|
+
return new Promise(function(resolve) {
|
|
1057
|
+
function doHandshake() {
|
|
1058
|
+
self.sendHandshake(context).then(resolve, resolve);
|
|
1059
|
+
}
|
|
1060
|
+
if (document.readyState === 'loading') {
|
|
1061
|
+
document.addEventListener('DOMContentLoaded', doHandshake);
|
|
1062
|
+
} else {
|
|
1063
|
+
setTimeout(doHandshake, 0);
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
991
1066
|
},
|
|
992
1067
|
handleMessage: function(context, event) {
|
|
993
1068
|
if (!this.isOriginTrusted(event.origin)) return;
|
|
@@ -1081,32 +1156,67 @@ var ExtAppsAdapter = {
|
|
|
1081
1156
|
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
|
|
1082
1157
|
});
|
|
1083
1158
|
},
|
|
1084
|
-
|
|
1159
|
+
sendHandshake: function(context) {
|
|
1160
|
+
// Send ui/initialize using '*' as target origin since TOFU hasn't
|
|
1161
|
+
// been established yet. The response from the host will establish
|
|
1162
|
+
// TOFU trust via handleMessage \u2192 isOriginTrusted.
|
|
1085
1163
|
var self = this;
|
|
1164
|
+
var id = ++this.requestId;
|
|
1086
1165
|
var params = {
|
|
1087
1166
|
appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
|
|
1088
1167
|
appCapabilities: { tools: { listChanged: false } },
|
|
1089
1168
|
protocolVersion: '2024-11-05'
|
|
1090
1169
|
};
|
|
1091
1170
|
|
|
1092
|
-
return
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1171
|
+
return new Promise(function(resolve, reject) {
|
|
1172
|
+
var timeout = setTimeout(function() {
|
|
1173
|
+
delete self.pendingRequests[id];
|
|
1174
|
+
// Handshake timeout is non-fatal \u2014 notifications may still arrive
|
|
1175
|
+
resolve();
|
|
1176
|
+
}, 10000);
|
|
1177
|
+
|
|
1178
|
+
self.pendingRequests[id] = {
|
|
1179
|
+
resolve: function(result) {
|
|
1180
|
+
self.hostCapabilities = result.hostCapabilities || {};
|
|
1181
|
+
self.capabilities = Object.assign({}, self.capabilities, {
|
|
1182
|
+
canCallTools: Boolean(self.hostCapabilities.serverTools || self.hostCapabilities.serverToolProxy),
|
|
1183
|
+
canSendMessages: true,
|
|
1184
|
+
canOpenLinks: Boolean(self.hostCapabilities.openLinks || self.hostCapabilities.openLink),
|
|
1185
|
+
supportsDisplayModes: true
|
|
1186
|
+
});
|
|
1187
|
+
if (result.hostContext) {
|
|
1188
|
+
Object.assign(context.hostContext, result.hostContext);
|
|
1189
|
+
}
|
|
1190
|
+
// Send ui/notifications/initialized to tell the host the view is ready.
|
|
1191
|
+
// Per MCP Apps spec, the host waits for this before sending tool-result.
|
|
1192
|
+
var targetOrigin = self.trustedOrigin || '*';
|
|
1193
|
+
window.parent.postMessage({
|
|
1194
|
+
jsonrpc: '2.0',
|
|
1195
|
+
method: 'ui/notifications/initialized',
|
|
1196
|
+
params: {}
|
|
1197
|
+
}, targetOrigin);
|
|
1198
|
+
resolve();
|
|
1199
|
+
},
|
|
1200
|
+
reject: function(err) { resolve(); }, // Non-fatal
|
|
1201
|
+
timeout: timeout
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
// Use '*' for the initial handshake \u2014 we don't know the host origin yet.
|
|
1205
|
+
// The sandbox proxy will relay this to the host.
|
|
1206
|
+
window.parent.postMessage({
|
|
1207
|
+
jsonrpc: '2.0', id: id, method: 'ui/initialize', params: params
|
|
1208
|
+
}, '*');
|
|
1103
1209
|
});
|
|
1104
1210
|
},
|
|
1211
|
+
performHandshake: function(context) {
|
|
1212
|
+
return this.sendHandshake(context);
|
|
1213
|
+
},
|
|
1105
1214
|
callTool: function(context, name, args) {
|
|
1106
|
-
if (!this.hostCapabilities.serverToolProxy) {
|
|
1215
|
+
if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
|
|
1107
1216
|
return Promise.reject(new Error('Server tool proxy not supported'));
|
|
1108
1217
|
}
|
|
1109
|
-
|
|
1218
|
+
// Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
|
|
1219
|
+
return this.sendRequest('tools/call', { name: name, arguments: args || {} });
|
|
1110
1220
|
},
|
|
1111
1221
|
sendMessage: function(context, content) {
|
|
1112
1222
|
return this.sendRequest('ui/message', { content: content });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transpiler.d.ts","sourceRoot":"","sources":["../../src/component/transpiler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAkB9E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,GACpB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"transpiler.d.ts","sourceRoot":"","sources":["../../src/component/transpiler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAkB9E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,GACpB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CA4GlB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUpE"}
|
package/esm/adapters/index.mjs
CHANGED
|
@@ -183,6 +183,7 @@ function safeJsonForScript(value) {
|
|
|
183
183
|
function buildDataInjectionScript(options) {
|
|
184
184
|
const { toolName, input, output, structuredContent } = options;
|
|
185
185
|
const lines = [
|
|
186
|
+
`window.__mcpAppsEnabled = true;`,
|
|
186
187
|
`window.__mcpToolName = ${safeJsonForScript(toolName)};`,
|
|
187
188
|
`window.__mcpToolInput = ${safeJsonForScript(input ?? null)};`,
|
|
188
189
|
`window.__mcpToolOutput = ${safeJsonForScript(output ?? null)};`,
|
|
@@ -273,6 +274,19 @@ function generateBridgeIIFE(options = {}) {
|
|
|
273
274
|
parts.push("});");
|
|
274
275
|
parts.push("");
|
|
275
276
|
parts.push("window.FrontMcpBridge = bridge;");
|
|
277
|
+
parts.push("function __showLoading() {");
|
|
278
|
+
parts.push(' var root = document.getElementById("root");');
|
|
279
|
+
parts.push(" if (root && !root.hasChildNodes()) {");
|
|
280
|
+
parts.push(
|
|
281
|
+
` 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>';`
|
|
282
|
+
);
|
|
283
|
+
parts.push(" }");
|
|
284
|
+
parts.push("}");
|
|
285
|
+
parts.push('if (document.readyState === "loading") {');
|
|
286
|
+
parts.push(' document.addEventListener("DOMContentLoaded", __showLoading);');
|
|
287
|
+
parts.push("} else {");
|
|
288
|
+
parts.push(" __showLoading();");
|
|
289
|
+
parts.push("}");
|
|
276
290
|
parts.push("})();");
|
|
277
291
|
const code = parts.join("\n");
|
|
278
292
|
if (minify) {
|
|
@@ -449,7 +463,20 @@ var ExtAppsAdapter = {
|
|
|
449
463
|
self.handleMessage(context, event);
|
|
450
464
|
});
|
|
451
465
|
|
|
452
|
-
|
|
466
|
+
// Defer handshake until after the document is fully loaded.
|
|
467
|
+
// During document.write() (used by the sandbox proxy), postMessage
|
|
468
|
+
// from the inner iframe may not reach the parent. Waiting for DOMContentLoaded
|
|
469
|
+
// or using setTimeout ensures the iframe is fully attached.
|
|
470
|
+
return new Promise(function(resolve) {
|
|
471
|
+
function doHandshake() {
|
|
472
|
+
self.sendHandshake(context).then(resolve, resolve);
|
|
473
|
+
}
|
|
474
|
+
if (document.readyState === 'loading') {
|
|
475
|
+
document.addEventListener('DOMContentLoaded', doHandshake);
|
|
476
|
+
} else {
|
|
477
|
+
setTimeout(doHandshake, 0);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
453
480
|
},
|
|
454
481
|
handleMessage: function(context, event) {
|
|
455
482
|
if (!this.isOriginTrusted(event.origin)) return;
|
|
@@ -543,32 +570,67 @@ var ExtAppsAdapter = {
|
|
|
543
570
|
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
|
|
544
571
|
});
|
|
545
572
|
},
|
|
546
|
-
|
|
573
|
+
sendHandshake: function(context) {
|
|
574
|
+
// Send ui/initialize using '*' as target origin since TOFU hasn't
|
|
575
|
+
// been established yet. The response from the host will establish
|
|
576
|
+
// TOFU trust via handleMessage \u2192 isOriginTrusted.
|
|
547
577
|
var self = this;
|
|
578
|
+
var id = ++this.requestId;
|
|
548
579
|
var params = {
|
|
549
580
|
appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
|
|
550
581
|
appCapabilities: { tools: { listChanged: false } },
|
|
551
582
|
protocolVersion: '2024-11-05'
|
|
552
583
|
};
|
|
553
584
|
|
|
554
|
-
return
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
585
|
+
return new Promise(function(resolve, reject) {
|
|
586
|
+
var timeout = setTimeout(function() {
|
|
587
|
+
delete self.pendingRequests[id];
|
|
588
|
+
// Handshake timeout is non-fatal \u2014 notifications may still arrive
|
|
589
|
+
resolve();
|
|
590
|
+
}, 10000);
|
|
591
|
+
|
|
592
|
+
self.pendingRequests[id] = {
|
|
593
|
+
resolve: function(result) {
|
|
594
|
+
self.hostCapabilities = result.hostCapabilities || {};
|
|
595
|
+
self.capabilities = Object.assign({}, self.capabilities, {
|
|
596
|
+
canCallTools: Boolean(self.hostCapabilities.serverTools || self.hostCapabilities.serverToolProxy),
|
|
597
|
+
canSendMessages: true,
|
|
598
|
+
canOpenLinks: Boolean(self.hostCapabilities.openLinks || self.hostCapabilities.openLink),
|
|
599
|
+
supportsDisplayModes: true
|
|
600
|
+
});
|
|
601
|
+
if (result.hostContext) {
|
|
602
|
+
Object.assign(context.hostContext, result.hostContext);
|
|
603
|
+
}
|
|
604
|
+
// Send ui/notifications/initialized to tell the host the view is ready.
|
|
605
|
+
// Per MCP Apps spec, the host waits for this before sending tool-result.
|
|
606
|
+
var targetOrigin = self.trustedOrigin || '*';
|
|
607
|
+
window.parent.postMessage({
|
|
608
|
+
jsonrpc: '2.0',
|
|
609
|
+
method: 'ui/notifications/initialized',
|
|
610
|
+
params: {}
|
|
611
|
+
}, targetOrigin);
|
|
612
|
+
resolve();
|
|
613
|
+
},
|
|
614
|
+
reject: function(err) { resolve(); }, // Non-fatal
|
|
615
|
+
timeout: timeout
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// Use '*' for the initial handshake \u2014 we don't know the host origin yet.
|
|
619
|
+
// The sandbox proxy will relay this to the host.
|
|
620
|
+
window.parent.postMessage({
|
|
621
|
+
jsonrpc: '2.0', id: id, method: 'ui/initialize', params: params
|
|
622
|
+
}, '*');
|
|
565
623
|
});
|
|
566
624
|
},
|
|
625
|
+
performHandshake: function(context) {
|
|
626
|
+
return this.sendHandshake(context);
|
|
627
|
+
},
|
|
567
628
|
callTool: function(context, name, args) {
|
|
568
|
-
if (!this.hostCapabilities.serverToolProxy) {
|
|
629
|
+
if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
|
|
569
630
|
return Promise.reject(new Error('Server tool proxy not supported'));
|
|
570
631
|
}
|
|
571
|
-
|
|
632
|
+
// Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
|
|
633
|
+
return this.sendRequest('tools/call', { name: name, arguments: args || {} });
|
|
572
634
|
},
|
|
573
635
|
sendMessage: function(context, content) {
|
|
574
636
|
return this.sendRequest('ui/message', { content: content });
|
|
@@ -1750,22 +1812,71 @@ function bundleFileSource(source, filename, resolveDir, componentName) {
|
|
|
1750
1812
|
const esbuild = __require("esbuild");
|
|
1751
1813
|
const mountCode = `
|
|
1752
1814
|
// --- Auto-generated mount ---
|
|
1815
|
+
import { createElement as __h } from 'react';
|
|
1753
1816
|
import { createRoot } from 'react-dom/client';
|
|
1754
1817
|
import { McpBridgeProvider } from '@frontmcp/ui/react';
|
|
1755
|
-
|
|
1756
|
-
const __root = document.getElementById('root');
|
|
1818
|
+
var __root = document.getElementById('root');
|
|
1757
1819
|
if (__root) {
|
|
1758
|
-
createRoot(__root)
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1820
|
+
var __reactRoot = createRoot(__root);
|
|
1821
|
+
function __hasData(v) { return v !== undefined; }
|
|
1822
|
+
function __render(output) {
|
|
1823
|
+
__reactRoot.render(
|
|
1824
|
+
__h(McpBridgeProvider, null,
|
|
1825
|
+
__h(${componentName}, { output: output !== undefined ? output : null, input: window.__mcpToolInput, loading: !__hasData(output) })
|
|
1826
|
+
)
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
// Render immediately (component shows loading state until data arrives)
|
|
1830
|
+
__render(undefined);
|
|
1831
|
+
// 1. Try OpenAI SDK (toolOutput set synchronously or after load)
|
|
1832
|
+
if (typeof window !== 'undefined') {
|
|
1833
|
+
if (!window.openai) window.openai = {};
|
|
1834
|
+
var __cur = window.openai.toolOutput;
|
|
1835
|
+
if (__hasData(__cur)) { __render(__cur); }
|
|
1836
|
+
Object.defineProperty(window.openai, 'toolOutput', {
|
|
1837
|
+
get: function() { return __cur; },
|
|
1838
|
+
set: function(v) { __cur = v; __render(v); },
|
|
1839
|
+
configurable: true, enumerable: true
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
// 2. Try injected data globals
|
|
1843
|
+
if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
|
|
1844
|
+
// 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
|
|
1845
|
+
var __bridge = window.FrontMcpBridge;
|
|
1846
|
+
if (__bridge && typeof __bridge.onToolResult === 'function') {
|
|
1847
|
+
__bridge.onToolResult(function(data) { __render(data); });
|
|
1848
|
+
} else {
|
|
1849
|
+
// 4. Fallback: listen for tool:result CustomEvent (standalone / MCP Inspector)
|
|
1850
|
+
window.addEventListener('tool:result', function(e) {
|
|
1851
|
+
var d = e.detail;
|
|
1852
|
+
if (d) __render(d.structuredContent !== undefined ? d.structuredContent : d.content !== undefined ? d.content : d);
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1763
1855
|
}`;
|
|
1764
1856
|
const loader = {
|
|
1765
1857
|
".tsx": "tsx",
|
|
1766
1858
|
".jsx": "jsx"
|
|
1767
1859
|
};
|
|
1768
1860
|
const stdinLoader = filename.endsWith(".tsx") ? "tsx" : "jsx";
|
|
1861
|
+
const alias = {};
|
|
1862
|
+
try {
|
|
1863
|
+
const nodePath = __require("path");
|
|
1864
|
+
const nodeFs = __require("fs");
|
|
1865
|
+
const candidates = [
|
|
1866
|
+
nodePath.join(process.cwd(), "node_modules", "@frontmcp", "ui", "dist", "esm"),
|
|
1867
|
+
nodePath.join(resolveDir, "node_modules", "@frontmcp", "ui", "dist", "esm")
|
|
1868
|
+
];
|
|
1869
|
+
for (const uiEsmBase of candidates) {
|
|
1870
|
+
if (!nodeFs.existsSync(uiEsmBase)) continue;
|
|
1871
|
+
const subpaths = ["components", "react", "theme", "bridge", "runtime"];
|
|
1872
|
+
for (const sub of subpaths) {
|
|
1873
|
+
const mjs = nodePath.join(uiEsmBase, sub, "index.mjs");
|
|
1874
|
+
if (nodeFs.existsSync(mjs)) alias[`@frontmcp/ui/${sub}`] = mjs;
|
|
1875
|
+
}
|
|
1876
|
+
if (Object.keys(alias).length > 0) break;
|
|
1877
|
+
}
|
|
1878
|
+
} catch {
|
|
1879
|
+
}
|
|
1769
1880
|
try {
|
|
1770
1881
|
const result = esbuild.buildSync({
|
|
1771
1882
|
stdin: {
|
|
@@ -1778,10 +1889,9 @@ if (__root) {
|
|
|
1778
1889
|
write: false,
|
|
1779
1890
|
format: "esm",
|
|
1780
1891
|
target: "es2020",
|
|
1781
|
-
jsx: "
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
external: ["react", "react-dom"],
|
|
1892
|
+
jsx: "automatic",
|
|
1893
|
+
external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
|
|
1894
|
+
alias,
|
|
1785
1895
|
define: { "process.env.NODE_ENV": '"production"' },
|
|
1786
1896
|
platform: "browser",
|
|
1787
1897
|
treeShaking: true,
|
|
@@ -2259,8 +2369,26 @@ function wrapDetectedContent(value) {
|
|
|
2259
2369
|
}
|
|
2260
2370
|
|
|
2261
2371
|
// libs/uipack/src/adapters/template-renderer.ts
|
|
2372
|
+
function buildCspConfig(resolver) {
|
|
2373
|
+
const cspResourceDomains = ["https://esm.sh"];
|
|
2374
|
+
const cspConnectDomains = ["https://esm.sh"];
|
|
2375
|
+
if (resolver && "overrides" in resolver) {
|
|
2376
|
+
const overrides = resolver.overrides;
|
|
2377
|
+
if (overrides) {
|
|
2378
|
+
for (const url of Object.values(overrides)) {
|
|
2379
|
+
try {
|
|
2380
|
+
const origin = new URL(url).origin;
|
|
2381
|
+
if (!cspResourceDomains.includes(origin)) cspResourceDomains.push(origin);
|
|
2382
|
+
if (!cspConnectDomains.includes(origin)) cspConnectDomains.push(origin);
|
|
2383
|
+
} catch {
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
return { resourceDomains: cspResourceDomains, connectDomains: cspConnectDomains };
|
|
2389
|
+
}
|
|
2262
2390
|
function renderToolTemplate(options) {
|
|
2263
|
-
const { toolName, input, output, template,
|
|
2391
|
+
const { toolName, input, output, template, resolver } = options;
|
|
2264
2392
|
const uiType = detectUIType(template);
|
|
2265
2393
|
const shellConfig = {
|
|
2266
2394
|
toolName,
|
|
@@ -2273,25 +2401,7 @@ function renderToolTemplate(options) {
|
|
|
2273
2401
|
let hash = "";
|
|
2274
2402
|
let size = 0;
|
|
2275
2403
|
if (typeof template === "object" && template !== null && "file" in template) {
|
|
2276
|
-
const
|
|
2277
|
-
const cspConnectDomains = ["https://esm.sh"];
|
|
2278
|
-
if (resolver && "overrides" in resolver) {
|
|
2279
|
-
const overrides = resolver.overrides;
|
|
2280
|
-
if (overrides) {
|
|
2281
|
-
for (const url of Object.values(overrides)) {
|
|
2282
|
-
try {
|
|
2283
|
-
const origin = new URL(url).origin;
|
|
2284
|
-
if (!cspResourceDomains.includes(origin)) cspResourceDomains.push(origin);
|
|
2285
|
-
if (!cspConnectDomains.includes(origin)) cspConnectDomains.push(origin);
|
|
2286
|
-
} catch {
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
}
|
|
2290
|
-
}
|
|
2291
|
-
const cspConfig = {
|
|
2292
|
-
resourceDomains: cspResourceDomains,
|
|
2293
|
-
connectDomains: cspConnectDomains
|
|
2294
|
-
};
|
|
2404
|
+
const cspConfig = buildCspConfig(resolver);
|
|
2295
2405
|
const result = renderComponent({ source: template }, { ...shellConfig, csp: cspConfig });
|
|
2296
2406
|
html = result.html;
|
|
2297
2407
|
hash = result.hash;
|
|
@@ -52,6 +52,19 @@ function generateBridgeIIFE(options = {}) {
|
|
|
52
52
|
parts.push("});");
|
|
53
53
|
parts.push("");
|
|
54
54
|
parts.push("window.FrontMcpBridge = bridge;");
|
|
55
|
+
parts.push("function __showLoading() {");
|
|
56
|
+
parts.push(' var root = document.getElementById("root");');
|
|
57
|
+
parts.push(" if (root && !root.hasChildNodes()) {");
|
|
58
|
+
parts.push(
|
|
59
|
+
` 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>';`
|
|
60
|
+
);
|
|
61
|
+
parts.push(" }");
|
|
62
|
+
parts.push("}");
|
|
63
|
+
parts.push('if (document.readyState === "loading") {');
|
|
64
|
+
parts.push(' document.addEventListener("DOMContentLoaded", __showLoading);');
|
|
65
|
+
parts.push("} else {");
|
|
66
|
+
parts.push(" __showLoading();");
|
|
67
|
+
parts.push("}");
|
|
55
68
|
parts.push("})();");
|
|
56
69
|
const code = parts.join("\n");
|
|
57
70
|
if (minify) {
|
|
@@ -228,7 +241,20 @@ var ExtAppsAdapter = {
|
|
|
228
241
|
self.handleMessage(context, event);
|
|
229
242
|
});
|
|
230
243
|
|
|
231
|
-
|
|
244
|
+
// Defer handshake until after the document is fully loaded.
|
|
245
|
+
// During document.write() (used by the sandbox proxy), postMessage
|
|
246
|
+
// from the inner iframe may not reach the parent. Waiting for DOMContentLoaded
|
|
247
|
+
// or using setTimeout ensures the iframe is fully attached.
|
|
248
|
+
return new Promise(function(resolve) {
|
|
249
|
+
function doHandshake() {
|
|
250
|
+
self.sendHandshake(context).then(resolve, resolve);
|
|
251
|
+
}
|
|
252
|
+
if (document.readyState === 'loading') {
|
|
253
|
+
document.addEventListener('DOMContentLoaded', doHandshake);
|
|
254
|
+
} else {
|
|
255
|
+
setTimeout(doHandshake, 0);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
232
258
|
},
|
|
233
259
|
handleMessage: function(context, event) {
|
|
234
260
|
if (!this.isOriginTrusted(event.origin)) return;
|
|
@@ -322,32 +348,67 @@ var ExtAppsAdapter = {
|
|
|
322
348
|
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
|
|
323
349
|
});
|
|
324
350
|
},
|
|
325
|
-
|
|
351
|
+
sendHandshake: function(context) {
|
|
352
|
+
// Send ui/initialize using '*' as target origin since TOFU hasn't
|
|
353
|
+
// been established yet. The response from the host will establish
|
|
354
|
+
// TOFU trust via handleMessage \u2192 isOriginTrusted.
|
|
326
355
|
var self = this;
|
|
356
|
+
var id = ++this.requestId;
|
|
327
357
|
var params = {
|
|
328
358
|
appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
|
|
329
359
|
appCapabilities: { tools: { listChanged: false } },
|
|
330
360
|
protocolVersion: '2024-11-05'
|
|
331
361
|
};
|
|
332
362
|
|
|
333
|
-
return
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
363
|
+
return new Promise(function(resolve, reject) {
|
|
364
|
+
var timeout = setTimeout(function() {
|
|
365
|
+
delete self.pendingRequests[id];
|
|
366
|
+
// Handshake timeout is non-fatal \u2014 notifications may still arrive
|
|
367
|
+
resolve();
|
|
368
|
+
}, 10000);
|
|
369
|
+
|
|
370
|
+
self.pendingRequests[id] = {
|
|
371
|
+
resolve: function(result) {
|
|
372
|
+
self.hostCapabilities = result.hostCapabilities || {};
|
|
373
|
+
self.capabilities = Object.assign({}, self.capabilities, {
|
|
374
|
+
canCallTools: Boolean(self.hostCapabilities.serverTools || self.hostCapabilities.serverToolProxy),
|
|
375
|
+
canSendMessages: true,
|
|
376
|
+
canOpenLinks: Boolean(self.hostCapabilities.openLinks || self.hostCapabilities.openLink),
|
|
377
|
+
supportsDisplayModes: true
|
|
378
|
+
});
|
|
379
|
+
if (result.hostContext) {
|
|
380
|
+
Object.assign(context.hostContext, result.hostContext);
|
|
381
|
+
}
|
|
382
|
+
// Send ui/notifications/initialized to tell the host the view is ready.
|
|
383
|
+
// Per MCP Apps spec, the host waits for this before sending tool-result.
|
|
384
|
+
var targetOrigin = self.trustedOrigin || '*';
|
|
385
|
+
window.parent.postMessage({
|
|
386
|
+
jsonrpc: '2.0',
|
|
387
|
+
method: 'ui/notifications/initialized',
|
|
388
|
+
params: {}
|
|
389
|
+
}, targetOrigin);
|
|
390
|
+
resolve();
|
|
391
|
+
},
|
|
392
|
+
reject: function(err) { resolve(); }, // Non-fatal
|
|
393
|
+
timeout: timeout
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// Use '*' for the initial handshake \u2014 we don't know the host origin yet.
|
|
397
|
+
// The sandbox proxy will relay this to the host.
|
|
398
|
+
window.parent.postMessage({
|
|
399
|
+
jsonrpc: '2.0', id: id, method: 'ui/initialize', params: params
|
|
400
|
+
}, '*');
|
|
344
401
|
});
|
|
345
402
|
},
|
|
403
|
+
performHandshake: function(context) {
|
|
404
|
+
return this.sendHandshake(context);
|
|
405
|
+
},
|
|
346
406
|
callTool: function(context, name, args) {
|
|
347
|
-
if (!this.hostCapabilities.serverToolProxy) {
|
|
407
|
+
if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
|
|
348
408
|
return Promise.reject(new Error('Server tool proxy not supported'));
|
|
349
409
|
}
|
|
350
|
-
|
|
410
|
+
// Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
|
|
411
|
+
return this.sendRequest('tools/call', { name: name, arguments: args || {} });
|
|
351
412
|
},
|
|
352
413
|
sendMessage: function(context, content) {
|
|
353
414
|
return this.sendRequest('ui/message', { content: content });
|