@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/adapters/index.js +146 -15
- package/bridge-runtime/iife-generator.d.ts.map +1 -1
- package/bridge-runtime/index.js +92 -11
- package/component/index.js +146 -15
- package/component/loader.d.ts.map +1 -1
- package/component/transpiler.d.ts.map +1 -1
- package/esm/adapters/index.mjs +146 -15
- package/esm/bridge-runtime/index.mjs +92 -11
- package/esm/component/index.mjs +146 -15
- package/esm/index.mjs +146 -15
- package/esm/package.json +8 -2
- package/esm/shell/index.mjs +92 -11
- package/index.js +146 -15
- package/package.json +8 -2
- package/shell/index.js +92 -11
- package/types/ui-config.d.ts +4 -2
- package/types/ui-config.d.ts.map +1 -1
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
|
-
|
|
264
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
}
|
package/esm/shell/index.mjs
CHANGED
|
@@ -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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|