@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.
@@ -855,6 +855,8 @@ ${json}
855
855
  }
856
856
 
857
857
  // libs/uipack/src/bridge-runtime/iife-generator.ts
858
+ var WIDGET_CALL_META_KEY = "frontmcp/widgetCall";
859
+ var MCP_APPS_DISPLAY_MODES = ["inline", "fullscreen", "pip"];
858
860
  function generateBridgeIIFE(options = {}) {
859
861
  const { debug = false, trustedOrigins = [], minify = false } = options;
860
862
  const adapters = options.adapters || ["openai", "ext-apps", "claude", "gemini", "generic"];
@@ -902,6 +904,7 @@ function generateBridgeIIFE(options = {}) {
902
904
  parts.push("bridge.initialize().then(function() {");
903
905
  parts.push(' log("Bridge initialized with adapter: " + bridge.adapterId);');
904
906
  parts.push(' window.dispatchEvent(new CustomEvent("bridge:ready", { detail: { adapter: bridge.adapterId } }));');
907
+ parts.push(" if (window.__mcpDisplayMode) bridge.requestDisplayMode(window.__mcpDisplayMode).catch(function() {});");
905
908
  parts.push("}).catch(function(err) {");
906
909
  parts.push(' console.error("[FrontMcpBridge] Init failed:", err);');
907
910
  parts.push(' window.dispatchEvent(new CustomEvent("bridge:error", { detail: { error: err } }));');
@@ -1235,6 +1238,9 @@ var OpenAIAdapter = {
1235
1238
  return Promise.resolve();
1236
1239
  },
1237
1240
  requestDisplayMode: function(context, mode) {
1241
+ if (window.openai && typeof window.openai.requestDisplayMode === 'function') {
1242
+ return Promise.resolve(window.openai.requestDisplayMode({ mode: mode }));
1243
+ }
1238
1244
  return Promise.resolve();
1239
1245
  },
1240
1246
  setSize: function(context, size) {
@@ -1272,6 +1278,7 @@ var ExtAppsAdapter = {
1272
1278
  hostCapabilities: {},
1273
1279
  handshakeSettled: false,
1274
1280
  pendingSize: null,
1281
+ modelContext: null,
1275
1282
  canHandle: function() {
1276
1283
  if (typeof window === 'undefined') return false;
1277
1284
  if (window.parent === window) return false;
@@ -1350,8 +1357,19 @@ var ExtAppsAdapter = {
1350
1357
 
1351
1358
  if ('method' in data && !('id' in data)) {
1352
1359
  this.handleNotification(context, data);
1360
+ return;
1361
+ }
1362
+
1363
+ if ('method' in data && 'id' in data) {
1364
+ this.handleRequest(context, data, event.origin);
1353
1365
  }
1354
1366
  },
1367
+ handleRequest: function(context, request, origin) {
1368
+ // ui/resource-teardown: the host is about to unmount the view; reply once listeners have cleaned up.
1369
+ if (request.method !== 'ui/resource-teardown') return;
1370
+ window.dispatchEvent(new CustomEvent('bridge:teardown', { detail: {} }));
1371
+ window.parent.postMessage({ jsonrpc: '2.0', id: request.id, result: {} }, origin);
1372
+ },
1355
1373
  handleNotification: function(context, notification) {
1356
1374
  var params = notification.params || {};
1357
1375
  switch (notification.method) {
@@ -1375,6 +1393,7 @@ var ExtAppsAdapter = {
1375
1393
  Object.assign(context.hostContext, params);
1376
1394
  context.notifyContextChange(params);
1377
1395
  break;
1396
+ case 'ui/notifications/tool-cancelled':
1378
1397
  case 'ui/notifications/cancelled':
1379
1398
  window.dispatchEvent(new CustomEvent('tool:cancelled', { detail: { reason: params.reason } }));
1380
1399
  break;
@@ -1439,7 +1458,7 @@ var ExtAppsAdapter = {
1439
1458
  var id = ++this.requestId;
1440
1459
  var params = {
1441
1460
  appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
1442
- appCapabilities: { tools: { listChanged: false } },
1461
+ appCapabilities: { tools: { listChanged: false }, availableDisplayModes: ${JSON.stringify(MCP_APPS_DISPLAY_MODES)} },
1443
1462
  protocolVersion: '2024-11-05'
1444
1463
  };
1445
1464
 
@@ -1507,20 +1526,25 @@ var ExtAppsAdapter = {
1507
1526
  return Promise.reject(new Error('Server tool proxy not supported'));
1508
1527
  }
1509
1528
  // Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
1510
- return this.sendRequest('tools/call', { name: name, arguments: args || {} });
1529
+ return this.sendRequest('tools/call', { name: name, arguments: args || {}, _meta: { '${WIDGET_CALL_META_KEY}': true } });
1511
1530
  },
1512
1531
  sendMessage: function(context, content) {
1513
1532
  return this.sendRequest('ui/message', { content: content });
1514
1533
  },
1515
1534
  openLink: function(context, url) {
1516
- if (!this.hostCapabilities.openLink) {
1535
+ if (!this.hostCapabilities.openLinks && !this.hostCapabilities.openLink) {
1517
1536
  window.open(url, '_blank', 'noopener,noreferrer');
1518
1537
  return Promise.resolve();
1519
1538
  }
1520
- return this.sendRequest('ui/openLink', { url: url });
1539
+ return this.sendRequest('ui/open-link', { url: url });
1521
1540
  },
1522
1541
  requestDisplayMode: function(context, mode) {
1523
- return this.sendRequest('ui/setDisplayMode', { mode: mode });
1542
+ // Ask only for a mode the host offers; it answers with the mode it actually set.
1543
+ var offered = context.hostContext.availableDisplayModes;
1544
+ if (!Array.isArray(offered) || offered.indexOf(mode) === -1) {
1545
+ return Promise.reject(new Error('Display mode "' + mode + '" is not available on this host'));
1546
+ }
1547
+ return this.sendRequest('ui/request-display-mode', { mode: mode });
1524
1548
  },
1525
1549
  setSize: function(context, size) {
1526
1550
  // Standard ext-apps sizing: the view tells the host its dimensions with the
@@ -1541,14 +1565,22 @@ var ExtAppsAdapter = {
1541
1565
  return this.sendNotification('ui/notifications/size-changed', params);
1542
1566
  },
1543
1567
  requestClose: function(context) {
1544
- return this.sendRequest('ui/close', {});
1568
+ // The host decides whether to tear the view down; it answers with ui/resource-teardown if it does.
1569
+ return this.sendNotification('ui/notifications/request-teardown', {});
1545
1570
  },
1546
1571
  // Extended ext-apps methods (full specification)
1547
1572
  updateModelContext: function(context, data, merge) {
1548
- if (!this.hostCapabilities.modelContextUpdate) {
1573
+ if (!this.hostCapabilities.updateModelContext && !this.hostCapabilities.modelContextUpdate) {
1549
1574
  return Promise.reject(new Error('Model context update not supported'));
1550
1575
  }
1551
- return this.sendRequest('ui/updateModelContext', { context: data, merge: merge !== false });
1576
+ // The host keeps only the latest update, so an object merges into the last one here.
1577
+ var isObject = data !== null && typeof data === 'object' && !Array.isArray(data);
1578
+ var modelContext = isObject && merge !== false ? Object.assign({}, this.modelContext, data) : data;
1579
+ this.modelContext = isObject ? modelContext : null;
1580
+ var text = typeof modelContext === 'string' ? modelContext : JSON.stringify(modelContext);
1581
+ var params = { content: [{ type: 'text', text: text }] };
1582
+ if (isObject) params.structuredContent = modelContext;
1583
+ return this.sendRequest('ui/update-model-context', params);
1552
1584
  },
1553
1585
  log: function(context, level, message, data) {
1554
1586
  if (!this.hostCapabilities.logging) {
@@ -1557,7 +1589,10 @@ var ExtAppsAdapter = {
1557
1589
  logFn('[Widget] ' + message, data);
1558
1590
  return Promise.resolve();
1559
1591
  }
1560
- return this.sendRequest('ui/log', { level: level, message: message, data: data });
1592
+ return this.sendNotification('notifications/message', {
1593
+ level: level === 'warn' ? 'warning' : level,
1594
+ data: data === undefined ? message : { message: message, data: data }
1595
+ });
1561
1596
  },
1562
1597
  registerTool: function(context, name, description, inputSchema) {
1563
1598
  if (!this.hostCapabilities.widgetTools) {
@@ -1954,8 +1989,10 @@ FrontMcpBridge.prototype.openLink = function(url) {
1954
1989
  FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1955
1990
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1956
1991
  var self = this;
1957
- return this._adapter.requestDisplayMode(this._context, mode).then(function() {
1958
- self._context.hostContext.displayMode = mode;
1992
+ return this._adapter.requestDisplayMode(this._context, mode).then(function(result) {
1993
+ // The host may set another mode than the one asked for, e.g. when it refuses the change.
1994
+ var setMode = result && result.mode;
1995
+ if (${JSON.stringify(MCP_APPS_DISPLAY_MODES)}.indexOf(setMode) !== -1) self._context.hostContext.displayMode = setMode;
1959
1996
  });
1960
1997
  };
1961
1998
 
@@ -2307,7 +2344,7 @@ function hasSizing(sizing) {
2307
2344
  return sizing.preferredHeight !== void 0 || sizing.minHeight !== void 0 || sizing.maxHeight !== void 0 || sizing.aspectRatio !== void 0 || sizing.autoResize !== void 0;
2308
2345
  }
2309
2346
  function buildDataInjectionScript(options) {
2310
- const { toolName, input, output, structuredContent, sizing } = options;
2347
+ const { toolName, input, output, structuredContent, sizing, displayMode } = options;
2311
2348
  const lines = [
2312
2349
  `window.__mcpAppsEnabled = true;`,
2313
2350
  `window.__mcpToolName = ${safeJsonForScript(toolName)};`,
@@ -2318,6 +2355,9 @@ function buildDataInjectionScript(options) {
2318
2355
  if (hasSizing(sizing)) {
2319
2356
  lines.push(`window.__mcpWidgetSizing = ${safeJsonForScript(sizing)};`);
2320
2357
  }
2358
+ if (displayMode && displayMode !== "inline") {
2359
+ lines.push(`window.__mcpDisplayMode = ${safeJsonForScript(displayMode)};`);
2360
+ }
2321
2361
  return `<script>
2322
2362
  ${lines.join("\n")}
2323
2363
  </script>`;
@@ -2383,7 +2423,8 @@ function resolveDataInjectionScript(args) {
2383
2423
  input: args.input,
2384
2424
  output: args.output,
2385
2425
  structuredContent: args.structuredContent,
2386
- sizing: args.sizing
2426
+ sizing: args.sizing,
2427
+ displayMode: args.displayMode
2387
2428
  });
2388
2429
  }
2389
2430
  function buildShell(content, config) {
@@ -2396,7 +2437,8 @@ function buildShell(content, config) {
2396
2437
  structuredContent,
2397
2438
  includeBridge = true,
2398
2439
  title,
2399
- sizing
2440
+ sizing,
2441
+ displayMode
2400
2442
  } = config;
2401
2443
  const { customShell, dataInjection } = config;
2402
2444
  const dataScript = resolveDataInjectionScript({
@@ -2405,7 +2447,8 @@ function buildShell(content, config) {
2405
2447
  input,
2406
2448
  output,
2407
2449
  structuredContent,
2408
- sizing
2450
+ sizing,
2451
+ displayMode
2409
2452
  });
2410
2453
  if (!withShell) {
2411
2454
  const html2 = `${dataScript}
@@ -2426,6 +2469,7 @@ ${content}`;
2426
2469
  includeBridge,
2427
2470
  title,
2428
2471
  sizing,
2472
+ displayMode,
2429
2473
  dataInjection
2430
2474
  });
2431
2475
  }
@@ -2481,7 +2525,8 @@ function buildCustomShell(content, customShell, ctx) {
2481
2525
  input: ctx.input,
2482
2526
  output: ctx.output,
2483
2527
  structuredContent: ctx.structuredContent,
2484
- sizing: ctx.sizing
2528
+ sizing: ctx.sizing,
2529
+ displayMode: ctx.displayMode
2485
2530
  });
2486
2531
  const sizingStyle = buildSizingStyleTag(ctx.sizing);
2487
2532
  const dataWithSizing = sizingStyle ? `${dataScript}
@@ -534,6 +534,8 @@ ${json}
534
534
  }
535
535
 
536
536
  // libs/uipack/src/bridge-runtime/iife-generator.ts
537
+ var WIDGET_CALL_META_KEY = "frontmcp/widgetCall";
538
+ var MCP_APPS_DISPLAY_MODES = ["inline", "fullscreen", "pip"];
537
539
  function generateBridgeIIFE(options = {}) {
538
540
  const { debug = false, trustedOrigins = [], minify = false } = options;
539
541
  const adapters = options.adapters || ["openai", "ext-apps", "claude", "gemini", "generic"];
@@ -581,6 +583,7 @@ function generateBridgeIIFE(options = {}) {
581
583
  parts.push("bridge.initialize().then(function() {");
582
584
  parts.push(' log("Bridge initialized with adapter: " + bridge.adapterId);');
583
585
  parts.push(' window.dispatchEvent(new CustomEvent("bridge:ready", { detail: { adapter: bridge.adapterId } }));');
586
+ parts.push(" if (window.__mcpDisplayMode) bridge.requestDisplayMode(window.__mcpDisplayMode).catch(function() {});");
584
587
  parts.push("}).catch(function(err) {");
585
588
  parts.push(' console.error("[FrontMcpBridge] Init failed:", err);');
586
589
  parts.push(' window.dispatchEvent(new CustomEvent("bridge:error", { detail: { error: err } }));');
@@ -914,6 +917,9 @@ var OpenAIAdapter = {
914
917
  return Promise.resolve();
915
918
  },
916
919
  requestDisplayMode: function(context, mode) {
920
+ if (window.openai && typeof window.openai.requestDisplayMode === 'function') {
921
+ return Promise.resolve(window.openai.requestDisplayMode({ mode: mode }));
922
+ }
917
923
  return Promise.resolve();
918
924
  },
919
925
  setSize: function(context, size) {
@@ -951,6 +957,7 @@ var ExtAppsAdapter = {
951
957
  hostCapabilities: {},
952
958
  handshakeSettled: false,
953
959
  pendingSize: null,
960
+ modelContext: null,
954
961
  canHandle: function() {
955
962
  if (typeof window === 'undefined') return false;
956
963
  if (window.parent === window) return false;
@@ -1029,8 +1036,19 @@ var ExtAppsAdapter = {
1029
1036
 
1030
1037
  if ('method' in data && !('id' in data)) {
1031
1038
  this.handleNotification(context, data);
1039
+ return;
1040
+ }
1041
+
1042
+ if ('method' in data && 'id' in data) {
1043
+ this.handleRequest(context, data, event.origin);
1032
1044
  }
1033
1045
  },
1046
+ handleRequest: function(context, request, origin) {
1047
+ // ui/resource-teardown: the host is about to unmount the view; reply once listeners have cleaned up.
1048
+ if (request.method !== 'ui/resource-teardown') return;
1049
+ window.dispatchEvent(new CustomEvent('bridge:teardown', { detail: {} }));
1050
+ window.parent.postMessage({ jsonrpc: '2.0', id: request.id, result: {} }, origin);
1051
+ },
1034
1052
  handleNotification: function(context, notification) {
1035
1053
  var params = notification.params || {};
1036
1054
  switch (notification.method) {
@@ -1054,6 +1072,7 @@ var ExtAppsAdapter = {
1054
1072
  Object.assign(context.hostContext, params);
1055
1073
  context.notifyContextChange(params);
1056
1074
  break;
1075
+ case 'ui/notifications/tool-cancelled':
1057
1076
  case 'ui/notifications/cancelled':
1058
1077
  window.dispatchEvent(new CustomEvent('tool:cancelled', { detail: { reason: params.reason } }));
1059
1078
  break;
@@ -1118,7 +1137,7 @@ var ExtAppsAdapter = {
1118
1137
  var id = ++this.requestId;
1119
1138
  var params = {
1120
1139
  appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
1121
- appCapabilities: { tools: { listChanged: false } },
1140
+ appCapabilities: { tools: { listChanged: false }, availableDisplayModes: ${JSON.stringify(MCP_APPS_DISPLAY_MODES)} },
1122
1141
  protocolVersion: '2024-11-05'
1123
1142
  };
1124
1143
 
@@ -1186,20 +1205,25 @@ var ExtAppsAdapter = {
1186
1205
  return Promise.reject(new Error('Server tool proxy not supported'));
1187
1206
  }
1188
1207
  // Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
1189
- return this.sendRequest('tools/call', { name: name, arguments: args || {} });
1208
+ return this.sendRequest('tools/call', { name: name, arguments: args || {}, _meta: { '${WIDGET_CALL_META_KEY}': true } });
1190
1209
  },
1191
1210
  sendMessage: function(context, content) {
1192
1211
  return this.sendRequest('ui/message', { content: content });
1193
1212
  },
1194
1213
  openLink: function(context, url) {
1195
- if (!this.hostCapabilities.openLink) {
1214
+ if (!this.hostCapabilities.openLinks && !this.hostCapabilities.openLink) {
1196
1215
  window.open(url, '_blank', 'noopener,noreferrer');
1197
1216
  return Promise.resolve();
1198
1217
  }
1199
- return this.sendRequest('ui/openLink', { url: url });
1218
+ return this.sendRequest('ui/open-link', { url: url });
1200
1219
  },
1201
1220
  requestDisplayMode: function(context, mode) {
1202
- return this.sendRequest('ui/setDisplayMode', { mode: mode });
1221
+ // Ask only for a mode the host offers; it answers with the mode it actually set.
1222
+ var offered = context.hostContext.availableDisplayModes;
1223
+ if (!Array.isArray(offered) || offered.indexOf(mode) === -1) {
1224
+ return Promise.reject(new Error('Display mode "' + mode + '" is not available on this host'));
1225
+ }
1226
+ return this.sendRequest('ui/request-display-mode', { mode: mode });
1203
1227
  },
1204
1228
  setSize: function(context, size) {
1205
1229
  // Standard ext-apps sizing: the view tells the host its dimensions with the
@@ -1220,14 +1244,22 @@ var ExtAppsAdapter = {
1220
1244
  return this.sendNotification('ui/notifications/size-changed', params);
1221
1245
  },
1222
1246
  requestClose: function(context) {
1223
- return this.sendRequest('ui/close', {});
1247
+ // The host decides whether to tear the view down; it answers with ui/resource-teardown if it does.
1248
+ return this.sendNotification('ui/notifications/request-teardown', {});
1224
1249
  },
1225
1250
  // Extended ext-apps methods (full specification)
1226
1251
  updateModelContext: function(context, data, merge) {
1227
- if (!this.hostCapabilities.modelContextUpdate) {
1252
+ if (!this.hostCapabilities.updateModelContext && !this.hostCapabilities.modelContextUpdate) {
1228
1253
  return Promise.reject(new Error('Model context update not supported'));
1229
1254
  }
1230
- return this.sendRequest('ui/updateModelContext', { context: data, merge: merge !== false });
1255
+ // The host keeps only the latest update, so an object merges into the last one here.
1256
+ var isObject = data !== null && typeof data === 'object' && !Array.isArray(data);
1257
+ var modelContext = isObject && merge !== false ? Object.assign({}, this.modelContext, data) : data;
1258
+ this.modelContext = isObject ? modelContext : null;
1259
+ var text = typeof modelContext === 'string' ? modelContext : JSON.stringify(modelContext);
1260
+ var params = { content: [{ type: 'text', text: text }] };
1261
+ if (isObject) params.structuredContent = modelContext;
1262
+ return this.sendRequest('ui/update-model-context', params);
1231
1263
  },
1232
1264
  log: function(context, level, message, data) {
1233
1265
  if (!this.hostCapabilities.logging) {
@@ -1236,7 +1268,10 @@ var ExtAppsAdapter = {
1236
1268
  logFn('[Widget] ' + message, data);
1237
1269
  return Promise.resolve();
1238
1270
  }
1239
- return this.sendRequest('ui/log', { level: level, message: message, data: data });
1271
+ return this.sendNotification('notifications/message', {
1272
+ level: level === 'warn' ? 'warning' : level,
1273
+ data: data === undefined ? message : { message: message, data: data }
1274
+ });
1240
1275
  },
1241
1276
  registerTool: function(context, name, description, inputSchema) {
1242
1277
  if (!this.hostCapabilities.widgetTools) {
@@ -1633,8 +1668,10 @@ FrontMcpBridge.prototype.openLink = function(url) {
1633
1668
  FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1634
1669
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1635
1670
  var self = this;
1636
- return this._adapter.requestDisplayMode(this._context, mode).then(function() {
1637
- self._context.hostContext.displayMode = mode;
1671
+ return this._adapter.requestDisplayMode(this._context, mode).then(function(result) {
1672
+ // The host may set another mode than the one asked for, e.g. when it refuses the change.
1673
+ var setMode = result && result.mode;
1674
+ if (${JSON.stringify(MCP_APPS_DISPLAY_MODES)}.indexOf(setMode) !== -1) self._context.hostContext.displayMode = setMode;
1638
1675
  });
1639
1676
  };
1640
1677
 
@@ -2009,7 +2046,7 @@ function hasSizing(sizing) {
2009
2046
  return sizing.preferredHeight !== void 0 || sizing.minHeight !== void 0 || sizing.maxHeight !== void 0 || sizing.aspectRatio !== void 0 || sizing.autoResize !== void 0;
2010
2047
  }
2011
2048
  function buildDataInjectionScript(options) {
2012
- const { toolName, input, output, structuredContent, sizing } = options;
2049
+ const { toolName, input, output, structuredContent, sizing, displayMode } = options;
2013
2050
  const lines = [
2014
2051
  `window.__mcpAppsEnabled = true;`,
2015
2052
  `window.__mcpToolName = ${safeJsonForScript(toolName)};`,
@@ -2020,6 +2057,9 @@ function buildDataInjectionScript(options) {
2020
2057
  if (hasSizing(sizing)) {
2021
2058
  lines.push(`window.__mcpWidgetSizing = ${safeJsonForScript(sizing)};`);
2022
2059
  }
2060
+ if (displayMode && displayMode !== "inline") {
2061
+ lines.push(`window.__mcpDisplayMode = ${safeJsonForScript(displayMode)};`);
2062
+ }
2023
2063
  return `<script>
2024
2064
  ${lines.join("\n")}
2025
2065
  </script>`;
@@ -2111,7 +2151,8 @@ function resolveDataInjectionScript(args) {
2111
2151
  input: args.input,
2112
2152
  output: args.output,
2113
2153
  structuredContent: args.structuredContent,
2114
- sizing: args.sizing
2154
+ sizing: args.sizing,
2155
+ displayMode: args.displayMode
2115
2156
  });
2116
2157
  }
2117
2158
  function buildShell(content, config) {
@@ -2124,7 +2165,8 @@ function buildShell(content, config) {
2124
2165
  structuredContent,
2125
2166
  includeBridge = true,
2126
2167
  title,
2127
- sizing
2168
+ sizing,
2169
+ displayMode
2128
2170
  } = config;
2129
2171
  const { customShell, dataInjection } = config;
2130
2172
  const dataScript = resolveDataInjectionScript({
@@ -2133,7 +2175,8 @@ function buildShell(content, config) {
2133
2175
  input,
2134
2176
  output,
2135
2177
  structuredContent,
2136
- sizing
2178
+ sizing,
2179
+ displayMode
2137
2180
  });
2138
2181
  if (!withShell) {
2139
2182
  const html3 = `${dataScript}
@@ -2154,6 +2197,7 @@ ${content}`;
2154
2197
  includeBridge,
2155
2198
  title,
2156
2199
  sizing,
2200
+ displayMode,
2157
2201
  dataInjection
2158
2202
  });
2159
2203
  }
@@ -2209,7 +2253,8 @@ function buildCustomShell(content, customShell, ctx) {
2209
2253
  input: ctx.input,
2210
2254
  output: ctx.output,
2211
2255
  structuredContent: ctx.structuredContent,
2212
- sizing: ctx.sizing
2256
+ sizing: ctx.sizing,
2257
+ displayMode: ctx.displayMode
2213
2258
  });
2214
2259
  const sizingStyle = buildSizingStyleTag(ctx.sizing);
2215
2260
  const dataWithSizing = sizingStyle ? `${dataScript}
@@ -3048,7 +3093,7 @@ function noticeStringResult(toolName, logger) {
3048
3093
  if (noticedStringResultTools.has(toolName)) return;
3049
3094
  noticedStringResultTools.add(toolName);
3050
3095
  logger.warn(
3051
- `[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}`
3096
+ `[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}`
3052
3097
  );
3053
3098
  }
3054
3099
  function renderUnwrappedResult(rawResult, options) {
@@ -3058,13 +3103,14 @@ function renderUnwrappedResult(rawResult, options) {
3058
3103
  if (typeof rawResult !== "string") {
3059
3104
  return `<pre>${escapeHtml(JSON.stringify(rawResult, null, 2))}</pre>`;
3060
3105
  }
3061
- if (detectContentType(rawResult) !== "html" || options.escapeStringResults === true) {
3062
- return escapeHtml(rawResult);
3106
+ const looksLikeMarkup = detectContentType(rawResult) === "html";
3107
+ if (looksLikeMarkup && options.escapeStringResults === false) {
3108
+ return rawResult;
3063
3109
  }
3064
- if (options.escapeStringResults === void 0) {
3110
+ if (looksLikeMarkup && options.escapeStringResults === void 0) {
3065
3111
  noticeStringResult(options.toolName, options.logger ?? console);
3066
3112
  }
3067
- return rawResult;
3113
+ return escapeHtml(rawResult);
3068
3114
  }
3069
3115
  var warnedComponentReferences = /* @__PURE__ */ new WeakMap();
3070
3116
  function warnComponentReference(template, toolName, logger) {
@@ -3076,7 +3122,18 @@ function warnComponentReference(template, toolName, logger) {
3076
3122
  `[FrontMCP] Tool "${toolName}": \`ui.template\` is a React component reference, which cannot be bundled (there is no source file to compile), so the widget would render an empty page. Use \`template: { file: './widget.tsx' }\` instead.`
3077
3123
  );
3078
3124
  }
3079
- function isReactElement(value) {
3125
+ var COMPILED_JSX_PATTERN = /\b(?:React\.createElement|_?jsx(?:s|DEV)?)\)?\s*\(/;
3126
+ function isComponentReference(template) {
3127
+ if (isReactTypedObject(template)) return true;
3128
+ if (typeof template !== "function" || detectUIType(template) !== "react") return false;
3129
+ return isDefinitelyReactComponent(template) || COMPILED_JSX_PATTERN.test(Function.prototype.toString.call(template));
3130
+ }
3131
+ function warnIfComponentReference(toolName, template, logger = console) {
3132
+ if (!isComponentReference(template)) return false;
3133
+ warnComponentReference(template, toolName, logger);
3134
+ return true;
3135
+ }
3136
+ function isReactTypedObject(value) {
3080
3137
  return typeof value === "object" && value !== null && "$$typeof" in value;
3081
3138
  }
3082
3139
  function isDefinitelyReactComponent(template) {
@@ -3095,6 +3152,7 @@ function renderToolTemplate(options) {
3095
3152
  includeBridge: true,
3096
3153
  resolver,
3097
3154
  sizing,
3155
+ displayMode: options.displayMode,
3098
3156
  csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
3099
3157
  };
3100
3158
  let html2;
@@ -3116,7 +3174,7 @@ function renderToolTemplate(options) {
3116
3174
  if (isHtmlBuilder) {
3117
3175
  try {
3118
3176
  rawResult = template(ctx);
3119
- isHtmlBuilder = !isReactElement(rawResult);
3177
+ isHtmlBuilder = !isReactTypedObject(rawResult);
3120
3178
  } catch (error) {
3121
3179
  if (uiType !== "react") throw error;
3122
3180
  isHtmlBuilder = false;
@@ -3145,6 +3203,7 @@ function renderToolTemplate(options) {
3145
3203
  hash = shellResult.hash;
3146
3204
  size = shellResult.size;
3147
3205
  } else {
3206
+ if (isReactTypedObject(template)) warnComponentReference(template, toolName, options.logger ?? console);
3148
3207
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
3149
3208
  html2 = shellResult.html;
3150
3209
  hash = shellResult.hash;
@@ -3258,5 +3317,6 @@ export {
3258
3317
  isUIRenderFailure,
3259
3318
  renderToolTemplate,
3260
3319
  resolveServingMode,
3320
+ warnIfComponentReference,
3261
3321
  wrapDetectedContent
3262
3322
  };
@@ -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
 
@@ -1329,7 +1366,9 @@ var BRIDGE_SCRIPT_TAGS = {
1329
1366
  };
1330
1367
  export {
1331
1368
  BRIDGE_SCRIPT_TAGS,
1369
+ MCP_APPS_DISPLAY_MODES,
1332
1370
  UNIVERSAL_BRIDGE_SCRIPT,
1371
+ WIDGET_CALL_META_KEY,
1333
1372
  generateBridgeIIFE,
1334
1373
  generatePlatformBundle
1335
1374
  };