@frontmcp/uipack 1.9.1 → 1.9.3

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.
@@ -2,6 +2,8 @@ var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuilt
2
2
  const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
3
  // __frontmcpCreateRequire
4
4
  // libs/uipack/src/bridge-runtime/iife-generator.ts
5
+ var WIDGET_CALL_META_KEY = "frontmcp/widgetCall";
6
+ var MCP_APPS_DISPLAY_MODES = ["inline", "fullscreen", "pip"];
5
7
  function generateBridgeIIFE(options = {}) {
6
8
  const { debug = false, trustedOrigins = [], minify = false } = options;
7
9
  const adapters = options.adapters || ["openai", "ext-apps", "claude", "gemini", "generic"];
@@ -49,6 +51,7 @@ function generateBridgeIIFE(options = {}) {
49
51
  parts.push("bridge.initialize().then(function() {");
50
52
  parts.push(' log("Bridge initialized with adapter: " + bridge.adapterId);');
51
53
  parts.push(' window.dispatchEvent(new CustomEvent("bridge:ready", { detail: { adapter: bridge.adapterId } }));');
54
+ parts.push(" if (window.__mcpDisplayMode) bridge.requestDisplayMode(window.__mcpDisplayMode).catch(function() {});");
52
55
  parts.push("}).catch(function(err) {");
53
56
  parts.push(' console.error("[FrontMcpBridge] Init failed:", err);');
54
57
  parts.push(' window.dispatchEvent(new CustomEvent("bridge:error", { detail: { error: err } }));');
@@ -382,6 +385,9 @@ var OpenAIAdapter = {
382
385
  return Promise.resolve();
383
386
  },
384
387
  requestDisplayMode: function(context, mode) {
388
+ if (window.openai && typeof window.openai.requestDisplayMode === 'function') {
389
+ return Promise.resolve(window.openai.requestDisplayMode({ mode: mode }));
390
+ }
385
391
  return Promise.resolve();
386
392
  },
387
393
  setSize: function(context, size) {
@@ -419,6 +425,7 @@ var ExtAppsAdapter = {
419
425
  hostCapabilities: {},
420
426
  handshakeSettled: false,
421
427
  pendingSize: null,
428
+ modelContext: null,
422
429
  canHandle: function() {
423
430
  if (typeof window === 'undefined') return false;
424
431
  if (window.parent === window) return false;
@@ -497,8 +504,19 @@ var ExtAppsAdapter = {
497
504
 
498
505
  if ('method' in data && !('id' in data)) {
499
506
  this.handleNotification(context, data);
507
+ return;
508
+ }
509
+
510
+ if ('method' in data && 'id' in data) {
511
+ this.handleRequest(context, data, event.origin);
500
512
  }
501
513
  },
514
+ handleRequest: function(context, request, origin) {
515
+ // ui/resource-teardown: the host is about to unmount the view; reply once listeners have cleaned up.
516
+ if (request.method !== 'ui/resource-teardown') return;
517
+ window.dispatchEvent(new CustomEvent('bridge:teardown', { detail: {} }));
518
+ window.parent.postMessage({ jsonrpc: '2.0', id: request.id, result: {} }, origin);
519
+ },
502
520
  handleNotification: function(context, notification) {
503
521
  var params = notification.params || {};
504
522
  switch (notification.method) {
@@ -522,6 +540,7 @@ var ExtAppsAdapter = {
522
540
  Object.assign(context.hostContext, params);
523
541
  context.notifyContextChange(params);
524
542
  break;
543
+ case 'ui/notifications/tool-cancelled':
525
544
  case 'ui/notifications/cancelled':
526
545
  window.dispatchEvent(new CustomEvent('tool:cancelled', { detail: { reason: params.reason } }));
527
546
  break;
@@ -586,7 +605,7 @@ var ExtAppsAdapter = {
586
605
  var id = ++this.requestId;
587
606
  var params = {
588
607
  appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
589
- appCapabilities: { tools: { listChanged: false } },
608
+ appCapabilities: { tools: { listChanged: false }, availableDisplayModes: ${JSON.stringify(MCP_APPS_DISPLAY_MODES)} },
590
609
  protocolVersion: '2024-11-05'
591
610
  };
592
611
 
@@ -654,20 +673,25 @@ var ExtAppsAdapter = {
654
673
  return Promise.reject(new Error('Server tool proxy not supported'));
655
674
  }
656
675
  // Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
657
- return this.sendRequest('tools/call', { name: name, arguments: args || {} });
676
+ return this.sendRequest('tools/call', { name: name, arguments: args || {}, _meta: { '${WIDGET_CALL_META_KEY}': true } });
658
677
  },
659
678
  sendMessage: function(context, content) {
660
679
  return this.sendRequest('ui/message', { content: content });
661
680
  },
662
681
  openLink: function(context, url) {
663
- if (!this.hostCapabilities.openLink) {
682
+ if (!this.hostCapabilities.openLinks && !this.hostCapabilities.openLink) {
664
683
  window.open(url, '_blank', 'noopener,noreferrer');
665
684
  return Promise.resolve();
666
685
  }
667
- return this.sendRequest('ui/openLink', { url: url });
686
+ return this.sendRequest('ui/open-link', { url: url });
668
687
  },
669
688
  requestDisplayMode: function(context, mode) {
670
- return this.sendRequest('ui/setDisplayMode', { mode: mode });
689
+ // Ask only for a mode the host offers; it answers with the mode it actually set.
690
+ var offered = context.hostContext.availableDisplayModes;
691
+ if (!Array.isArray(offered) || offered.indexOf(mode) === -1) {
692
+ return Promise.reject(new Error('Display mode "' + mode + '" is not available on this host'));
693
+ }
694
+ return this.sendRequest('ui/request-display-mode', { mode: mode });
671
695
  },
672
696
  setSize: function(context, size) {
673
697
  // Standard ext-apps sizing: the view tells the host its dimensions with the
@@ -688,14 +712,22 @@ var ExtAppsAdapter = {
688
712
  return this.sendNotification('ui/notifications/size-changed', params);
689
713
  },
690
714
  requestClose: function(context) {
691
- return this.sendRequest('ui/close', {});
715
+ // The host decides whether to tear the view down; it answers with ui/resource-teardown if it does.
716
+ return this.sendNotification('ui/notifications/request-teardown', {});
692
717
  },
693
718
  // Extended ext-apps methods (full specification)
694
719
  updateModelContext: function(context, data, merge) {
695
- if (!this.hostCapabilities.modelContextUpdate) {
720
+ if (!this.hostCapabilities.updateModelContext && !this.hostCapabilities.modelContextUpdate) {
696
721
  return Promise.reject(new Error('Model context update not supported'));
697
722
  }
698
- return this.sendRequest('ui/updateModelContext', { context: data, merge: merge !== false });
723
+ // The host keeps only the latest update, so an object merges into the last one here.
724
+ var isObject = data !== null && typeof data === 'object' && !Array.isArray(data);
725
+ var modelContext = isObject && merge !== false ? Object.assign({}, this.modelContext, data) : data;
726
+ this.modelContext = isObject ? modelContext : null;
727
+ var text = typeof modelContext === 'string' ? modelContext : JSON.stringify(modelContext);
728
+ var params = { content: [{ type: 'text', text: text }] };
729
+ if (isObject) params.structuredContent = modelContext;
730
+ return this.sendRequest('ui/update-model-context', params);
699
731
  },
700
732
  log: function(context, level, message, data) {
701
733
  if (!this.hostCapabilities.logging) {
@@ -704,7 +736,10 @@ var ExtAppsAdapter = {
704
736
  logFn('[Widget] ' + message, data);
705
737
  return Promise.resolve();
706
738
  }
707
- return this.sendRequest('ui/log', { level: level, message: message, data: data });
739
+ return this.sendNotification('notifications/message', {
740
+ level: level === 'warn' ? 'warning' : level,
741
+ data: data === undefined ? message : { message: message, data: data }
742
+ });
708
743
  },
709
744
  registerTool: function(context, name, description, inputSchema) {
710
745
  if (!this.hostCapabilities.widgetTools) {
@@ -1101,8 +1136,10 @@ FrontMcpBridge.prototype.openLink = function(url) {
1101
1136
  FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1102
1137
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1103
1138
  var self = this;
1104
- return this._adapter.requestDisplayMode(this._context, mode).then(function() {
1105
- self._context.hostContext.displayMode = mode;
1139
+ return this._adapter.requestDisplayMode(this._context, mode).then(function(result) {
1140
+ // The host may set another mode than the one asked for, e.g. when it refuses the change.
1141
+ var setMode = result && result.mode;
1142
+ if (${JSON.stringify(MCP_APPS_DISPLAY_MODES)}.indexOf(setMode) !== -1) self._context.hostContext.displayMode = setMode;
1106
1143
  });
1107
1144
  };
1108
1145
 
@@ -1527,7 +1564,7 @@ function hasSizing(sizing) {
1527
1564
  return sizing.preferredHeight !== void 0 || sizing.minHeight !== void 0 || sizing.maxHeight !== void 0 || sizing.aspectRatio !== void 0 || sizing.autoResize !== void 0;
1528
1565
  }
1529
1566
  function buildDataInjectionScript(options) {
1530
- const { toolName, input, output, structuredContent, sizing } = options;
1567
+ const { toolName, input, output, structuredContent, sizing, displayMode } = options;
1531
1568
  const lines = [
1532
1569
  `window.__mcpAppsEnabled = true;`,
1533
1570
  `window.__mcpToolName = ${safeJsonForScript(toolName)};`,
@@ -1538,6 +1575,9 @@ function buildDataInjectionScript(options) {
1538
1575
  if (hasSizing(sizing)) {
1539
1576
  lines.push(`window.__mcpWidgetSizing = ${safeJsonForScript(sizing)};`);
1540
1577
  }
1578
+ if (displayMode && displayMode !== "inline") {
1579
+ lines.push(`window.__mcpDisplayMode = ${safeJsonForScript(displayMode)};`);
1580
+ }
1541
1581
  return `<script>
1542
1582
  ${lines.join("\n")}
1543
1583
  </script>`;
@@ -1629,7 +1669,8 @@ function resolveDataInjectionScript(args) {
1629
1669
  input: args.input,
1630
1670
  output: args.output,
1631
1671
  structuredContent: args.structuredContent,
1632
- sizing: args.sizing
1672
+ sizing: args.sizing,
1673
+ displayMode: args.displayMode
1633
1674
  });
1634
1675
  }
1635
1676
  function buildShell(content, config) {
@@ -1642,7 +1683,8 @@ function buildShell(content, config) {
1642
1683
  structuredContent,
1643
1684
  includeBridge = true,
1644
1685
  title,
1645
- sizing
1686
+ sizing,
1687
+ displayMode
1646
1688
  } = config;
1647
1689
  const { customShell, dataInjection } = config;
1648
1690
  const dataScript = resolveDataInjectionScript({
@@ -1651,7 +1693,8 @@ function buildShell(content, config) {
1651
1693
  input,
1652
1694
  output,
1653
1695
  structuredContent,
1654
- sizing
1696
+ sizing,
1697
+ displayMode
1655
1698
  });
1656
1699
  if (!withShell) {
1657
1700
  const html3 = `${dataScript}
@@ -1672,6 +1715,7 @@ ${content}`;
1672
1715
  includeBridge,
1673
1716
  title,
1674
1717
  sizing,
1718
+ displayMode,
1675
1719
  dataInjection
1676
1720
  });
1677
1721
  }
@@ -1727,7 +1771,8 @@ function buildCustomShell(content, customShell, ctx) {
1727
1771
  input: ctx.input,
1728
1772
  output: ctx.output,
1729
1773
  structuredContent: ctx.structuredContent,
1730
- sizing: ctx.sizing
1774
+ sizing: ctx.sizing,
1775
+ displayMode: ctx.displayMode
1731
1776
  });
1732
1777
  const sizingStyle = buildSizingStyleTag(ctx.sizing);
1733
1778
  const dataWithSizing = sizingStyle ? `${dataScript}
package/index.js CHANGED
@@ -121,6 +121,7 @@ __export(index_exports, {
121
121
  validateCSPDomain: () => validateCSPDomain,
122
122
  validateImportMap: () => validateImportMap,
123
123
  validateShellTemplate: () => validateShellTemplate,
124
+ warnIfComponentReference: () => warnIfComponentReference,
124
125
  wrapDetectedContent: () => wrapDetectedContent
125
126
  });
126
127
  module.exports = __toCommonJS(index_exports);
@@ -252,6 +253,8 @@ function safeJsonForScript(value) {
252
253
  }
253
254
 
254
255
  // libs/uipack/src/bridge-runtime/iife-generator.ts
256
+ var WIDGET_CALL_META_KEY = "frontmcp/widgetCall";
257
+ var MCP_APPS_DISPLAY_MODES = ["inline", "fullscreen", "pip"];
255
258
  function generateBridgeIIFE(options = {}) {
256
259
  const { debug = false, trustedOrigins = [], minify = false } = options;
257
260
  const adapters = options.adapters || ["openai", "ext-apps", "claude", "gemini", "generic"];
@@ -299,6 +302,7 @@ function generateBridgeIIFE(options = {}) {
299
302
  parts.push("bridge.initialize().then(function() {");
300
303
  parts.push(' log("Bridge initialized with adapter: " + bridge.adapterId);');
301
304
  parts.push(' window.dispatchEvent(new CustomEvent("bridge:ready", { detail: { adapter: bridge.adapterId } }));');
305
+ parts.push(" if (window.__mcpDisplayMode) bridge.requestDisplayMode(window.__mcpDisplayMode).catch(function() {});");
302
306
  parts.push("}).catch(function(err) {");
303
307
  parts.push(' console.error("[FrontMcpBridge] Init failed:", err);');
304
308
  parts.push(' window.dispatchEvent(new CustomEvent("bridge:error", { detail: { error: err } }));');
@@ -632,6 +636,9 @@ var OpenAIAdapter = {
632
636
  return Promise.resolve();
633
637
  },
634
638
  requestDisplayMode: function(context, mode) {
639
+ if (window.openai && typeof window.openai.requestDisplayMode === 'function') {
640
+ return Promise.resolve(window.openai.requestDisplayMode({ mode: mode }));
641
+ }
635
642
  return Promise.resolve();
636
643
  },
637
644
  setSize: function(context, size) {
@@ -669,6 +676,7 @@ var ExtAppsAdapter = {
669
676
  hostCapabilities: {},
670
677
  handshakeSettled: false,
671
678
  pendingSize: null,
679
+ modelContext: null,
672
680
  canHandle: function() {
673
681
  if (typeof window === 'undefined') return false;
674
682
  if (window.parent === window) return false;
@@ -747,8 +755,19 @@ var ExtAppsAdapter = {
747
755
 
748
756
  if ('method' in data && !('id' in data)) {
749
757
  this.handleNotification(context, data);
758
+ return;
759
+ }
760
+
761
+ if ('method' in data && 'id' in data) {
762
+ this.handleRequest(context, data, event.origin);
750
763
  }
751
764
  },
765
+ handleRequest: function(context, request, origin) {
766
+ // ui/resource-teardown: the host is about to unmount the view; reply once listeners have cleaned up.
767
+ if (request.method !== 'ui/resource-teardown') return;
768
+ window.dispatchEvent(new CustomEvent('bridge:teardown', { detail: {} }));
769
+ window.parent.postMessage({ jsonrpc: '2.0', id: request.id, result: {} }, origin);
770
+ },
752
771
  handleNotification: function(context, notification) {
753
772
  var params = notification.params || {};
754
773
  switch (notification.method) {
@@ -772,6 +791,7 @@ var ExtAppsAdapter = {
772
791
  Object.assign(context.hostContext, params);
773
792
  context.notifyContextChange(params);
774
793
  break;
794
+ case 'ui/notifications/tool-cancelled':
775
795
  case 'ui/notifications/cancelled':
776
796
  window.dispatchEvent(new CustomEvent('tool:cancelled', { detail: { reason: params.reason } }));
777
797
  break;
@@ -836,7 +856,7 @@ var ExtAppsAdapter = {
836
856
  var id = ++this.requestId;
837
857
  var params = {
838
858
  appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
839
- appCapabilities: { tools: { listChanged: false } },
859
+ appCapabilities: { tools: { listChanged: false }, availableDisplayModes: ${JSON.stringify(MCP_APPS_DISPLAY_MODES)} },
840
860
  protocolVersion: '2024-11-05'
841
861
  };
842
862
 
@@ -904,20 +924,25 @@ var ExtAppsAdapter = {
904
924
  return Promise.reject(new Error('Server tool proxy not supported'));
905
925
  }
906
926
  // Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
907
- return this.sendRequest('tools/call', { name: name, arguments: args || {} });
927
+ return this.sendRequest('tools/call', { name: name, arguments: args || {}, _meta: { '${WIDGET_CALL_META_KEY}': true } });
908
928
  },
909
929
  sendMessage: function(context, content) {
910
930
  return this.sendRequest('ui/message', { content: content });
911
931
  },
912
932
  openLink: function(context, url) {
913
- if (!this.hostCapabilities.openLink) {
933
+ if (!this.hostCapabilities.openLinks && !this.hostCapabilities.openLink) {
914
934
  window.open(url, '_blank', 'noopener,noreferrer');
915
935
  return Promise.resolve();
916
936
  }
917
- return this.sendRequest('ui/openLink', { url: url });
937
+ return this.sendRequest('ui/open-link', { url: url });
918
938
  },
919
939
  requestDisplayMode: function(context, mode) {
920
- return this.sendRequest('ui/setDisplayMode', { mode: mode });
940
+ // Ask only for a mode the host offers; it answers with the mode it actually set.
941
+ var offered = context.hostContext.availableDisplayModes;
942
+ if (!Array.isArray(offered) || offered.indexOf(mode) === -1) {
943
+ return Promise.reject(new Error('Display mode "' + mode + '" is not available on this host'));
944
+ }
945
+ return this.sendRequest('ui/request-display-mode', { mode: mode });
921
946
  },
922
947
  setSize: function(context, size) {
923
948
  // Standard ext-apps sizing: the view tells the host its dimensions with the
@@ -938,14 +963,22 @@ var ExtAppsAdapter = {
938
963
  return this.sendNotification('ui/notifications/size-changed', params);
939
964
  },
940
965
  requestClose: function(context) {
941
- return this.sendRequest('ui/close', {});
966
+ // The host decides whether to tear the view down; it answers with ui/resource-teardown if it does.
967
+ return this.sendNotification('ui/notifications/request-teardown', {});
942
968
  },
943
969
  // Extended ext-apps methods (full specification)
944
970
  updateModelContext: function(context, data, merge) {
945
- if (!this.hostCapabilities.modelContextUpdate) {
971
+ if (!this.hostCapabilities.updateModelContext && !this.hostCapabilities.modelContextUpdate) {
946
972
  return Promise.reject(new Error('Model context update not supported'));
947
973
  }
948
- return this.sendRequest('ui/updateModelContext', { context: data, merge: merge !== false });
974
+ // The host keeps only the latest update, so an object merges into the last one here.
975
+ var isObject = data !== null && typeof data === 'object' && !Array.isArray(data);
976
+ var modelContext = isObject && merge !== false ? Object.assign({}, this.modelContext, data) : data;
977
+ this.modelContext = isObject ? modelContext : null;
978
+ var text = typeof modelContext === 'string' ? modelContext : JSON.stringify(modelContext);
979
+ var params = { content: [{ type: 'text', text: text }] };
980
+ if (isObject) params.structuredContent = modelContext;
981
+ return this.sendRequest('ui/update-model-context', params);
949
982
  },
950
983
  log: function(context, level, message, data) {
951
984
  if (!this.hostCapabilities.logging) {
@@ -954,7 +987,10 @@ var ExtAppsAdapter = {
954
987
  logFn('[Widget] ' + message, data);
955
988
  return Promise.resolve();
956
989
  }
957
- return this.sendRequest('ui/log', { level: level, message: message, data: data });
990
+ return this.sendNotification('notifications/message', {
991
+ level: level === 'warn' ? 'warning' : level,
992
+ data: data === undefined ? message : { message: message, data: data }
993
+ });
958
994
  },
959
995
  registerTool: function(context, name, description, inputSchema) {
960
996
  if (!this.hostCapabilities.widgetTools) {
@@ -1351,8 +1387,10 @@ FrontMcpBridge.prototype.openLink = function(url) {
1351
1387
  FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1352
1388
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1353
1389
  var self = this;
1354
- return this._adapter.requestDisplayMode(this._context, mode).then(function() {
1355
- self._context.hostContext.displayMode = mode;
1390
+ return this._adapter.requestDisplayMode(this._context, mode).then(function(result) {
1391
+ // The host may set another mode than the one asked for, e.g. when it refuses the change.
1392
+ var setMode = result && result.mode;
1393
+ if (${JSON.stringify(MCP_APPS_DISPLAY_MODES)}.indexOf(setMode) !== -1) self._context.hostContext.displayMode = setMode;
1356
1394
  });
1357
1395
  };
1358
1396
 
@@ -3218,7 +3256,7 @@ function hasSizing(sizing) {
3218
3256
  return sizing.preferredHeight !== void 0 || sizing.minHeight !== void 0 || sizing.maxHeight !== void 0 || sizing.aspectRatio !== void 0 || sizing.autoResize !== void 0;
3219
3257
  }
3220
3258
  function buildDataInjectionScript(options) {
3221
- const { toolName, input, output, structuredContent, sizing } = options;
3259
+ const { toolName, input, output, structuredContent, sizing, displayMode } = options;
3222
3260
  const lines = [
3223
3261
  `window.__mcpAppsEnabled = true;`,
3224
3262
  `window.__mcpToolName = ${safeJsonForScript(toolName)};`,
@@ -3229,6 +3267,9 @@ function buildDataInjectionScript(options) {
3229
3267
  if (hasSizing(sizing)) {
3230
3268
  lines.push(`window.__mcpWidgetSizing = ${safeJsonForScript(sizing)};`);
3231
3269
  }
3270
+ if (displayMode && displayMode !== "inline") {
3271
+ lines.push(`window.__mcpDisplayMode = ${safeJsonForScript(displayMode)};`);
3272
+ }
3232
3273
  return `<script>
3233
3274
  ${lines.join("\n")}
3234
3275
  </script>`;
@@ -3320,7 +3361,8 @@ function resolveDataInjectionScript(args) {
3320
3361
  input: args.input,
3321
3362
  output: args.output,
3322
3363
  structuredContent: args.structuredContent,
3323
- sizing: args.sizing
3364
+ sizing: args.sizing,
3365
+ displayMode: args.displayMode
3324
3366
  });
3325
3367
  }
3326
3368
  function buildShell(content, config) {
@@ -3333,7 +3375,8 @@ function buildShell(content, config) {
3333
3375
  structuredContent,
3334
3376
  includeBridge = true,
3335
3377
  title,
3336
- sizing
3378
+ sizing,
3379
+ displayMode
3337
3380
  } = config;
3338
3381
  const { customShell, dataInjection } = config;
3339
3382
  const dataScript = resolveDataInjectionScript({
@@ -3342,7 +3385,8 @@ function buildShell(content, config) {
3342
3385
  input,
3343
3386
  output,
3344
3387
  structuredContent,
3345
- sizing
3388
+ sizing,
3389
+ displayMode
3346
3390
  });
3347
3391
  if (!withShell) {
3348
3392
  const html3 = `${dataScript}
@@ -3363,6 +3407,7 @@ ${content}`;
3363
3407
  includeBridge,
3364
3408
  title,
3365
3409
  sizing,
3410
+ displayMode,
3366
3411
  dataInjection
3367
3412
  });
3368
3413
  }
@@ -3418,7 +3463,8 @@ function buildCustomShell(content, customShell, ctx) {
3418
3463
  input: ctx.input,
3419
3464
  output: ctx.output,
3420
3465
  structuredContent: ctx.structuredContent,
3421
- sizing: ctx.sizing
3466
+ sizing: ctx.sizing,
3467
+ displayMode: ctx.displayMode
3422
3468
  });
3423
3469
  const sizingStyle = buildSizingStyleTag(ctx.sizing);
3424
3470
  const dataWithSizing = sizingStyle ? `${dataScript}
@@ -4436,7 +4482,7 @@ function noticeStringResult(toolName, logger) {
4436
4482
  if (noticedStringResultTools.has(toolName)) return;
4437
4483
  noticedStringResultTools.add(toolName);
4438
4484
  logger.warn(
4439
- `[frontmcp] The UI template of tool "${toolName}" returned a plain string containing markup, which is rendered as HTML. FrontMCP 1.9 will HTML-escape plain string results by default. Build the markup with ctx.helpers.html\`\u2026\` (interpolated values are escaped) or wrap safe markup with ctx.helpers.trustedHtml(), then set ui.escapeStringResults: true (or escapeStringResults: false to keep the current behaviour without this notice). See ${STRING_RESULT_DOCS}`
4485
+ `[frontmcp] The UI template of tool "${toolName}" returned a plain string containing markup. Since 1.9.2, FrontMCP HTML-escapes plain string results by default, so it is shown as text. Build the markup with ctx.helpers.html\`\u2026\` (interpolated values are escaped) or wrap safe markup with ctx.helpers.trustedHtml(). Set ui.escapeStringResults: false to render plain strings as markup, or true to silence this notice. See ${STRING_RESULT_DOCS}`
4440
4486
  );
4441
4487
  }
4442
4488
  function renderUnwrappedResult(rawResult, options) {
@@ -4446,13 +4492,14 @@ function renderUnwrappedResult(rawResult, options) {
4446
4492
  if (typeof rawResult !== "string") {
4447
4493
  return `<pre>${escapeHtml(JSON.stringify(rawResult, null, 2))}</pre>`;
4448
4494
  }
4449
- if (detectContentType(rawResult) !== "html" || options.escapeStringResults === true) {
4450
- return escapeHtml(rawResult);
4495
+ const looksLikeMarkup = detectContentType(rawResult) === "html";
4496
+ if (looksLikeMarkup && options.escapeStringResults === false) {
4497
+ return rawResult;
4451
4498
  }
4452
- if (options.escapeStringResults === void 0) {
4499
+ if (looksLikeMarkup && options.escapeStringResults === void 0) {
4453
4500
  noticeStringResult(options.toolName, options.logger ?? console);
4454
4501
  }
4455
- return rawResult;
4502
+ return escapeHtml(rawResult);
4456
4503
  }
4457
4504
  var warnedComponentReferences = /* @__PURE__ */ new WeakMap();
4458
4505
  function warnComponentReference(template, toolName, logger) {
@@ -4464,7 +4511,18 @@ function warnComponentReference(template, toolName, logger) {
4464
4511
  `[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.`
4465
4512
  );
4466
4513
  }
4467
- function isReactElement(value) {
4514
+ var COMPILED_JSX_PATTERN = /\b(?:React\.createElement|_?jsx(?:s|DEV)?)\)?\s*\(/;
4515
+ function isComponentReference(template) {
4516
+ if (isReactTypedObject(template)) return true;
4517
+ if (typeof template !== "function" || detectUIType(template) !== "react") return false;
4518
+ return isDefinitelyReactComponent(template) || COMPILED_JSX_PATTERN.test(Function.prototype.toString.call(template));
4519
+ }
4520
+ function warnIfComponentReference(toolName, template, logger = console) {
4521
+ if (!isComponentReference(template)) return false;
4522
+ warnComponentReference(template, toolName, logger);
4523
+ return true;
4524
+ }
4525
+ function isReactTypedObject(value) {
4468
4526
  return typeof value === "object" && value !== null && "$$typeof" in value;
4469
4527
  }
4470
4528
  function isDefinitelyReactComponent(template) {
@@ -4483,6 +4541,7 @@ function renderToolTemplate(options) {
4483
4541
  includeBridge: true,
4484
4542
  resolver,
4485
4543
  sizing,
4544
+ displayMode: options.displayMode,
4486
4545
  csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
4487
4546
  };
4488
4547
  let html2;
@@ -4504,7 +4563,7 @@ function renderToolTemplate(options) {
4504
4563
  if (isHtmlBuilder) {
4505
4564
  try {
4506
4565
  rawResult = template(ctx);
4507
- isHtmlBuilder = !isReactElement(rawResult);
4566
+ isHtmlBuilder = !isReactTypedObject(rawResult);
4508
4567
  } catch (error) {
4509
4568
  if (uiType !== "react") throw error;
4510
4569
  isHtmlBuilder = false;
@@ -4533,6 +4592,7 @@ function renderToolTemplate(options) {
4533
4592
  hash = shellResult.hash;
4534
4593
  size = shellResult.size;
4535
4594
  } else {
4595
+ if (isReactTypedObject(template)) warnComponentReference(template, toolName, options.logger ?? console);
4536
4596
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
4537
4597
  html2 = shellResult.html;
4538
4598
  hash = shellResult.hash;
@@ -4735,5 +4795,6 @@ function buildCDNInfoForUIType(uiType) {
4735
4795
  validateCSPDomain,
4736
4796
  validateImportMap,
4737
4797
  validateShellTemplate,
4798
+ warnIfComponentReference,
4738
4799
  wrapDetectedContent
4739
4800
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/uipack",
3
- "version": "1.9.1",
3
+ "version": "1.9.3",
4
4
  "description": "FrontMCP UIpack - HTML shell builder, pluggable import resolver, and NPM component loader for MCP UI (React-free core)",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "homepage": "https://docs.agentfront.dev",
@@ -61,7 +61,7 @@
61
61
  "typescript": "^5.9.3"
62
62
  },
63
63
  "peerDependencies": {
64
- "@frontmcp/lazy-zod": "1.9.1",
64
+ "@frontmcp/lazy-zod": "1.9.3",
65
65
  "esbuild": ">=0.27.0 <1"
66
66
  },
67
67
  "peerDependenciesMeta": {
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../src/shell/builder.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH,OAAO,KAAK,EAAE,WAAW,EAAgC,WAAW,EAAE,MAAM,SAAS,CAAC;AA8BtF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,WAAW,CA4F5E"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../src/shell/builder.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH,OAAO,KAAK,EAAE,WAAW,EAAgC,WAAW,EAAE,MAAM,SAAS,CAAC;AAgCtF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,WAAW,CA+F5E"}
@@ -21,7 +21,8 @@ export declare function hasSizing(sizing?: WidgetSizing): sizing is WidgetSizing
21
21
  * Sets `window.__mcpToolName`, `window.__mcpToolInput`,
22
22
  * `window.__mcpToolOutput`, and `window.__mcpStructuredContent`. When `sizing`
23
23
  * is provided it also sets `window.__mcpWidgetSizing` so the bridge runtime can
24
- * apply CSS and drive auto-resize.
24
+ * apply CSS and drive auto-resize, and a `displayMode` other than `'inline'` sets
25
+ * `window.__mcpDisplayMode`, which the bridge asks the host for once it connects.
25
26
  */
26
27
  export declare function buildDataInjectionScript(options: {
27
28
  toolName: string;
@@ -29,6 +30,7 @@ export declare function buildDataInjectionScript(options: {
29
30
  output?: unknown;
30
31
  structuredContent?: unknown;
31
32
  sizing?: WidgetSizing;
33
+ displayMode?: string;
32
34
  }): string;
33
35
  /**
34
36
  * Generic data-injection descriptor: override the injected window global.
@@ -1 +1 @@
1
- {"version":3,"file":"data-injector.d.ts","sourceRoot":"","sources":["../../src/shell/data-injector.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACrE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,IAAI,YAAY,CASvE;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB,GAAG,MAAM,CAgBT;AAED;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAAC,UAAU,EAAE;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,MAAM,CAeT;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,CAAC;IACrC,gCAAgC;IAChC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7D,kCAAkC;IAClC,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9D,4CAA4C;IAC5C,QAAQ,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACtC,qCAAqC;IACrC,SAAS,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,MAAM,CAAC;IACrC,8FAA8F;IAC9F,IAAI,EAAE,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,WAAW,CAAC;IAC3E,gEAAgE;IAChE,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,WAAW,CAAC;CAC9C;AAID;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,eAAe,CA+BvD"}
1
+ {"version":3,"file":"data-injector.d.ts","sourceRoot":"","sources":["../../src/shell/data-injector.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACrE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,IAAI,YAAY,CASvE;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,MAAM,CAoBT;AAED;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAAC,UAAU,EAAE;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,MAAM,CAeT;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,CAAC;IACrC,gCAAgC;IAChC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7D,kCAAkC;IAClC,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9D,4CAA4C;IAC5C,QAAQ,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACtC,qCAAqC;IACrC,SAAS,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,MAAM,CAAC;IACrC,8FAA8F;IAC9F,IAAI,EAAE,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,WAAW,CAAC;IAC3E,gEAAgE;IAChE,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,WAAW,CAAC;CAC9C;AAID;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,eAAe,CA+BvD"}