@frontmcp/uipack 1.8.4 → 1.8.6
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 +146 -15
- package/bridge-runtime/iife-generator.d.ts.map +1 -1
- package/bridge-runtime/index.js +92 -11
- package/component/index.js +146 -15
- package/component/loader.d.ts.map +1 -1
- package/component/transpiler.d.ts.map +1 -1
- package/esm/adapters/index.mjs +146 -15
- package/esm/bridge-runtime/index.mjs +92 -11
- package/esm/component/index.mjs +146 -15
- package/esm/index.mjs +146 -15
- package/esm/package.json +8 -2
- package/esm/shell/index.mjs +92 -11
- package/index.js +146 -15
- package/package.json +8 -2
- package/shell/index.js +92 -11
- package/types/ui-config.d.ts +4 -2
- package/types/ui-config.d.ts.map +1 -1
package/component/index.js
CHANGED
|
@@ -498,8 +498,24 @@ function isFrontmcpUiResolvable(...candidatePaths) {
|
|
|
498
498
|
}
|
|
499
499
|
|
|
500
500
|
// libs/uipack/src/component/transpiler.ts
|
|
501
|
+
function loadEsbuild(filename) {
|
|
502
|
+
try {
|
|
503
|
+
return require("esbuild");
|
|
504
|
+
} catch (err) {
|
|
505
|
+
if (!isMissingEsbuild(err)) throw err;
|
|
506
|
+
const subject = filename ? `FileSource widget "${filename}"` : "A FileSource widget";
|
|
507
|
+
throw new Error(
|
|
508
|
+
`${subject} needs esbuild, which is not installed. @frontmcp/uipack loads esbuild on demand to bundle FileSource widgets (.tsx/.jsx) when the tool is called, so it must be installed where the server runs: \`npm install esbuild\` (in "dependencies", not "devDependencies"). Projects created with \`frontmcp create\` already get it through the \`frontmcp\` package.`,
|
|
509
|
+
{ cause: err }
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function isMissingEsbuild(err) {
|
|
514
|
+
if (!(err instanceof Error)) return false;
|
|
515
|
+
return err.code === "MODULE_NOT_FOUND" && /Cannot find module ['"]esbuild['"]/.test(err.message);
|
|
516
|
+
}
|
|
501
517
|
function transpileReactSource(source, filename) {
|
|
502
|
-
const esbuild =
|
|
518
|
+
const esbuild = loadEsbuild(filename);
|
|
503
519
|
const loader = filename?.endsWith(".tsx") ? "tsx" : "jsx";
|
|
504
520
|
const result = esbuild.transformSync(source, {
|
|
505
521
|
loader,
|
|
@@ -518,7 +534,7 @@ function bundleFileSource(source, filename, resolveDir, componentName, options =
|
|
|
518
534
|
`FileSource widget "${filename}" requires the @frontmcp/ui package, which provides the React bridge mount that's injected at bundle time. Install it (e.g. \`npm install @frontmcp/ui\` or \`yarn add @frontmcp/ui\`) and try again.`
|
|
519
535
|
);
|
|
520
536
|
}
|
|
521
|
-
const esbuild =
|
|
537
|
+
const esbuild = loadEsbuild(filename);
|
|
522
538
|
const mountCode = `
|
|
523
539
|
// --- Auto-generated mount ---
|
|
524
540
|
import { createElement as __h } from 'react';
|
|
@@ -615,7 +631,8 @@ if (__root) {
|
|
|
615
631
|
} catch (err) {
|
|
616
632
|
const message = err instanceof Error ? err.message : String(err);
|
|
617
633
|
throw new Error(
|
|
618
|
-
`Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project
|
|
634
|
+
`Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`,
|
|
635
|
+
{ cause: err }
|
|
619
636
|
);
|
|
620
637
|
}
|
|
621
638
|
}
|
|
@@ -677,6 +694,37 @@ function resolveImportSource(source) {
|
|
|
677
694
|
peerDependencies: []
|
|
678
695
|
};
|
|
679
696
|
}
|
|
697
|
+
var BUILD_DIR_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "out"]);
|
|
698
|
+
function missingWidgetError(filePath, cause) {
|
|
699
|
+
const sourceWidget = findSourceWidget(filePath);
|
|
700
|
+
const error = new Error(
|
|
701
|
+
`FileSource widget not found (ENOENT): no file at "${filePath}". The widget source is read from this path when the tool is called, so it has to exist where the compiled tool runs \u2014 a path built from \`__dirname\` or \`import.meta.url\` points into the build output once the tool is compiled. tsc does not copy \`*.widget.tsx\` / \`*.widget.jsx\` files into its output; \`frontmcp build\` does, and any other build needs a step that copies them next to the compiled tool.` + (sourceWidget ? ` The source widget exists at "${sourceWidget}" \u2014 copy it to "${filePath}", or build with \`frontmcp build\`.` : ""),
|
|
702
|
+
{ cause }
|
|
703
|
+
);
|
|
704
|
+
error.code = "ENOENT";
|
|
705
|
+
error.path = filePath;
|
|
706
|
+
return error;
|
|
707
|
+
}
|
|
708
|
+
function findSourceWidget(filePath) {
|
|
709
|
+
const path = require("path");
|
|
710
|
+
const fs = require("fs");
|
|
711
|
+
const parts = filePath.split(path.sep);
|
|
712
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
713
|
+
if (!BUILD_DIR_SEGMENTS.has(parts[i])) continue;
|
|
714
|
+
const candidates = [[...parts.slice(0, i), "src", ...parts.slice(i + 1)]];
|
|
715
|
+
if (parts.length - i > 2) candidates.push([...parts.slice(0, i), "src", ...parts.slice(i + 2)]);
|
|
716
|
+
candidates.push([...parts.slice(0, i), ...parts.slice(i + 1)]);
|
|
717
|
+
for (const candidate of candidates) {
|
|
718
|
+
const candidatePath = candidate.join(path.sep);
|
|
719
|
+
try {
|
|
720
|
+
if (fs.existsSync(candidatePath)) return candidatePath;
|
|
721
|
+
} catch {
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return void 0;
|
|
725
|
+
}
|
|
726
|
+
return void 0;
|
|
727
|
+
}
|
|
680
728
|
function resolveFileSource(source, options = {}) {
|
|
681
729
|
const path = require("path");
|
|
682
730
|
const ext = path.extname(source.file).toLowerCase();
|
|
@@ -691,9 +739,11 @@ function resolveFileSource(source, options = {}) {
|
|
|
691
739
|
const isNotFound = err?.code === "ENOENT";
|
|
692
740
|
if (isNotFound && wasRelative) {
|
|
693
741
|
throw new Error(
|
|
694
|
-
`FileSource widget "${source.file}" not found at "${filePath}". Relative paths are resolved against process.cwd() ("${process.cwd()}"), not the tool file's directory. To anchor the path to the tool file, pass an absolute path \u2014 e.g. \`{ file: fileURLToPath(new URL('./widget.tsx', import.meta.url)) }\` from \`node:url\` (see issue #444)
|
|
742
|
+
`FileSource widget "${source.file}" not found at "${filePath}". Relative paths are resolved against process.cwd() ("${process.cwd()}"), not the tool file's directory. To anchor the path to the tool file, pass an absolute path \u2014 e.g. \`{ file: fileURLToPath(new URL('./widget.tsx', import.meta.url)) }\` from \`node:url\` (see issue #444).`,
|
|
743
|
+
{ cause: err }
|
|
695
744
|
);
|
|
696
745
|
}
|
|
746
|
+
if (isNotFound) throw missingWidgetError(filePath, err);
|
|
697
747
|
throw err;
|
|
698
748
|
}
|
|
699
749
|
const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
|
|
@@ -911,6 +961,23 @@ function __applySizingCss(sizing) {
|
|
|
911
961
|
}
|
|
912
962
|
}
|
|
913
963
|
|
|
964
|
+
function __measureHeight() {
|
|
965
|
+
// <html> at fit-content includes body margins and collapsed child margins;
|
|
966
|
+
// content overflowing a fixed-height body is added; a px max-height clamps.
|
|
967
|
+
var de = document.documentElement;
|
|
968
|
+
var body = document.body;
|
|
969
|
+
if (!de || !body) return 0;
|
|
970
|
+
var prev = de.style.height;
|
|
971
|
+
de.style.height = 'fit-content';
|
|
972
|
+
var dr = de.getBoundingClientRect();
|
|
973
|
+
var br = body.getBoundingClientRect();
|
|
974
|
+
var h = Math.max(dr.height, br.top + body.scrollHeight + Math.max(0, dr.bottom - br.bottom) - dr.top);
|
|
975
|
+
de.style.height = prev;
|
|
976
|
+
var maxHeight = getComputedStyle(de).maxHeight;
|
|
977
|
+
var mx = maxHeight && maxHeight.slice(-2) === 'px' ? parseFloat(maxHeight) : NaN;
|
|
978
|
+
return Math.ceil(isNaN(mx) ? h : Math.min(h, mx));
|
|
979
|
+
}
|
|
980
|
+
|
|
914
981
|
function __initAutoResize() {
|
|
915
982
|
if (typeof window === 'undefined') return;
|
|
916
983
|
// Idempotent: a re-injected IIFE must not stack observers.
|
|
@@ -933,23 +1000,30 @@ function __initAutoResize() {
|
|
|
933
1000
|
if (typeof ResizeObserver === 'undefined') return;
|
|
934
1001
|
|
|
935
1002
|
function startObserving() {
|
|
936
|
-
|
|
937
|
-
|
|
1003
|
+
if (!document.body) return;
|
|
1004
|
+
var targets = [document.documentElement, document.body];
|
|
1005
|
+
var root = document.getElementById('root');
|
|
1006
|
+
if (root) targets.push(root);
|
|
938
1007
|
|
|
939
1008
|
var rafId = null;
|
|
940
1009
|
var lastReported = -1;
|
|
941
1010
|
function report() {
|
|
942
1011
|
rafId = null;
|
|
943
1012
|
try {
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1013
|
+
// Before the handshake an ext-apps host can't receive the report; the
|
|
1014
|
+
// bridge:ready listener below sends it once the bridge is up.
|
|
1015
|
+
if (window.FrontMcpBridge && window.FrontMcpBridge.initialized === false) return;
|
|
1016
|
+
var height = __measureHeight();
|
|
1017
|
+
var width = Math.ceil(document.documentElement.getBoundingClientRect().width);
|
|
947
1018
|
if (height === lastReported || height <= 0) return;
|
|
948
1019
|
lastReported = height;
|
|
949
1020
|
var payload = { height: height, width: width };
|
|
950
1021
|
if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
|
|
951
1022
|
if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
|
|
952
|
-
window.FrontMcpBridge.setSize(payload).catch(function() {
|
|
1023
|
+
window.FrontMcpBridge.setSize(payload).catch(function() {
|
|
1024
|
+
// Forget a rejected report so the same height is sent again.
|
|
1025
|
+
if (lastReported === height) lastReported = -1;
|
|
1026
|
+
});
|
|
953
1027
|
}
|
|
954
1028
|
window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
|
|
955
1029
|
} catch (e) {}
|
|
@@ -970,9 +1044,17 @@ function __initAutoResize() {
|
|
|
970
1044
|
window.__mcpResizeObserver.disconnect();
|
|
971
1045
|
}
|
|
972
1046
|
var ro = new ResizeObserver(function() { schedule(); });
|
|
973
|
-
ro.observe(
|
|
1047
|
+
for (var i = 0; i < targets.length; i++) ro.observe(targets[i]);
|
|
974
1048
|
window.__mcpResizeObserver = ro;
|
|
975
1049
|
} catch (e) {}
|
|
1050
|
+
// Report once the bridge is ready (the report scheduled below is skipped
|
|
1051
|
+
// while the handshake is still pending).
|
|
1052
|
+
var readySent = false;
|
|
1053
|
+
window.addEventListener('bridge:ready', function() {
|
|
1054
|
+
if (readySent) return;
|
|
1055
|
+
readySent = true;
|
|
1056
|
+
schedule();
|
|
1057
|
+
});
|
|
976
1058
|
// Report once on init so the host gets an initial measurement.
|
|
977
1059
|
schedule();
|
|
978
1060
|
}
|
|
@@ -1020,6 +1102,25 @@ function detectViewport() {
|
|
|
1020
1102
|
return undefined;
|
|
1021
1103
|
}
|
|
1022
1104
|
|
|
1105
|
+
function __applyHostTheme(theme) {
|
|
1106
|
+
// Follow a host-supplied theme: <meta name="color-scheme"> sets the frame's
|
|
1107
|
+
// canvas and form controls (a widget's own :root color-scheme still wins),
|
|
1108
|
+
// <html data-theme> is a styling hook. The OS fallback is never written.
|
|
1109
|
+
if (theme !== 'light' && theme !== 'dark') return;
|
|
1110
|
+
if (typeof document === 'undefined' || !document.documentElement) return;
|
|
1111
|
+
try {
|
|
1112
|
+
var meta = document.querySelector('meta[name="color-scheme"]');
|
|
1113
|
+
var head = document.head || document.getElementsByTagName('head')[0];
|
|
1114
|
+
if (!meta && head) {
|
|
1115
|
+
meta = document.createElement('meta');
|
|
1116
|
+
meta.setAttribute('name', 'color-scheme');
|
|
1117
|
+
head.appendChild(meta);
|
|
1118
|
+
}
|
|
1119
|
+
if (meta) meta.setAttribute('content', theme);
|
|
1120
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
1121
|
+
} catch (e) {}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1023
1124
|
function readInjectedData() {
|
|
1024
1125
|
var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
|
|
1025
1126
|
if (typeof window !== 'undefined') {
|
|
@@ -1126,6 +1227,8 @@ var ExtAppsAdapter = {
|
|
|
1126
1227
|
pendingRequests: {},
|
|
1127
1228
|
requestId: 0,
|
|
1128
1229
|
hostCapabilities: {},
|
|
1230
|
+
handshakeSettled: false,
|
|
1231
|
+
pendingSize: null,
|
|
1129
1232
|
canHandle: function() {
|
|
1130
1233
|
if (typeof window === 'undefined') return false;
|
|
1131
1234
|
if (window.parent === window) return false;
|
|
@@ -1291,6 +1394,7 @@ var ExtAppsAdapter = {
|
|
|
1291
1394
|
var timeout = setTimeout(function() {
|
|
1292
1395
|
delete self.pendingRequests[id];
|
|
1293
1396
|
// Handshake timeout is non-fatal \u2014 notifications may still arrive
|
|
1397
|
+
self.settleHandshake();
|
|
1294
1398
|
resolve();
|
|
1295
1399
|
}, 10000);
|
|
1296
1400
|
|
|
@@ -1304,7 +1408,9 @@ var ExtAppsAdapter = {
|
|
|
1304
1408
|
supportsDisplayModes: true
|
|
1305
1409
|
});
|
|
1306
1410
|
if (result.hostContext) {
|
|
1307
|
-
|
|
1411
|
+
// Notify (not a silent merge) so onContextChange listeners and the
|
|
1412
|
+
// host theme see the handshake's context.
|
|
1413
|
+
context.notifyContextChange(result.hostContext);
|
|
1308
1414
|
}
|
|
1309
1415
|
// Send ui/notifications/initialized to tell the host the view is ready.
|
|
1310
1416
|
// Per MCP Apps spec, the host waits for this before sending tool-result.
|
|
@@ -1314,9 +1420,10 @@ var ExtAppsAdapter = {
|
|
|
1314
1420
|
method: 'ui/notifications/initialized',
|
|
1315
1421
|
params: {}
|
|
1316
1422
|
}, targetOrigin);
|
|
1423
|
+
self.settleHandshake();
|
|
1317
1424
|
resolve();
|
|
1318
1425
|
},
|
|
1319
|
-
reject: function(err) { resolve(); }, // Non-fatal
|
|
1426
|
+
reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
|
|
1320
1427
|
timeout: timeout
|
|
1321
1428
|
};
|
|
1322
1429
|
|
|
@@ -1330,6 +1437,18 @@ var ExtAppsAdapter = {
|
|
|
1330
1437
|
performHandshake: function(context) {
|
|
1331
1438
|
return this.sendHandshake(context);
|
|
1332
1439
|
},
|
|
1440
|
+
settleHandshake: function() {
|
|
1441
|
+
// Deliver the latest size requested while no host origin was trusted.
|
|
1442
|
+
this.handshakeSettled = true;
|
|
1443
|
+
var pending = this.pendingSize;
|
|
1444
|
+
if (!pending) return;
|
|
1445
|
+
this.pendingSize = null;
|
|
1446
|
+
this.sendRequest('ui/setSize', pending.params).then(function(result) {
|
|
1447
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
|
|
1448
|
+
}, function(err) {
|
|
1449
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
|
|
1450
|
+
});
|
|
1451
|
+
},
|
|
1333
1452
|
callTool: function(context, name, args) {
|
|
1334
1453
|
if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
|
|
1335
1454
|
return Promise.reject(new Error('Server tool proxy not supported'));
|
|
@@ -1353,11 +1472,22 @@ var ExtAppsAdapter = {
|
|
|
1353
1472
|
setSize: function(context, size) {
|
|
1354
1473
|
// FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
|
|
1355
1474
|
// measured/desired widget dimensions to the host.
|
|
1356
|
-
|
|
1475
|
+
var params = {
|
|
1357
1476
|
height: size && size.height,
|
|
1358
1477
|
width: size && size.width,
|
|
1359
1478
|
aspectRatio: size && size.aspectRatio
|
|
1360
|
-
}
|
|
1479
|
+
};
|
|
1480
|
+
if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
1481
|
+
// No host origin yet: keep only the latest size and send it when the
|
|
1482
|
+
// handshake settles; every caller's promise follows that one request.
|
|
1483
|
+
var self = this;
|
|
1484
|
+
if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
|
|
1485
|
+
this.pendingSize.params = params;
|
|
1486
|
+
return new Promise(function(resolve, reject) {
|
|
1487
|
+
self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
return this.sendRequest('ui/setSize', params);
|
|
1361
1491
|
},
|
|
1362
1492
|
requestClose: function(context) {
|
|
1363
1493
|
return this.sendRequest('ui/close', {});
|
|
@@ -1603,6 +1733,7 @@ function FrontMcpBridge() {
|
|
|
1603
1733
|
toolResultListeners: [],
|
|
1604
1734
|
notifyContextChange: function(changes) {
|
|
1605
1735
|
Object.assign(this.hostContext, changes);
|
|
1736
|
+
if (changes && changes.theme) __applyHostTheme(changes.theme);
|
|
1606
1737
|
for (var i = 0; i < this.contextListeners.length; i++) {
|
|
1607
1738
|
try { this.contextListeners[i](changes); } catch(e) {}
|
|
1608
1739
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/component/loader.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAGxE,OAAO,EACL,iBAAiB,EAUjB,KAAK,iBAAiB,EACtB,KAAK,QAAQ,EACd,MAAM,SAAS,CAAC;AAOjB;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,QAAQ,EAChB,OAAO,CAAC,EAAE;IACR,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,GACA,iBAAiB,CA2BnB;
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/component/loader.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAGxE,OAAO,EACL,iBAAiB,EAUjB,KAAK,iBAAiB,EACtB,KAAK,QAAQ,EACd,MAAM,SAAS,CAAC;AAOjB;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,QAAQ,EAChB,OAAO,CAAC,EAAE;IACR,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,GACA,iBAAiB,CA2BnB;AAsMD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CA6B9G;AAmBD;;GAEG;AACH,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transpiler.d.ts","sourceRoot":"","sources":["../../src/component/transpiler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"transpiler.d.ts","sourceRoot":"","sources":["../../src/component/transpiler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAyCH;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAiB9E;AAED;;;;;;;;;;;;GAYG;AACH;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,uBAA4B,GACpC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CA8HlB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUpE"}
|
package/esm/adapters/index.mjs
CHANGED
|
@@ -637,6 +637,23 @@ function __applySizingCss(sizing) {
|
|
|
637
637
|
}
|
|
638
638
|
}
|
|
639
639
|
|
|
640
|
+
function __measureHeight() {
|
|
641
|
+
// <html> at fit-content includes body margins and collapsed child margins;
|
|
642
|
+
// content overflowing a fixed-height body is added; a px max-height clamps.
|
|
643
|
+
var de = document.documentElement;
|
|
644
|
+
var body = document.body;
|
|
645
|
+
if (!de || !body) return 0;
|
|
646
|
+
var prev = de.style.height;
|
|
647
|
+
de.style.height = 'fit-content';
|
|
648
|
+
var dr = de.getBoundingClientRect();
|
|
649
|
+
var br = body.getBoundingClientRect();
|
|
650
|
+
var h = Math.max(dr.height, br.top + body.scrollHeight + Math.max(0, dr.bottom - br.bottom) - dr.top);
|
|
651
|
+
de.style.height = prev;
|
|
652
|
+
var maxHeight = getComputedStyle(de).maxHeight;
|
|
653
|
+
var mx = maxHeight && maxHeight.slice(-2) === 'px' ? parseFloat(maxHeight) : NaN;
|
|
654
|
+
return Math.ceil(isNaN(mx) ? h : Math.min(h, mx));
|
|
655
|
+
}
|
|
656
|
+
|
|
640
657
|
function __initAutoResize() {
|
|
641
658
|
if (typeof window === 'undefined') return;
|
|
642
659
|
// Idempotent: a re-injected IIFE must not stack observers.
|
|
@@ -659,23 +676,30 @@ function __initAutoResize() {
|
|
|
659
676
|
if (typeof ResizeObserver === 'undefined') return;
|
|
660
677
|
|
|
661
678
|
function startObserving() {
|
|
662
|
-
|
|
663
|
-
|
|
679
|
+
if (!document.body) return;
|
|
680
|
+
var targets = [document.documentElement, document.body];
|
|
681
|
+
var root = document.getElementById('root');
|
|
682
|
+
if (root) targets.push(root);
|
|
664
683
|
|
|
665
684
|
var rafId = null;
|
|
666
685
|
var lastReported = -1;
|
|
667
686
|
function report() {
|
|
668
687
|
rafId = null;
|
|
669
688
|
try {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
689
|
+
// Before the handshake an ext-apps host can't receive the report; the
|
|
690
|
+
// bridge:ready listener below sends it once the bridge is up.
|
|
691
|
+
if (window.FrontMcpBridge && window.FrontMcpBridge.initialized === false) return;
|
|
692
|
+
var height = __measureHeight();
|
|
693
|
+
var width = Math.ceil(document.documentElement.getBoundingClientRect().width);
|
|
673
694
|
if (height === lastReported || height <= 0) return;
|
|
674
695
|
lastReported = height;
|
|
675
696
|
var payload = { height: height, width: width };
|
|
676
697
|
if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
|
|
677
698
|
if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
|
|
678
|
-
window.FrontMcpBridge.setSize(payload).catch(function() {
|
|
699
|
+
window.FrontMcpBridge.setSize(payload).catch(function() {
|
|
700
|
+
// Forget a rejected report so the same height is sent again.
|
|
701
|
+
if (lastReported === height) lastReported = -1;
|
|
702
|
+
});
|
|
679
703
|
}
|
|
680
704
|
window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
|
|
681
705
|
} catch (e) {}
|
|
@@ -696,9 +720,17 @@ function __initAutoResize() {
|
|
|
696
720
|
window.__mcpResizeObserver.disconnect();
|
|
697
721
|
}
|
|
698
722
|
var ro = new ResizeObserver(function() { schedule(); });
|
|
699
|
-
ro.observe(
|
|
723
|
+
for (var i = 0; i < targets.length; i++) ro.observe(targets[i]);
|
|
700
724
|
window.__mcpResizeObserver = ro;
|
|
701
725
|
} catch (e) {}
|
|
726
|
+
// Report once the bridge is ready (the report scheduled below is skipped
|
|
727
|
+
// while the handshake is still pending).
|
|
728
|
+
var readySent = false;
|
|
729
|
+
window.addEventListener('bridge:ready', function() {
|
|
730
|
+
if (readySent) return;
|
|
731
|
+
readySent = true;
|
|
732
|
+
schedule();
|
|
733
|
+
});
|
|
702
734
|
// Report once on init so the host gets an initial measurement.
|
|
703
735
|
schedule();
|
|
704
736
|
}
|
|
@@ -746,6 +778,25 @@ function detectViewport() {
|
|
|
746
778
|
return undefined;
|
|
747
779
|
}
|
|
748
780
|
|
|
781
|
+
function __applyHostTheme(theme) {
|
|
782
|
+
// Follow a host-supplied theme: <meta name="color-scheme"> sets the frame's
|
|
783
|
+
// canvas and form controls (a widget's own :root color-scheme still wins),
|
|
784
|
+
// <html data-theme> is a styling hook. The OS fallback is never written.
|
|
785
|
+
if (theme !== 'light' && theme !== 'dark') return;
|
|
786
|
+
if (typeof document === 'undefined' || !document.documentElement) return;
|
|
787
|
+
try {
|
|
788
|
+
var meta = document.querySelector('meta[name="color-scheme"]');
|
|
789
|
+
var head = document.head || document.getElementsByTagName('head')[0];
|
|
790
|
+
if (!meta && head) {
|
|
791
|
+
meta = document.createElement('meta');
|
|
792
|
+
meta.setAttribute('name', 'color-scheme');
|
|
793
|
+
head.appendChild(meta);
|
|
794
|
+
}
|
|
795
|
+
if (meta) meta.setAttribute('content', theme);
|
|
796
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
797
|
+
} catch (e) {}
|
|
798
|
+
}
|
|
799
|
+
|
|
749
800
|
function readInjectedData() {
|
|
750
801
|
var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
|
|
751
802
|
if (typeof window !== 'undefined') {
|
|
@@ -852,6 +903,8 @@ var ExtAppsAdapter = {
|
|
|
852
903
|
pendingRequests: {},
|
|
853
904
|
requestId: 0,
|
|
854
905
|
hostCapabilities: {},
|
|
906
|
+
handshakeSettled: false,
|
|
907
|
+
pendingSize: null,
|
|
855
908
|
canHandle: function() {
|
|
856
909
|
if (typeof window === 'undefined') return false;
|
|
857
910
|
if (window.parent === window) return false;
|
|
@@ -1017,6 +1070,7 @@ var ExtAppsAdapter = {
|
|
|
1017
1070
|
var timeout = setTimeout(function() {
|
|
1018
1071
|
delete self.pendingRequests[id];
|
|
1019
1072
|
// Handshake timeout is non-fatal \u2014 notifications may still arrive
|
|
1073
|
+
self.settleHandshake();
|
|
1020
1074
|
resolve();
|
|
1021
1075
|
}, 10000);
|
|
1022
1076
|
|
|
@@ -1030,7 +1084,9 @@ var ExtAppsAdapter = {
|
|
|
1030
1084
|
supportsDisplayModes: true
|
|
1031
1085
|
});
|
|
1032
1086
|
if (result.hostContext) {
|
|
1033
|
-
|
|
1087
|
+
// Notify (not a silent merge) so onContextChange listeners and the
|
|
1088
|
+
// host theme see the handshake's context.
|
|
1089
|
+
context.notifyContextChange(result.hostContext);
|
|
1034
1090
|
}
|
|
1035
1091
|
// Send ui/notifications/initialized to tell the host the view is ready.
|
|
1036
1092
|
// Per MCP Apps spec, the host waits for this before sending tool-result.
|
|
@@ -1040,9 +1096,10 @@ var ExtAppsAdapter = {
|
|
|
1040
1096
|
method: 'ui/notifications/initialized',
|
|
1041
1097
|
params: {}
|
|
1042
1098
|
}, targetOrigin);
|
|
1099
|
+
self.settleHandshake();
|
|
1043
1100
|
resolve();
|
|
1044
1101
|
},
|
|
1045
|
-
reject: function(err) { resolve(); }, // Non-fatal
|
|
1102
|
+
reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
|
|
1046
1103
|
timeout: timeout
|
|
1047
1104
|
};
|
|
1048
1105
|
|
|
@@ -1056,6 +1113,18 @@ var ExtAppsAdapter = {
|
|
|
1056
1113
|
performHandshake: function(context) {
|
|
1057
1114
|
return this.sendHandshake(context);
|
|
1058
1115
|
},
|
|
1116
|
+
settleHandshake: function() {
|
|
1117
|
+
// Deliver the latest size requested while no host origin was trusted.
|
|
1118
|
+
this.handshakeSettled = true;
|
|
1119
|
+
var pending = this.pendingSize;
|
|
1120
|
+
if (!pending) return;
|
|
1121
|
+
this.pendingSize = null;
|
|
1122
|
+
this.sendRequest('ui/setSize', pending.params).then(function(result) {
|
|
1123
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
|
|
1124
|
+
}, function(err) {
|
|
1125
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
|
|
1126
|
+
});
|
|
1127
|
+
},
|
|
1059
1128
|
callTool: function(context, name, args) {
|
|
1060
1129
|
if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
|
|
1061
1130
|
return Promise.reject(new Error('Server tool proxy not supported'));
|
|
@@ -1079,11 +1148,22 @@ var ExtAppsAdapter = {
|
|
|
1079
1148
|
setSize: function(context, size) {
|
|
1080
1149
|
// FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
|
|
1081
1150
|
// measured/desired widget dimensions to the host.
|
|
1082
|
-
|
|
1151
|
+
var params = {
|
|
1083
1152
|
height: size && size.height,
|
|
1084
1153
|
width: size && size.width,
|
|
1085
1154
|
aspectRatio: size && size.aspectRatio
|
|
1086
|
-
}
|
|
1155
|
+
};
|
|
1156
|
+
if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
1157
|
+
// No host origin yet: keep only the latest size and send it when the
|
|
1158
|
+
// handshake settles; every caller's promise follows that one request.
|
|
1159
|
+
var self = this;
|
|
1160
|
+
if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
|
|
1161
|
+
this.pendingSize.params = params;
|
|
1162
|
+
return new Promise(function(resolve, reject) {
|
|
1163
|
+
self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
return this.sendRequest('ui/setSize', params);
|
|
1087
1167
|
},
|
|
1088
1168
|
requestClose: function(context) {
|
|
1089
1169
|
return this.sendRequest('ui/close', {});
|
|
@@ -1329,6 +1409,7 @@ function FrontMcpBridge() {
|
|
|
1329
1409
|
toolResultListeners: [],
|
|
1330
1410
|
notifyContextChange: function(changes) {
|
|
1331
1411
|
Object.assign(this.hostContext, changes);
|
|
1412
|
+
if (changes && changes.theme) __applyHostTheme(changes.theme);
|
|
1332
1413
|
for (var i = 0; i < this.contextListeners.length; i++) {
|
|
1333
1414
|
try { this.contextListeners[i](changes); } catch(e) {}
|
|
1334
1415
|
}
|
|
@@ -2113,8 +2194,24 @@ function isFrontmcpUiResolvable(...candidatePaths) {
|
|
|
2113
2194
|
}
|
|
2114
2195
|
|
|
2115
2196
|
// libs/uipack/src/component/transpiler.ts
|
|
2197
|
+
function loadEsbuild(filename) {
|
|
2198
|
+
try {
|
|
2199
|
+
return __require("esbuild");
|
|
2200
|
+
} catch (err) {
|
|
2201
|
+
if (!isMissingEsbuild(err)) throw err;
|
|
2202
|
+
const subject = filename ? `FileSource widget "${filename}"` : "A FileSource widget";
|
|
2203
|
+
throw new Error(
|
|
2204
|
+
`${subject} needs esbuild, which is not installed. @frontmcp/uipack loads esbuild on demand to bundle FileSource widgets (.tsx/.jsx) when the tool is called, so it must be installed where the server runs: \`npm install esbuild\` (in "dependencies", not "devDependencies"). Projects created with \`frontmcp create\` already get it through the \`frontmcp\` package.`,
|
|
2205
|
+
{ cause: err }
|
|
2206
|
+
);
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
function isMissingEsbuild(err) {
|
|
2210
|
+
if (!(err instanceof Error)) return false;
|
|
2211
|
+
return err.code === "MODULE_NOT_FOUND" && /Cannot find module ['"]esbuild['"]/.test(err.message);
|
|
2212
|
+
}
|
|
2116
2213
|
function transpileReactSource(source, filename) {
|
|
2117
|
-
const esbuild =
|
|
2214
|
+
const esbuild = loadEsbuild(filename);
|
|
2118
2215
|
const loader = filename?.endsWith(".tsx") ? "tsx" : "jsx";
|
|
2119
2216
|
const result = esbuild.transformSync(source, {
|
|
2120
2217
|
loader,
|
|
@@ -2133,7 +2230,7 @@ function bundleFileSource(source, filename, resolveDir, componentName, options =
|
|
|
2133
2230
|
`FileSource widget "${filename}" requires the @frontmcp/ui package, which provides the React bridge mount that's injected at bundle time. Install it (e.g. \`npm install @frontmcp/ui\` or \`yarn add @frontmcp/ui\`) and try again.`
|
|
2134
2231
|
);
|
|
2135
2232
|
}
|
|
2136
|
-
const esbuild =
|
|
2233
|
+
const esbuild = loadEsbuild(filename);
|
|
2137
2234
|
const mountCode = `
|
|
2138
2235
|
// --- Auto-generated mount ---
|
|
2139
2236
|
import { createElement as __h } from 'react';
|
|
@@ -2230,7 +2327,8 @@ if (__root) {
|
|
|
2230
2327
|
} catch (err) {
|
|
2231
2328
|
const message = err instanceof Error ? err.message : String(err);
|
|
2232
2329
|
throw new Error(
|
|
2233
|
-
`Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project
|
|
2330
|
+
`Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`,
|
|
2331
|
+
{ cause: err }
|
|
2234
2332
|
);
|
|
2235
2333
|
}
|
|
2236
2334
|
}
|
|
@@ -2306,6 +2404,37 @@ function resolveImportSource(source) {
|
|
|
2306
2404
|
peerDependencies: []
|
|
2307
2405
|
};
|
|
2308
2406
|
}
|
|
2407
|
+
var BUILD_DIR_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "out"]);
|
|
2408
|
+
function missingWidgetError(filePath, cause) {
|
|
2409
|
+
const sourceWidget = findSourceWidget(filePath);
|
|
2410
|
+
const error = new Error(
|
|
2411
|
+
`FileSource widget not found (ENOENT): no file at "${filePath}". The widget source is read from this path when the tool is called, so it has to exist where the compiled tool runs \u2014 a path built from \`__dirname\` or \`import.meta.url\` points into the build output once the tool is compiled. tsc does not copy \`*.widget.tsx\` / \`*.widget.jsx\` files into its output; \`frontmcp build\` does, and any other build needs a step that copies them next to the compiled tool.` + (sourceWidget ? ` The source widget exists at "${sourceWidget}" \u2014 copy it to "${filePath}", or build with \`frontmcp build\`.` : ""),
|
|
2412
|
+
{ cause }
|
|
2413
|
+
);
|
|
2414
|
+
error.code = "ENOENT";
|
|
2415
|
+
error.path = filePath;
|
|
2416
|
+
return error;
|
|
2417
|
+
}
|
|
2418
|
+
function findSourceWidget(filePath) {
|
|
2419
|
+
const path = __require("path");
|
|
2420
|
+
const fs = __require("fs");
|
|
2421
|
+
const parts = filePath.split(path.sep);
|
|
2422
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
2423
|
+
if (!BUILD_DIR_SEGMENTS.has(parts[i])) continue;
|
|
2424
|
+
const candidates = [[...parts.slice(0, i), "src", ...parts.slice(i + 1)]];
|
|
2425
|
+
if (parts.length - i > 2) candidates.push([...parts.slice(0, i), "src", ...parts.slice(i + 2)]);
|
|
2426
|
+
candidates.push([...parts.slice(0, i), ...parts.slice(i + 1)]);
|
|
2427
|
+
for (const candidate of candidates) {
|
|
2428
|
+
const candidatePath = candidate.join(path.sep);
|
|
2429
|
+
try {
|
|
2430
|
+
if (fs.existsSync(candidatePath)) return candidatePath;
|
|
2431
|
+
} catch {
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
return void 0;
|
|
2435
|
+
}
|
|
2436
|
+
return void 0;
|
|
2437
|
+
}
|
|
2309
2438
|
function resolveFileSource(source, options = {}) {
|
|
2310
2439
|
const path = __require("path");
|
|
2311
2440
|
const ext = path.extname(source.file).toLowerCase();
|
|
@@ -2320,9 +2449,11 @@ function resolveFileSource(source, options = {}) {
|
|
|
2320
2449
|
const isNotFound = err?.code === "ENOENT";
|
|
2321
2450
|
if (isNotFound && wasRelative) {
|
|
2322
2451
|
throw new Error(
|
|
2323
|
-
`FileSource widget "${source.file}" not found at "${filePath}". Relative paths are resolved against process.cwd() ("${process.cwd()}"), not the tool file's directory. To anchor the path to the tool file, pass an absolute path \u2014 e.g. \`{ file: fileURLToPath(new URL('./widget.tsx', import.meta.url)) }\` from \`node:url\` (see issue #444)
|
|
2452
|
+
`FileSource widget "${source.file}" not found at "${filePath}". Relative paths are resolved against process.cwd() ("${process.cwd()}"), not the tool file's directory. To anchor the path to the tool file, pass an absolute path \u2014 e.g. \`{ file: fileURLToPath(new URL('./widget.tsx', import.meta.url)) }\` from \`node:url\` (see issue #444).`,
|
|
2453
|
+
{ cause: err }
|
|
2324
2454
|
);
|
|
2325
2455
|
}
|
|
2456
|
+
if (isNotFound) throw missingWidgetError(filePath, err);
|
|
2326
2457
|
throw err;
|
|
2327
2458
|
}
|
|
2328
2459
|
const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
|