@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.
@@ -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 = require("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 = require("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
- var target = document.getElementById('root') || document.body;
937
- if (!target) return;
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
- var rect = target.getBoundingClientRect();
945
- var height = Math.ceil(rect.height);
946
- var width = Math.ceil(rect.width);
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(target);
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
- Object.assign(context.hostContext, result.hostContext);
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
- return this.sendRequest('ui/setSize', {
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;AA6ID;;;;;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
+ {"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;AAMH;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAkB9E;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,CA6HlB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUpE"}
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"}
@@ -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
- var target = document.getElementById('root') || document.body;
663
- if (!target) return;
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
- var rect = target.getBoundingClientRect();
671
- var height = Math.ceil(rect.height);
672
- var width = Math.ceil(rect.width);
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(target);
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
- Object.assign(context.hostContext, result.hostContext);
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
- return this.sendRequest('ui/setSize', {
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 = __require("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 = __require("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";