@frontmcp/uipack 1.8.6 → 1.9.0

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.
@@ -1,3 +1,6 @@
1
+ var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
1
4
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
5
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
6
  }) : x)(function(x) {
@@ -797,12 +800,38 @@ function __applyHostTheme(theme) {
797
800
  } catch (e) {}
798
801
  }
799
802
 
803
+ function extractToolOutput(params) {
804
+ // A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
805
+ if (!params) return undefined;
806
+ if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
807
+ var content = params.content;
808
+ if (Array.isArray(content)) {
809
+ for (var i = 0; i < content.length; i++) {
810
+ var item = content[i];
811
+ if (item && item.type === 'text' && typeof item.text === 'string') {
812
+ try { return JSON.parse(item.text); } catch (e) { return item.text; }
813
+ }
814
+ }
815
+ }
816
+ return content;
817
+ }
818
+
800
819
  function readInjectedData() {
801
820
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
802
821
  if (typeof window !== 'undefined') {
803
822
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
804
823
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
805
824
  if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
825
+ // The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
826
+ // (toolOutput is the tool's structuredContent).
827
+ var oa = window.openai;
828
+ if (oa && typeof oa === 'object') {
829
+ if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
830
+ if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
831
+ if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
832
+ if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
833
+ }
834
+ }
806
835
  }
807
836
  return data;
808
837
  }
@@ -851,7 +880,24 @@ var OpenAIAdapter = {
851
880
  if (sdk.displayMode) {
852
881
  context.hostContext.displayMode = sdk.displayMode;
853
882
  }
854
- // Note: OpenAI SDK does not have an onContextChange equivalent
883
+ // The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
884
+ window.addEventListener('openai:set_globals', function(event) {
885
+ var globals = (event && event.detail && event.detail.globals) || window.openai || {};
886
+ if (globals.toolInput !== undefined && globals.toolInput !== null) {
887
+ context.toolInput = globals.toolInput;
888
+ window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
889
+ }
890
+ if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
891
+ context.structuredContent = globals.toolOutput;
892
+ context.notifyToolResult(globals.toolOutput);
893
+ }
894
+ if (globals.theme && globals.theme !== context.hostContext.theme) {
895
+ context.notifyContextChange({ theme: globals.theme });
896
+ }
897
+ if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
898
+ context.notifyContextChange({ displayMode: globals.displayMode });
899
+ }
900
+ });
855
901
  return Promise.resolve();
856
902
  },
857
903
  callTool: function(context, name, args) {
@@ -998,9 +1044,10 @@ var ExtAppsAdapter = {
998
1044
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
999
1045
  break;
1000
1046
  case 'ui/notifications/tool-result':
1001
- context.toolOutput = params.content;
1047
+ var resultOutput = extractToolOutput(params);
1048
+ context.toolOutput = resultOutput;
1002
1049
  context.structuredContent = params.structuredContent;
1003
- context.notifyToolResult(params.content);
1050
+ context.notifyToolResult(resultOutput);
1004
1051
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
1005
1052
  break;
1006
1053
  case 'ui/notifications/host-context-changed':
@@ -1054,6 +1101,15 @@ var ExtAppsAdapter = {
1054
1101
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
1055
1102
  });
1056
1103
  },
1104
+ sendNotification: function(method, params) {
1105
+ // JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
1106
+ if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
1107
+ return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
1108
+ }
1109
+ var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
1110
+ window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
1111
+ return Promise.resolve();
1112
+ },
1057
1113
  sendHandshake: function(context) {
1058
1114
  // Send ui/initialize using '*' as target origin since TOFU hasn't
1059
1115
  // been established yet. The response from the host will establish
@@ -1119,7 +1175,7 @@ var ExtAppsAdapter = {
1119
1175
  var pending = this.pendingSize;
1120
1176
  if (!pending) return;
1121
1177
  this.pendingSize = null;
1122
- this.sendRequest('ui/setSize', pending.params).then(function(result) {
1178
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
1123
1179
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
1124
1180
  }, function(err) {
1125
1181
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
@@ -1146,13 +1202,11 @@ var ExtAppsAdapter = {
1146
1202
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
1147
1203
  },
1148
1204
  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
- };
1205
+ // Standard ext-apps sizing: the view tells the host its dimensions with the
1206
+ // ui/notifications/size-changed notification ({ width?, height? } in px).
1207
+ var params = {};
1208
+ if (size && typeof size.width === 'number') params.width = size.width;
1209
+ if (size && typeof size.height === 'number') params.height = size.height;
1156
1210
  if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
1157
1211
  // No host origin yet: keep only the latest size and send it when the
1158
1212
  // handshake settles; every caller's promise follows that one request.
@@ -1163,7 +1217,7 @@ var ExtAppsAdapter = {
1163
1217
  self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
1164
1218
  });
1165
1219
  }
1166
- return this.sendRequest('ui/setSize', params);
1220
+ return this.sendNotification('ui/notifications/size-changed', params);
1167
1221
  },
