@frontmcp/uipack 1.8.5 → 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/esm/index.mjs CHANGED
@@ -238,6 +238,23 @@ function __applySizingCss(sizing) {
238
238
  }
239
239
  }
240
240
 
241
+ function __measureHeight() {
242
+ // <html> at fit-content includes body margins and collapsed child margins;
243
+ // content overflowing a fixed-height body is added; a px max-height clamps.
244
+ var de = document.documentElement;
245
+ var body = document.body;
246
+ if (!de || !body) return 0;
247
+ var prev = de.style.height;
248
+ de.style.height = 'fit-content';
249
+ var dr = de.getBoundingClientRect();
250
+ var br = body.getBoundingClientRect();
251
+ var h = Math.max(dr.height, br.top + body.scrollHeight + Math.max(0, dr.bottom - br.bottom) - dr.top);
252
+ de.style.height = prev;
253
+ var maxHeight = getComputedStyle(de).maxHeight;
254
+ var mx = maxHeight && maxHeight.slice(-2) === 'px' ? parseFloat(maxHeight) : NaN;
255
+ return Math.ceil(isNaN(mx) ? h : Math.min(h, mx));
256
+ }
257
+
241
258
  function __initAutoResize() {
242
259
  if (typeof window === 'undefined') return;
243
260
  // Idempotent: a re-injected IIFE must not stack observers.
@@ -260,23 +277,30 @@ function __initAutoResize() {
260
277
  if (typeof ResizeObserver === 'undefined') return;
261
278
 
262
279
  function startObserving() {
263
- var target = document.getElementById('root') || document.body;
264
- if (!target) return;
280
+ if (!document.body) return;
281
+ var targets = [document.documentElement, document.body];
282
+ var root = document.getElementById('root');
283
+ if (root) targets.push(root);
265
284
 
266
285
  var rafId = null;
267
286
  var lastReported = -1;
268
287
  function report() {
269
288
  rafId = null;
270
289
  try {
271
- var rect = target.getBoundingClientRect();
272
- var height = Math.ceil(rect.height);
273
- var width = Math.ceil(rect.width);
290
+ // Before the handshake an ext-apps host can't receive the report; the
291
+ // bridge:ready listener below sends it once the bridge is up.
292
+ if (window.FrontMcpBridge && window.FrontMcpBridge.initialized === false) return;
293
+ var height = __measureHeight();
294
+ var width = Math.ceil(document.documentElement.getBoundingClientRect().width);
274
295
  if (height === lastReported || height <= 0) return;
275
296
  lastReported = height;
276
297
  var payload = { height: height, width: width };
277
298
  if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
278
299
  if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
279
- window.FrontMcpBridge.setSize(payload).catch(function() {});
300
+ window.FrontMcpBridge.setSize(payload).catch(function() {
301
+ // Forget a rejected report so the same height is sent again.
302
+ if (lastReported === height) lastReported = -1;
303
+ });
280
304
  }
281
305
  window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
282
306
  } catch (e) {}
@@ -297,9 +321,17 @@ function __initAutoResize() {
297
321
  window.__mcpResizeObserver.disconnect();
298
322
  }
299
323
  var ro = new ResizeObserver(function() { schedule(); });
300
- ro.observe(target);
324
+ for (var i = 0; i < targets.length; i++) ro.observe(targets[i]);
301
325
  window.__mcpResizeObserver = ro;
302
326
  } catch (e) {}
327
+ // Report once the bridge is ready (the report scheduled below is skipped
328
+ // while the handshake is still pending).
329
+ var readySent = false;
330
+ window.addEventListener('bridge:ready', function() {
331
+ if (readySent) return;
332
+ readySent = true;
333
+ schedule();
334
+ });
303
335
  // Report once on init so the host gets an initial measurement.
304
336
  schedule();
305
337
  }
@@ -347,6 +379,25 @@ function detectViewport() {
347
379
  return undefined;
348
380
  }
349
381
 
382
+ function __applyHostTheme(theme) {
383
+ // Follow a host-supplied theme: <meta name="color-scheme"> sets the frame's
384
+ // canvas and form controls (a widget's own :root color-scheme still wins),
385
+ // <html data-theme> is a styling hook. The OS fallback is never written.
386
+ if (theme !== 'light' && theme !== 'dark') return;
387
+ if (typeof document === 'undefined' || !document.documentElement) return;
388
+ try {
389
+ var meta = document.querySelector('meta[name="color-scheme"]');
390
+ var head = document.head || document.getElementsByTagName('head')[0];
391
+ if (!meta && head) {
392
+ meta = document.createElement('meta');
393
+ meta.setAttribute('name', 'color-scheme');
394
+ head.appendChild(meta);
395
+ }
396
+ if (meta) meta.setAttribute('content', theme);
397
+ document.documentElement.setAttribute('data-theme', theme);
398
+ } catch (e) {}
399
+ }
400
+
350
401
  function readInjectedData() {
351
402
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
352
403
  if (typeof window !== 'undefined') {
@@ -453,6 +504,8 @@ var ExtAppsAdapter = {
453
504
  pendingRequests: {},
454
505
  requestId: 0,
455
506
  hostCapabilities: {},
507
+ handshakeSettled: false,
508
+ pendingSize: null,
456
509
  canHandle: function() {
457
510
  if (typeof window === 'undefined') return false;
458
511
  if (window.parent === window) return false;
@@ -618,6 +671,7 @@ var ExtAppsAdapter = {
618
671
  var timeout = setTimeout(function() {
619
672
  delete self.pendingRequests[id];
620
673
  // Handshake timeout is non-fatal \u2014 notifications may still arrive
674
+ self.settleHandshake();
621
675
  resolve();
622
676
  }, 10000);
623
677
 
@@ -631,7 +685,9 @@ var ExtAppsAdapter = {
631
685
  supportsDisplayModes: true
632
686
  });
633
687
  if (result.hostContext) {
634
- Object.assign(context.hostContext, result.hostContext);
688
+ // Notify (not a silent merge) so onContextChange listeners and the
689
+ // host theme see the handshake's context.
690
+ context.notifyContextChange(result.hostContext);
635
691
  }
636
692
  // Send ui/notifications/initialized to tell the host the view is ready.
637
693
  // Per MCP Apps spec, the host waits for this before sending tool-result.
@@ -641,9 +697,10 @@ var ExtAppsAdapter = {
641
697
  method: 'ui/notifications/initialized',
642
698
  params: {}
643
699
  }, targetOrigin);
700
+ self.settleHandshake();
644
701
  resolve();
645
702
  },
646
- reject: function(err) { resolve(); }, // Non-fatal
703
+ reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
647
704
  timeout: timeout
648
705
  };
649
706
 
@@ -657,6 +714,18 @@ var ExtAppsAdapter = {
657
714
  performHandshake: function(context) {
658
715
  return this.sendHandshake(context);
659
716
  },
717
+ settleHandshake: function() {
718
+ // Deliver the latest size requested while no host origin was trusted.
719
+ this.handshakeSettled = true;
720
+ var pending = this.pendingSize;
721
+ if (!pending) return;
722
+ this.pendingSize = null;
723
+ this.sendRequest('ui/setSize', pending.params).then(function(result) {
724
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
725
+ }, function(err) {
726
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
727
+ });
728
+ },
660
729
  callTool: function(context, name, args) {
661
730
  if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
662
731
  return Promise.reject(new Error('Server tool proxy not supported'));
@@ -680,11 +749,22 @@ var ExtAppsAdapter = {
680
749
  setSize: function(context, size) {
681
750
  // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
682
751
  // measured/desired widget dimensions to the host.
683
- return this.sendRequest('ui/setSize', {
752
+ var params = {
684
753
  height: size && size.height,
685
754
  width: size && size.width,
686
755
  aspectRatio: size && size.aspectRatio
687
- });
756
+ };
757
+ if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
758
+ // No host origin yet: keep only the latest size and send it when the
759
+ // handshake settles; every caller's promise follows that one request.
760
+ var self = this;
761
+ if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
762
+ this.pendingSize.params = params;
763
+ return new Promise(function(resolve, reject) {
764
+ self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
765
+ });
766
+ }
767
+ return this.sendRequest('ui/setSize', params);
688
768
  },
689
769
  requestClose: function(context) {
690
770
  return this.sendRequest('ui/close', {});
@@ -930,6 +1010,7 @@ function FrontMcpBridge() {
930
1010
  toolResultListeners: [],
931
1011
  notifyContextChange: function(changes) {
932
1012
  Object.assign(this.hostContext, changes);
1013
+ if (changes && changes.theme) __applyHostTheme(changes.theme);
933
1014
  for (var i = 0; i < this.contextListeners.length; i++) {
934
1015
  try { this.contextListeners[i](changes); } catch(e) {}
935
1016
  }
@@ -3329,8 +3410,24 @@ function isFrontmcpUiResolvable(...candidatePaths) {
3329
3410
  }
3330
3411
 
3331
3412
  // libs/uipack/src/component/transpiler.ts
3413
+ function loadEsbuild(filename) {
3414
+ try {
3415
+ return __require("esbuild");
3416
+ } catch (err) {
3417
+ if (!isMissingEsbuild(err)) throw err;
3418
+ const subject = filename ? `FileSource widget "${filename}"` : "A FileSource widget";
3419
+ throw new Error(
3420
+ `${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.`,
3421
+ { cause: err }
3422
+ );
3423
+ }
3424
+ }
3425
+ function isMissingEsbuild(err) {
3426
+ if (!(err instanceof Error)) return false;
3427
+ return err.code === "MODULE_NOT_FOUND" && /Cannot find module ['"]esbuild['"]/.test(err.message);
3428
+ }
3332
3429
  function transpileReactSource(source, filename) {
3333
- const esbuild = __require("esbuild");
3430
+ const esbuild = loadEsbuild(filename);
3334
3431
  const loader = filename?.endsWith(".tsx") ? "tsx" : "jsx";
3335
3432
  const result = esbuild.transformSync(source, {
3336
3433
  loader,
@@ -3349,7 +3446,7 @@ function bundleFileSource(source, filename, resolveDir, componentName, options =
3349
3446
  `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.`
3350
3447
  );
3351
3448
  }
3352
- const esbuild = __require("esbuild");
3449
+ const esbuild = loadEsbuild(filename);
3353
3450
  const mountCode = `
3354
3451
  // --- Auto-generated mount ---
3355
3452
  import { createElement as __h } from 'react';
@@ -3446,7 +3543,8 @@ if (__root) {
3446
3543
  } catch (err) {
3447
3544
  const message = err instanceof Error ? err.message : String(err);
3448
3545
  throw new Error(
3449
- `Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`
3546
+ `Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`,
3547
+ { cause: err }
3450
3548
  );
3451
3549
  }
3452
3550
  }
@@ -3508,6 +3606,37 @@ function resolveImportSource(source) {
3508
3606
  peerDependencies: []
3509
3607
  };
3510
3608
  }
3609
+ var BUILD_DIR_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "out"]);
3610
+ function missingWidgetError(filePath, cause) {
3611
+ const sourceWidget = findSourceWidget(filePath);
3612
+ const error = new Error(
3613
+ `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\`.` : ""),
3614
+ { cause }
3615
+ );
3616
+ error.code = "ENOENT";
3617
+ error.path = filePath;
3618
+ return error;
3619
+ }
3620
+ function findSourceWidget(filePath) {
3621
+ const path = __require("path");
3622
+ const fs = __require("fs");
3623
+ const parts = filePath.split(path.sep);
3624
+ for (let i = parts.length - 2; i >= 0; i--) {
3625
+ if (!BUILD_DIR_SEGMENTS.has(parts[i])) continue;
3626
+ const candidates = [[...parts.slice(0, i), "src", ...parts.slice(i + 1)]];
3627
+ if (parts.length - i > 2) candidates.push([...parts.slice(0, i), "src", ...parts.slice(i + 2)]);
3628
+ candidates.push([...parts.slice(0, i), ...parts.slice(i + 1)]);
3629
+ for (const candidate of candidates) {
3630
+ const candidatePath = candidate.join(path.sep);
3631
+ try {
3632
+ if (fs.existsSync(candidatePath)) return candidatePath;
3633
+ } catch {
3634
+ }
3635
+ }
3636
+ return void 0;
3637
+ }
3638
+ return void 0;
3639
+ }
3511
3640
  function resolveFileSource(source, options = {}) {
3512
3641
  const path = __require("path");
3513
3642
  const ext = path.extname(source.file).toLowerCase();
@@ -3522,9 +3651,11 @@ function resolveFileSource(source, options = {}) {
3522
3651
  const isNotFound = err?.code === "ENOENT";
3523
3652
  if (isNotFound && wasRelative) {
3524
3653
  throw new Error(
3525
- `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).`
3654
+ `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).`,
3655
+ { cause: err }
3526
3656
  );
3527
3657
  }
3658
+ if (isNotFound) throw missingWidgetError(filePath, err);
3528
3659
  throw err;
3529
3660
  }
3530
3661
  const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
package/esm/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/uipack",
3
- "version": "1.8.5",
3
+ "version": "1.8.6",
4
4
  "description": "FrontMCP UIpack - HTML shell builder, pluggable import resolver, and NPM component loader for MCP UI (React-free core)",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "homepage": "https://docs.agentfront.dev",
@@ -61,6 +61,12 @@
61
61
  "typescript": "^5.9.3"
62
62
  },
63
63
  "peerDependencies": {
64
- "@frontmcp/lazy-zod": "1.8.5"
64
+ "@frontmcp/lazy-zod": "1.8.6",
65
+ "esbuild": ">=0.27.0 <1"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "esbuild": {
69
+ "optional": true
70
+ }
65
71
  }
66
72
  }
@@ -105,6 +105,23 @@ function __applySizingCss(sizing) {
105
105
  }
106
106
  }
107
107
 
108
+ function __measureHeight() {
109
+ // <html> at fit-content includes body margins and collapsed child margins;
110
+ // content overflowing a fixed-height body is added; a px max-height clamps.
111
+ var de = document.documentElement;
112
+ var body = document.body;
113
+ if (!de || !body) return 0;
114
+ var prev = de.style.height;
115
+ de.style.height = 'fit-content';
116
+ var dr = de.getBoundingClientRect();
117
+ var br = body.getBoundingClientRect();
118
+ var h = Math.max(dr.height, br.top + body.scrollHeight + Math.max(0, dr.bottom - br.bottom) - dr.top);
119
+ de.style.height = prev;
120
+ var maxHeight = getComputedStyle(de).maxHeight;
121
+ var mx = maxHeight && maxHeight.slice(-2) === 'px' ? parseFloat(maxHeight) : NaN;
122
+ return Math.ceil(isNaN(mx) ? h : Math.min(h, mx));
123
+ }
124
+
108
125
  function __initAutoResize() {
109
126
  if (typeof window === 'undefined') return;
110
127
  // Idempotent: a re-injected IIFE must not stack observers.
@@ -127,23 +144,30 @@ function __initAutoResize() {
127
144
  if (typeof ResizeObserver === 'undefined') return;
128
145
 
129
146
  function startObserving() {
130
- var target = document.getElementById('root') || document.body;
131
- if (!target) return;
147
+ if (!document.body) return;
148
+ var targets = [document.documentElement, document.body];
149
+ var root = document.getElementById('root');
150
+ if (root) targets.push(root);
132
151
 
133
152
  var rafId = null;
134
153
  var lastReported = -1;
135
154
  function report() {
136
155
  rafId = null;
137
156
  try {
138
- var rect = target.getBoundingClientRect();
139
- var height = Math.ceil(rect.height);
140
- var width = Math.ceil(rect.width);
157
+ // Before the handshake an ext-apps host can't receive the report; the
158
+ // bridge:ready listener below sends it once the bridge is up.
159
+ if (window.FrontMcpBridge && window.FrontMcpBridge.initialized === false) return;
160
+ var height = __measureHeight();
161
+ var width = Math.ceil(document.documentElement.getBoundingClientRect().width);
141
162
  if (height === lastReported || height <= 0) return;
142
163
  lastReported = height;
143
164
  var payload = { height: height, width: width };
144
165
  if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
145
166
  if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
146
- window.FrontMcpBridge.setSize(payload).catch(function() {});
167
+ window.FrontMcpBridge.setSize(payload).catch(function() {
168
+ // Forget a rejected report so the same height is sent again.
169
+ if (lastReported === height) lastReported = -1;
170
+ });
147
171
  }
148
172
  window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
149
173
  } catch (e) {}
@@ -164,9 +188,17 @@ function __initAutoResize() {
164
188
  window.__mcpResizeObserver.disconnect();
165
189
  }
166
190
  var ro = new ResizeObserver(function() { schedule(); });
167
- ro.observe(target);
191
+ for (var i = 0; i < targets.length; i++) ro.observe(targets[i]);
168
192
  window.__mcpResizeObserver = ro;
169
193
  } catch (e) {}
194
+ // Report once the bridge is ready (the report scheduled below is skipped
195
+ // while the handshake is still pending).
196
+ var readySent = false;
197
+ window.addEventListener('bridge:ready', function() {
198
+ if (readySent) return;
199
+ readySent = true;
200
+ schedule();
201
+ });
170
202
  // Report once on init so the host gets an initial measurement.
171
203
  schedule();
172
204
  }
@@ -214,6 +246,25 @@ function detectViewport() {
214
246
  return undefined;
215
247
  }
216
248
 
249
+ function __applyHostTheme(theme) {
250
+ // Follow a host-supplied theme: <meta name="color-scheme"> sets the frame's
251
+ // canvas and form controls (a widget's own :root color-scheme still wins),
252
+ // <html data-theme> is a styling hook. The OS fallback is never written.
253
+ if (theme !== 'light' && theme !== 'dark') return;
254
+ if (typeof document === 'undefined' || !document.documentElement) return;
255
+ try {
256
+ var meta = document.querySelector('meta[name="color-scheme"]');
257
+ var head = document.head || document.getElementsByTagName('head')[0];
258
+ if (!meta && head) {
259
+ meta = document.createElement('meta');
260
+ meta.setAttribute('name', 'color-scheme');
261
+ head.appendChild(meta);
262
+ }
263
+ if (meta) meta.setAttribute('content', theme);
264
+ document.documentElement.setAttribute('data-theme', theme);
265
+ } catch (e) {}
266
+ }
267
+
217
268
  function readInjectedData() {
218
269
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
219
270
  if (typeof window !== 'undefined') {
@@ -320,6 +371,8 @@ var ExtAppsAdapter = {
320
371
  pendingRequests: {},
321
372
  requestId: 0,
322
373
  hostCapabilities: {},
374
+ handshakeSettled: false,
375
+ pendingSize: null,
323
376
  canHandle: function() {
324
377
  if (typeof window === 'undefined') return false;
325
378
  if (window.parent === window) return false;
@@ -485,6 +538,7 @@ var ExtAppsAdapter = {
485
538
  var timeout = setTimeout(function() {
486
539
  delete self.pendingRequests[id];
487
540
  // Handshake timeout is non-fatal \u2014 notifications may still arrive
541
+ self.settleHandshake();
488
542
  resolve();
489
543
  }, 10000);
490
544
 
@@ -498,7 +552,9 @@ var ExtAppsAdapter = {
498
552
  supportsDisplayModes: true
499
553
  });
500
554
  if (result.hostContext) {
501
- Object.assign(context.hostContext, result.hostContext);
555
+ // Notify (not a silent merge) so onContextChange listeners and the
556
+ // host theme see the handshake's context.
557
+ context.notifyContextChange(result.hostContext);
502
558
  }
503
559
  // Send ui/notifications/initialized to tell the host the view is ready.
504
560
  // Per MCP Apps spec, the host waits for this before sending tool-result.
@@ -508,9 +564,10 @@ var ExtAppsAdapter = {
508
564
  method: 'ui/notifications/initialized',
509
565
  params: {}
510
566
  }, targetOrigin);
567
+ self.settleHandshake();
511
568
  resolve();
512
569
  },
513
- reject: function(err) { resolve(); }, // Non-fatal
570
+ reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
514
571
  timeout: timeout
515
572
  };
516
573
 
@@ -524,6 +581,18 @@ var ExtAppsAdapter = {
524
581
  performHandshake: function(context) {
525
582
  return this.sendHandshake(context);
526
583
  },
584
+ settleHandshake: function() {
585
+ // Deliver the latest size requested while no host origin was trusted.
586
+ this.handshakeSettled = true;
587
+ var pending = this.pendingSize;
588
+ if (!pending) return;
589
+ this.pendingSize = null;
590
+ this.sendRequest('ui/setSize', pending.params).then(function(result) {
591
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
592
+ }, function(err) {
593
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
594
+ });
595
+ },
527
596
  callTool: function(context, name, args) {
528
597
  if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
529
598
  return Promise.reject(new Error('Server tool proxy not supported'));
@@ -547,11 +616,22 @@ var ExtAppsAdapter = {
547
616
  setSize: function(context, size) {
548
617
  // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
549
618
  // measured/desired widget dimensions to the host.
550
- return this.sendRequest('ui/setSize', {
619
+ var params = {
551
620
  height: size && size.height,
552
621
  width: size && size.width,
553
622
  aspectRatio: size && size.aspectRatio
554
- });
623
+ };
624
+ if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
625
+ // No host origin yet: keep only the latest size and send it when the
626
+ // handshake settles; every caller's promise follows that one request.
627
+ var self = this;
628
+ if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
629
+ this.pendingSize.params = params;
630
+ return new Promise(function(resolve, reject) {
631
+ self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
632
+ });
633
+ }
634
+ return this.sendRequest('ui/setSize', params);
555
635
  },
556
636
  requestClose: function(context) {
557
637
  return this.sendRequest('ui/close', {});
@@ -797,6 +877,7 @@ function FrontMcpBridge() {
797
877
  toolResultListeners: [],
798
878
  notifyContextChange: function(changes) {
799
879
  Object.assign(this.hostContext, changes);
880
+ if (changes && changes.theme) __applyHostTheme(changes.theme);
800
881
  for (var i = 0; i < this.contextListeners.length; i++) {
801
882
  try { this.contextListeners[i](changes); } catch(e) {}
802
883
  }