@frontmcp/uipack 1.8.6 → 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.
@@ -1121,12 +1121,38 @@ function __applyHostTheme(theme) {
1121
1121
  } catch (e) {}
1122
1122
  }
1123
1123
 
1124
+ function extractToolOutput(params) {
1125
+ // A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
1126
+ if (!params) return undefined;
1127
+ if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
1128
+ var content = params.content;
1129
+ if (Array.isArray(content)) {
1130
+ for (var i = 0; i < content.length; i++) {
1131
+ var item = content[i];
1132
+ if (item && item.type === 'text' && typeof item.text === 'string') {
1133
+ try { return JSON.parse(item.text); } catch (e) { return item.text; }
1134
+ }
1135
+ }
1136
+ }
1137
+ return content;
1138
+ }
1139
+
1124
1140
  function readInjectedData() {
1125
1141
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
1126
1142
  if (typeof window !== 'undefined') {
1127
1143
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
1128
1144
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
1129
1145
  if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
1146
+ // The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
1147
+ // (toolOutput is the tool's structuredContent).
1148
+ var oa = window.openai;
1149
+ if (oa && typeof oa === 'object') {
1150
+ if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
1151
+ if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
1152
+ if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
1153
+ if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
1154
+ }
1155
+ }
1130
1156
  }
1131
1157
  return data;
1132
1158
  }
@@ -1175,7 +1201,24 @@ var OpenAIAdapter = {
1175
1201
  if (sdk.displayMode) {
1176
1202
  context.hostContext.displayMode = sdk.displayMode;
1177
1203
  }
1178
- // Note: OpenAI SDK does not have an onContextChange equivalent
1204
+ // The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
1205
+ window.addEventListener('openai:set_globals', function(event) {
1206
+ var globals = (event && event.detail && event.detail.globals) || window.openai || {};
1207
+ if (globals.toolInput !== undefined && globals.toolInput !== null) {
1208
+ context.toolInput = globals.toolInput;
1209
+ window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
1210
+ }
1211
+ if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
1212
+ context.structuredContent = globals.toolOutput;
1213
+ context.notifyToolResult(globals.toolOutput);
1214
+ }
1215
+ if (globals.theme && globals.theme !== context.hostContext.theme) {
1216
+ context.notifyContextChange({ theme: globals.theme });
1217
+ }
1218
+ if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
1219
+ context.notifyContextChange({ displayMode: globals.displayMode });
1220
+ }
1221
+ });
1179
1222
  return Promise.resolve();
1180
1223
  },
1181
1224
  callTool: function(context, name, args) {
@@ -1322,9 +1365,10 @@ var ExtAppsAdapter = {
1322
1365
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
1323
1366
  break;
1324
1367
  case 'ui/notifications/tool-result':
1325
- context.toolOutput = params.content;
1368
+ var resultOutput = extractToolOutput(params);
1369
+ context.toolOutput = resultOutput;
1326
1370
  context.structuredContent = params.structuredContent;
1327
- context.notifyToolResult(params.content);
1371
+ context.notifyToolResult(resultOutput);
1328
1372
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
1329
1373
  break;
1330
1374
  case 'ui/notifications/host-context-changed':
@@ -1378,6 +1422,15 @@ var ExtAppsAdapter = {
1378
1422
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
1379
1423
  });
1380
1424
  },