1168
1222
  requestClose: function(context) {
1169
1223
  return this.sendRequest('ui/close', {});
@@ -1586,7 +1640,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1586
1640
 
1587
1641
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1588
1642
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1589
- // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1643
+ // ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
1590
1644
  FrontMcpBridge.prototype.setSize = function(size) {
1591
1645
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1592
1646
  if (!this._adapter.setSize) return Promise.resolve();
@@ -1688,7 +1742,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
1688
1742
  target.classList.add('opacity-50', 'cursor-not-allowed');
1689
1743
 
1690
1744
  // 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>';
1745
+ 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
1746
  if (target.tagName === 'BUTTON') {
1693
1747
  target.innerHTML = spinner + 'Loading...';
1694
1748
  }
@@ -1769,7 +1823,22 @@ function minifyJS(code) {
1769
1823
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
1770
1824
  return code;
1771
1825
  }
1772
- return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\s+/g, " ").replace(/\s*([{};,:()[\]])\s*/g, "$1").replace(/;\}/g, "}").trim();
1826
+ const out = [];
1827
+ let inBlockComment = false;
1828
+ for (const raw of code.split("\n")) {
1829
+ const line = raw.trim();
1830
+ if (inBlockComment) {
1831
+ if (line.includes("*/")) inBlockComment = false;
1832
+ continue;
1833
+ }
1834
+ if (line === "" || line.startsWith("//")) continue;
1835
+ if (line.startsWith("/*")) {
1836
+ if (!line.includes("*/")) inBlockComment = true;
1837
+ continue;
1838
+ }
1839
+ out.push(line);
1840
+ }
1841
+ return out.join("\n");
1773
1842
  }
