@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.
package/adapters/index.js CHANGED
@@ -669,6 +669,23 @@ function __applySizingCss(sizing) {
669
669
  }
670
670
  }
671
671
 
672
+ function __measureHeight() {
673
+ // <html> at fit-content includes body margins and collapsed child margins;
674
+ // content overflowing a fixed-height body is added; a px max-height clamps.
675
+ var de = document.documentElement;
676
+ var body = document.body;
677
+ if (!de || !body) return 0;
678
+ var prev = de.style.height;
679
+ de.style.height = 'fit-content';
680
+ var dr = de.getBoundingClientRect();
681
+ var br = body.getBoundingClientRect();
682
+ var h = Math.max(dr.height, br.top + body.scrollHeight + Math.max(0, dr.bottom - br.bottom) - dr.top);
683
+ de.style.height = prev;
684
+ var maxHeight = getComputedStyle(de).maxHeight;
685
+ var mx = maxHeight && maxHeight.slice(-2) === 'px' ? parseFloat(maxHeight) : NaN;
686
+ return Math.ceil(isNaN(mx) ? h : Math.min(h, mx));
687
+ }
688
+
672
689
  function __initAutoResize() {
673
690
  if (typeof window === 'undefined') return;
674
691
  // Idempotent: a re-injected IIFE must not stack observers.
@@ -691,23 +708,30 @@ function __initAutoResize() {
691
708
  if (typeof ResizeObserver === 'undefined') return;
692
709
 
693
710
  function startObserving() {
694
- var target = document.getElementById('root') || document.body;
695
- if (!target) return;
711
+ if (!document.body) return;
712
+ var targets = [document.documentElement, document.body];
713
+ var root = document.getElementById('root');
714
+ if (root) targets.push(root);
696
715
 
697
716
  var rafId = null;
698
717
  var lastReported = -1;
699
718
  function report() {
700
719
  rafId = null;
701
720
  try {
702
- var rect = target.getBoundingClientRect();
703
- var height = Math.ceil(rect.height);
704
- var width = Math.ceil(rect.width);
721
+ // Before the handshake an ext-apps host can't receive the report; the
722
+ // bridge:ready listener below sends it once the bridge is up.
723
+ if (window.FrontMcpBridge && window.FrontMcpBridge.initialized === false) return;
724
+ var height = __measureHeight();
725
+ var width = Math.ceil(document.documentElement.getBoundingClientRect().width);
705
726
  if (height === lastReported || height <= 0) return;
706
727
  lastReported = height;
707
728
  var payload = { height: height, width: width };
708
729
  if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
709
730
  if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
710
- window.FrontMcpBridge.setSize(payload).catch(function() {});
731
+ window.FrontMcpBridge.setSize(payload).catch(function() {
732
+ // Forget a rejected report so the same height is sent again.
733
+ if (lastReported === height) lastReported = -1;
734
+ });
711
735
  }
712
736
  window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
713
737
  } catch (e) {}
@@ -728,9 +752,17 @@ function __initAutoResize() {
728
752
  window.__mcpResizeObserver.disconnect();
729
753
  }
730
754
  var ro = new ResizeObserver(function() { schedule(); });
731
- ro.observe(target);
755
+ for (var i = 0; i < targets.length; i++) ro.observe(targets[i]);
732
756
  window.__mcpResizeObserver = ro;
733
757
  } catch (e) {}
758
+ // Report once the bridge is ready (the report scheduled below is skipped
759
+ // while the handshake is still pending).
760
+ var readySent = false;
761
+ window.addEventListener('bridge:ready', function() {
762
+ if (readySent) return;
763
+ readySent = true;
764
+ schedule();
765
+ });
734
766
  // Report once on init so the host gets an initial measurement.
735
767
  schedule();
736
768
  }
@@ -778,12 +810,57 @@ function detectViewport() {
778
810
  return undefined;
779
811
  }
780
812
 
813
+ function __applyHostTheme(theme) {
814
+ // Follow a host-supplied theme: <meta name="color-scheme"> sets the frame's
815
+ // canvas and form controls (a widget's own :root color-scheme still wins),
816
+ // <html data-theme> is a styling hook. The OS fallback is never written.
817
+ if (theme !== 'light' && theme !== 'dark') return;
818
+ if (typeof document === 'undefined' || !document.documentElement) return;
819
+ try {
820
+ var meta = document.querySelector('meta[name="color-scheme"]');
821
+ var head = document.head || document.getElementsByTagName('head')[0];
822
+ if (!meta && head) {
823
+ meta = document.createElement('meta');
824
+ meta.setAttribute('name', 'color-scheme');
825
+ head.appendChild(meta);
826
+ }
827
+ if (meta) meta.setAttribute('content', theme);
828
+ document.documentElement.setAttribute('data-theme', theme);
829
+ } catch (e) {}
830
+ }
831
+
832
+ function extractToolOutput(params) {
833
+ // A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
834
+ if (!params) return undefined;
835
+ if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
836
+ var content = params.content;
837
+ if (Array.isArray(content)) {
838
+ for (var i = 0; i < content.length; i++) {
839
+ var item = content[i];
840
+ if (item && item.type === 'text' && typeof item.text === 'string') {
841
+ try { return JSON.parse(item.text); } catch (e) { return item.text; }
842
+ }
843
+ }
844
+ }
845
+ return content;
846
+ }
847
+
781
848
  function readInjectedData() {
782
849
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
783
850
  if (typeof window !== 'undefined') {
784
851
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
785
852
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
786
853
  if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
854
+ // The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
855
+ // (toolOutput is the tool's structuredContent).
856
+ var oa = window.openai;
857
+ if (oa && typeof oa === 'object') {
858
+ if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
859
+ if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
860
+ if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
861
+ if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
862
+ }
863
+ }
787
864
  }
788
865
  return data;
789
866
  }
