@frontmcp/uipack 1.8.5 → 1.8.7

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.
@@ -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,12 +778,57 @@ 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
+
800
+ function extractToolOutput(params) {
801
+ // A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
802
+ if (!params) return undefined;
803
+ if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
804
+ var content = params.content;
805
+ if (Array.isArray(content)) {
806
+ for (var i = 0; i < content.length; i++) {
807
+ var item = content[i];
808
+ if (item && item.type === 'text' && typeof item.text === 'string') {
809
+ try { return JSON.parse(item.text); } catch (e) { return item.text; }
810
+ }
811
+ }
812
+ }
813
+ return content;
814
+ }
815
+
749
816
  function readInjectedData() {
750
817
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
751
818
  if (typeof window !== 'undefined') {
752
819
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
753
820
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
754
821
  if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
822
+ // The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
823
+ // (toolOutput is the tool's structuredContent).
824
+ var oa = window.openai;
825
+ if (oa && typeof oa === 'object') {
826
+ if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
827
+ if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
828
+ if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
829
+ if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
830
+ }
831
+ }
755
832
  }
756
833
  return data;
757
834
  }
@@ -800,7 +877,24 @@ var OpenAIAdapter = {
800
877
  if (sdk.displayMode) {
801
878
  context.hostContext.displayMode = sdk.displayMode;
802
879
  }
803
- // Note: OpenAI SDK does not have an onContextChange equivalent
880
+ // The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
881
+ window.addEventListener('openai:set_globals', function(event) {
882
+ var globals = (event && event.detail && event.detail.globals) || window.openai || {};
883
+ if (globals.toolInput !== undefined && globals.toolInput !== null) {
884
+ context.toolInput = globals.toolInput;
885
+ window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
886
+ }
887
+ if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
888
+ context.structuredContent = globals.toolOutput;
889
+ context.notifyToolResult(globals.toolOutput);
890
+ }
891
+ if (globals.theme && globals.theme !== context.hostContext.theme) {
892
+ context.notifyContextChange({ theme: globals.theme });
893
+ }
894
+ if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
895
+ context.notifyContextChange({ displayMode: globals.displayMode });
896
+ }
897
+ });
804
898
  return Promise.resolve();
805
899
  },
806
900
  callTool: function(context, name, args) {
@@ -852,6 +946,8 @@ var ExtAppsAdapter = {
852
946
  pendingRequests: {},
853
947
  requestId: 0,
854
948
  hostCapabilities: {},
949
+ handshakeSettled: false,
950
+ pendingSize: null,
855
951
  canHandle: function() {
856
952
  if (typeof window === 'undefined') return false;
857
953
  if (window.parent === window) return false;
@@ -945,9 +1041,10 @@ var ExtAppsAdapter = {
945
1041
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
946
1042
  break;
947
1043
  case 'ui/notifications/tool-result':
948
- context.toolOutput = params.content;
1044
+ var resultOutput = extractToolOutput(params);
1045
+ context.toolOutput = resultOutput;
949
1046
  context.structuredContent = params.structuredContent;
950
- context.notifyToolResult(params.content);
1047
+ context.notifyToolResult(resultOutput);
951
1048
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
952
1049
  break;
953
1050
  case 'ui/notifications/host-context-changed':
@@ -1001,6 +1098,15 @@ var ExtAppsAdapter = {
1001
1098
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
1002
1099
  });
1003
1100
  },
1101
+ sendNotification: function(method, params) {
1102
+ // JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
1103
+ if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
1104
+ return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
1105
+ }
1106
+ var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
1107
+ window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
1108
+ return Promise.resolve();
1109
+ },
1004
1110
  sendHandshake: function(context) {
1005
1111
  // Send ui/initialize using '*' as target origin since TOFU hasn't
1006
1112
  // been established yet. The response from the host will establish
@@ -1017,6 +1123,7 @@ var ExtAppsAdapter = {
1017
1123
  var timeout = setTimeout(function() {
1018
1124
  delete self.pendingRequests[id];
1019
1125
  // Handshake timeout is non-fatal \u2014 notifications may still arrive
1126
+ self.settleHandshake();
1020
1127
  resolve();
1021
1128
  }, 10000);
1022
1129
 
@@ -1030,7 +1137,9 @@ var ExtAppsAdapter = {
1030
1137
  supportsDisplayModes: true
1031
1138
  });
1032
1139
  if (result.hostContext) {
1033
- Object.assign(context.hostContext, result.hostContext);
1140
+ // Notify (not a silent merge) so onContextChange listeners and the
1141
+ // host theme see the handshake's context.
1142
+ context.notifyContextChange(result.hostContext);
1034
1143
  }
1035
1144
  // Send ui/notifications/initialized to tell the host the view is ready.
1036
1145
  // Per MCP Apps spec, the host waits for this before sending tool-result.
@@ -1040,9 +1149,10 @@ var ExtAppsAdapter = {
1040
1149
  method: 'ui/notifications/initialized',
1041
1150
  params: {}
1042
1151
  }, targetOrigin);
1152
+ self.settleHandshake();
1043
1153
  resolve();
1044
1154
  },