1774
1843
  function generatePlatformBundle(platform, options = {}) {
1775
1844
  const platformAdapters = {
@@ -1824,11 +1893,8 @@ function buildCSPDirectives(csp) {
1824
1893
  directives.push(`img-src ${imgSources.join(" ")}`);
1825
1894
  const fontSources = ["'self'", "data:", ...allResourceDomains];
1826
1895
  directives.push(`font-src ${fontSources.join(" ")}`);
1827
- if (validConnectDomains.length) {
1828
- directives.push(`connect-src ${validConnectDomains.join(" ")}`);
1829
- } else {
1830
- directives.push(`connect-src ${allResourceDomains.join(" ")}`);
1831
- }
1896
+ const connectSources = [.../* @__PURE__ */ new Set([...allResourceDomains, ...validConnectDomains])];
1897
+ directives.push(`connect-src ${connectSources.join(" ")}`);
1832
1898
  directives.push("object-src 'self' data:");
1833
1899
  return directives;
1834
1900
  }
@@ -1837,26 +1903,34 @@ function buildCSPMetaTag(csp) {
1837
1903
  const content = directives.join("; ");
1838
1904
  return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
1839
1905
  }
1906
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1840
1907
  function validateCSPDomain(domain) {
1841
- if (domain.startsWith("https://*.")) {
1842
- const rest = domain.slice(10);
1843
- return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$/.test(rest);
1908
+ if (typeof domain !== "string" || /[\s;,'"]/.test(domain)) return false;
1909
+ if (/[?#]/.test(domain)) return false;
1910
+ if (domain.includes("*")) {
1911
+ const wildcard = /^(?:https|wss):\/\/\*\.(.*)$/.exec(domain);
1912
+ return wildcard !== null && /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(wildcard[1]);
1844
1913
  }
1845
1914
  try {
1846
1915
  const url = new URL(domain);
1847
- return url.protocol === "https:";
1916
+ if (url.protocol === "https:" || url.protocol === "wss:") return true;
1917
+ return (url.protocol === "http:" || url.protocol === "ws:") && LOOPBACK_HOSTS.has(url.hostname);
1848
1918
  } catch {
1849
1919
  return false;
1850
1920
  }
1851
1921
  }
1922
+ var reportedInvalidDomains = /* @__PURE__ */ new Set();
1852
1923
  function sanitizeCSPDomains(domains) {
1853
1924
  if (!domains) return [];
1854
1925
  const valid = [];
1855
1926
  for (const domain of domains) {
1856
1927
  if (validateCSPDomain(domain)) {
1857
1928
  valid.push(domain);
1858
- } else {
1859
- console.warn(`Invalid CSP domain ignored: ${domain}`);
1929
+ } else if (!reportedInvalidDomains.has(domain)) {
1930
+ reportedInvalidDomains.add(domain);
1931
+ console.warn(
1932
+ `Invalid CSP domain ignored: ${domain} (expected an https:// or wss:// origin, or http:// / ws:// on localhost)`
1933
+ );
1860
1934
  }
1861
1935
  }
1862
1936
  return valid;
@@ -2260,8 +2334,8 @@ if (__root) {
2260
2334
  configurable: true, enumerable: true
2261
2335
  });
2262
2336
  }
2263
- // 2. Try injected data globals
2264
- if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
2337
+ // 2. Try injected data globals (null: the page carries no call's data, e.g. compiled at startup)
2338
+ if (__hasData(window.__mcpToolOutput) && window.__mcpToolOutput !== null) { __render(window.__mcpToolOutput); }
2265
2339
  // 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
2266
2340
  var __bridge = window.FrontMcpBridge;
2267
2341
  if (__bridge && typeof __bridge.onToolResult === 'function') {
@@ -2527,7 +2601,7 @@ createRoot(document.getElementById('root'))
2527
2601
  return `${componentImport}
2528
2602
  import React from 'react';
2529
2603
  import { createRoot } from 'react-dom/client';
2530
- const output = window.__mcpToolOutput;
2604
+ const output = window.__mcpToolOutput || {};
2531
2605
  const props = ${propsCode};
2532
2606
  createRoot(document.getElementById('root'))
2533
2607
  .render(React.createElement(Component, props));`;
@@ -2831,6 +2905,86 @@ function wrapDetectedContent(value) {
2831
2905
  }
2832
2906
  }
2833
2907
 
2908
+ // libs/uipack/src/adapters/markdown.ts
2909
+ var SAFE_HREF = /^(https?:|mailto:|\/|#)/i;
2910
+ function emphasis(text) {
2911
+ return text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
2912
+ }
2913
+ function inline(text) {
2914
+ const stash = [];
2915
+ const hold = (html2) => `\uE000${stash.push(html2) - 1}\uE000`;
2916
+ let out = escapeHtml(text.replace(/\uE000/g, "")).replace(
2917
+ /`([^`]+)`/g,
2918
+ (_m, code) => hold(`<code>${code}</code>`)
2919
+ );
2920
+ out = out.replace(
2921
+ /\[([^\]]+)\]\(([^)\s]+)\)/g,
2922
+ (_m, label, href) => SAFE_HREF.test(href) ? hold(`<a href="${href}" rel="noopener noreferrer">${emphasis(label)}</a>`) : label
2923
+ );
2924
+ out = emphasis(out);
2925
+ for (let i = 0; i < 2; i++) out = out.replace(/\uE000(\d+)\uE000/g, (_m, n) => stash[Number(n)]);
2926
+ return out;
2927
+ }
2928
+ function markdownToHtml(source) {
2929
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
2930
+ const blocks = [];
2931
+ let paragraph = [];
2932
+ let list;
2933
+ const flushParagraph = () => {
2934
+ if (paragraph.length > 0) blocks.push(`<p>${inline(paragraph.join(" "))}</p>`);
2935
+ paragraph = [];
2936
+ };
2937
+ const flushList = () => {
2938
+ if (list)
2939
+ blocks.push(`<${list.tag}>${list.items.map((item) => `<li>${inline(item)}</li>`).join("")}</${list.tag}>`);
2940
+ list = void 0;
2941
+ };
2942
+ for (let i = 0; i < lines.length; i++) {
2943
+ const line = lines[i];
2944
+ const fence = /^\s*(`{3,})/.exec(line);
2945
+ if (fence) {
2946
+ flushParagraph();
2947
+ flushList();
2948
+ const fenceLength = fence[1].length;
2949
+ const isClosing = (l) => {
2950
+ const m = /^\s*(`{3,})\s*$/.exec(l);
2951
+ return m !== null && m[1].length >= fenceLength;
2952
+ };
2953
+ const code = [];
2954
+ for (i++; i < lines.length && !isClosing(lines[i]); i++) code.push(lines[i]);
2955
+ blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}
2956
+ </code></pre>`);
2957
+ continue;
2958
+ }
2959
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
2960
+ if (heading) {
2961
+ flushParagraph();
2962
+ flushList();
2963
+ blocks.push(`<h${heading[1].length}>${inline(heading[2].trim())}</h${heading[1].length}>`);
2964
+ continue;
2965
+ }
2966
+ const item = /^\s*([-*+]|\d+\.)\s+(.*)$/.exec(line);
2967
+ if (item) {
2968
+ flushParagraph();
2969
+ const tag = /\d/.test(item[1]) ? "ol" : "ul";
2970
+ if (list && list.tag !== tag) flushList();
2971
+ list ??= { tag, items: [] };
2972
+ list.items.push(item[2]);
2973
+ continue;
2974
+ }
2975
+ if (line.trim() === "") {
2976
+ flushParagraph();
2977
+ flushList();
2978
+ continue;
2979
+ }
2980
+ flushList();
2981
+ paragraph.push(line.trim());
2982
+ }
2983
+ flushParagraph();
2984
+ flushList();
2985
+ return blocks.join("\n");
2986
+ }
2987
+
2834
2988
  // libs/uipack/src/adapters/type-detector.ts
2835
2989
  function detectUIType(template) {
2836
2990
  if (template === null || template === void 0) {
@@ -2864,9 +3018,15 @@ function detectUIType(template) {
2864
3018
  }
2865
3019
 
2866
3020
  // libs/uipack/src/adapters/template-renderer.ts
2867
- function buildCspConfig(resolver) {
3021
+ function buildCspConfig(resolver, extra) {
2868
3022
  const cspResourceDomains = ["https://esm.sh"];
2869
3023
  const cspConnectDomains = ["https://esm.sh"];
3024
+ for (const domain of extra?.resourceDomains ?? []) {
3025
+ if (!cspResourceDomains.includes(domain)) cspResourceDomains.push(domain);
3026
+ }
3027
+ for (const domain of extra?.connectDomains ?? []) {
3028
+ if (!cspConnectDomains.includes(domain)) cspConnectDomains.push(domain);
3029
+ }
2870
3030
  if (resolver && "overrides" in resolver) {
2871
3031
  const overrides = resolver.overrides;
2872
3032
  if (overrides) {
@@ -2906,6 +3066,24 @@ function renderUnwrappedResult(rawResult, options) {
2906
3066
  }
2907
3067
  return rawResult;
2908
3068
  }
3069
+ var warnedComponentReferences = /* @__PURE__ */ new WeakMap();
3070
+ function warnComponentReference(template, toolName, logger) {
3071
+ const warned = warnedComponentReferences.get(template) ?? /* @__PURE__ */ new Set();
3072
+ if (warned.has(toolName)) return;
3073
+ warned.add(toolName);
3074
+ warnedComponentReferences.set(template, warned);
3075
+ logger.warn(
3076
+ `[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.`
3077
+ );
3078
+ }
3079
+ function isReactElement(value) {
3080
+ return typeof value === "object" && value !== null && "$$typeof" in value;
3081
+ }
3082
+ function isDefinitelyReactComponent(template) {
3083
+ if (typeof template !== "function") return false;
3084
+ const proto = template.prototype;
3085
+ return typeof proto?.render === "function" || template["$$typeof"] !== void 0;
3086
+ }
2909
3087
  function renderToolTemplate(options) {
2910
3088
  const { toolName, input, output, template, resolver, platformType, sizing } = options;
2911
3089
  const uiType = detectUIType(template);
@@ -2916,30 +3094,41 @@ function renderToolTemplate(options) {
2916
3094
  output,
2917
3095
  includeBridge: true,
2918
3096
  resolver,
2919
- sizing
3097
+ sizing,
3098
+ csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
2920
3099
  };
2921
3100
  let html2;
2922
3101
  let hash = "";
2923
3102
  let size = 0;
2924
3103
  if (typeof template === "object" && template !== null && "file" in template) {
2925
- const cspConfig = buildCspConfig(resolver);
2926
3104
  const result = renderComponent(
2927
3105
  { source: template, inlineReact: resourceMode === "inline" },
2928
- { ...shellConfig, csp: cspConfig }
3106
+ { ...shellConfig, csp: buildCspConfig(resolver, options.csp) }
2929
3107
  );
2930
3108
  html2 = result.html;
2931
3109
  hash = result.hash;
2932
3110
  size = result.size;
2933
3111
  } else if (typeof template === "function") {
2934
- if (uiType === "react") {
3112
+ const helpers = createTemplateHelpers();
3113
+ const ctx = { input: input ?? {}, output: output ?? {}, helpers };
3114
+ let rawResult;
3115
+ let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
3116
+ if (isHtmlBuilder) {
3117
+ try {
3118
+ rawResult = template(ctx);
3119
+ isHtmlBuilder = !isReactElement(rawResult);
3120
+ } catch (error) {
3121
+ if (uiType !== "react") throw error;
3122
+ isHtmlBuilder = false;
3123
+ }
3124
+ }
3125
+ if (!isHtmlBuilder) {
3126
+ warnComponentReference(template, toolName, options.logger ?? console);
2935
3127
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
2936
3128
  html2 = shellResult.html;
2937
3129
  hash = shellResult.hash;
2938
3130
  size = shellResult.size;
2939
3131
  } else {
2940
- const helpers = createTemplateHelpers();
2941
- const ctx = { input, output, helpers };
2942
- const rawResult = template(ctx);
2943
3132
  const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
2944
3133
  if (wrapped) {
2945
3134
  html2 = wrapped;
@@ -2951,7 +3140,7 @@ function renderToolTemplate(options) {
2951
3140
  }
2952
3141
  }
2953
3142
  } else if (typeof template === "string") {
2954
- const shellResult = buildShell(template, shellConfig);
3143
+ const shellResult = buildShell(uiType === "markdown" ? markdownToHtml(template) : template, shellConfig);
2955
3144
  html2 = shellResult.html;
2956
3145
  hash = shellResult.hash;
2957
3146
  size = shellResult.size;
@@ -1,3 +1,6 @@
1
+ var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
1
4
  // libs/uipack/src/bridge-runtime/iife-generator.ts
2
5
  function generateBridgeIIFE(options = {}) {
3
6
  const { debug = false, trustedOrigins = [], minify = false } = options;
@@ -265,12 +268,38 @@ function __applyHostTheme(theme) {
265
268
  } catch (e) {}
266
269
  }
267
270
 
271
+ function extractToolOutput(params) {
272
+ // A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
273
+ if (!params) return undefined;
274
+ if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
275
+ var content = params.content;
276
+ if (Array.isArray(content)) {
277
+ for (var i = 0; i < content.length; i++) {
278
+ var item = content[i];
279
+ if (item && item.type === 'text' && typeof item.text === 'string') {
280
+ try { return JSON.parse(item.text); } catch (e) { return item.text; }
281
+ }
282
+ }
283
+ }
284
+ return content;
285
+ }
286
+
268
287
  function readInjectedData() {
269
288
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
270
289
  if (typeof window !== 'undefined') {
271
290
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
272
291
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
273
292
  if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
293
+ // The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
294
+ // (toolOutput is the tool's structuredContent).
295
+ var oa = window.openai;
296
+ if (oa && typeof oa === 'object') {
297
+ if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
298
+ if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
299
+ if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
300
+ if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
301
+ }
302
+ }
274
303
  }
275
304
  return data;
276
305
  }
@@ -319,7 +348,24 @@ var OpenAIAdapter = {
319
348
  if (sdk.displayMode) {
320
349
  context.hostContext.displayMode = sdk.displayMode;
321
350
  }
322
- // Note: OpenAI SDK does not have an onContextChange equivalent
351
+ // The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
352
+ window.addEventListener('openai:set_globals', function(event) {
353
+ var globals = (event && event.detail && event.detail.globals) || window.openai || {};
354
+ if (globals.toolInput !== undefined && globals.toolInput !== null) {
355
+ context.toolInput = globals.toolInput;
356
+ window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
357
+ }
358
+ if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
359
+ context.structuredContent = globals.toolOutput;
360
+ context.notifyToolResult(globals.toolOutput);
361
+ }
362
+ if (globals.theme && globals.theme !== context.hostContext.theme) {
363
+ context.notifyContextChange({ theme: globals.theme });
364
+ }
365
+ if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
366
+ context.notifyContextChange({ displayMode: globals.displayMode });
367
+ }
368
+ });
323
369
  return Promise.resolve();
324
370
  },
325
371
  callTool: function(context, name, args) {
@@ -466,9 +512,10 @@ var ExtAppsAdapter = {
466
512
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
467
513
  break;
468
514
  case 'ui/notifications/tool-result':
469
- context.toolOutput = params.content;
515
+ var resultOutput = extractToolOutput(params);
516
+ context.toolOutput = resultOutput;
470
517
  context.structuredContent = params.structuredContent;
471
- context.notifyToolResult(params.content);
518
+ context.notifyToolResult(resultOutput);
472
519
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
473
520
  break;
474
521
  case 'ui/notifications/host-context-changed':
@@ -522,6 +569,15 @@ var ExtAppsAdapter = {
522
569
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
523
570
  });
524
571
  },
572
+ sendNotification: function(method, params) {
573
+ // JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
574
+ if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
575
+ return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
576
+ }
577
+ var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
578
+ window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
579
+ return Promise.resolve();
580
+ },
525
581
  sendHandshake: function(context) {
526
582
  // Send ui/initialize using '*' as target origin since TOFU hasn't
527
583
  // been established yet. The response from the host will establish
@@ -587,7 +643,7 @@ var ExtAppsAdapter = {
587
643
  var pending = this.pendingSize;
588
644
  if (!pending) return;
589
645
  this.pendingSize = null;
590
- this.sendRequest('ui/setSize', pending.params).then(function(result) {
646
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
591
647
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
592
648
  }, function(err) {
593
649
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
@@ -614,13 +670,11 @@ var ExtAppsAdapter = {
614
670
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
615
671
  },
616
672
  setSize: function(context, size) {
617
- // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
618
- // measured/desired widget dimensions to the host.
619
- var params = {
620
- height: size && size.height,
621
- width: size && size.width,
622
- aspectRatio: size && size.aspectRatio
623
- };
673
+ // Standard ext-apps sizing: the view tells the host its dimensions with the
674
+ // ui/notifications/size-changed notification ({ width?, height? } in px).
675
+ var params = {};
676
+ if (size && typeof size.width === 'number') params.width = size.width;
677
+ if (size && typeof size.height === 'number') params.height = size.height;
624
678
  if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
625
679
  // No host origin yet: keep only the latest size and send it when the
626
680
  // handshake settles; every caller's promise follows that one request.
@@ -631,7 +685,7 @@ var ExtAppsAdapter = {
631
685
  self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
632
686
  });
633
687
  }
634
- return this.sendRequest('ui/setSize', params);
688
+ return this.sendNotification('ui/notifications/size-changed', params);
635
689
  },
636
690
  requestClose: function(context) {
637
691
  return this.sendRequest('ui/close', {});
@@ -1054,7 +1108,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1054
1108
 
1055
1109
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1056
1110
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1057
- // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1111
+ // ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
1058
1112
  FrontMcpBridge.prototype.setSize = function(size) {
1059
1113
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1060
1114
  if (!this._adapter.setSize) return Promise.resolve();
@@ -1156,7 +1210,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
1156
1210
  target.classList.add('opacity-50', 'cursor-not-allowed');
1157
1211
 
1158
1212
  // Add spinner for buttons
1159
- 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>';
1213
+ 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>';
1160
1214
  if (target.tagName === 'BUTTON') {
1161
1215
  target.innerHTML = spinner + 'Loading...';
1162
1216
  }
@@ -1237,7 +1291,22 @@ function minifyJS(code) {
1237
1291
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
1238
1292
  return code;
1239
1293
  }
1240
- return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\s+/g, " ").replace(/\s*([{};,:()[\]])\s*/g, "$1").replace(/;\}/g, "}").trim();
1294
+ const out = [];
1295
+ let inBlockComment = false;
1296
+ for (const raw of code.split("\n")) {
1297
+ const line = raw.trim();
1298
+ if (inBlockComment) {
1299
+ if (line.includes("*/")) inBlockComment = false;
1300
+ continue;
1301
+ }
1302
+ if (line === "" || line.startsWith("//")) continue;
1303
+ if (line.startsWith("/*")) {
1304
+ if (!line.includes("*/")) inBlockComment = true;
1305
+ continue;
1306
+ }
1307
+ out.push(line);
1308
+ }
1309
+ return out.join("\n");
1241
1310
  }
1242
1311
  function generatePlatformBundle(platform, options = {}) {
1243
1312
  const platformAdapters = {