@@ -832,7 +909,24 @@ var OpenAIAdapter = {
832
909
  if (sdk.displayMode) {
833
910
  context.hostContext.displayMode = sdk.displayMode;
834
911
  }
835
- // Note: OpenAI SDK does not have an onContextChange equivalent
912
+ // The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
913
+ window.addEventListener('openai:set_globals', function(event) {
914
+ var globals = (event && event.detail && event.detail.globals) || window.openai || {};
915
+ if (globals.toolInput !== undefined && globals.toolInput !== null) {
916
+ context.toolInput = globals.toolInput;
917
+ window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
918
+ }
919
+ if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
920
+ context.structuredContent = globals.toolOutput;
921
+ context.notifyToolResult(globals.toolOutput);
922
+ }
923
+ if (globals.theme && globals.theme !== context.hostContext.theme) {
924
+ context.notifyContextChange({ theme: globals.theme });
925
+ }
926
+ if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
927
+ context.notifyContextChange({ displayMode: globals.displayMode });
928
+ }
929
+ });
836
930
  return Promise.resolve();
837
931
  },
838
932
  callTool: function(context, name, args) {
@@ -884,6 +978,8 @@ var ExtAppsAdapter = {
884
978
  pendingRequests: {},
885
979
  requestId: 0,
886
980
  hostCapabilities: {},
981
+ handshakeSettled: false,
982
+ pendingSize: null,
887
983
  canHandle: function() {
888
984
  if (typeof window === 'undefined') return false;
889
985
  if (window.parent === window) return false;
@@ -977,9 +1073,10 @@ var ExtAppsAdapter = {
977
1073
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
978
1074
  break;
979
1075
  case 'ui/notifications/tool-result':
980
- context.toolOutput = params.content;
1076
+ var resultOutput = extractToolOutput(params);
1077
+ context.toolOutput = resultOutput;
981
1078
  context.structuredContent = params.structuredContent;
982
- context.notifyToolResult(params.content);
1079
+ context.notifyToolResult(resultOutput);
983
1080
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
984
1081
  break;
985
1082
  case 'ui/notifications/host-context-changed':
@@ -1033,6 +1130,15 @@ var ExtAppsAdapter = {
1033
1130
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
1034
1131
  });
1035
1132
  },
1133
+ sendNotification: function(method, params) {
1134
+ // JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
1135
+ if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
1136
+ return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
1137
+ }
1138
+ var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
1139
+ window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
1140
+ return Promise.resolve();
1141
+ },
1036
1142
  sendHandshake: function(context) {
1037
1143
  // Send ui/initialize using '*' as target origin since TOFU hasn't
1038
1144
  // been established yet. The response from the host will establish
@@ -1049,6 +1155,7 @@ var ExtAppsAdapter = {
1049
1155
  var timeout = setTimeout(function() {
1050
1156
  delete self.pendingRequests[id];
1051
1157
  // Handshake timeout is non-fatal \u2014 notifications may still arrive
1158
+ self.settleHandshake();
1052
1159
  resolve();
1053
1160
  }, 10000);
1054
1161
 
@@ -1062,7 +1169,9 @@ var ExtAppsAdapter = {
1062
1169
  supportsDisplayModes: true
1063
1170
  });
1064
1171
  if (result.hostContext) {
1065
- Object.assign(context.hostContext, result.hostContext);
1172
+ // Notify (not a silent merge) so onContextChange listeners and the
1173
+ // host theme see the handshake's context.
1174
+ context.notifyContextChange(result.hostContext);
1066
1175
  }
1067
1176
  // Send ui/notifications/initialized to tell the host the view is ready.
1068
1177
  // Per MCP Apps spec, the host waits for this before sending tool-result.
@@ -1072,9 +1181,10 @@ var ExtAppsAdapter = {
1072
1181
  method: 'ui/notifications/initialized',
1073
1182
  params: {}
1074
1183
  }, targetOrigin);
1184
+ self.settleHandshake();
1075
1185
  resolve();
1076
1186
  },
