@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/shell/index.js CHANGED
@@ -317,12 +317,38 @@ function __applyHostTheme(theme) {
317
317
  } catch (e) {}
318
318
  }
319
319
 
320
+ function extractToolOutput(params) {
321
+ // A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
322
+ if (!params) return undefined;
323
+ if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
324
+ var content = params.content;
325
+ if (Array.isArray(content)) {
326
+ for (var i = 0; i < content.length; i++) {
327
+ var item = content[i];
328
+ if (item && item.type === 'text' && typeof item.text === 'string') {
329
+ try { return JSON.parse(item.text); } catch (e) { return item.text; }
330
+ }
331
+ }
332
+ }
333
+ return content;
334
+ }
335
+
320
336
  function readInjectedData() {
321
337
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
322
338
  if (typeof window !== 'undefined') {
323
339
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
324
340
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
325
341
  if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
342
+ // The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
343
+ // (toolOutput is the tool's structuredContent).
344
+ var oa = window.openai;
345
+ if (oa && typeof oa === 'object') {
346
+ if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
347
+ if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
348
+ if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
349
+ if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
350
+ }
351
+ }
326
352
  }
327
353
  return data;
328
354
  }
@@ -371,7 +397,24 @@ var OpenAIAdapter = {
371
397
  if (sdk.displayMode) {
372
398
  context.hostContext.displayMode = sdk.displayMode;
373
399
  }
374
- // Note: OpenAI SDK does not have an onContextChange equivalent
400
+ // The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
401
+ window.addEventListener('openai:set_globals', function(event) {
402
+ var globals = (event && event.detail && event.detail.globals) || window.openai || {};
403
+ if (globals.toolInput !== undefined && globals.toolInput !== null) {
404
+ context.toolInput = globals.toolInput;
405
+ window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
406
+ }
407
+ if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
408
+ context.structuredContent = globals.toolOutput;
409
+ context.notifyToolResult(globals.toolOutput);
410
+ }
411
+ if (globals.theme && globals.theme !== context.hostContext.theme) {
412
+ context.notifyContextChange({ theme: globals.theme });
413
+ }
414
+ if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
415
+ context.notifyContextChange({ displayMode: globals.displayMode });
416
+ }
417
+ });
375
418
  return Promise.resolve();
376
419
  },
377
420
  callTool: function(context, name, args) {
@@ -518,9 +561,10 @@ var ExtAppsAdapter = {
518
561
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
519
562
  break;
520
563
  case 'ui/notifications/tool-result':
521
- context.toolOutput = params.content;
564
+ var resultOutput = extractToolOutput(params);
565
+ context.toolOutput = resultOutput;
522
566
  context.structuredContent = params.structuredContent;
523
- context.notifyToolResult(params.content);
567
+ context.notifyToolResult(resultOutput);
524
568
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
525
569
  break;
526
570
  case 'ui/notifications/host-context-changed':
@@ -574,6 +618,15 @@ var ExtAppsAdapter = {
574
618
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
575
619
  });
576
620
  },
