@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.
@@ -11,7 +11,7 @@ export { MCP_APPS_MIME_TYPE, MCP_APPS_EXTENSION_ID } from './constants';
11
11
  export { resolveServingMode, type ResolveServingModeOptions, type ServingModeResult, type AdapterPlatformType, } from './serving-mode';
12
12
  export { buildToolResponseContent, type ToolResponseContent, type BuildToolResponseContentOptions, } from './response-builder';
13
13
  export { isUIRenderFailure, type UIRenderFailure } from './render-failure';
14
- export { renderToolTemplate, type RenderToolTemplateOptions, type RenderToolTemplateResult } from './template-renderer';
14
+ export { renderToolTemplate, warnIfComponentReference, type RenderToolTemplateOptions, type RenderToolTemplateResult, } from './template-renderer';
15
15
  export { detectContentType, type DetectedContentType } from './content-detector';
16
16
  export { buildChartHtml, buildMermaidHtml, buildPdfHtml, wrapDetectedContent } from './content-renderers';
17
17
  export { createDefaultBaseTemplate, type DefaultBaseTemplateOptions } from './base-template';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAKxE,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,EAC9B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AAKxB,OAAO,EACL,wBAAwB,EACxB,KAAK,mBAAmB,EACxB,KAAK,+BAA+B,GACrC,MAAM,oBAAoB,CAAC;AAK5B,OAAO,EAAE,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAK3E,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,KAAK,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAKxH,OAAO,EAAE,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAK1G,OAAO,EAAE,yBAAyB,EAAE,KAAK,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAK7F,OAAO,EAAE,qBAAqB,EAAE,KAAK,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAKzF,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAKxE,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,EAC9B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AAKxB,OAAO,EACL,wBAAwB,EACxB,KAAK,mBAAmB,EACxB,KAAK,+BAA+B,GACrC,MAAM,oBAAoB,CAAC;AAK5B,OAAO,EAAE,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAK3E,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,GAC9B,MAAM,qBAAqB,CAAC;AAK7B,OAAO,EAAE,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAK1G,OAAO,EAAE,yBAAyB,EAAE,KAAK,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAK7F,OAAO,EAAE,qBAAqB,EAAE,KAAK,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAKzF,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
package/adapters/index.js CHANGED
@@ -33,6 +33,7 @@ __export(adapters_exports, {
33
33
  isUIRenderFailure: () => isUIRenderFailure,
34
34
  renderToolTemplate: () => renderToolTemplate,
35
35
  resolveServingMode: () => resolveServingMode,
36
+ warnIfComponentReference: () => warnIfComponentReference,
36
37
  wrapDetectedContent: () => wrapDetectedContent
37
38
  });
38
39
  module.exports = __toCommonJS(adapters_exports);
@@ -563,6 +564,8 @@ ${json}
563
564
  }
564
565
 
565
566
  // libs/uipack/src/bridge-runtime/iife-generator.ts