1425
+ sendNotification: function(method, params) {
1426
+ // JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
1427
+ if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
1428
+ return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
1429
+ }
1430
+ var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
1431
+ window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
1432
+ return Promise.resolve();
1433
+ },
1381
1434
  sendHandshake: function(context) {
1382
1435
  // Send ui/initialize using '*' as target origin since TOFU hasn't
1383
1436
  // been established yet. The response from the host will establish
@@ -1443,7 +1496,7 @@ var ExtAppsAdapter = {
1443
1496
  var pending = this.pendingSize;
1444
1497
  if (!pending) return;
1445
1498
  this.pendingSize = null;
1446
- this.sendRequest('ui/setSize', pending.params).then(function(result) {
1499
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
1447
1500
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
1448
1501
  }, function(err) {
1449
1502
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
@@ -1470,13 +1523,11 @@ var ExtAppsAdapter = {
1470
1523
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
1471
1524
  },
1472
1525
  setSize: function(context, size) {
1473
- // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
1474
- // measured/desired widget dimensions to the host.
1475
- var params = {
1476
- height: size && size.height,
1477
- width: size && size.width,
1478
- aspectRatio: size && size.aspectRatio
1479
- };
1526
+ // Standard ext-apps sizing: the view tells the host its dimensions with the
1527
+ // ui/notifications/size-changed notification ({ width?, height? } in px).
1528
+ var params = {};
1529
+ if (size && typeof size.width === 'number') params.width = size.width;
1530
+ if (size && typeof size.height === 'number') params.height = size.height;
1480
1531
  if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
1481
1532
  // No host origin yet: keep only the latest size and send it when the
1482
1533
  // handshake settles; every caller's promise follows that one request.
@@ -1487,7 +1538,7 @@ var ExtAppsAdapter = {
1487
1538
  self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
1488
1539
  });
1489
1540
  }
1490
- return this.sendRequest('ui/setSize', params);
1541
+ return this.sendNotification('ui/notifications/size-changed', params);
1491
1542
  },
1492
1543
  requestClose: function(context) {
1493
1544
  return this.sendRequest('ui/close', {});
@@ -1910,7 +1961,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1910
1961
 
1911
1962
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1912
1963
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1913
- // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1964
+ // ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
1914
1965
  FrontMcpBridge.prototype.setSize = function(size) {
1915
1966
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1916
1967
  if (!this._adapter.setSize) return Promise.resolve();
@@ -2012,7 +2063,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
2012
2063
  target.classList.add('opacity-50', 'cursor-not-allowed');
2013
2064
 
2014
2065
  // Add spinner for buttons
2015
- 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>';
2066
+ 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>';
2016
2067
  if (target.tagName === 'BUTTON') {
2017
2068
  target.innerHTML = spinner + 'Loading...';
2018
2069
  }
@@ -2093,7 +2144,22 @@ function minifyJS(code) {
2093
2144
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
2094
2145
  return code;
2095
2146
  }
2096
- return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\s+/g, " ").replace(/\s*([{};,:()[\]])\s*/g, "$1").replace(/;\}/g, "}").trim();
2147
+ const out = [];
2148
+ let inBlockComment = false;
2149
+ for (const raw of code.split("\n")) {
2150
+ const line = raw.trim();
2151
+ if (inBlockComment) {
2152
+ if (line.includes("*/")) inBlockComment = false;
2153
+ continue;
2154
+ }
2155
+ if (line === "" || line.startsWith("//")) continue;
2156
+ if (line.startsWith("/*")) {
2157
+ if (!line.includes("*/")) inBlockComment = true;
2158
+ continue;
2159
+ }
2160
+ out.push(line);
2161
+ }
2162
+ return out.join("\n");
2097
2163
  }
2098
2164
  function generatePlatformBundle(platform, options = {}) {
2099
2165
  const platformAdapters = {
@@ -797,12 +797,38 @@ function __applyHostTheme(theme) {
797
797
  } catch (e) {}
798
798
  }
799
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
+
800
816
  function readInjectedData() {
801
817
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
802
818
  if (typeof window !== 'undefined') {
803
819
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
804
820
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
805
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
+ }
806
832
  }
807
833
  return data;
808
834
  }
@@ -851,7 +877,24 @@ var OpenAIAdapter = {
851
877
  if (sdk.displayMode) {
852
878
  context.hostContext.displayMode = sdk.displayMode;
853
879
  }
854
- // 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
+ });
855
898
  return Promise.resolve();
856
899
  },