621
+ sendNotification: function(method, params) {
622
+ // JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
623
+ if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
624
+ return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
625
+ }
626
+ var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
627
+ window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
628
+ return Promise.resolve();
629
+ },
577
630
  sendHandshake: function(context) {
578
631
  // Send ui/initialize using '*' as target origin since TOFU hasn't
579
632
  // been established yet. The response from the host will establish
@@ -639,7 +692,7 @@ var ExtAppsAdapter = {
639
692
  var pending = this.pendingSize;
640
693
  if (!pending) return;
641
694
  this.pendingSize = null;
642
- this.sendRequest('ui/setSize', pending.params).then(function(result) {
695
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
643
696
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
644
697
  }, function(err) {
645
698
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
@@ -666,13 +719,11 @@ var ExtAppsAdapter = {
666
719
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
667
720
  },
668
721
  setSize: function(context, size) {
669
- // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
670
- // measured/desired widget dimensions to the host.
671
- var params = {
672
- height: size && size.height,
673
- width: size && size.width,
674
- aspectRatio: size && size.aspectRatio
675
- };
722
+ // Standard ext-apps sizing: the view tells the host its dimensions with the
723
+ // ui/notifications/size-changed notification ({ width?, height? } in px).
724
+ var params = {};
725
+ if (size && typeof size.width === 'number') params.width = size.width;
726
+ if (size && typeof size.height === 'number') params.height = size.height;
676
727
  if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
677
728
  // No host origin yet: keep only the latest size and send it when the
678
729
  // handshake settles; every caller's promise follows that one request.
@@ -683,7 +734,7 @@ var ExtAppsAdapter = {
683
734
  self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
684
735
  });
685
736
  }
686
- return this.sendRequest('ui/setSize', params);
737
+ return this.sendNotification('ui/notifications/size-changed', params);
687
738
  },
688
739
  requestClose: function(context) {
689
740
  return this.sendRequest('ui/close', {});
@@ -1106,7 +1157,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1106
1157
 
1107
1158
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1108
1159
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1109
- // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1160
+ // ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
1110
1161
  FrontMcpBridge.prototype.setSize = function(size) {
1111
1162
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1112
1163
  if (!this._adapter.setSize) return Promise.resolve();
@@ -1208,7 +1259,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
1208
1259
  target.classList.add('opacity-50', 'cursor-not-allowed');
1209
1260
 
1210
1261
  // Add spinner for buttons
1211
- 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>';
1262
+ 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>';
1212
1263
  if (target.tagName === 'BUTTON') {
1213
1264
  target.innerHTML = spinner + 'Loading...';
1214
1265
  }
@@ -1289,7 +1340,22 @@ function minifyJS(code) {
1289
1340
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
1290
1341
  return code;
1291
1342
  }
1292
- return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\s+/g, " ").replace(/\s*([{};,:()[\]])\s*/g, "$1").replace(/;\}/g, "}").trim();
1343
+ const out = [];
1344
+ let inBlockComment = false;
1345
+ for (const raw of code.split("\n")) {
1346
+ const line = raw.trim();
1347
+ if (inBlockComment) {
1348
+ if (line.includes("*/")) inBlockComment = false;
1349
+ continue;
1350
+ }
1351
+ if (line === "" || line.startsWith("//")) continue;
1352
+ if (line.startsWith("/*")) {
1353
+ if (!line.includes("*/")) inBlockComment = true;
1354
+ continue;
1355
+ }
1356
+ out.push(line);
1357
+ }
1358
+ return out.join("\n");
1293
1359
  }
1294
1360
  function generatePlatformBundle(platform, options = {}) {
1295
1361
  const platformAdapters = {
@@ -1353,11 +1419,8 @@ function buildCSPDirectives(csp) {
1353
1419
  directives.push(`img-src ${imgSources.join(" ")}`);
1354
1420
  const fontSources = ["'self'", "data:", ...allResourceDomains];
1355
1421
  directives.push(`font-src ${fontSources.join(" ")}`);
1356
- if (validConnectDomains.length) {
1357
- directives.push(`connect-src ${validConnectDomains.join(" ")}`);
1358
- } else {
1359
- directives.push(`connect-src ${allResourceDomains.join(" ")}`);
1360
- }
1422
+ const connectSources = [.../* @__PURE__ */ new Set([...allResourceDomains, ...validConnectDomains])];
1423
+ directives.push(`connect-src ${connectSources.join(" ")}`);
1361
1424
  directives.push("object-src 'self' data:");
1362
1425
  return directives;
1363
1426
  }
@@ -1366,26 +1429,34 @@ function buildCSPMetaTag(csp) {
1366
1429
  const content = directives.join("; ");
1367
1430
  return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
1368
1431
  }
1432
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1369
1433
  function validateCSPDomain(domain) {
1370
- if (domain.startsWith("https://*.")) {
1371
- const rest = domain.slice(10);
1372
- return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$/.test(rest);
1434
+ if (typeof domain !== "string" || /[\s;,'"]/.test(domain)) return false;
1435
+ if (/[?#]/.test(domain)) return false;
1436
+ if (domain.includes("*")) {
1437
+ const wildcard = /^(?:https|wss):\/\/\*\.(.*)$/.exec(domain);
1438
+ return wildcard !== null && /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(wildcard[1]);
1373
1439
  }
1374
1440
  try {
1375
1441
  const url = new URL(domain);
1376
- return url.protocol === "https:";
1442
+ if (url.protocol === "https:" || url.protocol === "wss:") return true;
1443
+ return (url.protocol === "http:" || url.protocol === "ws:") && LOOPBACK_HOSTS.has(url.hostname);
1377
1444
  } catch {
1378
1445
  return false;
1379
1446
  }
1380
1447
  }
1448
+ var reportedInvalidDomains = /* @__PURE__ */ new Set();
1381
1449
  function sanitizeCSPDomains(domains) {
1382
1450
  if (!domains) return [];
1383
1451
  const valid = [];
1384
1452
  for (const domain of domains) {
1385
1453
  if (validateCSPDomain(domain)) {
1386
1454
  valid.push(domain);
1387
- } else {
1388
- console.warn(`Invalid CSP domain ignored: ${domain}`);
1455
+ } else if (!reportedInvalidDomains.has(domain)) {
1456
+ reportedInvalidDomains.add(domain);
1457
+ console.warn(
1458
+ `Invalid CSP domain ignored: ${domain} (expected an https:// or wss:// origin, or http:// / ws:// on localhost)`
1459
+ );
1389
1460
  }
1390
1461
  }
1391
1462
  return valid;
@@ -418,7 +418,7 @@ export interface UITemplateConfig<In = unknown, Out = unknown> {
418
418
  * - Claude / static widgets: the host measures DOM height itself, so this is
419
419
  * a CSS-only no-op.
420
420
  * - OpenAI: forwarded through the Apps SDK sizing API.
421
- * - ext-apps hosts: reported via the `ui/setSize` request.
421
+ * - ext-apps hosts: reported with the standard `ui/notifications/size-changed` notification.
422
422
  *
423
423
  * @default true (when the widget configures sizing)
424
424
  */