567
+ var WIDGET_CALL_META_KEY = "frontmcp/widgetCall";
568
+ var MCP_APPS_DISPLAY_MODES = ["inline", "fullscreen", "pip"];
566
569
  function generateBridgeIIFE(options = {}) {
567
570
  const { debug = false, trustedOrigins = [], minify = false } = options;
568
571
  const adapters = options.adapters || ["openai", "ext-apps", "claude", "gemini", "generic"];
@@ -610,6 +613,7 @@ function generateBridgeIIFE(options = {}) {
610
613
  parts.push("bridge.initialize().then(function() {");
611
614
  parts.push(' log("Bridge initialized with adapter: " + bridge.adapterId);');
612
615
  parts.push(' window.dispatchEvent(new CustomEvent("bridge:ready", { detail: { adapter: bridge.adapterId } }));');
616
+ parts.push(" if (window.__mcpDisplayMode) bridge.requestDisplayMode(window.__mcpDisplayMode).catch(function() {});");
613
617
  parts.push("}).catch(function(err) {");
614
618
  parts.push(' console.error("[FrontMcpBridge] Init failed:", err);');
615
619
  parts.push(' window.dispatchEvent(new CustomEvent("bridge:error", { detail: { error: err } }));');
@@ -943,6 +947,9 @@ var OpenAIAdapter = {
943
947
  return Promise.resolve();
944
948
  },
945
949
  requestDisplayMode: function(context, mode) {
950
+ if (window.openai && typeof window.openai.requestDisplayMode === 'function') {
951
+ return Promise.resolve(window.openai.requestDisplayMode({ mode: mode }));
952
+ }
946
953
  return Promise.resolve();
947
954
  },
948
955
  setSize: function(context, size) {
@@ -980,6 +987,7 @@ var ExtAppsAdapter = {
980
987
  hostCapabilities: {},
981
988
  handshakeSettled: false,
982
989
  pendingSize: null,
990
+ modelContext: null,
983
991
  canHandle: function() {
984
992
  if (typeof window === 'undefined') return false;
985
993
  if (window.parent === window) return false;
@@ -1058,8 +1066,19 @@ var ExtAppsAdapter = {
1058
1066
 
1059
1067
  if ('method' in data && !('id' in data)) {
1060
1068
  this.handleNotification(context, data);
1069
+ return;
1070
+ }
1071
+
1072
+ if ('method' in data && 'id' in data) {
1073
+ this.handleRequest(context, data, event.origin);
1061
1074
  }
1062
1075
  },
1076
+ handleRequest: function(context, request, origin) {
1077
+ // ui/resource-teardown: the host is about to unmount the view; reply once listeners have cleaned up.
1078
+ if (request.method !== 'ui/resource-teardown') return;
1079
+ window.dispatchEvent(new CustomEvent('bridge:teardown', { detail: {} }));
1080
+ window.parent.postMessage({ jsonrpc: '2.0', id: request.id, result: {} }, origin);
1081
+ },
1063
1082
  handleNotification: function(context, notification) {
1064
1083
  var params = notification.params || {};
1065
1084
  switch (notification.method) {
@@ -1083,6 +1102,7 @@ var ExtAppsAdapter = {
1083
1102
  Object.assign(context.hostContext, params);
1084
1103
  context.notifyContextChange(params);
1085
1104
  break;
1105
+ case 'ui/notifications/tool-cancelled':
1086
1106
  case 'ui/notifications/cancelled':
1087
1107
  window.dispatchEvent(new CustomEvent('tool:cancelled', { detail: { reason: params.reason } }));
1088
1108
  break;
@@ -1147,7 +1167,7 @@ var ExtAppsAdapter = {
1147
1167
  var id = ++this.requestId;
1148
1168
  var params = {
1149
1169
  appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
1150
- appCapabilities: { tools: { listChanged: false } },
1170
+ appCapabilities: { tools: { listChanged: false }, availableDisplayModes: ${JSON.stringify(MCP_APPS_DISPLAY_MODES)} },
1151
1171
  protocolVersion: '2024-11-05'
1152
1172
  };
1153
1173
 
@@ -1215,20 +1235,25 @@ var ExtAppsAdapter = {
1215
1235
  return Promise.reject(new Error('Server tool proxy not supported'));
1216
1236
  }
1217
1237
  // Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
1218
- return this.sendRequest('tools/call', { name: name, arguments: args || {} });
1238
+ return this.sendRequest('tools/call', { name: name, arguments: args || {}, _meta: { '${WIDGET_CALL_META_KEY}': true } });
1219
1239
  },
1220
1240
  sendMessage: function(context, content) {
1221
1241
  return this.sendRequest('ui/message', { content: content });
1222
1242
  },
1223
1243
  openLink: function(context, url) {
1224
- if (!this.hostCapabilities.openLink) {
1244
+ if (!this.hostCapabilities.openLinks && !this.hostCapabilities.openLink) {
1225
1245
  window.open(url, '_blank', 'noopener,noreferrer');
1226
1246
  return Promise.resolve();
1227
1247
  }
1228
- return this.sendRequest('ui/openLink', { url: url });
1248
+ return this.sendRequest('ui/open-link', { url: url });
1229
1249
  },
1230
1250
  requestDisplayMode: function(context, mode) {
1231
- return this.sendRequest('ui/setDisplayMode', { mode: mode });
1251
+ // Ask only for a mode the host offers; it answers with the mode it actually set.
1252
+ var offered = context.hostContext.availableDisplayModes;
1253
+ if (!Array.isArray(offered) || offered.indexOf(mode) === -1) {
1254
+ return Promise.reject(new Error('Display mode "' + mode + '" is not available on this host'));
1255
+ }
1256
+ return this.sendRequest('ui/request-display-mode', { mode: mode });
1232
1257
  },
1233
1258
  setSize: function(context, size) {
1234
1259
  // Standard ext-apps sizing: the view tells the host its dimensions with the
@@ -1249,14 +1274,22 @@ var ExtAppsAdapter = {
1249
1274
  return this.sendNotification('ui/notifications/size-changed', params);
1250
1275
  },
1251
1276
  requestClose: function(context) {
1252
- return this.sendRequest('ui/close', {});
1277
+ // The host decides whether to tear the view down; it answers with ui/resource-teardown if it does.
1278
+ return this.sendNotification('ui/notifications/request-teardown', {});
1253
1279
  },
1254
1280
  // Extended ext-apps methods (full specification)
1255
1281
  updateModelContext: function(context, data, merge) {
1256
- if (!this.hostCapabilities.modelContextUpdate) {
1282
+ if (!this.hostCapabilities.updateModelContext && !this.hostCapabilities.modelContextUpdate) {
1257
1283
  return Promise.reject(new Error('Model context update not supported'));
1258
1284
  }
1259
- return this.sendRequest('ui/updateModelContext', { context: data, merge: merge !== false });
1285
+ // The host keeps only the latest update, so an object merges into the last one here.
1286
+ var isObject = data !== null && typeof data === 'object' && !Array.isArray(data);
1287
+ var modelContext = isObject && merge !== false ? Object.assign({}, this.modelContext, data) : data;
1288
+ this.modelContext = isObject ? modelContext : null;
1289
+ var text = typeof modelContext === 'string' ? modelContext : JSON.stringify(modelContext);
1290
+ var params = { content: [{ type: 'text', text: text }] };
1291
+ if (isObject) params.structuredContent = modelContext;
1292
+ return this.sendRequest('ui/update-model-context', params);
1260
1293
  },
1261
1294
  log: function(context, level, message, data) {
1262
1295
  if (!this.hostCapabilities.logging) {
@@ -1265,7 +1298,10 @@ var ExtAppsAdapter = {
1265
1298
  logFn('[Widget] ' + message, data);
1266
1299
  return Promise.resolve();
1267
1300
  }
1268
- return this.sendRequest('ui/log', { level: level, message: message, data: data });
1301
+ return this.sendNotification('notifications/message', {
1302
+ level: level === 'warn' ? 'warning' : level,
1303
+ data: data === undefined ? message : { message: message, data: data }
1304
+ });
1269
1305
  },
1270
1306
  registerTool: function(context, name, description, inputSchema) {
1271
1307
  if (!this.hostCapabilities.widgetTools) {
@@ -1662,8 +1698,10 @@ FrontMcpBridge.prototype.openLink = function(url) {
1662
1698
  FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1663
1699
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1664
1700
  var self = this;
1665
- return this._adapter.requestDisplayMode(this._context, mode).then(function() {
1666
- self._context.hostContext.displayMode = mode;
1701
+ return this._adapter.requestDisplayMode(this._context, mode).then(function(result) {
1702
+ // The host may set another mode than the one asked for, e.g. when it refuses the change.
1703
+ var setMode = result && result.mode;
1704
+ if (${JSON.stringify(MCP_APPS_DISPLAY_MODES)}.indexOf(setMode) !== -1) self._context.hostContext.displayMode = setMode;
1667
1705
  });
1668
1706
  };
1669
1707
 
@@ -2038,7 +2076,7 @@ function hasSizing(sizing) {
2038
2076
  return sizing.preferredHeight !== void 0 || sizing.minHeight !== void 0 || sizing.maxHeight !== void 0 || sizing.aspectRatio !== void 0 || sizing.autoResize !== void 0;
2039
2077
  }
2040
2078
  function buildDataInjectionScript(options) {
2041
- const { toolName, input, output, structuredContent, sizing } = options;
2079
+ const { toolName, input, output, structuredContent, sizing, displayMode } = options;
2042
2080
  const lines = [
2043
2081
  `window.__mcpAppsEnabled = true;`,
2044
2082
  `window.__mcpToolName = ${safeJsonForScript(toolName)};`,
@@ -2049,6 +2087,9 @@ function buildDataInjectionScript(options) {
2049
2087
  if (hasSizing(sizing)) {
2050
2088
  lines.push(`window.__mcpWidgetSizing = ${safeJsonForScript(sizing)};`);
2051
2089
  }
2090
+ if (displayMode && displayMode !== "inline") {
2091
+ lines.push(`window.__mcpDisplayMode = ${safeJsonForScript(displayMode)};`);
2092
+ }
2052
2093
  return `<script>
2053
2094
  ${lines.join("\n")}
2054
2095
  </script>`;
@@ -2140,7 +2181,8 @@ function resolveDataInjectionScript(args) {
2140
2181
  input: args.input,
2141
2182
  output: args.output,
2142
2183
  structuredContent: args.structuredContent,
2143
- sizing: args.sizing
2184
+ sizing: args.sizing,
2185
+ displayMode: args.displayMode
2144
2186
  });
2145
2187
  }
2146
2188
  function buildShell(content, config) {
@@ -2153,7 +2195,8 @@ function buildShell(content, config) {
2153
2195
  structuredContent,
2154
2196
  includeBridge = true,
2155
2197
  title,
2156
- sizing
2198
+ sizing,
2199
+ displayMode
2157
2200
  } = config;
2158
2201
  const { customShell, dataInjection } = config;
2159
2202
  const dataScript = resolveDataInjectionScript({
@@ -2162,7 +2205,8 @@ function buildShell(content, config) {
2162
2205
  input,
2163
2206
  output,
2164
2207
  structuredContent,
2165
- sizing
2208
+ sizing,
2209
+ displayMode
2166
2210
  });
2167
2211
  if (!withShell) {
2168
2212
  const html3 = `${dataScript}
@@ -2183,6 +2227,7 @@ ${content}`;
2183
2227
  includeBridge,
2184
2228
  title,
2185
2229
  sizing,
2230
+ displayMode,
2186
2231
  dataInjection
2187
2232
  });
2188
2233
  }
@@ -2238,7 +2283,8 @@ function buildCustomShell(content, customShell, ctx) {
2238
2283
  input: ctx.input,
2239
2284
  output: ctx.output,
2240
2285
  structuredContent: ctx.structuredContent,
2241
- sizing: ctx.sizing
2286
+ sizing: ctx.sizing,
2287
+ displayMode: ctx.displayMode
2242
2288
  });
2243
2289
  const sizingStyle = buildSizingStyleTag(ctx.sizing);
2244
2290
  const dataWithSizing = sizingStyle ? `${dataScript}
@@ -3077,7 +3123,7 @@ function noticeStringResult(toolName, logger) {
3077
3123
  if (noticedStringResultTools.has(toolName)) return;
3078
3124
  noticedStringResultTools.add(toolName);
3079
3125
  logger.warn(
3080
- `[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}`
3126
+ `[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}`
3081
3127
  );
3082
3128
  }
3083
3129
  function renderUnwrappedResult(rawResult, options) {
@@ -3087,13 +3133,14 @@ function renderUnwrappedResult(rawResult, options) {
3087
3133
  if (typeof rawResult !== "string") {
3088
3134
  return `<pre>${escapeHtml(JSON.stringify(rawResult, null, 2))}</pre>`;
3089
3135
  }
3090
- if (detectContentType(rawResult) !== "html" || options.escapeStringResults === true) {
3091
- return escapeHtml(rawResult);
3136
+ const looksLikeMarkup = detectContentType(rawResult) === "html";
3137
+ if (looksLikeMarkup && options.escapeStringResults === false) {
3138
+ return rawResult;
3092
3139
  }
3093
- if (options.escapeStringResults === void 0) {
3140
+ if (looksLikeMarkup && options.escapeStringResults === void 0) {
3094
3141
  noticeStringResult(options.toolName, options.logger ?? console);
3095
3142
  }
3096
- return rawResult;
3143
+ return escapeHtml(rawResult);
3097
3144
  }
3098
3145
  var warnedComponentReferences = /* @__PURE__ */ new WeakMap();
3099
3146
  function warnComponentReference(template, toolName, logger) {
@@ -3105,7 +3152,18 @@ function warnComponentReference(template, toolName, logger) {
3105
3152
  `[FrontMCP] Tool "${toolName}": \`ui.template\` is a React component reference, which cannot be bundled (there is no source file to compile), so the widget would render an empty page. Use \`template: { file: './widget.tsx' }\` instead.`
3106
3153
  );
3107
3154
  }
3108
- function isReactElement(value) {
3155
+ var COMPILED_JSX_PATTERN = /\b(?:React\.createElement|_?jsx(?:s|DEV)?)\)?\s*\(/;
3156
+ function isComponentReference(template) {
3157
+ if (isReactTypedObject(template)) return true;
3158
+ if (typeof template !== "function" || detectUIType(template) !== "react") return false;
3159
+ return isDefinitelyReactComponent(template) || COMPILED_JSX_PATTERN.test(Function.prototype.toString.call(template));
3160
+ }
3161
+ function warnIfComponentReference(toolName, template, logger = console) {
3162
+ if (!isComponentReference(template)) return false;
3163
+ warnComponentReference(template, toolName, logger);
3164
+ return true;
3165
+ }
3166
+ function isReactTypedObject(value) {
3109
3167
  return typeof value === "object" && value !== null && "$$typeof" in value;
3110
3168
  }
3111
3169
  function isDefinitelyReactComponent(template) {
@@ -3124,6 +3182,7 @@ function renderToolTemplate(options) {
3124
3182
  includeBridge: true,
3125
3183
  resolver,
3126
3184
  sizing,
3185
+ displayMode: options.displayMode,
3127
3186
  csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
3128
3187
  };
3129
3188
  let html2;
@@ -3145,7 +3204,7 @@ function renderToolTemplate(options) {
3145
3204
  if (isHtmlBuilder) {
3146
3205
  try {
3147
3206
  rawResult = template(ctx);
3148
- isHtmlBuilder = !isReactElement(rawResult);
3207
+ isHtmlBuilder = !isReactTypedObject(rawResult);
3149
3208
  } catch (error) {
3150
3209
  if (uiType !== "react") throw error;
3151
3210
  isHtmlBuilder = false;
@@ -3174,6 +3233,7 @@ function renderToolTemplate(options) {
3174
3233
  hash = shellResult.hash;
3175
3234
  size = shellResult.size;
3176
3235
  } else {
3236
+ if (isReactTypedObject(template)) warnComponentReference(template, toolName, options.logger ?? console);
3177
3237
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
3178
3238
  html2 = shellResult.html;
3179
3239
  hash = shellResult.hash;
@@ -3288,5 +3348,6 @@ function buildCDNInfoForUIType(uiType) {
3288
3348
  isUIRenderFailure,
3289
3349
  renderToolTemplate,
3290
3350
  resolveServingMode,
3351
+ warnIfComponentReference,
3291
3352
  wrapDetectedContent
3292
3353
  });
@@ -6,7 +6,7 @@
6
6
  * @packageDocumentation
7
7
  */
8
8
  import type { ImportResolver } from '../resolver/types';
9
- import type { WidgetSizing } from '../shell/types';
9
+ import type { ShellConfig, WidgetSizing } from '../shell/types';
10
10
  /**
11
11
  * Options for rendering a tool template.
12
12
  */
@@ -36,6 +36,8 @@ export interface RenderToolTemplateOptions {
36
36
  * shell, and is mirrored onto the returned `meta` as `ui/preferredHeight` etc.
37
37
  */
38
38
  sizing?: WidgetSizing;
39
+ /** `ui.displayMode`: the display mode the page asks the host for once its bridge connects. */
40
+ displayMode?: ShellConfig['displayMode'];
39
41
  /**
40
42
  * Extra origins the widget may load from (`resourceDomains`) or call (`connectDomains`), from the
41
43
  * tool's `ui.csp`. Merged into the page's own Content-Security-Policy on every template path.
@@ -46,8 +48,9 @@ export interface RenderToolTemplateOptions {
46
48
  };
47
49
  /**
48
50
  * HTML-escape a plain string returned by a template function; results built with
49
- * `html` / `trustedHtml` stay markup. When unset, strings that look like HTML render as
50
- * markup and a one-time notice per tool is logged; `false` keeps that without the notice.
51
+ * `html` / `trustedHtml` stay markup. Unset is the default since 1.9.2: strings are escaped, and a
52
+ * one-time notice per tool is logged when one looked like markup. `true` escapes without the
53
+ * notice; `false` renders strings that look like HTML as markup.
51
54
  */
52
55
  escapeStringResults?: boolean;
53
56
  /** Receives the one-time string-result notice. Defaults to `console`. */
@@ -70,6 +73,15 @@ export interface RenderToolTemplateResult {
70
73
  /** Platform-specific metadata */
71
74
  meta: Record<string, unknown>;
72
75
  }
76
+ /**
77
+ * Warn, once per tool, that `template` is a React component reference the widget cannot bundle.
78
+ * The server calls this while it registers UI tools, so the warning appears at startup (#769).
79
+ *
80
+ * @returns Whether the template is a component reference
81
+ */
82
+ export declare function warnIfComponentReference(toolName: string, template: unknown, logger?: {
83
+ warn: (message: string) => void;
84
+ }): boolean;
73
85
  /**
74
86
  * Render a tool template into HTML.
75
87
  *
@@ -1 +1 @@
1
- {"version":3,"file":"template-renderer.d.ts","sourceRoot":"","sources":["../../src/adapters/template-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAIxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAQnD;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,kBAAkB;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,qDAAqD;IACrD,QAAQ,EAAE,OAAO,CAAC;IAClB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAChC;;;;OAIG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;OAGG;IACH,GAAG,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChE;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,yEAAyE;IACzE,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CAC9C;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AA8FD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,CAoH/F"}
1
+ {"version":3,"file":"template-renderer.d.ts","sourceRoot":"","sources":["../../src/adapters/template-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAIxD,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAQhE;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,kBAAkB;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,qDAAqD;IACrD,QAAQ,EAAE,OAAO,CAAC;IAClB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAChC;;;;OAIG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,8FAA8F;IAC9F,WAAW,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,CAAC;IACzC;;;OAGG;IACH,GAAG,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChE;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,yEAAyE;IACzE,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CAC9C;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAiGD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,OAAO,EACjB,MAAM,GAAE;IAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAAY,GACpD,OAAO,CAIT;AAeD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,CAsH/F"}
@@ -6,6 +6,13 @@
6
6
  *
7
7
  * @packageDocumentation
8
8
  */
9
+ /**
10
+ * The `_meta` key the bridge sets on a widget's own `tools/call`, so the server answers with the
11
+ * data and does not render the tool's page again into a widget that is already on screen.
12
+ */
13
+ export declare const WIDGET_CALL_META_KEY = "frontmcp/widgetCall";
14
+ /** The display modes a widget supports, declared to an MCP Apps host in `ui/initialize`. */
15
+ export declare const MCP_APPS_DISPLAY_MODES: readonly ["inline", "fullscreen", "pip"];
9
16
  /**
10
17
  * Options for generating the bridge IIFE.
11
18
  */
@@ -1 +1 @@
1
- {"version":3,"file":"iife-generator.d.ts","sourceRoot":"","sources":["../../src/bridge-runtime/iife-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC,EAAE,CAAC;IACvE,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,wBAAwB;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,CA8G7E;AA8xCD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,EACvD,OAAO,GAAE,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAM,GACnD,MAAM,CAYR;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,QAAuB,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;CAK9B,CAAC"}
1
+ {"version":3,"file":"iife-generator.d.ts","sourceRoot":"","sources":["../../src/bridge-runtime/iife-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,wBAAwB,CAAC;AAE1D,4FAA4F;AAC5F,eAAO,MAAM,sBAAsB,0CAA2C,CAAC;AAE/E;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC,EAAE,CAAC;IACvE,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,wBAAwB;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAgH7E;AAg0CD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,EACvD,OAAO,GAAE,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAM,GACnD,MAAM,CAYR;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,QAAuB,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;CAK9B,CAAC"}
@@ -6,5 +6,5 @@
6
6
  *
7
7
  * @packageDocumentation
8
8
  */
9
- export { generateBridgeIIFE, generatePlatformBundle, UNIVERSAL_BRIDGE_SCRIPT, BRIDGE_SCRIPT_TAGS, type IIFEGeneratorOptions, } from './iife-generator';
9
+ export { generateBridgeIIFE, generatePlatformBundle, UNIVERSAL_BRIDGE_SCRIPT, BRIDGE_SCRIPT_TAGS, WIDGET_CALL_META_KEY, MCP_APPS_DISPLAY_MODES, type IIFEGeneratorOptions, } from './iife-generator';
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bridge-runtime/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,KAAK,oBAAoB,GAC1B,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bridge-runtime/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,EACtB,KAAK,oBAAoB,GAC1B,MAAM,kBAAkB,CAAC"}
@@ -21,13 +21,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var bridge_runtime_exports = {};
22
22
  __export(bridge_runtime_exports, {
23
23
  BRIDGE_SCRIPT_TAGS: () => BRIDGE_SCRIPT_TAGS,
24
+ MCP_APPS_DISPLAY_MODES: () => MCP_APPS_DISPLAY_MODES,
24
25
  UNIVERSAL_BRIDGE_SCRIPT: () => UNIVERSAL_BRIDGE_SCRIPT,
26
+ WIDGET_CALL_META_KEY: () => WIDGET_CALL_META_KEY,
25
27
  generateBridgeIIFE: () => generateBridgeIIFE,
26
28
  generatePlatformBundle: () => generatePlatformBundle
27
29
  });
28
30
  module.exports = __toCommonJS(bridge_runtime_exports);
29
31
 
30
32
  // libs/uipack/src/bridge-runtime/iife-generator.ts
33
+ var WIDGET_CALL_META_KEY = "frontmcp/widgetCall";
34
+ var MCP_APPS_DISPLAY_MODES = ["inline", "fullscreen", "pip"];
31
35
  function generateBridgeIIFE(options = {}) {
32
36
  const { debug = false, trustedOrigins = [], minify = false } = options;
33
37
  const adapters = options.adapters || ["openai", "ext-apps", "claude", "gemini", "generic"];
@@ -75,6 +79,7 @@ function generateBridgeIIFE(options = {}) {
75
79
  parts.push("bridge.initialize().then(function() {");
76
80
  parts.push(' log("Bridge initialized with adapter: " + bridge.adapterId);');
77
81
  parts.push(' window.dispatchEvent(new CustomEvent("bridge:ready", { detail: { adapter: bridge.adapterId } }));');
82
+ parts.push(" if (window.__mcpDisplayMode) bridge.requestDisplayMode(window.__mcpDisplayMode).catch(function() {});");
78
83
  parts.push("}).catch(function(err) {");
79
84
  parts.push(' console.error("[FrontMcpBridge] Init failed:", err);');
80
85
  parts.push(' window.dispatchEvent(new CustomEvent("bridge:error", { detail: { error: err } }));');
@@ -408,6 +413,9 @@ var OpenAIAdapter = {
408
413
  return Promise.resolve();
409
414
  },
410
415
  requestDisplayMode: function(context, mode) {
416
+ if (window.openai && typeof window.openai.requestDisplayMode === 'function') {
417
+ return Promise.resolve(window.openai.requestDisplayMode({ mode: mode }));
418
+ }
411
419
  return Promise.resolve();
412
420
  },
413
421
  setSize: function(context, size) {
@@ -445,6 +453,7 @@ var ExtAppsAdapter = {
445
453
  hostCapabilities: {},
446
454
  handshakeSettled: false,
447
455
  pendingSize: null,
456
+ modelContext: null,
448
457
  canHandle: function() {
449
458
  if (typeof window === 'undefined') return false;
450
459
  if (window.parent === window) return false;
@@ -523,8 +532,19 @@ var ExtAppsAdapter = {
523
532
 
524
533
  if ('method' in data && !('id' in data)) {
525
534
  this.handleNotification(context, data);
535
+ return;
536
+ }
537
+
538
+ if ('method' in data && 'id' in data) {
539
+ this.handleRequest(context, data, event.origin);
526
540
  }
527
541
  },
542
+ handleRequest: function(context, request, origin) {
543
+ // ui/resource-teardown: the host is about to unmount the view; reply once listeners have cleaned up.
544
+ if (request.method !== 'ui/resource-teardown') return;
545
+ window.dispatchEvent(new CustomEvent('bridge:teardown', { detail: {} }));
546
+ window.parent.postMessage({ jsonrpc: '2.0', id: request.id, result: {} }, origin);
547
+ },
528
548
  handleNotification: function(context, notification) {
529
549
  var params = notification.params || {};
530
550
  switch (notification.method) {
@@ -548,6 +568,7 @@ var ExtAppsAdapter = {
548
568
  Object.assign(context.hostContext, params);
549
569
  context.notifyContextChange(params);
550
570
  break;
571
+ case 'ui/notifications/tool-cancelled':
551
572
  case 'ui/notifications/cancelled':
552
573
  window.dispatchEvent(new CustomEvent('tool:cancelled', { detail: { reason: params.reason } }));
553
574
  break;
@@ -612,7 +633,7 @@ var ExtAppsAdapter = {
612
633
  var id = ++this.requestId;
613
634
  var params = {
614
635
  appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
615
- appCapabilities: { tools: { listChanged: false } },
636
+ appCapabilities: { tools: { listChanged: false }, availableDisplayModes: ${JSON.stringify(MCP_APPS_DISPLAY_MODES)} },
616
637
  protocolVersion: '2024-11-05'
617
638
  };
618
639
 
@@ -680,20 +701,25 @@ var ExtAppsAdapter = {
680
701
  return Promise.reject(new Error('Server tool proxy not supported'));
681
702
  }
682
703
  // Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
683
- return this.sendRequest('tools/call', { name: name, arguments: args || {} });
704
+ return this.sendRequest('tools/call', { name: name, arguments: args || {}, _meta: { '${WIDGET_CALL_META_KEY}': true } });
684
705
  },
685
706
  sendMessage: function(context, content) {
686
707
  return this.sendRequest('ui/message', { content: content });
687
708
  },
688
709
  openLink: function(context, url) {
689
- if (!this.hostCapabilities.openLink) {
710
+ if (!this.hostCapabilities.openLinks && !this.hostCapabilities.openLink) {
690
711
  window.open(url, '_blank', 'noopener,noreferrer');
691
712
  return Promise.resolve();
692
713
  }
693
- return this.sendRequest('ui/openLink', { url: url });
714
+ return this.sendRequest('ui/open-link', { url: url });
694
715
  },
695
716
  requestDisplayMode: function(context, mode) {
696
- return this.sendRequest('ui/setDisplayMode', { mode: mode });
717
+ // Ask only for a mode the host offers; it answers with the mode it actually set.
718
+ var offered = context.hostContext.availableDisplayModes;
719
+ if (!Array.isArray(offered) || offered.indexOf(mode) === -1) {
720
+ return Promise.reject(new Error('Display mode "' + mode + '" is not available on this host'));
721
+ }
722
+ return this.sendRequest('ui/request-display-mode', { mode: mode });
697
723
  },
698
724
  setSize: function(context, size) {
699
725
  // Standard ext-apps sizing: the view tells the host its dimensions with the
@@ -714,14 +740,22 @@ var ExtAppsAdapter = {
714
740
  return this.sendNotification('ui/notifications/size-changed', params);
715
741
  },
716
742
  requestClose: function(context) {
717
- return this.sendRequest('ui/close', {});
743
+ // The host decides whether to tear the view down; it answers with ui/resource-teardown if it does.
744
+ return this.sendNotification('ui/notifications/request-teardown', {});
718
745
  },
719
746
  // Extended ext-apps methods (full specification)
720
747
  updateModelContext: function(context, data, merge) {
721
- if (!this.hostCapabilities.modelContextUpdate) {
748
+ if (!this.hostCapabilities.updateModelContext && !this.hostCapabilities.modelContextUpdate) {
722
749
  return Promise.reject(new Error('Model context update not supported'));
723
750
  }
724
- return this.sendRequest('ui/updateModelContext', { context: data, merge: merge !== false });
751
+ // The host keeps only the latest update, so an object merges into the last one here.
752
+ var isObject = data !== null && typeof data === 'object' && !Array.isArray(data);
753
+ var modelContext = isObject && merge !== false ? Object.assign({}, this.modelContext, data) : data;
754
+ this.modelContext = isObject ? modelContext : null;
755
+ var text = typeof modelContext === 'string' ? modelContext : JSON.stringify(modelContext);
756
+ var params = { content: [{ type: 'text', text: text }] };
757
+ if (isObject) params.structuredContent = modelContext;
758
+ return this.sendRequest('ui/update-model-context', params);
725
759
  },
726
760
  log: function(context, level, message, data) {
727
761
  if (!this.hostCapabilities.logging) {
@@ -730,7 +764,10 @@ var ExtAppsAdapter = {
730
764
  logFn('[Widget] ' + message, data);
731
765
  return Promise.resolve();
732
766
  }
733
- return this.sendRequest('ui/log', { level: level, message: message, data: data });
767
+ return this.sendNotification('notifications/message', {
768
+ level: level === 'warn' ? 'warning' : level,
769
+ data: data === undefined ? message : { message: message, data: data }
770
+ });
734
771
  },
735
772
  registerTool: function(context, name, description, inputSchema) {
736
773
  if (!this.hostCapabilities.widgetTools) {
@@ -1127,8 +1164,10 @@ FrontMcpBridge.prototype.openLink = function(url) {
1127
1164
  FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1128
1165
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1129
1166
  var self = this;
1130
- return this._adapter.requestDisplayMode(this._context, mode).then(function() {
1131
- self._context.hostContext.displayMode = mode;
1167
+ return this._adapter.requestDisplayMode(this._context, mode).then(function(result) {
1168
+ // The host may set another mode than the one asked for, e.g. when it refuses the change.
1169
+ var setMode = result && result.mode;
1170
+ if (${JSON.stringify(MCP_APPS_DISPLAY_MODES)}.indexOf(setMode) !== -1) self._context.hostContext.displayMode = setMode;
1132
1171
  });
1133
1172
  };
1134
1173
 
@@ -1356,7 +1395,9 @@ var BRIDGE_SCRIPT_TAGS = {
1356
1395
  // Annotate the CommonJS export names for ESM import in node:
1357
1396
  0 && (module.exports = {
1358
1397
  BRIDGE_SCRIPT_TAGS,
1398
+ MCP_APPS_DISPLAY_MODES,
1359
1399
  UNIVERSAL_BRIDGE_SCRIPT,
1400
+ WIDGET_CALL_META_KEY,
1360
1401
  generateBridgeIIFE,
1361
1402
  generatePlatformBundle
1362
1403
  });