@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.
package/adapters/index.js CHANGED
@@ -829,12 +829,38 @@ function __applyHostTheme(theme) {
829
829
  } catch (e) {}
830
830
  }
831
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
+
832
848
  function readInjectedData() {
833
849
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
834
850
  if (typeof window !== 'undefined') {
835
851
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
836
852
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
837
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
+ }
838
864
  }
839
865
  return data;
840
866
  }
@@ -883,7 +909,24 @@ var OpenAIAdapter = {
883
909
  if (sdk.displayMode) {
884
910
  context.hostContext.displayMode = sdk.displayMode;
885
911
  }
886
- // 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
+ });
887
930
  return Promise.resolve();
888
931
  },
889
932
  callTool: function(context, name, args) {
@@ -1030,9 +1073,10 @@ var ExtAppsAdapter = {
1030
1073
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
1031
1074
  break;
1032
1075
  case 'ui/notifications/tool-result':
1033
- context.toolOutput = params.content;
1076
+ var resultOutput = extractToolOutput(params);
1077
+ context.toolOutput = resultOutput;
1034
1078
  context.structuredContent = params.structuredContent;
1035
- context.notifyToolResult(params.content);
1079
+ context.notifyToolResult(resultOutput);
1036
1080
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
1037
1081
  break;
1038
1082
  case 'ui/notifications/host-context-changed':
@@ -1086,6 +1130,15 @@ var ExtAppsAdapter = {
1086
1130
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
1087
1131
  });
1088
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
+ },
1089
1142
  sendHandshake: function(context) {
1090
1143
  // Send ui/initialize using '*' as target origin since TOFU hasn't
1091
1144
  // been established yet. The response from the host will establish
@@ -1151,7 +1204,7 @@ var ExtAppsAdapter = {
1151
1204
  var pending = this.pendingSize;
1152
1205
  if (!pending) return;
1153
1206
  this.pendingSize = null;
1154
- this.sendRequest('ui/setSize', pending.params).then(function(result) {
1207
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
1155
1208
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
1156
1209
  }, function(err) {
1157
1210
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
@@ -1178,13 +1231,11 @@ var ExtAppsAdapter = {
1178
1231
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
1179
1232
  },
1180
1233
  setSize: function(context, size) {
1181
- // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
1182
- // measured/desired widget dimensions to the host.
1183
- var params = {
1184
- height: size && size.height,
1185
- width: size && size.width,
1186
- aspectRatio: size && size.aspectRatio
1187
- };
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;
1188
1239
  if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
1189
1240
  // No host origin yet: keep only the latest size and send it when the
1190
1241
  // handshake settles; every caller's promise follows that one request.
@@ -1195,7 +1246,7 @@ var ExtAppsAdapter = {
1195
1246
  self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
1196
1247
  });
1197
1248
  }
1198
- return this.sendRequest('ui/setSize', params);
1249
+ return this.sendNotification('ui/notifications/size-changed', params);
1199
1250
  },
1200
1251
  requestClose: function(context) {
1201
1252
  return this.sendRequest('ui/close', {});
@@ -1618,7 +1669,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1618
1669
 
1619
1670
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1620
1671
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1621
- // 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.
1622
1673
  FrontMcpBridge.prototype.setSize = function(size) {
1623
1674
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1624
1675
  if (!this._adapter.setSize) return Promise.resolve();
@@ -1720,7 +1771,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
1720
1771
  target.classList.add('opacity-50', 'cursor-not-allowed');
1721
1772
 
1722
1773
  // Add spinner for buttons
1723
- 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>';
1724
1775
  if (target.tagName === 'BUTTON') {
1725
1776
  target.innerHTML = spinner + 'Loading...';
1726
1777
  }
@@ -1801,7 +1852,22 @@ function minifyJS(code) {
1801
1852
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
1802
1853
  return code;
1803
1854
  }
1804
- 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");
1805
1871
  }
1806
1872
  function generatePlatformBundle(platform, options = {}) {
1807
1873
  const platformAdapters = {
@@ -1856,11 +1922,8 @@ function buildCSPDirectives(csp) {
1856
1922
  directives.push(`img-src ${imgSources.join(" ")}`);
1857
1923
  const fontSources = ["'self'", "data:", ...allResourceDomains];
1858
1924
  directives.push(`font-src ${fontSources.join(" ")}`);
1859
- if (validConnectDomains.length) {
1860
- directives.push(`connect-src ${validConnectDomains.join(" ")}`);
1861
- } else {
1862
- directives.push(`connect-src ${allResourceDomains.join(" ")}`);
1863
- }
1925
+ const connectSources = [.../* @__PURE__ */ new Set([...allResourceDomains, ...validConnectDomains])];
1926
+ directives.push(`connect-src ${connectSources.join(" ")}`);
1864
1927
  directives.push("object-src 'self' data:");
1865
1928
  return directives;
1866
1929
  }
@@ -1869,26 +1932,34 @@ function buildCSPMetaTag(csp) {
1869
1932
  const content = directives.join("; ");
1870
1933
  return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
1871
1934
  }
1935
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1872
1936
  function validateCSPDomain(domain) {
1873
- if (domain.startsWith("https://*.")) {
1874
- const rest = domain.slice(10);
1875
- return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$/.test(rest);
1937
+ if (typeof domain !== "string" || /[\s;,'"]/.test(domain)) return false;
1938
+ if (/[?#]/.test(domain)) return false;
1939
+ if (domain.includes("*")) {
1940
+ const wildcard = /^(?:https|wss):\/\/\*\.(.*)$/.exec(domain);
1941
+ return wildcard !== null && /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(wildcard[1]);
1876
1942
  }
1877
1943
  try {
1878
1944
  const url = new URL(domain);
1879
- return url.protocol === "https:";
1945
+ if (url.protocol === "https:" || url.protocol === "wss:") return true;
1946
+ return (url.protocol === "http:" || url.protocol === "ws:") && LOOPBACK_HOSTS.has(url.hostname);
1880
1947
  } catch {
1881
1948
  return false;
1882
1949
  }
1883
1950
  }
1951
+ var reportedInvalidDomains = /* @__PURE__ */ new Set();
1884
1952
  function sanitizeCSPDomains(domains) {
1885
1953
  if (!domains) return [];
1886
1954
  const valid = [];
1887
1955
  for (const domain of domains) {
1888
1956
  if (validateCSPDomain(domain)) {
1889
1957
  valid.push(domain);
1890
- } else {
1891
- console.warn(`Invalid CSP domain ignored: ${domain}`);
1958
+ } else if (!reportedInvalidDomains.has(domain)) {
1959
+ reportedInvalidDomains.add(domain);
1960
+ console.warn(
1961
+ `Invalid CSP domain ignored: ${domain} (expected an https:// or wss:// origin, or http:// / ws:// on localhost)`
1962
+ );
1892
1963
  }
1893
1964
  }
1894
1965
  return valid;
@@ -2292,8 +2363,8 @@ if (__root) {
2292
2363
  configurable: true, enumerable: true
2293
2364
  });
2294
2365
  }
2295
- // 2. Try injected data globals
2296
- if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
2366
+ // 2. Try injected data globals (null: the page carries no call's data, e.g. compiled at startup)
2367
+ if (__hasData(window.__mcpToolOutput) && window.__mcpToolOutput !== null) { __render(window.__mcpToolOutput); }
2297
2368
  // 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
2298
2369
  var __bridge = window.FrontMcpBridge;
2299
2370
  if (__bridge && typeof __bridge.onToolResult === 'function') {
@@ -2559,7 +2630,7 @@ createRoot(document.getElementById('root'))
2559
2630
  return `${componentImport}
2560
2631
  import React from 'react';
2561
2632
  import { createRoot } from 'react-dom/client';
2562
- const output = window.__mcpToolOutput;
2633
+ const output = window.__mcpToolOutput || {};
2563
2634
  const props = ${propsCode};
2564
2635
  createRoot(document.getElementById('root'))
2565
2636
  .render(React.createElement(Component, props));`;
@@ -2863,6 +2934,86 @@ function wrapDetectedContent(value) {
2863
2934
  }
2864
2935
  }
2865
2936
 
2937
+ // libs/uipack/src/adapters/markdown.ts
2938
+ var SAFE_HREF = /^(https?:|mailto:|\/|#)/i;
2939
+ function emphasis(text) {
2940
+ return text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
2941
+ }
2942
+ function inline(text) {
2943
+ const stash = [];
2944
+ const hold = (html2) => `\uE000${stash.push(html2) - 1}\uE000`;
2945
+ let out = escapeHtml(text.replace(/\uE000/g, "")).replace(
2946
+ /`([^`]+)`/g,
2947
+ (_m, code) => hold(`<code>${code}</code>`)
2948
+ );
2949
+ out = out.replace(
2950
+ /\[([^\]]+)\]\(([^)\s]+)\)/g,
2951
+ (_m, label, href) => SAFE_HREF.test(href) ? hold(`<a href="${href}" rel="noopener noreferrer">${emphasis(label)}</a>`) : label
2952
+ );
2953
+ out = emphasis(out);
2954
+ for (let i = 0; i < 2; i++) out = out.replace(/\uE000(\d+)\uE000/g, (_m, n) => stash[Number(n)]);
2955
+ return out;
2956
+ }
2957
+ function markdownToHtml(source) {
2958
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
2959
+ const blocks = [];
2960
+ let paragraph = [];
2961
+ let list;
2962
+ const flushParagraph = () => {
2963
+ if (paragraph.length > 0) blocks.push(`<p>${inline(paragraph.join(" "))}</p>`);
2964
+ paragraph = [];
2965
+ };
2966
+ const flushList = () => {
2967
+ if (list)
2968
+ blocks.push(`<${list.tag}>${list.items.map((item) => `<li>${inline(item)}</li>`).join("")}</${list.tag}>`);
2969
+ list = void 0;
2970
+ };
2971
+ for (let i = 0; i < lines.length; i++) {
2972
+ const line = lines[i];
2973
+ const fence = /^\s*(`{3,})/.exec(line);
2974
+ if (fence) {
2975
+ flushParagraph();
2976
+ flushList();
2977
+ const fenceLength = fence[1].length;
2978
+ const isClosing = (l) => {
2979
+ const m = /^\s*(`{3,})\s*$/.exec(l);
2980
+ return m !== null && m[1].length >= fenceLength;
2981
+ };
2982
+ const code = [];
2983
+ for (i++; i < lines.length && !isClosing(lines[i]); i++) code.push(lines[i]);
2984
+ blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}
2985
+ </code></pre>`);
2986
+ continue;
2987
+ }
2988
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
2989
+ if (heading) {
2990
+ flushParagraph();
2991
+ flushList();
2992
+ blocks.push(`<h${heading[1].length}>${inline(heading[2].trim())}</h${heading[1].length}>`);
2993
+ continue;
2994
+ }
2995
+ const item = /^\s*([-*+]|\d+\.)\s+(.*)$/.exec(line);
2996
+ if (item) {
2997
+ flushParagraph();
2998
+ const tag = /\d/.test(item[1]) ? "ol" : "ul";
2999
+ if (list && list.tag !== tag) flushList();
3000
+ list ??= { tag, items: [] };
3001
+ list.items.push(item[2]);
3002
+ continue;
3003
+ }
3004
+ if (line.trim() === "") {
3005
+ flushParagraph();
3006
+ flushList();
3007
+ continue;
3008
+ }
3009
+ flushList();
3010
+ paragraph.push(line.trim());
3011
+ }
3012
+ flushParagraph();
3013
+ flushList();
3014
+ return blocks.join("\n");
3015
+ }
3016
+
2866
3017
  // libs/uipack/src/adapters/type-detector.ts
2867
3018
  function detectUIType(template) {
2868
3019
  if (template === null || template === void 0) {
@@ -2896,9 +3047,15 @@ function detectUIType(template) {
2896
3047
  }
2897
3048
 
2898
3049
  // libs/uipack/src/adapters/template-renderer.ts
2899
- function buildCspConfig(resolver) {
3050
+ function buildCspConfig(resolver, extra) {
2900
3051
  const cspResourceDomains = ["https://esm.sh"];
2901
3052
  const cspConnectDomains = ["https://esm.sh"];
3053
+ for (const domain of extra?.resourceDomains ?? []) {
3054
+ if (!cspResourceDomains.includes(domain)) cspResourceDomains.push(domain);
3055
+ }
3056
+ for (const domain of extra?.connectDomains ?? []) {
3057
+ if (!cspConnectDomains.includes(domain)) cspConnectDomains.push(domain);
3058
+ }
2902
3059
  if (resolver && "overrides" in resolver) {
2903
3060
  const overrides = resolver.overrides;
2904
3061
  if (overrides) {
@@ -2938,6 +3095,24 @@ function renderUnwrappedResult(rawResult, options) {
2938
3095
  }
2939
3096
  return rawResult;
2940
3097
  }
3098
+ var warnedComponentReferences = /* @__PURE__ */ new WeakMap();
3099
+ function warnComponentReference(template, toolName, logger) {
3100
+ const warned = warnedComponentReferences.get(template) ?? /* @__PURE__ */ new Set();
3101
+ if (warned.has(toolName)) return;
3102
+ warned.add(toolName);
3103
+ warnedComponentReferences.set(template, warned);
3104
+ logger.warn(
3105
+ `[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.`
3106
+ );
3107
+ }
3108
+ function isReactElement(value) {
3109
+ return typeof value === "object" && value !== null && "$$typeof" in value;
3110
+ }
3111
+ function isDefinitelyReactComponent(template) {
3112
+ if (typeof template !== "function") return false;
3113
+ const proto = template.prototype;
3114
+ return typeof proto?.render === "function" || template["$$typeof"] !== void 0;
3115
+ }
2941
3116
  function renderToolTemplate(options) {
2942
3117
  const { toolName, input, output, template, resolver, platformType, sizing } = options;
2943
3118
  const uiType = detectUIType(template);
@@ -2948,30 +3123,41 @@ function renderToolTemplate(options) {
2948
3123
  output,
2949
3124
  includeBridge: true,
2950
3125
  resolver,
2951
- sizing
3126
+ sizing,
3127
+ csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
2952
3128
  };
2953
3129
  let html2;
2954
3130
  let hash = "";
2955
3131
  let size = 0;
2956
3132
  if (typeof template === "object" && template !== null && "file" in template) {
2957
- const cspConfig = buildCspConfig(resolver);
2958
3133
  const result = renderComponent(
2959
3134
  { source: template, inlineReact: resourceMode === "inline" },
2960
- { ...shellConfig, csp: cspConfig }
3135
+ { ...shellConfig, csp: buildCspConfig(resolver, options.csp) }
2961
3136
  );
2962
3137
  html2 = result.html;
2963
3138
  hash = result.hash;
2964
3139
  size = result.size;
2965
3140
  } else if (typeof template === "function") {
2966
- if (uiType === "react") {
3141
+ const helpers = createTemplateHelpers();
3142
+ const ctx = { input: input ?? {}, output: output ?? {}, helpers };
3143
+ let rawResult;
3144
+ let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
3145
+ if (isHtmlBuilder) {
3146
+ try {
3147
+ rawResult = template(ctx);
3148
+ isHtmlBuilder = !isReactElement(rawResult);
3149
+ } catch (error) {
3150
+ if (uiType !== "react") throw error;
3151
+ isHtmlBuilder = false;
3152
+ }
3153
+ }
3154
+ if (!isHtmlBuilder) {
3155
+ warnComponentReference(template, toolName, options.logger ?? console);
2967
3156
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
2968
3157
  html2 = shellResult.html;
2969
3158
  hash = shellResult.hash;
2970
3159
  size = shellResult.size;
2971
3160
  } else {
2972
- const helpers = createTemplateHelpers();
2973
- const ctx = { input, output, helpers };
2974
- const rawResult = template(ctx);
2975
3161
  const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
2976
3162
  if (wrapped) {
2977
3163
  html2 = wrapped;
@@ -2983,7 +3169,7 @@ function renderToolTemplate(options) {
2983
3169
  }
2984
3170
  }
2985
3171
  } else if (typeof template === "string") {
2986
- const shellResult = buildShell(template, shellConfig);
3172
+ const shellResult = buildShell(uiType === "markdown" ? markdownToHtml(template) : template, shellConfig);
2987
3173
  html2 = shellResult.html;
2988
3174
  hash = shellResult.hash;
2989
3175
  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;AA8FD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,CAoH/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;AAguCD;;;;;;;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"}
@@ -294,12 +294,38 @@ function __applyHostTheme(theme) {
294
294
  } catch (e) {}
295
295
  }
296
296
 
297
+ function extractToolOutput(params) {
298
+ // A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
299
+ if (!params) return undefined;
300
+ if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
301
+ var content = params.content;
302
+ if (Array.isArray(content)) {
303
+ for (var i = 0; i < content.length; i++) {
304
+ var item = content[i];
305
+ if (item && item.type === 'text' && typeof item.text === 'string') {
306
+ try { return JSON.parse(item.text); } catch (e) { return item.text; }
307
+ }
308
+ }
309
+ }
310
+ return content;
311
+ }
312
+
297
313
  function readInjectedData() {
298
314
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
299
315
  if (typeof window !== 'undefined') {
300
316
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
301
317
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
302
318
  if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
319
+ // The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
320
+ // (toolOutput is the tool's structuredContent).
321
+ var oa = window.openai;
322
+ if (oa && typeof oa === 'object') {
323
+ if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
324
+ if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
325
+ if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
326
+ if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
327
+ }
328
+ }
303
329
  }
304
330
  return data;
305
331
  }
@@ -348,7 +374,24 @@ var OpenAIAdapter = {
348
374
  if (sdk.displayMode) {
349
375
  context.hostContext.displayMode = sdk.displayMode;
350
376
  }
351
- // Note: OpenAI SDK does not have an onContextChange equivalent
377
+ // The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
378
+ window.addEventListener('openai:set_globals', function(event) {
379
+ var globals = (event && event.detail && event.detail.globals) || window.openai || {};
380
+ if (globals.toolInput !== undefined && globals.toolInput !== null) {
381
+ context.toolInput = globals.toolInput;
382
+ window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
383
+ }
384
+ if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
385
+ context.structuredContent = globals.toolOutput;
386
+ context.notifyToolResult(globals.toolOutput);
387
+ }
388
+ if (globals.theme && globals.theme !== context.hostContext.theme) {
389
+ context.notifyContextChange({ theme: globals.theme });
390
+ }
391
+ if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
392
+ context.notifyContextChange({ displayMode: globals.displayMode });
393
+ }
394
+ });
352
395
  return Promise.resolve();
353
396
  },
354
397
  callTool: function(context, name, args) {
@@ -495,9 +538,10 @@ var ExtAppsAdapter = {
495
538
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
496
539
  break;
497
540
  case 'ui/notifications/tool-result':
498
- context.toolOutput = params.content;
541
+ var resultOutput = extractToolOutput(params);
542
+ context.toolOutput = resultOutput;
499
543
  context.structuredContent = params.structuredContent;
500
- context.notifyToolResult(params.content);
544
+ context.notifyToolResult(resultOutput);
501
545
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
502
546
  break;
503
547
  case 'ui/notifications/host-context-changed':
@@ -551,6 +595,15 @@ var ExtAppsAdapter = {
551
595
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
552
596
  });
553
597
  },
598
+ sendNotification: function(method, params) {
599
+ // JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
600
+ if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
601
+ return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
602
+ }
603
+ var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
604
+ window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
605
+ return Promise.resolve();
606
+ },
554
607
  sendHandshake: function(context) {
555
608
  // Send ui/initialize using '*' as target origin since TOFU hasn't
556
609
  // been established yet. The response from the host will establish
@@ -616,7 +669,7 @@ var ExtAppsAdapter = {
616
669
  var pending = this.pendingSize;
617
670
  if (!pending) return;
618
671
  this.pendingSize = null;
619
- this.sendRequest('ui/setSize', pending.params).then(function(result) {
672
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
620
673
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
621
674
  }, function(err) {
622
675
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
@@ -643,13 +696,11 @@ var ExtAppsAdapter = {
643
696
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
644
697
  },
645
698
  setSize: function(context, size) {
646
- // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
647
- // measured/desired widget dimensions to the host.
648
- var params = {
649
- height: size && size.height,
650
- width: size && size.width,
651
- aspectRatio: size && size.aspectRatio
652
- };
699
+ // Standard ext-apps sizing: the view tells the host its dimensions with the
700
+ // ui/notifications/size-changed notification ({ width?, height? } in px).
701
+ var params = {};
702
+ if (size && typeof size.width === 'number') params.width = size.width;
703
+ if (size && typeof size.height === 'number') params.height = size.height;
653
704
  if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
654
705
  // No host origin yet: keep only the latest size and send it when the
655
706
  // handshake settles; every caller's promise follows that one request.
@@ -660,7 +711,7 @@ var ExtAppsAdapter = {
660
711
  self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
661
712
  });
662
713
  }
663
- return this.sendRequest('ui/setSize', params);
714
+ return this.sendNotification('ui/notifications/size-changed', params);
664
715
  },
665
716
  requestClose: function(context) {
666
717
  return this.sendRequest('ui/close', {});
@@ -1083,7 +1134,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1083
1134
 
1084
1135
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1085
1136
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1086
- // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1137
+ // ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
1087
1138
  FrontMcpBridge.prototype.setSize = function(size) {
1088
1139
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1089
1140
  if (!this._adapter.setSize) return Promise.resolve();
@@ -1185,7 +1236,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
1185
1236
  target.classList.add('opacity-50', 'cursor-not-allowed');
1186
1237
 
1187
1238
  // Add spinner for buttons
1188
- 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>';
1239
+ 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>';
1189
1240
  if (target.tagName === 'BUTTON') {
1190
1241
  target.innerHTML = spinner + 'Loading...';
1191
1242
  }
@@ -1266,7 +1317,22 @@ function minifyJS(code) {
1266
1317
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
1267
1318
  return code;
1268
1319
  }
1269
- return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\s+/g, " ").replace(/\s*([{};,:()[\]])\s*/g, "$1").replace(/;\}/g, "}").trim();
1320
+ const out = [];
1321
+ let inBlockComment = false;
1322
+ for (const raw of code.split("\n")) {
1323
+ const line = raw.trim();
1324
+ if (inBlockComment) {
1325
+ if (line.includes("*/")) inBlockComment = false;
1326
+ continue;
1327
+ }
1328
+ if (line === "" || line.startsWith("//")) continue;
1329
+ if (line.startsWith("/*")) {
1330
+ if (!line.includes("*/")) inBlockComment = true;
1331
+ continue;
1332
+ }
1333
+ out.push(line);
1334
+ }
1335
+ return out.join("\n");
1270
1336
  }
1271
1337
  function generatePlatformBundle(platform, options = {}) {
1272
1338
  const platformAdapters = {