1045
- reject: function(err) { resolve(); }, // Non-fatal
1155
+ reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
1046
1156
  timeout: timeout
1047
1157
  };
1048
1158
 
@@ -1056,6 +1166,18 @@ var ExtAppsAdapter = {
1056
1166
  performHandshake: function(context) {
1057
1167
  return this.sendHandshake(context);
1058
1168
  },
1169
+ settleHandshake: function() {
1170
+ // Deliver the latest size requested while no host origin was trusted.
1171
+ this.handshakeSettled = true;
1172
+ var pending = this.pendingSize;
1173
+ if (!pending) return;
1174
+ this.pendingSize = null;
1175
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
1176
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
1177
+ }, function(err) {
1178
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
1179
+ });
1180
+ },
1059
1181
  callTool: function(context, name, args) {
1060
1182
  if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
1061
1183
  return Promise.reject(new Error('Server tool proxy not supported'));
@@ -1077,13 +1199,22 @@ var ExtAppsAdapter = {
1077
1199
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
1078
1200
  },
1079
1201
  setSize: function(context, size) {
1080
- // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
1081
- // measured/desired widget dimensions to the host.
1082
- return this.sendRequest('ui/setSize', {
1083
- height: size && size.height,
1084
- width: size && size.width,
1085
- aspectRatio: size && size.aspectRatio
1086
- });
1202
+ // Standard ext-apps sizing: the view tells the host its dimensions with the
1203
+ // ui/notifications/size-changed notification ({ width?, height? } in px).
1204
+ var params = {};
1205
+ if (size && typeof size.width === 'number') params.width = size.width;
1206
+ if (size && typeof size.height === 'number') params.height = size.height;
1207
+ if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
1208
+ // No host origin yet: keep only the latest size and send it when the
1209
+ // handshake settles; every caller's promise follows that one request.
1210
+ var self = this;
1211
+ if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
1212
+ this.pendingSize.params = params;
1213
+ return new Promise(function(resolve, reject) {
1214
+ self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
1215
+ });
1216
+ }
1217
+ return this.sendNotification('ui/notifications/size-changed', params);
1087
1218
  },