857
900
  callTool: function(context, name, args) {
@@ -998,9 +1041,10 @@ var ExtAppsAdapter = {
998
1041
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
999
1042
  break;
1000
1043
  case 'ui/notifications/tool-result':
1001
- context.toolOutput = params.content;
1044
+ var resultOutput = extractToolOutput(params);
1045
+ context.toolOutput = resultOutput;
1002
1046
  context.structuredContent = params.structuredContent;
1003
- context.notifyToolResult(params.content);
1047
+ context.notifyToolResult(resultOutput);
1004
1048
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
1005
1049
  break;
1006
1050
  case 'ui/notifications/host-context-changed':
@@ -1054,6 +1098,15 @@ var ExtAppsAdapter = {
1054
1098
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
1055
1099
  });
1056
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
+ },
1057
1110
  sendHandshake: function(context) {
1058
1111
  // Send ui/initialize using '*' as target origin since TOFU hasn't
1059
1112
  // been established yet. The response from the host will establish
@@ -1119,7 +1172,7 @@ var ExtAppsAdapter = {
1119
1172
  var pending = this.pendingSize;
1120
1173
  if (!pending) return;
1121
1174
  this.pendingSize = null;
1122
- this.sendRequest('ui/setSize', pending.params).then(function(result) {
1175
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
1123
1176
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
1124
1177
  }, function(err) {
1125
1178
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
@@ -1146,13 +1199,11 @@ var ExtAppsAdapter = {
1146
1199
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
1147
1200
  },
1148
1201
  setSize: function(context, size) {
1149
- // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
1150
- // measured/desired widget dimensions to the host.
1151
- var params = {
1152
- height: size && size.height,
1153
- width: size && size.width,
1154
- aspectRatio: size && size.aspectRatio
1155
- };
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;
1156
1207
  if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
1157
1208
  // No host origin yet: keep only the latest size and send it when the
1158
1209
  // handshake settles; every caller's promise follows that one request.
@@ -1163,7 +1214,7 @@ var ExtAppsAdapter = {
1163
1214
  self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
1164
1215
  });
1165
1216
  }
1166
- return this.sendRequest('ui/setSize', params);
1217
+ return this.sendNotification('ui/notifications/size-changed', params);
1167
1218
  },
1168
1219
  requestClose: function(context) {
1169
1220
  return this.sendRequest('ui/close', {});
@@ -1586,7 +1637,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1586
1637
 
1587
1638
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1588
1639
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1589
- // 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.
1590
1641
  FrontMcpBridge.prototype.setSize = function(size) {
1591
1642
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1592
1643
  if (!this._adapter.setSize) return Promise.resolve();
@@ -1688,7 +1739,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
1688
1739
  target.classList.add('opacity-50', 'cursor-not-allowed');
1689
1740
 
1690
1741
  // Add spinner for buttons
1691
- 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>';
1692
1743
  if (target.tagName === 'BUTTON') {
1693
1744
  target.innerHTML = spinner + 'Loading...';
1694
1745
  }
@@ -1769,7 +1820,22 @@ function minifyJS(code) {
1769
1820
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
1770
1821
  return code;
1771
1822
  }
1772
- 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");
1773
1839
  }
1774
1840
  function generatePlatformBundle(platform, options = {}) {
1775
1841
  const platformAdapters = {
@@ -2831,6 +2897,86 @@ function wrapDetectedContent(value) {
2831
2897
  }
2832
2898
  }
2833
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
+
2834
2980
  // libs/uipack/src/adapters/type-detector.ts
2835
2981
  function detectUIType(template) {
2836
2982
  if (template === null || template === void 0) {
@@ -2864,9 +3010,15 @@ function detectUIType(template) {
2864
3010
  }
2865
3011
 
2866
3012
  // libs/uipack/src/adapters/template-renderer.ts
2867
- function buildCspConfig(resolver) {
3013
+ function buildCspConfig(resolver, extra) {
2868
3014
  const cspResourceDomains = ["https://esm.sh"];
2869
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
+ }
2870
3022
  if (resolver && "overrides" in resolver) {
2871
3023
  const overrides = resolver.overrides;
2872
3024
  if (overrides) {
@@ -2906,6 +3058,14 @@ function renderUnwrappedResult(rawResult, options) {
2906
3058
  }
2907
3059
  return rawResult;
2908
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
+ }
2909
3069
  function renderToolTemplate(options) {
2910
3070
  const { toolName, input, output, template, resolver, platformType, sizing } = options;
2911
3071
  const uiType = detectUIType(template);
@@ -2916,30 +3076,43 @@ function renderToolTemplate(options) {
2916
3076
  output,
2917
3077
  includeBridge: true,
2918
3078
  resolver,
2919
- sizing
3079
+ sizing,
3080
+ csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
2920
3081
  };
2921
3082
  let html2;
2922
3083
  let hash = "";
2923
3084
  let size = 0;
2924
3085
  if (typeof template === "object" && template !== null && "file" in template) {
2925
- const cspConfig = buildCspConfig(resolver);
2926
3086
  const result = renderComponent(
2927
3087
  { source: template, inlineReact: resourceMode === "inline" },
2928
- { ...shellConfig, csp: cspConfig }
3088
+ { ...shellConfig, csp: buildCspConfig(resolver, options.csp) }
2929
3089
  );
2930
3090
  html2 = result.html;
2931
3091
  hash = result.hash;
2932
3092
  size = result.size;
2933
3093
  } else if (typeof template === "function") {
2934
- 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
+ );
2935
3111
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
2936
3112
  html2 = shellResult.html;
2937
3113
  hash = shellResult.hash;
2938
3114
  size = shellResult.size;
2939
3115
  } else {
2940
- const helpers = createTemplateHelpers();
2941
- const ctx = { input, output, helpers };
2942
- const rawResult = template(ctx);
2943
3116
  const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
2944
3117
  if (wrapped) {
2945
3118
  html2 = wrapped;
@@ -2951,7 +3124,7 @@ function renderToolTemplate(options) {
2951
3124
  }
2952
3125
  }
2953
3126
  } else if (typeof template === "string") {
2954
- const shellResult = buildShell(template, shellConfig);
3127
+ const shellResult = buildShell(uiType === "markdown" ? markdownToHtml(template) : template, shellConfig);
2955
3128
  html2 = shellResult.html;
2956
3129
  hash = shellResult.hash;
2957
3130
  size = shellResult.size;