@frontmcp/uipack 1.8.4 → 1.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/adapters/index.js CHANGED
@@ -669,6 +669,23 @@ function __applySizingCss(sizing) {
669
669
  }
670
670
  }
671
671
 
672
+ function __measureHeight() {
673
+ // <html> at fit-content includes body margins and collapsed child margins;
674
+ // content overflowing a fixed-height body is added; a px max-height clamps.
675
+ var de = document.documentElement;
676
+ var body = document.body;
677
+ if (!de || !body) return 0;
678
+ var prev = de.style.height;
679
+ de.style.height = 'fit-content';
680
+ var dr = de.getBoundingClientRect();
681
+ var br = body.getBoundingClientRect();
682
+ var h = Math.max(dr.height, br.top + body.scrollHeight + Math.max(0, dr.bottom - br.bottom) - dr.top);
683
+ de.style.height = prev;
684
+ var maxHeight = getComputedStyle(de).maxHeight;
685
+ var mx = maxHeight && maxHeight.slice(-2) === 'px' ? parseFloat(maxHeight) : NaN;
686
+ return Math.ceil(isNaN(mx) ? h : Math.min(h, mx));
687
+ }
688
+
672
689
  function __initAutoResize() {
673
690
  if (typeof window === 'undefined') return;
674
691
  // Idempotent: a re-injected IIFE must not stack observers.
@@ -691,23 +708,30 @@ function __initAutoResize() {
691
708
  if (typeof ResizeObserver === 'undefined') return;
692
709
 
693
710
  function startObserving() {
694
- var target = document.getElementById('root') || document.body;
695
- if (!target) return;
711
+ if (!document.body) return;
712
+ var targets = [document.documentElement, document.body];
713
+ var root = document.getElementById('root');
714
+ if (root) targets.push(root);
696
715
 
697
716
  var rafId = null;
698
717
  var lastReported = -1;
699
718
  function report() {
700
719
  rafId = null;
701
720
  try {
702
- var rect = target.getBoundingClientRect();
703
- var height = Math.ceil(rect.height);
704
- var width = Math.ceil(rect.width);
721
+ // Before the handshake an ext-apps host can't receive the report; the
722
+ // bridge:ready listener below sends it once the bridge is up.
723
+ if (window.FrontMcpBridge && window.FrontMcpBridge.initialized === false) return;
724
+ var height = __measureHeight();
725
+ var width = Math.ceil(document.documentElement.getBoundingClientRect().width);
705
726
  if (height === lastReported || height <= 0) return;
706
727
  lastReported = height;
707
728
  var payload = { height: height, width: width };
708
729
  if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
709
730
  if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
710
- window.FrontMcpBridge.setSize(payload).catch(function() {});
731
+ window.FrontMcpBridge.setSize(payload).catch(function() {
732
+ // Forget a rejected report so the same height is sent again.
733
+ if (lastReported === height) lastReported = -1;
734
+ });
711
735
  }
712
736
  window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
713
737
  } catch (e) {}
@@ -728,9 +752,17 @@ function __initAutoResize() {
728
752
  window.__mcpResizeObserver.disconnect();
729
753
  }
730
754
  var ro = new ResizeObserver(function() { schedule(); });
731
- ro.observe(target);
755
+ for (var i = 0; i < targets.length; i++) ro.observe(targets[i]);
732
756
  window.__mcpResizeObserver = ro;
733
757
  } catch (e) {}
758
+ // Report once the bridge is ready (the report scheduled below is skipped
759
+ // while the handshake is still pending).
760
+ var readySent = false;
761
+ window.addEventListener('bridge:ready', function() {
762
+ if (readySent) return;
763
+ readySent = true;
764
+ schedule();
765
+ });
734
766
  // Report once on init so the host gets an initial measurement.
735
767
  schedule();
736
768
  }
@@ -778,6 +810,25 @@ function detectViewport() {
778
810
  return undefined;
779
811
  }
780
812
 
813
+ function __applyHostTheme(theme) {
814
+ // Follow a host-supplied theme: <meta name="color-scheme"> sets the frame's
815
+ // canvas and form controls (a widget's own :root color-scheme still wins),
816
+ // <html data-theme> is a styling hook. The OS fallback is never written.
817
+ if (theme !== 'light' && theme !== 'dark') return;
818
+ if (typeof document === 'undefined' || !document.documentElement) return;
819
+ try {
820
+ var meta = document.querySelector('meta[name="color-scheme"]');
821
+ var head = document.head || document.getElementsByTagName('head')[0];
822
+ if (!meta && head) {
823
+ meta = document.createElement('meta');
824
+ meta.setAttribute('name', 'color-scheme');
825
+ head.appendChild(meta);
826
+ }
827
+ if (meta) meta.setAttribute('content', theme);
828
+ document.documentElement.setAttribute('data-theme', theme);
829
+ } catch (e) {}
830
+ }
831
+
781
832
  function readInjectedData() {
782
833
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
783
834
  if (typeof window !== 'undefined') {
@@ -884,6 +935,8 @@ var ExtAppsAdapter = {
884
935
  pendingRequests: {},
885
936
  requestId: 0,
886
937
  hostCapabilities: {},
938
+ handshakeSettled: false,
939
+ pendingSize: null,
887
940
  canHandle: function() {
888
941
  if (typeof window === 'undefined') return false;
889
942
  if (window.parent === window) return false;
@@ -1049,6 +1102,7 @@ var ExtAppsAdapter = {
1049
1102
  var timeout = setTimeout(function() {
1050
1103
  delete self.pendingRequests[id];
1051
1104
  // Handshake timeout is non-fatal \u2014 notifications may still arrive
1105
+ self.settleHandshake();
1052
1106
  resolve();
1053
1107
  }, 10000);
1054
1108
 
@@ -1062,7 +1116,9 @@ var ExtAppsAdapter = {
1062
1116
  supportsDisplayModes: true
1063
1117
  });
1064
1118
  if (result.hostContext) {
1065
- Object.assign(context.hostContext, result.hostContext);
1119
+ // Notify (not a silent merge) so onContextChange listeners and the
1120
+ // host theme see the handshake's context.
1121
+ context.notifyContextChange(result.hostContext);
1066
1122
  }
1067
1123
  // Send ui/notifications/initialized to tell the host the view is ready.
1068
1124
  // Per MCP Apps spec, the host waits for this before sending tool-result.
@@ -1072,9 +1128,10 @@ var ExtAppsAdapter = {
1072
1128
  method: 'ui/notifications/initialized',
1073
1129
  params: {}
1074
1130
  }, targetOrigin);
1131
+ self.settleHandshake();
1075
1132
  resolve();
1076
1133
  },
1077
- reject: function(err) { resolve(); }, // Non-fatal
1134
+ reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
1078
1135
  timeout: timeout
1079
1136
  };
1080
1137
 
@@ -1088,6 +1145,18 @@ var ExtAppsAdapter = {
1088
1145
  performHandshake: function(context) {
1089
1146
  return this.sendHandshake(context);
1090
1147
  },
1148
+ settleHandshake: function() {
1149
+ // Deliver the latest size requested while no host origin was trusted.
1150
+ this.handshakeSettled = true;
1151
+ var pending = this.pendingSize;
1152
+ if (!pending) return;
1153
+ this.pendingSize = null;
1154
+ this.sendRequest('ui/setSize', pending.params).then(function(result) {
1155
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
1156
+ }, function(err) {
1157
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
1158
+ });
1159
+ },
1091
1160
  callTool: function(context, name, args) {
1092
1161
  if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
1093
1162
  return Promise.reject(new Error('Server tool proxy not supported'));
@@ -1111,11 +1180,22 @@ var ExtAppsAdapter = {
1111
1180
  setSize: function(context, size) {
1112
1181
  // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
1113
1182
  // measured/desired widget dimensions to the host.
1114
- return this.sendRequest('ui/setSize', {
1183
+ var params = {
1115
1184
  height: size && size.height,
1116
1185
  width: size && size.width,
1117
1186
  aspectRatio: size && size.aspectRatio
1118
- });
1187
+ };
1188
+ if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
1189
+ // No host origin yet: keep only the latest size and send it when the
1190
+ // handshake settles; every caller's promise follows that one request.
1191
+ var self = this;
1192
+ if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
1193
+ this.pendingSize.params = params;
1194
+ return new Promise(function(resolve, reject) {
1195
+ self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
1196
+ });
1197
+ }
1198
+ return this.sendRequest('ui/setSize', params);
1119
1199
  },
1120
1200
  requestClose: function(context) {
1121
1201
  return this.sendRequest('ui/close', {});
@@ -1361,6 +1441,7 @@ function FrontMcpBridge() {
1361
1441
  toolResultListeners: [],
1362
1442
  notifyContextChange: function(changes) {
1363
1443
  Object.assign(this.hostContext, changes);
1444
+ if (changes && changes.theme) __applyHostTheme(changes.theme);
1364
1445
  for (var i = 0; i < this.contextListeners.length; i++) {
1365
1446
  try { this.contextListeners[i](changes); } catch(e) {}
1366
1447
  }
@@ -2145,8 +2226,24 @@ function isFrontmcpUiResolvable(...candidatePaths) {
2145
2226
  }
2146
2227
 
2147
2228
  // libs/uipack/src/component/transpiler.ts
2229
+ function loadEsbuild(filename) {
2230
+ try {
2231
+ return require("esbuild");
2232
+ } catch (err) {
2233
+ if (!isMissingEsbuild(err)) throw err;
2234
+ const subject = filename ? `FileSource widget "${filename}"` : "A FileSource widget";
2235
+ throw new Error(
2236
+ `${subject} needs esbuild, which is not installed. @frontmcp/uipack loads esbuild on demand to bundle FileSource widgets (.tsx/.jsx) when the tool is called, so it must be installed where the server runs: \`npm install esbuild\` (in "dependencies", not "devDependencies"). Projects created with \`frontmcp create\` already get it through the \`frontmcp\` package.`,
2237
+ { cause: err }
2238
+ );
2239
+ }
2240
+ }
2241
+ function isMissingEsbuild(err) {
2242
+ if (!(err instanceof Error)) return false;
2243
+ return err.code === "MODULE_NOT_FOUND" && /Cannot find module ['"]esbuild['"]/.test(err.message);
2244
+ }
2148
2245
  function transpileReactSource(source, filename) {
2149
- const esbuild = require("esbuild");
2246
+ const esbuild = loadEsbuild(filename);
2150
2247
  const loader = filename?.endsWith(".tsx") ? "tsx" : "jsx";
2151
2248
  const result = esbuild.transformSync(source, {
2152
2249
  loader,
@@ -2165,7 +2262,7 @@ function bundleFileSource(source, filename, resolveDir, componentName, options =
2165
2262
  `FileSource widget "${filename}" requires the @frontmcp/ui package, which provides the React bridge mount that's injected at bundle time. Install it (e.g. \`npm install @frontmcp/ui\` or \`yarn add @frontmcp/ui\`) and try again.`
2166
2263
  );
2167
2264
  }
2168
- const esbuild = require("esbuild");
2265
+ const esbuild = loadEsbuild(filename);
2169
2266
  const mountCode = `
2170
2267
  // --- Auto-generated mount ---
2171
2268
  import { createElement as __h } from 'react';
@@ -2262,7 +2359,8 @@ if (__root) {
2262
2359
  } catch (err) {
2263
2360
  const message = err instanceof Error ? err.message : String(err);
2264
2361
  throw new Error(
2265
- `Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`
2362
+ `Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`,
2363
+ { cause: err }
2266
2364
  );
2267
2365
  }
2268
2366
  }
@@ -2338,6 +2436,37 @@ function resolveImportSource(source) {
2338
2436
  peerDependencies: []
2339
2437
  };
2340
2438
  }
2439
+ var BUILD_DIR_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "out"]);
2440
+ function missingWidgetError(filePath, cause) {
2441
+ const sourceWidget = findSourceWidget(filePath);
2442
+ const error = new Error(
2443
+ `FileSource widget not found (ENOENT): no file at "${filePath}". The widget source is read from this path when the tool is called, so it has to exist where the compiled tool runs \u2014 a path built from \`__dirname\` or \`import.meta.url\` points into the build output once the tool is compiled. tsc does not copy \`*.widget.tsx\` / \`*.widget.jsx\` files into its output; \`frontmcp build\` does, and any other build needs a step that copies them next to the compiled tool.` + (sourceWidget ? ` The source widget exists at "${sourceWidget}" \u2014 copy it to "${filePath}", or build with \`frontmcp build\`.` : ""),
2444
+ { cause }
2445
+ );
2446
+ error.code = "ENOENT";
2447
+ error.path = filePath;
2448
+ return error;
2449
+ }
2450
+ function findSourceWidget(filePath) {
2451
+ const path = require("path");
2452
+ const fs = require("fs");
2453
+ const parts = filePath.split(path.sep);
2454
+ for (let i = parts.length - 2; i >= 0; i--) {
2455
+ if (!BUILD_DIR_SEGMENTS.has(parts[i])) continue;
2456
+ const candidates = [[...parts.slice(0, i), "src", ...parts.slice(i + 1)]];
2457
+ if (parts.length - i > 2) candidates.push([...parts.slice(0, i), "src", ...parts.slice(i + 2)]);
2458
+ candidates.push([...parts.slice(0, i), ...parts.slice(i + 1)]);
2459
+ for (const candidate of candidates) {
2460
+ const candidatePath = candidate.join(path.sep);
2461
+ try {
2462
+ if (fs.existsSync(candidatePath)) return candidatePath;
2463
+ } catch {
2464
+ }
2465
+ }
2466
+ return void 0;
2467
+ }
2468
+ return void 0;
2469
+ }
2341
2470
  function resolveFileSource(source, options = {}) {
2342
2471
  const path = require("path");
2343
2472
  const ext = path.extname(source.file).toLowerCase();
@@ -2352,9 +2481,11 @@ function resolveFileSource(source, options = {}) {
2352
2481
  const isNotFound = err?.code === "ENOENT";
2353
2482
  if (isNotFound && wasRelative) {
2354
2483
  throw new Error(
2355
- `FileSource widget "${source.file}" not found at "${filePath}". Relative paths are resolved against process.cwd() ("${process.cwd()}"), not the tool file's directory. To anchor the path to the tool file, pass an absolute path \u2014 e.g. \`{ file: fileURLToPath(new URL('./widget.tsx', import.meta.url)) }\` from \`node:url\` (see issue #444).`
2484
+ `FileSource widget "${source.file}" not found at "${filePath}". Relative paths are resolved against process.cwd() ("${process.cwd()}"), not the tool file's directory. To anchor the path to the tool file, pass an absolute path \u2014 e.g. \`{ file: fileURLToPath(new URL('./widget.tsx', import.meta.url)) }\` from \`node:url\` (see issue #444).`,
2485
+ { cause: err }
2356
2486
  );
2357
2487
  }
2488
+ if (isNotFound) throw missingWidgetError(filePath, err);
2358
2489
  throw err;
2359
2490
  }
2360
2491
  const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
@@ -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;AAooCD;;;;;;;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;;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;AAguCD;;;;;;;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"}
@@ -134,6 +134,23 @@ function __applySizingCss(sizing) {
134
134
  }
135
135
  }
136
136
 
137
+ function __measureHeight() {
138
+ // <html> at fit-content includes body margins and collapsed child margins;
139
+ // content overflowing a fixed-height body is added; a px max-height clamps.
140
+ var de = document.documentElement;
141
+ var body = document.body;
142
+ if (!de || !body) return 0;
143
+ var prev = de.style.height;
144
+ de.style.height = 'fit-content';
145
+ var dr = de.getBoundingClientRect();
146
+ var br = body.getBoundingClientRect();
147
+ var h = Math.max(dr.height, br.top + body.scrollHeight + Math.max(0, dr.bottom - br.bottom) - dr.top);
148
+ de.style.height = prev;
149
+ var maxHeight = getComputedStyle(de).maxHeight;
150
+ var mx = maxHeight && maxHeight.slice(-2) === 'px' ? parseFloat(maxHeight) : NaN;
151
+ return Math.ceil(isNaN(mx) ? h : Math.min(h, mx));
152
+ }
153
+
137
154
  function __initAutoResize() {
138
155
  if (typeof window === 'undefined') return;
139
156
  // Idempotent: a re-injected IIFE must not stack observers.
@@ -156,23 +173,30 @@ function __initAutoResize() {
156
173
  if (typeof ResizeObserver === 'undefined') return;
157
174
 
158
175
  function startObserving() {
159
- var target = document.getElementById('root') || document.body;
160
- if (!target) return;
176
+ if (!document.body) return;
177
+ var targets = [document.documentElement, document.body];
178
+ var root = document.getElementById('root');
179
+ if (root) targets.push(root);
161
180
 
162
181
  var rafId = null;
163
182
  var lastReported = -1;
164
183
  function report() {
165
184
  rafId = null;
166
185
  try {
167
- var rect = target.getBoundingClientRect();
168
- var height = Math.ceil(rect.height);
169
- var width = Math.ceil(rect.width);
186
+ // Before the handshake an ext-apps host can't receive the report; the
187
+ // bridge:ready listener below sends it once the bridge is up.
188
+ if (window.FrontMcpBridge && window.FrontMcpBridge.initialized === false) return;
189
+ var height = __measureHeight();
190
+ var width = Math.ceil(document.documentElement.getBoundingClientRect().width);
170
191
  if (height === lastReported || height <= 0) return;
171
192
  lastReported = height;
172
193
  var payload = { height: height, width: width };
173
194
  if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
174
195
  if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
175
- window.FrontMcpBridge.setSize(payload).catch(function() {});
196
+ window.FrontMcpBridge.setSize(payload).catch(function() {
197
+ // Forget a rejected report so the same height is sent again.
198
+ if (lastReported === height) lastReported = -1;
199
+ });
176
200
  }
177
201
  window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
178
202
  } catch (e) {}
@@ -193,9 +217,17 @@ function __initAutoResize() {
193
217
  window.__mcpResizeObserver.disconnect();
194
218
  }
195
219
  var ro = new ResizeObserver(function() { schedule(); });
196
- ro.observe(target);
220
+ for (var i = 0; i < targets.length; i++) ro.observe(targets[i]);
197
221
  window.__mcpResizeObserver = ro;
198
222
  } catch (e) {}
223
+ // Report once the bridge is ready (the report scheduled below is skipped
224
+ // while the handshake is still pending).
225
+ var readySent = false;
226
+ window.addEventListener('bridge:ready', function() {
227
+ if (readySent) return;
228
+ readySent = true;
229
+ schedule();
230
+ });
199
231
  // Report once on init so the host gets an initial measurement.
200
232
  schedule();
201
233
  }
@@ -243,6 +275,25 @@ function detectViewport() {
243
275
  return undefined;
244
276
  }
245
277
 
278
+ function __applyHostTheme(theme) {
279
+ // Follow a host-supplied theme: <meta name="color-scheme"> sets the frame's
280
+ // canvas and form controls (a widget's own :root color-scheme still wins),
281
+ // <html data-theme> is a styling hook. The OS fallback is never written.
282
+ if (theme !== 'light' && theme !== 'dark') return;
283
+ if (typeof document === 'undefined' || !document.documentElement) return;
284
+ try {
285
+ var meta = document.querySelector('meta[name="color-scheme"]');
286
+ var head = document.head || document.getElementsByTagName('head')[0];
287
+ if (!meta && head) {
288
+ meta = document.createElement('meta');
289
+ meta.setAttribute('name', 'color-scheme');
290
+ head.appendChild(meta);
291
+ }
292
+ if (meta) meta.setAttribute('content', theme);
293
+ document.documentElement.setAttribute('data-theme', theme);
294
+ } catch (e) {}
295
+ }
296
+
246
297
  function readInjectedData() {
247
298
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
248
299
  if (typeof window !== 'undefined') {
@@ -349,6 +400,8 @@ var ExtAppsAdapter = {
349
400
  pendingRequests: {},
350
401
  requestId: 0,
351
402
  hostCapabilities: {},
403
+ handshakeSettled: false,
404
+ pendingSize: null,
352
405
  canHandle: function() {
353
406
  if (typeof window === 'undefined') return false;
354
407
  if (window.parent === window) return false;
@@ -514,6 +567,7 @@ var ExtAppsAdapter = {
514
567
  var timeout = setTimeout(function() {
515
568
  delete self.pendingRequests[id];
516
569
  // Handshake timeout is non-fatal \u2014 notifications may still arrive
570
+ self.settleHandshake();
517
571
  resolve();
518
572
  }, 10000);
519
573
 
@@ -527,7 +581,9 @@ var ExtAppsAdapter = {
527
581
  supportsDisplayModes: true
528
582
  });
529
583
  if (result.hostContext) {
530
- Object.assign(context.hostContext, result.hostContext);
584
+ // Notify (not a silent merge) so onContextChange listeners and the
585
+ // host theme see the handshake's context.
586
+ context.notifyContextChange(result.hostContext);
531
587
  }
532
588
  // Send ui/notifications/initialized to tell the host the view is ready.
533
589
  // Per MCP Apps spec, the host waits for this before sending tool-result.
@@ -537,9 +593,10 @@ var ExtAppsAdapter = {
537
593
  method: 'ui/notifications/initialized',
538
594
  params: {}
539
595
  }, targetOrigin);
596
+ self.settleHandshake();
540
597
  resolve();
541
598
  },
542
- reject: function(err) { resolve(); }, // Non-fatal
599
+ reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
543
600
  timeout: timeout
544
601
  };
545
602
 
@@ -553,6 +610,18 @@ var ExtAppsAdapter = {
553
610
  performHandshake: function(context) {
554
611
  return this.sendHandshake(context);
555
612
  },
613
+ settleHandshake: function() {
614
+ // Deliver the latest size requested while no host origin was trusted.
615
+ this.handshakeSettled = true;
616
+ var pending = this.pendingSize;
617
+ if (!pending) return;
618
+ this.pendingSize = null;
619
+ this.sendRequest('ui/setSize', pending.params).then(function(result) {
620
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
621
+ }, function(err) {
622
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
623
+ });
624
+ },
556
625
  callTool: function(context, name, args) {
557
626
  if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
558
627
  return Promise.reject(new Error('Server tool proxy not supported'));
@@ -576,11 +645,22 @@ var ExtAppsAdapter = {
576
645
  setSize: function(context, size) {
577
646
  // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
578
647
  // measured/desired widget dimensions to the host.
579
- return this.sendRequest('ui/setSize', {
648
+ var params = {
580
649
  height: size && size.height,
581
650
  width: size && size.width,
582
651
  aspectRatio: size && size.aspectRatio
583
- });
652
+ };
653
+ if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
654
+ // No host origin yet: keep only the latest size and send it when the
655
+ // handshake settles; every caller's promise follows that one request.
656
+ var self = this;
657
+ if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
658
+ this.pendingSize.params = params;
659
+ return new Promise(function(resolve, reject) {
660
+ self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
661
+ });
662
+ }
663
+ return this.sendRequest('ui/setSize', params);
584
664
  },
585
665
  requestClose: function(context) {
586
666
  return this.sendRequest('ui/close', {});
@@ -826,6 +906,7 @@ function FrontMcpBridge() {
826
906
  toolResultListeners: [],
827
907
  notifyContextChange: function(changes) {
828
908
  Object.assign(this.hostContext, changes);
909
+ if (changes && changes.theme) __applyHostTheme(changes.theme);
829
910
  for (var i = 0; i < this.contextListeners.length; i++) {
830
911
  try { this.contextListeners[i](changes); } catch(e) {}
831
912
  }