1088
1219
  requestClose: function(context) {
1089
1220
  return this.sendRequest('ui/close', {});
@@ -1329,6 +1460,7 @@ function FrontMcpBridge() {
1329
1460
  toolResultListeners: [],
1330
1461
  notifyContextChange: function(changes) {
1331
1462
  Object.assign(this.hostContext, changes);
1463
+ if (changes && changes.theme) __applyHostTheme(changes.theme);
1332
1464
  for (var i = 0; i < this.contextListeners.length; i++) {
1333
1465
  try { this.contextListeners[i](changes); } catch(e) {}
1334
1466
  }
@@ -1505,7 +1637,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1505
1637
 
1506
1638
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1507
1639
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1508
- // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1640
+ // ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
1509
1641
  FrontMcpBridge.prototype.setSize = function(size) {
1510
1642
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1511
1643
  if (!this._adapter.setSize) return Promise.resolve();
@@ -1607,7 +1739,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
1607
1739
  target.classList.add('opacity-50', 'cursor-not-allowed');
1608
1740
 
1609
1741
  // Add spinner for buttons
1610
- var spinner = '<svg class="animate-spin -ml-1 mr-2 h-4 w-4 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
1742
+ var spinner = '<svg class="animate-spin -ml-1 mr-2 h-4 w-4 inline" style="width:1em;height:1em;vertical-align:-0.125em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
1611
1743
  if (target.tagName === 'BUTTON') {
1612
1744
  target.innerHTML = spinner + 'Loading...';
1613
1745
  }
@@ -1688,7 +1820,22 @@ function minifyJS(code) {
1688
1820
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
1689
1821
  return code;
1690
1822
  }
1691
- return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\s+/g, " ").replace(/\s*([{};,:()[\]])\s*/g, "$1").replace(/;\}/g, "}").trim();
1823
+ const out = [];
1824
+ let inBlockComment = false;
1825
+ for (const raw of code.split("\n")) {
1826
+ const line = raw.trim();
1827
+ if (inBlockComment) {
1828
+ if (line.includes("*/")) inBlockComment = false;
1829
+ continue;
1830
+ }
1831
+ if (line === "" || line.startsWith("//")) continue;
1832
+ if (line.startsWith("/*")) {
1833
+ if (!line.includes("*/")) inBlockComment = true;
1834
+ continue;
1835
+ }
1836
+ out.push(line);
1837
+ }
1838
+ return out.join("\n");
1692
1839
  }
1693
1840
  function generatePlatformBundle(platform, options = {}) {
1694
1841
  const platformAdapters = {
@@ -2113,8 +2260,24 @@ function isFrontmcpUiResolvable(...candidatePaths) {
2113
2260
  }
2114
2261
 
2115
2262
  // libs/uipack/src/component/transpiler.ts
2263
+ function loadEsbuild(filename) {
2264
+ try {
2265
+ return __require("esbuild");
2266
+ } catch (err) {
2267
+ if (!isMissingEsbuild(err)) throw err;
2268
+ const subject = filename ? `FileSource widget "${filename}"` : "A FileSource widget";
2269
+ throw new Error(
2270
+ `${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.`,
2271
+ { cause: err }
2272
+ );
2273
+ }
2274
+ }
2275
+ function isMissingEsbuild(err) {
2276
+ if (!(err instanceof Error)) return false;
2277
+ return err.code === "MODULE_NOT_FOUND" && /Cannot find module ['"]esbuild['"]/.test(err.message);
2278
+ }
2116
2279
  function transpileReactSource(source, filename) {
2117
- const esbuild = __require("esbuild");
2280
+ const esbuild = loadEsbuild(filename);
2118
2281
  const loader = filename?.endsWith(".tsx") ? "tsx" : "jsx";
2119
2282
  const result = esbuild.transformSync(source, {
2120
2283
  loader,
@@ -2133,7 +2296,7 @@ function bundleFileSource(source, filename, resolveDir, componentName, options =
2133
2296
  `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
2297
  );
2135
2298
  }
2136
- const esbuild = __require("esbuild");
2299
+ const esbuild = loadEsbuild(filename);
2137
2300
  const mountCode = `
2138
2301
  // --- Auto-generated mount ---
2139
2302
  import { createElement as __h } from 'react';
@@ -2230,7 +2393,8 @@ if (__root) {
2230
2393
  } catch (err) {
2231
2394
  const message = err instanceof Error ? err.message : String(err);
2232
2395
  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.`
2396
+ `Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`,
2397
+ { cause: err }
2234
2398
  );
2235
2399
  }
2236
2400
  }
@@ -2306,6 +2470,37 @@ function resolveImportSource(source) {
2306
2470
  peerDependencies: []
2307
2471
  };
2308
2472
  }
2473
+ var BUILD_DIR_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "out"]);
2474
+ function missingWidgetError(filePath, cause) {
2475
+ const sourceWidget = findSourceWidget(filePath);
2476
+ const error = new Error(
2477
+ `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\`.` : ""),
2478
+ { cause }
2479
+ );
2480
+ error.code = "ENOENT";
2481
+ error.path = filePath;
2482
+ return error;
2483
+ }
2484
+ function findSourceWidget(filePath) {
2485
+ const path = __require("path");
2486
+ const fs = __require("fs");
2487
+ const parts = filePath.split(path.sep);
2488
+ for (let i = parts.length - 2; i >= 0; i--) {
2489
+ if (!BUILD_DIR_SEGMENTS.has(parts[i])) continue;
2490
+ const candidates = [[...parts.slice(0, i), "src", ...parts.slice(i + 1)]];
2491
+ if (parts.length - i > 2) candidates.push([...parts.slice(0, i), "src", ...parts.slice(i + 2)]);
2492
+ candidates.push([...parts.slice(0, i), ...parts.slice(i + 1)]);
2493
+ for (const candidate of candidates) {
2494
+ const candidatePath = candidate.join(path.sep);
2495
+ try {
2496
+ if (fs.existsSync(candidatePath)) return candidatePath;
2497
+ } catch {
2498
+ }
2499
+ }
2500
+ return void 0;
2501
+ }
2502
+ return void 0;
2503
+ }
2309
2504
  function resolveFileSource(source, options = {}) {
2310
2505
  const path = __require("path");
2311
2506
  const ext = path.extname(source.file).toLowerCase();
@@ -2320,9 +2515,11 @@ function resolveFileSource(source, options = {}) {
2320
2515
  const isNotFound = err?.code === "ENOENT";
2321
2516
  if (isNotFound && wasRelative) {
2322
2517
  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).`
2518
+ `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).`,
2519
+ { cause: err }
2324
2520
  );
2325
2521
  }
2522
+ if (isNotFound) throw missingWidgetError(filePath, err);
2326
2523
  throw err;
2327
2524
  }
2328
2525
  const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
@@ -2700,6 +2897,86 @@ function wrapDetectedContent(value) {
2700
2897
  }
2701
2898
  }
2702
2899
 
2900
+ // libs/uipack/src/adapters/markdown.ts
2901
+ var SAFE_HREF = /^(https?:|mailto:|\/|#)/i;
2902
+ function emphasis(text) {
2903
+ return text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
2904
+ }
2905
+ function inline(text) {
2906
+ const stash = [];
2907
+ const hold = (html2) => `\uE000${stash.push(html2) - 1}\uE000`;
2908
+ let out = escapeHtml(text.replace(/\uE000/g, "")).replace(
2909
+ /`([^`]+)`/g,
2910
+ (_m, code) => hold(`<code>${code}</code>`)
2911
+ );
2912
+ out = out.replace(
2913
+ /\[([^\]]+)\]\(([^)\s]+)\)/g,
2914
+ (_m, label, href) => SAFE_HREF.test(href) ? hold(`<a href="${href}" rel="noopener noreferrer">${emphasis(label)}</a>`) : label
2915
+ );
2916
+ out = emphasis(out);
2917
+ for (let i = 0; i < 2; i++) out = out.replace(/\uE000(\d+)\uE000/g, (_m, n) => stash[Number(n)]);
2918
+ return out;
2919
+ }
2920
+ function markdownToHtml(source) {
2921
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
2922
+ const blocks = [];
2923
+ let paragraph = [];
2924
+ let list;
2925
+ const flushParagraph = () => {
2926
+ if (paragraph.length > 0) blocks.push(`<p>${inline(paragraph.join(" "))}</p>`);
2927
+ paragraph = [];
2928
+ };
2929
+ const flushList = () => {
2930
+ if (list)
2931
+ blocks.push(`<${list.tag}>${list.items.map((item) => `<li>${inline(item)}</li>`).join("")}</${list.tag}>`);
2932
+ list = void 0;
2933
+ };
2934
+ for (let i = 0; i < lines.length; i++) {
2935
+ const line = lines[i];
2936
+ const fence = /^\s*(`{3,})/.exec(line);
2937
+ if (fence) {
2938
+ flushParagraph();
2939
+ flushList();
2940
+ const fenceLength = fence[1].length;
2941
+ const isClosing = (l) => {
2942
+ const m = /^\s*(`{3,})\s*$/.exec(l);
2943
+ return m !== null && m[1].length >= fenceLength;
2944
+ };
2945
+ const code = [];
2946
+ for (i++; i < lines.length && !isClosing(lines[i]); i++) code.push(lines[i]);
2947
+ blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}
2948
+ </code></pre>`);
2949
+ continue;
2950
+ }
2951
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
2952
+ if (heading) {
2953
+ flushParagraph();
2954
+ flushList();
2955
+ blocks.push(`<h${heading[1].length}>${inline(heading[2].trim())}</h${heading[1].length}>`);
2956
+ continue;
2957
+ }
2958
+ const item = /^\s*([-*+]|\d+\.)\s+(.*)$/.exec(line);
2959
+ if (item) {
2960
+ flushParagraph();
2961
+ const tag = /\d/.test(item[1]) ? "ol" : "ul";
2962
+ if (list && list.tag !== tag) flushList();
2963
+ list ??= { tag, items: [] };
2964
+ list.items.push(item[2]);
2965
+ continue;
2966
+ }
2967
+ if (line.trim() === "") {
2968
+ flushParagraph();
2969
+ flushList();
2970
+ continue;
2971
+ }
2972
+ flushList();
2973
+ paragraph.push(line.trim());
2974
+ }
2975
+ flushParagraph();
2976
+ flushList();
2977
+ return blocks.join("\n");
2978
+ }
2979
+
2703
2980
  // libs/uipack/src/adapters/type-detector.ts
2704
2981
  function detectUIType(template) {
2705
2982
  if (template === null || template === void 0) {
@@ -2733,9 +3010,15 @@ function detectUIType(template) {
2733
3010
  }
2734
3011
 
2735
3012
  // libs/uipack/src/adapters/template-renderer.ts
2736
- function buildCspConfig(resolver) {
3013
+ function buildCspConfig(resolver, extra) {
2737
3014
  const cspResourceDomains = ["https://esm.sh"];
2738
3015
  const cspConnectDomains = ["https://esm.sh"];
3016
+ for (const domain of extra?.resourceDomains ?? []) {
3017
+ if (!cspResourceDomains.includes(domain)) cspResourceDomains.push(domain);
3018
+ }
3019
+ for (const domain of extra?.connectDomains ?? []) {
3020
+ if (!cspConnectDomains.includes(domain)) cspConnectDomains.push(domain);
3021
+ }
2739
3022
  if (resolver && "overrides" in resolver) {
2740
3023
  const overrides = resolver.overrides;
2741
3024
  if (overrides) {
@@ -2775,6 +3058,14 @@ function renderUnwrappedResult(rawResult, options) {
2775
3058
  }
2776
3059
  return rawResult;
2777
3060
  }
3061
+ function isReactElement(value) {
3062
+ return typeof value === "object" && value !== null && "$$typeof" in value;
3063
+ }
3064
+ function isDefinitelyReactComponent(template) {
3065
+ if (typeof template !== "function") return false;
3066
+ const proto = template.prototype;
3067
+ return typeof proto?.render === "function" || template["$$typeof"] !== void 0;
3068
+ }
2778
3069
  function renderToolTemplate(options) {
2779
3070
  const { toolName, input, output, template, resolver, platformType, sizing } = options;
2780
3071
  const uiType = detectUIType(template);
@@ -2785,30 +3076,43 @@ function renderToolTemplate(options) {
2785
3076
  output,
2786
3077
  includeBridge: true,
2787
3078
  resolver,
2788
- sizing
3079
+ sizing,
3080
+ csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
2789
3081
  };
2790
3082
  let html2;
2791
3083
  let hash = "";
2792
3084
  let size = 0;
2793
3085
  if (typeof template === "object" && template !== null && "file" in template) {
2794
- const cspConfig = buildCspConfig(resolver);
2795
3086
  const result = renderComponent(
2796
3087
  { source: template, inlineReact: resourceMode === "inline" },
2797
- { ...shellConfig, csp: cspConfig }
3088
+ { ...shellConfig, csp: buildCspConfig(resolver, options.csp) }
2798
3089
  );
2799
3090
  html2 = result.html;
2800
3091
  hash = result.hash;
2801
3092
  size = result.size;
2802
3093
  } else if (typeof template === "function") {
2803
- if (uiType === "react") {
3094
+ const helpers = createTemplateHelpers();
3095
+ const ctx = { input, output, helpers };
3096
+ let rawResult;
3097
+ let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
3098
+ if (isHtmlBuilder) {
3099
+ try {
3100
+ rawResult = template(ctx);
3101
+ isHtmlBuilder = !isReactElement(rawResult);
3102
+ } catch (error) {
3103
+ if (uiType !== "react") throw error;
3104
+ isHtmlBuilder = false;
3105
+ }
3106
+ }
3107
+ if (!isHtmlBuilder) {
3108
+ (options.logger ?? console).warn(
3109
+ `[FrontMCP] Tool "${toolName}": \`ui.template\` is a React component reference, which cannot be bundled (there is no source file to compile), so the widget would render an empty page. Use \`template: { file: './widget.tsx' }\` instead.`
3110
+ );
2804
3111
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
2805
3112
  html2 = shellResult.html;
2806
3113
  hash = shellResult.hash;
2807
3114
  size = shellResult.size;
2808
3115
  } else {
2809
- const helpers = createTemplateHelpers();
2810
- const ctx = { input, output, helpers };
2811
- const rawResult = template(ctx);
2812
3116
  const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
2813
3117
  if (wrapped) {
2814
3118
  html2 = wrapped;
@@ -2820,7 +3124,7 @@ function renderToolTemplate(options) {
2820
3124
  }
2821
3125
  }
2822
3126
  } else if (typeof template === "string") {
2823
- const shellResult = buildShell(template, shellConfig);
3127
+ const shellResult = buildShell(uiType === "markdown" ? markdownToHtml(template) : template, shellConfig);
2824
3128
  html2 = shellResult.html;
2825
3129
  hash = shellResult.hash;
2826
3130
  size = shellResult.size;