@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.
@@ -564,8 +564,8 @@ if (__root) {
564
564
  configurable: true, enumerable: true
565
565
  });
566
566
  }
567
- // 2. Try injected data globals
568
- if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
567
+ // 2. Try injected data globals (null: the page carries no call's data, e.g. compiled at startup)
568
+ if (__hasData(window.__mcpToolOutput) && window.__mcpToolOutput !== null) { __render(window.__mcpToolOutput); }
569
569
  // 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
570
570
  var __bridge = window.FrontMcpBridge;
571
571
  if (__bridge && typeof __bridge.onToolResult === 'function') {
@@ -817,7 +817,7 @@ createRoot(document.getElementById('root'))
817
817
  return `${componentImport}
818
818
  import React from 'react';
819
819
  import { createRoot } from 'react-dom/client';
820
- const output = window.__mcpToolOutput;
820
+ const output = window.__mcpToolOutput || {};
821
821
  const props = ${propsCode};
822
822
  createRoot(document.getElementById('root'))
823
823
  .render(React.createElement(Component, props));`;
@@ -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 = {
@@ -2148,11 +2214,8 @@ function buildCSPDirectives(csp) {
2148
2214
  directives.push(`img-src ${imgSources.join(" ")}`);
2149
2215
  const fontSources = ["'self'", "data:", ...allResourceDomains];
2150
2216
  directives.push(`font-src ${fontSources.join(" ")}`);
2151
- if (validConnectDomains.length) {
2152
- directives.push(`connect-src ${validConnectDomains.join(" ")}`);
2153
- } else {
2154
- directives.push(`connect-src ${allResourceDomains.join(" ")}`);
2155
- }
2217
+ const connectSources = [.../* @__PURE__ */ new Set([...allResourceDomains, ...validConnectDomains])];
2218
+ directives.push(`connect-src ${connectSources.join(" ")}`);
2156
2219
  directives.push("object-src 'self' data:");
2157
2220
  return directives;
2158
2221
  }
@@ -2161,26 +2224,34 @@ function buildCSPMetaTag(csp) {
2161
2224
  const content = directives.join("; ");
2162
2225
  return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
2163
2226
  }
2227
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
2164
2228
  function validateCSPDomain(domain) {
2165
- if (domain.startsWith("https://*.")) {
2166
- const rest = domain.slice(10);
2167
- return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$/.test(rest);
2229
+ if (typeof domain !== "string" || /[\s;,'"]/.test(domain)) return false;
2230
+ if (/[?#]/.test(domain)) return false;
2231
+ if (domain.includes("*")) {
2232
+ const wildcard = /^(?:https|wss):\/\/\*\.(.*)$/.exec(domain);
2233
+ return wildcard !== null && /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(wildcard[1]);
2168
2234
  }
2169
2235
  try {
2170
2236
  const url = new URL(domain);
2171
- return url.protocol === "https:";
2237
+ if (url.protocol === "https:" || url.protocol === "wss:") return true;
2238
+ return (url.protocol === "http:" || url.protocol === "ws:") && LOOPBACK_HOSTS.has(url.hostname);
2172
2239
  } catch {
2173
2240
  return false;
2174
2241
  }
2175
2242
  }
2243
+ var reportedInvalidDomains = /* @__PURE__ */ new Set();
2176
2244
  function sanitizeCSPDomains(domains) {
2177
2245
  if (!domains) return [];
2178
2246
  const valid = [];
2179
2247
  for (const domain of domains) {
2180
2248
  if (validateCSPDomain(domain)) {
2181
2249
  valid.push(domain);
2182
- } else {
2183
- console.warn(`Invalid CSP domain ignored: ${domain}`);
2250
+ } else if (!reportedInvalidDomains.has(domain)) {
2251
+ reportedInvalidDomains.add(domain);
2252
+ console.warn(
2253
+ `Invalid CSP domain ignored: ${domain} (expected an https:// or wss:// origin, or http:// / ws:// on localhost)`
2254
+ );
2184
2255
  }
2185
2256
  }
2186
2257
  return valid;