1077
- reject: function(err) { resolve(); }, // Non-fatal
1187
+ reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
1078
1188
  timeout: timeout
1079
1189
  };
1080
1190
 
@@ -1088,6 +1198,18 @@ var ExtAppsAdapter = {
1088
1198
  performHandshake: function(context) {
1089
1199
  return this.sendHandshake(context);
1090
1200
  },
1201
+ settleHandshake: function() {
1202
+ // Deliver the latest size requested while no host origin was trusted.
1203
+ this.handshakeSettled = true;
1204
+ var pending = this.pendingSize;
1205
+ if (!pending) return;
1206
+ this.pendingSize = null;
1207
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
1208
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
1209
+ }, function(err) {
1210
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
1211
+ });
1212
+ },
1091
1213
  callTool: function(context, name, args) {
1092
1214
  if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
1093
1215
  return Promise.reject(new Error('Server tool proxy not supported'));
@@ -1109,13 +1231,22 @@ var ExtAppsAdapter = {
1109
1231
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
1110
1232
  },
1111
1233
  setSize: function(context, size) {
1112
- // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
1113
- // measured/desired widget dimensions to the host.
1114
- return this.sendRequest('ui/setSize', {
1115
- height: size && size.height,
1116
- width: size && size.width,
1117
- aspectRatio: size && size.aspectRatio
1118
- });
1234
+ // Standard ext-apps sizing: the view tells the host its dimensions with the
1235
+ // ui/notifications/size-changed notification ({ width?, height? } in px).
1236
+ var params = {};
1237
+ if (size && typeof size.width === 'number') params.width = size.width;
1238
+ if (size && typeof size.height === 'number') params.height = size.height;
1239
+ if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
1240
+ // No host origin yet: keep only the latest size and send it when the
1241
+ // handshake settles; every caller's promise follows that one request.
1242
+ var self = this;
1243
+ if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
1244
+ this.pendingSize.params = params;
1245
+ return new Promise(function(resolve, reject) {
1246
+ self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
1247
+ });
1248
+ }
1249
+ return this.sendNotification('ui/notifications/size-changed', params);
1119
1250
  },
1120
1251
  requestClose: function(context) {
1121
1252
  return this.sendRequest('ui/close', {});
@@ -1361,6 +1492,7 @@ function FrontMcpBridge() {
1361
1492
  toolResultListeners: [],
1362
1493
  notifyContextChange: function(changes) {
1363
1494
  Object.assign(this.hostContext, changes);
1495
+ if (changes && changes.theme) __applyHostTheme(changes.theme);
1364
1496
  for (var i = 0; i < this.contextListeners.length; i++) {
1365
1497
  try { this.contextListeners[i](changes); } catch(e) {}
1366
1498
  }
@@ -1537,7 +1669,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1537
1669
 
1538
1670
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1539
1671
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1540
- // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1672
+ // ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
1541
1673
  FrontMcpBridge.prototype.setSize = function(size) {
1542
1674
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1543
1675
  if (!this._adapter.setSize) return Promise.resolve();
@@ -1639,7 +1771,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
1639
1771
  target.classList.add('opacity-50', 'cursor-not-allowed');
1640
1772
 
1641
1773
  // Add spinner for buttons
1642
- 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>';
1774
+ 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>';
1643
1775
  if (target.tagName === 'BUTTON') {
1644
1776
  target.innerHTML = spinner + 'Loading...';
1645
1777
  }
@@ -1720,7 +1852,22 @@ function minifyJS(code) {
1720
1852
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
1721
1853
  return code;
1722
1854
  }
1723
- return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\s+/g, " ").replace(/\s*([{};,:()[\]])\s*/g, "$1").replace(/;\}/g, "}").trim();
1855
+ const out = [];
1856
+ let inBlockComment = false;
1857
+ for (const raw of code.split("\n")) {
1858
+ const line = raw.trim();
1859
+ if (inBlockComment) {
1860
+ if (line.includes("*/")) inBlockComment = false;
1861
+ continue;
1862
+ }
1863
+ if (line === "" || line.startsWith("//")) continue;
1864
+ if (line.startsWith("/*")) {
1865
+ if (!line.includes("*/")) inBlockComment = true;
1866
+ continue;
1867
+ }
1868
+ out.push(line);
1869
+ }
1870
+ return out.join("\n");
1724
1871
  }
1725
1872
  function generatePlatformBundle(platform, options = {}) {
1726
1873
  const platformAdapters = {
@@ -2145,8 +2292,24 @@ function isFrontmcpUiResolvable(...candidatePaths) {
2145
2292
  }
2146
2293
 
2147
2294
  // libs/uipack/src/component/transpiler.ts
2295
+ function loadEsbuild(filename) {
2296
+ try {
2297
+ return require("esbuild");
2298
+ } catch (err) {
2299
+ if (!isMissingEsbuild(err)) throw err;
2300
+ const subject = filename ? `FileSource widget "${filename}"` : "A FileSource widget";
2301
+ throw new Error(
2302
+ `${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.`,
2303
+ { cause: err }
2304
+ );
2305
+ }
2306
+ }
2307
+ function isMissingEsbuild(err) {
2308
+ if (!(err instanceof Error)) return false;
2309
+ return err.code === "MODULE_NOT_FOUND" && /Cannot find module ['"]esbuild['"]/.test(err.message);
2310
+ }
2148
2311
  function transpileReactSource(source, filename) {
2149
- const esbuild = require("esbuild");
2312
+ const esbuild = loadEsbuild(filename);
2150
2313
  const loader = filename?.endsWith(".tsx") ? "tsx" : "jsx";
2151
2314
  const result = esbuild.transformSync(source, {
2152
2315
  loader,
@@ -2165,7 +2328,7 @@ function bundleFileSource(source, filename, resolveDir, componentName, options =
2165
2328
  `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.`
2166
2329
  );
2167
2330
  }
2168
- const esbuild = require("esbuild");
2331
+ const esbuild = loadEsbuild(filename);
2169
2332
  const mountCode = `
2170
2333
  // --- Auto-generated mount ---
2171
2334
  import { createElement as __h } from 'react';
@@ -2262,7 +2425,8 @@ if (__root) {
2262
2425
  } catch (err) {
2263
2426
  const message = err instanceof Error ? err.message : String(err);
2264
2427
  throw new Error(
2265
- `Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`
2428
+ `Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`,
2429
+ { cause: err }
2266
2430
  );
2267
2431
  }
2268
2432
  }
@@ -2338,6 +2502,37 @@ function resolveImportSource(source) {
2338
2502
  peerDependencies: []
2339
2503
  };
2340
2504
  }
2505
+ var BUILD_DIR_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "out"]);
2506
+ function missingWidgetError(filePath, cause) {
2507
+ const sourceWidget = findSourceWidget(filePath);
2508
+ const error = new Error(
2509
+ `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\`.` : ""),
2510
+ { cause }
2511
+ );
2512
+ error.code = "ENOENT";
2513
+ error.path = filePath;
2514
+ return error;
2515
+ }
2516
+ function findSourceWidget(filePath) {
2517
+ const path = require("path");
2518
+ const fs = require("fs");
2519
+ const parts = filePath.split(path.sep);
2520
+ for (let i = parts.length - 2; i >= 0; i--) {
2521
+ if (!BUILD_DIR_SEGMENTS.has(parts[i])) continue;
2522
+ const candidates = [[...parts.slice(0, i), "src", ...parts.slice(i + 1)]];
2523
+ if (parts.length - i > 2) candidates.push([...parts.slice(0, i), "src", ...parts.slice(i + 2)]);
2524
+ candidates.push([...parts.slice(0, i), ...parts.slice(i + 1)]);
2525
+ for (const candidate of candidates) {
2526
+ const candidatePath = candidate.join(path.sep);
2527
+ try {
2528
+ if (fs.existsSync(candidatePath)) return candidatePath;
2529
+ } catch {
2530
+ }
2531
+ }
2532
+ return void 0;
2533
+ }
2534
+ return void 0;
2535
+ }
2341
2536
  function resolveFileSource(source, options = {}) {
2342
2537
  const path = require("path");
2343
2538
  const ext = path.extname(source.file).toLowerCase();
@@ -2352,9 +2547,11 @@ function resolveFileSource(source, options = {}) {
2352
2547
  const isNotFound = err?.code === "ENOENT";
2353
2548
  if (isNotFound && wasRelative) {
2354
2549
  throw new Error(
2355
- `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).`
2550
+ `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).`,
2551
+ { cause: err }
2356
2552
  );
2357
2553
  }
2554
+ if (isNotFound) throw missingWidgetError(filePath, err);
2358
2555
  throw err;
2359
2556
  }
2360
2557
  const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
@@ -2732,6 +2929,86 @@ function wrapDetectedContent(value) {
2732
2929
  }
2733
2930
  }
2734
2931
 
2932
+ // libs/uipack/src/adapters/markdown.ts
2933
+ var SAFE_HREF = /^(https?:|mailto:|\/|#)/i;
2934
+ function emphasis(text) {
2935
+ return text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
2936
+ }
2937
+ function inline(text) {
2938
+ const stash = [];
2939
+ const hold = (html2) => `\uE000${stash.push(html2) - 1}\uE000`;
2940
+ let out = escapeHtml(text.replace(/\uE000/g, "")).replace(
2941
+ /`([^`]+)`/g,
2942
+ (_m, code) => hold(`<code>${code}</code>`)
2943
+ );
2944
+ out = out.replace(
2945
+ /\[([^\]]+)\]\(([^)\s]+)\)/g,
2946
+ (_m, label, href) => SAFE_HREF.test(href) ? hold(`<a href="${href}" rel="noopener noreferrer">${emphasis(label)}</a>`) : label
2947
+ );
2948
+ out = emphasis(out);
2949
+ for (let i = 0; i < 2; i++) out = out.replace(/\uE000(\d+)\uE000/g, (_m, n) => stash[Number(n)]);
2950
+ return out;
2951
+ }
2952
+ function markdownToHtml(source) {
2953
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
2954
+ const blocks = [];
2955
+ let paragraph = [];
2956
+ let list;
2957
+ const flushParagraph = () => {
2958
+ if (paragraph.length > 0) blocks.push(`<p>${inline(paragraph.join(" "))}</p>`);
2959
+ paragraph = [];
2960
+ };
2961
+ const flushList = () => {
2962
+ if (list)
2963
+ blocks.push(`<${list.tag}>${list.items.map((item) => `<li>${inline(item)}</li>`).join("")}</${list.tag}>`);
2964
+ list = void 0;
2965
+ };
2966
+ for (let i = 0; i < lines.length; i++) {
2967
+ const line = lines[i];
2968
+ const fence = /^\s*(`{3,})/.exec(line);
2969
+ if (fence) {
2970
+ flushParagraph();
2971
+ flushList();
2972
+ const fenceLength = fence[1].length;
2973
+ const isClosing = (l) => {
2974
+ const m = /^\s*(`{3,})\s*$/.exec(l);
2975
+ return m !== null && m[1].length >= fenceLength;
2976
+ };
2977
+ const code = [];
2978
+ for (i++; i < lines.length && !isClosing(lines[i]); i++) code.push(lines[i]);
2979
+ blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}
2980
+ </code></pre>`);
2981
+ continue;
2982
+ }
2983
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
2984
+ if (heading) {
2985
+ flushParagraph();
2986
+ flushList();
2987
+ blocks.push(`<h${heading[1].length}>${inline(heading[2].trim())}</h${heading[1].length}>`);
2988
+ continue;
2989
+ }
2990
+ const item = /^\s*([-*+]|\d+\.)\s+(.*)$/.exec(line);
2991
+ if (item) {
2992
+ flushParagraph();
2993
+ const tag = /\d/.test(item[1]) ? "ol" : "ul";
2994
+ if (list && list.tag !== tag) flushList();
2995
+ list ??= { tag, items: [] };
2996
+ list.items.push(item[2]);
2997
+ continue;
2998
+ }
2999
+ if (line.trim() === "") {
3000
+ flushParagraph();
3001
+ flushList();
3002
+ continue;
3003
+ }
3004
+ flushList();
3005
+ paragraph.push(line.trim());
3006
+ }
3007
+ flushParagraph();
3008
+ flushList();
3009
+ return blocks.join("\n");
3010
+ }
3011
+
2735
3012
  // libs/uipack/src/adapters/type-detector.ts
2736
3013
  function detectUIType(template) {
2737
3014
  if (template === null || template === void 0) {
@@ -2765,9 +3042,15 @@ function detectUIType(template) {
2765
3042
  }
2766
3043
 
2767
3044
  // libs/uipack/src/adapters/template-renderer.ts
2768
- function buildCspConfig(resolver) {
3045
+ function buildCspConfig(resolver, extra) {
2769
3046
  const cspResourceDomains = ["https://esm.sh"];
2770
3047
  const cspConnectDomains = ["https://esm.sh"];
3048
+ for (const domain of extra?.resourceDomains ?? []) {
3049
+ if (!cspResourceDomains.includes(domain)) cspResourceDomains.push(domain);
3050
+ }
3051
+ for (const domain of extra?.connectDomains ?? []) {
3052
+ if (!cspConnectDomains.includes(domain)) cspConnectDomains.push(domain);
3053
+ }
2771
3054
  if (resolver && "overrides" in resolver) {
2772
3055
  const overrides = resolver.overrides;
2773
3056
  if (overrides) {
@@ -2807,6 +3090,14 @@ function renderUnwrappedResult(rawResult, options) {
2807
3090
  }
2808
3091
  return rawResult;
2809
3092
  }
3093
+ function isReactElement(value) {
3094
+ return typeof value === "object" && value !== null && "$$typeof" in value;
3095
+ }
3096
+ function isDefinitelyReactComponent(template) {
3097
+ if (typeof template !== "function") return false;
3098
+ const proto = template.prototype;
3099
+ return typeof proto?.render === "function" || template["$$typeof"] !== void 0;
3100
+ }
2810
3101
  function renderToolTemplate(options) {
2811
3102
  const { toolName, input, output, template, resolver, platformType, sizing } = options;
2812
3103
  const uiType = detectUIType(template);
@@ -2817,30 +3108,43 @@ function renderToolTemplate(options) {
2817
3108
  output,
2818
3109
  includeBridge: true,
2819
3110
  resolver,
2820
- sizing
3111
+ sizing,
3112
+ csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
2821
3113
  };
2822
3114
  let html2;
2823
3115
  let hash = "";
2824
3116
  let size = 0;
2825
3117
  if (typeof template === "object" && template !== null && "file" in template) {
2826
- const cspConfig = buildCspConfig(resolver);
2827
3118
  const result = renderComponent(
2828
3119
  { source: template, inlineReact: resourceMode === "inline" },
2829
- { ...shellConfig, csp: cspConfig }
3120
+ { ...shellConfig, csp: buildCspConfig(resolver, options.csp) }
2830
3121
  );
2831
3122
  html2 = result.html;
2832
3123
  hash = result.hash;
2833
3124
  size = result.size;
2834
3125
  } else if (typeof template === "function") {
2835
- if (uiType === "react") {
3126
+ const helpers = createTemplateHelpers();
3127
+ const ctx = { input, output, helpers };
3128
+ let rawResult;
3129
+ let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
3130
+ if (isHtmlBuilder) {
3131
+ try {
3132
+ rawResult = template(ctx);
3133
+ isHtmlBuilder = !isReactElement(rawResult);
3134
+ } catch (error) {
3135
+ if (uiType !== "react") throw error;
3136
+ isHtmlBuilder = false;
3137
+ }
3138
+ }
3139
+ if (!isHtmlBuilder) {
3140
+ (options.logger ?? console).warn(
3141
+ `[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.`
3142
+ );
2836
3143
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
2837
3144
  html2 = shellResult.html;
2838
3145
  hash = shellResult.hash;
2839
3146
  size = shellResult.size;
2840
3147
  } else {
2841
- const helpers = createTemplateHelpers();
2842
- const ctx = { input, output, helpers };
2843
- const rawResult = template(ctx);
2844
3148
  const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
2845
3149
  if (wrapped) {
2846
3150
  html2 = wrapped;
@@ -2852,7 +3156,7 @@ function renderToolTemplate(options) {
2852
3156
  }
2853
3157
  }
2854
3158
  } else if (typeof template === "string") {
2855
- const shellResult = buildShell(template, shellConfig);
3159
+ const shellResult = buildShell(uiType === "markdown" ? markdownToHtml(template) : template, shellConfig);
2856
3160
  html2 = shellResult.html;
2857
3161
  hash = shellResult.hash;
2858
3162
  size = shellResult.size;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Minimal, safe Markdown to HTML conversion for `ui.template` strings.
3
+ *
4
+ * Supports headings, paragraphs, fenced code, ordered/unordered lists and inline
5
+ * bold, italic, code and http(s)/mailto links. Everything else is escaped text; raw HTML in the
6
+ * source is never passed through.
7
+ */
8
+ export declare function markdownToHtml(source: string): string;
9
+ //# sourceMappingURL=markdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/adapters/markdown.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA2BH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAgErD"}
@@ -36,6 +36,14 @@ export interface RenderToolTemplateOptions {
36
36
  * shell, and is mirrored onto the returned `meta` as `ui/preferredHeight` etc.
37
37
  */
38
38
  sizing?: WidgetSizing;
39
+ /**
40
+ * Extra origins the widget may load from (`resourceDomains`) or call (`connectDomains`), from the
41
+ * tool's `ui.csp`. Merged into the page's own Content-Security-Policy on every template path.
42
+ */
43
+ csp?: {
44
+ connectDomains?: string[];
45
+ resourceDomains?: string[];
46
+ };
39
47
  /**
40
48
  * HTML-escape a plain string returned by a template function; results built with
41
49
  * `html` / `trustedHtml` stay markup. When unset, strings that look like HTML render as
@@ -1 +1 @@
1
- {"version":3,"file":"template-renderer.d.ts","sourceRoot":"","sources":["../../src/adapters/template-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAIxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAOnD;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,kBAAkB;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,qDAAqD;IACrD,QAAQ,EAAE,OAAO,CAAC;IAClB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAChC;;;;OAIG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,yEAAyE;IACzE,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CAC9C;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AA6DD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,CAsG/F"}
1
+ {"version":3,"file":"template-renderer.d.ts","sourceRoot":"","sources":["../../src/adapters/template-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAIxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAQnD;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,kBAAkB;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,qDAAqD;IACrD,QAAQ,EAAE,OAAO,CAAC;IAClB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAChC;;;;OAIG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;OAGG;IACH,GAAG,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChE;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,yEAAyE;IACzE,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CAC9C;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AA+ED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,CAsH/F"}
@@ -1 +1 @@
1
- {"version":3,"file":"iife-generator.d.ts","sourceRoot":"","sources":["../../src/bridge-runtime/iife-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC,EAAE,CAAC;IACvE,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,wBAAwB;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,CA8G7E;AAooCD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,EACvD,OAAO,GAAE,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAM,GACnD,MAAM,CAYR;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,QAAuB,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;CAK9B,CAAC"}
1
+ {"version":3,"file":"iife-generator.d.ts","sourceRoot":"","sources":["../../src/bridge-runtime/iife-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC,EAAE,CAAC;IACvE,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,wBAAwB;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,CA8G7E;AA8xCD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,EACvD,OAAO,GAAE,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAM,GACnD,MAAM,CAYR;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,QAAuB,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;CAK9B,CAAC"}