@frontmcp/uipack 1.8.5 → 1.8.7
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 +339 -35
- package/adapters/markdown.d.ts +9 -0
- package/adapters/markdown.d.ts.map +1 -0
- package/adapters/template-renderer.d.ts +8 -0
- package/adapters/template-renderer.d.ts.map +1 -1
- package/bridge-runtime/iife-generator.d.ts.map +1 -1
- package/bridge-runtime/index.js +169 -22
- package/component/index.js +223 -26
- package/component/loader.d.ts.map +1 -1
- package/component/transpiler.d.ts.map +1 -1
- package/esm/adapters/index.mjs +339 -35
- package/esm/bridge-runtime/index.mjs +169 -22
- package/esm/component/index.mjs +223 -26
- package/esm/index.mjs +339 -35
- package/esm/package.json +8 -2
- package/esm/shell/index.mjs +169 -22
- package/index.js +339 -35
- package/package.json +8 -2
- package/shell/index.js +169 -22
- package/types/ui-config.d.ts +5 -3
- package/types/ui-config.d.ts.map +1 -1
package/index.js
CHANGED
|
@@ -358,6 +358,23 @@ function __applySizingCss(sizing) {
|
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
function __measureHeight() {
|
|
362
|
+
// <html> at fit-content includes body margins and collapsed child margins;
|
|
363
|
+
// content overflowing a fixed-height body is added; a px max-height clamps.
|
|
364
|
+
var de = document.documentElement;
|
|
365
|
+
var body = document.body;
|
|
366
|
+
if (!de || !body) return 0;
|
|
367
|
+
var prev = de.style.height;
|
|
368
|
+
de.style.height = 'fit-content';
|
|
369
|
+
var dr = de.getBoundingClientRect();
|
|
370
|
+
var br = body.getBoundingClientRect();
|
|
371
|
+
var h = Math.max(dr.height, br.top + body.scrollHeight + Math.max(0, dr.bottom - br.bottom) - dr.top);
|
|
372
|
+
de.style.height = prev;
|
|
373
|
+
var maxHeight = getComputedStyle(de).maxHeight;
|
|
374
|
+
var mx = maxHeight && maxHeight.slice(-2) === 'px' ? parseFloat(maxHeight) : NaN;
|
|
375
|
+
return Math.ceil(isNaN(mx) ? h : Math.min(h, mx));
|
|
376
|
+
}
|
|
377
|
+
|
|
361
378
|
function __initAutoResize() {
|
|
362
379
|
if (typeof window === 'undefined') return;
|
|
363
380
|
// Idempotent: a re-injected IIFE must not stack observers.
|
|
@@ -380,23 +397,30 @@ function __initAutoResize() {
|
|
|
380
397
|
if (typeof ResizeObserver === 'undefined') return;
|
|
381
398
|
|
|
382
399
|
function startObserving() {
|
|
383
|
-
|
|
384
|
-
|
|
400
|
+
if (!document.body) return;
|
|
401
|
+
var targets = [document.documentElement, document.body];
|
|
402
|
+
var root = document.getElementById('root');
|
|
403
|
+
if (root) targets.push(root);
|
|
385
404
|
|
|
386
405
|
var rafId = null;
|
|
387
406
|
var lastReported = -1;
|
|
388
407
|
function report() {
|
|
389
408
|
rafId = null;
|
|
390
409
|
try {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
410
|
+
// Before the handshake an ext-apps host can't receive the report; the
|
|
411
|
+
// bridge:ready listener below sends it once the bridge is up.
|
|
412
|
+
if (window.FrontMcpBridge && window.FrontMcpBridge.initialized === false) return;
|
|
413
|
+
var height = __measureHeight();
|
|
414
|
+
var width = Math.ceil(document.documentElement.getBoundingClientRect().width);
|
|
394
415
|
if (height === lastReported || height <= 0) return;
|
|
395
416
|
lastReported = height;
|
|
396
417
|
var payload = { height: height, width: width };
|
|
397
418
|
if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
|
|
398
419
|
if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
|
|
399
|
-
window.FrontMcpBridge.setSize(payload).catch(function() {
|
|
420
|
+
window.FrontMcpBridge.setSize(payload).catch(function() {
|
|
421
|
+
// Forget a rejected report so the same height is sent again.
|
|
422
|
+
if (lastReported === height) lastReported = -1;
|
|
423
|
+
});
|
|
400
424
|
}
|
|
401
425
|
window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
|
|
402
426
|
} catch (e) {}
|
|
@@ -417,9 +441,17 @@ function __initAutoResize() {
|
|
|
417
441
|
window.__mcpResizeObserver.disconnect();
|
|
418
442
|
}
|
|
419
443
|
var ro = new ResizeObserver(function() { schedule(); });
|
|
420
|
-
ro.observe(
|
|
444
|
+
for (var i = 0; i < targets.length; i++) ro.observe(targets[i]);
|
|
421
445
|
window.__mcpResizeObserver = ro;
|
|
422
446
|
} catch (e) {}
|
|
447
|
+
// Report once the bridge is ready (the report scheduled below is skipped
|
|
448
|
+
// while the handshake is still pending).
|
|
449
|
+
var readySent = false;
|
|
450
|
+
window.addEventListener('bridge:ready', function() {
|
|
451
|
+
if (readySent) return;
|
|
452
|
+
readySent = true;
|
|
453
|
+
schedule();
|
|
454
|
+
});
|
|
423
455
|
// Report once on init so the host gets an initial measurement.
|
|
424
456
|
schedule();
|
|
425
457
|
}
|
|
@@ -467,12 +499,57 @@ function detectViewport() {
|
|
|
467
499
|
return undefined;
|
|
468
500
|
}
|
|
469
501
|
|
|
502
|
+
function __applyHostTheme(theme) {
|
|
503
|
+
// Follow a host-supplied theme: <meta name="color-scheme"> sets the frame's
|
|
504
|
+
// canvas and form controls (a widget's own :root color-scheme still wins),
|
|
505
|
+
// <html data-theme> is a styling hook. The OS fallback is never written.
|
|
506
|
+
if (theme !== 'light' && theme !== 'dark') return;
|
|
507
|
+
if (typeof document === 'undefined' || !document.documentElement) return;
|
|
508
|
+
try {
|
|
509
|
+
var meta = document.querySelector('meta[name="color-scheme"]');
|
|
510
|
+
var head = document.head || document.getElementsByTagName('head')[0];
|
|
511
|
+
if (!meta && head) {
|
|
512
|
+
meta = document.createElement('meta');
|
|
513
|
+
meta.setAttribute('name', 'color-scheme');
|
|
514
|
+
head.appendChild(meta);
|
|
515
|
+
}
|
|
516
|
+
if (meta) meta.setAttribute('content', theme);
|
|
517
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
518
|
+
} catch (e) {}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function extractToolOutput(params) {
|
|
522
|
+
// A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
|
|
523
|
+
if (!params) return undefined;
|
|
524
|
+
if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
|
|
525
|
+
var content = params.content;
|
|
526
|
+
if (Array.isArray(content)) {
|
|
527
|
+
for (var i = 0; i < content.length; i++) {
|
|
528
|
+
var item = content[i];
|
|
529
|
+
if (item && item.type === 'text' && typeof item.text === 'string') {
|
|
530
|
+
try { return JSON.parse(item.text); } catch (e) { return item.text; }
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return content;
|
|
535
|
+
}
|
|
536
|
+
|
|
470
537
|
function readInjectedData() {
|
|
471
538
|
var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
|
|
472
539
|
if (typeof window !== 'undefined') {
|
|
473
540
|
if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
|
|
474
541
|
if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
|
|
475
542
|
if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
|
|
543
|
+
// The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
|
|
544
|
+
// (toolOutput is the tool's structuredContent).
|
|
545
|
+
var oa = window.openai;
|
|
546
|
+
if (oa && typeof oa === 'object') {
|
|
547
|
+
if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
|
|
548
|
+
if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
|
|
549
|
+
if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
|
|
550
|
+
if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
476
553
|
}
|
|
477
554
|
return data;
|
|
478
555
|
}
|
|
@@ -521,7 +598,24 @@ var OpenAIAdapter = {
|
|
|
521
598
|
if (sdk.displayMode) {
|
|
522
599
|
context.hostContext.displayMode = sdk.displayMode;
|
|
523
600
|
}
|
|
524
|
-
//
|
|
601
|
+
// The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
|
|
602
|
+
window.addEventListener('openai:set_globals', function(event) {
|
|
603
|
+
var globals = (event && event.detail && event.detail.globals) || window.openai || {};
|
|
604
|
+
if (globals.toolInput !== undefined && globals.toolInput !== null) {
|
|
605
|
+
context.toolInput = globals.toolInput;
|
|
606
|
+
window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
|
|
607
|
+
}
|
|
608
|
+
if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
|
|
609
|
+
context.structuredContent = globals.toolOutput;
|
|
610
|
+
context.notifyToolResult(globals.toolOutput);
|
|
611
|
+
}
|
|
612
|
+
if (globals.theme && globals.theme !== context.hostContext.theme) {
|
|
613
|
+
context.notifyContextChange({ theme: globals.theme });
|
|
614
|
+
}
|
|
615
|
+
if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
|
|
616
|
+
context.notifyContextChange({ displayMode: globals.displayMode });
|
|
617
|
+
}
|
|
618
|
+
});
|
|
525
619
|
return Promise.resolve();
|
|
526
620
|
},
|
|
527
621
|
callTool: function(context, name, args) {
|
|
@@ -573,6 +667,8 @@ var ExtAppsAdapter = {
|
|
|
573
667
|
pendingRequests: {},
|
|
574
668
|
requestId: 0,
|
|
575
669
|
hostCapabilities: {},
|
|
670
|
+
handshakeSettled: false,
|
|
671
|
+
pendingSize: null,
|
|
576
672
|
canHandle: function() {
|
|
577
673
|
if (typeof window === 'undefined') return false;
|
|
578
674
|
if (window.parent === window) return false;
|
|
@@ -666,9 +762,10 @@ var ExtAppsAdapter = {
|
|
|
666
762
|
window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
|
|
667
763
|
break;
|
|
668
764
|
case 'ui/notifications/tool-result':
|
|
669
|
-
|
|
765
|
+
var resultOutput = extractToolOutput(params);
|
|
766
|
+
context.toolOutput = resultOutput;
|
|
670
767
|
context.structuredContent = params.structuredContent;
|
|
671
|
-
context.notifyToolResult(
|
|
768
|
+
context.notifyToolResult(resultOutput);
|
|
672
769
|
window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
|
|
673
770
|
break;
|
|
674
771
|
case 'ui/notifications/host-context-changed':
|
|
@@ -722,6 +819,15 @@ var ExtAppsAdapter = {
|
|
|
722
819
|
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
|
|
723
820
|
});
|
|
724
821
|
},
|
|
822
|
+
sendNotification: function(method, params) {
|
|
823
|
+
// JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
|
|
824
|
+
if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
825
|
+
return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
|
|
826
|
+
}
|
|
827
|
+
var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
|
|
828
|
+
window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
|
|
829
|
+
return Promise.resolve();
|
|
830
|
+
},
|
|
725
831
|
sendHandshake: function(context) {
|
|
726
832
|
// Send ui/initialize using '*' as target origin since TOFU hasn't
|
|
727
833
|
// been established yet. The response from the host will establish
|
|
@@ -738,6 +844,7 @@ var ExtAppsAdapter = {
|
|
|
738
844
|
var timeout = setTimeout(function() {
|
|
739
845
|
delete self.pendingRequests[id];
|
|
740
846
|
// Handshake timeout is non-fatal \u2014 notifications may still arrive
|
|
847
|
+
self.settleHandshake();
|
|
741
848
|
resolve();
|
|
742
849
|
}, 10000);
|
|
743
850
|
|
|
@@ -751,7 +858,9 @@ var ExtAppsAdapter = {
|
|
|
751
858
|
supportsDisplayModes: true
|
|
752
859
|
});
|
|
753
860
|
if (result.hostContext) {
|
|
754
|
-
|
|
861
|
+
// Notify (not a silent merge) so onContextChange listeners and the
|
|
862
|
+
// host theme see the handshake's context.
|
|
863
|
+
context.notifyContextChange(result.hostContext);
|
|
755
864
|
}
|
|
756
865
|
// Send ui/notifications/initialized to tell the host the view is ready.
|
|
757
866
|
// Per MCP Apps spec, the host waits for this before sending tool-result.
|
|
@@ -761,9 +870,10 @@ var ExtAppsAdapter = {
|
|
|
761
870
|
method: 'ui/notifications/initialized',
|
|
762
871
|
params: {}
|
|
763
872
|
}, targetOrigin);
|
|
873
|
+
self.settleHandshake();
|
|
764
874
|
resolve();
|
|
765
875
|
},
|
|
766
|
-
reject: function(err) { resolve(); }, // Non-fatal
|
|
876
|
+
reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
|
|
767
877
|
timeout: timeout
|
|
768
878
|
};
|
|
769
879
|
|
|
@@ -777,6 +887,18 @@ var ExtAppsAdapter = {
|
|
|
777
887
|
performHandshake: function(context) {
|
|
778
888
|
return this.sendHandshake(context);
|
|
779
889
|
},
|
|
890
|
+
settleHandshake: function() {
|
|
891
|
+
// Deliver the latest size requested while no host origin was trusted.
|
|
892
|
+
this.handshakeSettled = true;
|
|
893
|
+
var pending = this.pendingSize;
|
|
894
|
+
if (!pending) return;
|
|
895
|
+
this.pendingSize = null;
|
|
896
|
+
this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
|
|
897
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
|
|
898
|
+
}, function(err) {
|
|
899
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
|
|
900
|
+
});
|
|
901
|
+
},
|
|
780
902
|
callTool: function(context, name, args) {
|
|
781
903
|
if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
|
|
782
904
|
return Promise.reject(new Error('Server tool proxy not supported'));
|
|
@@ -798,13 +920,22 @@ var ExtAppsAdapter = {
|
|
|
798
920
|
return this.sendRequest('ui/setDisplayMode', { mode: mode });
|
|
799
921
|
},
|
|
800
922
|
setSize: function(context, size) {
|
|
801
|
-
//
|
|
802
|
-
//
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
923
|
+
// Standard ext-apps sizing: the view tells the host its dimensions with the
|
|
924
|
+
// ui/notifications/size-changed notification ({ width?, height? } in px).
|
|
925
|
+
var params = {};
|
|
926
|
+
if (size && typeof size.width === 'number') params.width = size.width;
|
|
927
|
+
if (size && typeof size.height === 'number') params.height = size.height;
|
|
928
|
+
if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
929
|
+
// No host origin yet: keep only the latest size and send it when the
|
|
930
|
+
// handshake settles; every caller's promise follows that one request.
|
|
931
|
+
var self = this;
|
|
932
|
+
if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
|
|
933
|
+
this.pendingSize.params = params;
|
|
934
|
+
return new Promise(function(resolve, reject) {
|
|
935
|
+
self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
return this.sendNotification('ui/notifications/size-changed', params);
|
|
808
939
|
},
|
|
809
940
|
requestClose: function(context) {
|
|
810
941
|
return this.sendRequest('ui/close', {});
|
|
@@ -1050,6 +1181,7 @@ function FrontMcpBridge() {
|
|
|
1050
1181
|
toolResultListeners: [],
|
|
1051
1182
|
notifyContextChange: function(changes) {
|
|
1052
1183
|
Object.assign(this.hostContext, changes);
|
|
1184
|
+
if (changes && changes.theme) __applyHostTheme(changes.theme);
|
|
1053
1185
|
for (var i = 0; i < this.contextListeners.length; i++) {
|
|
1054
1186
|
try { this.contextListeners[i](changes); } catch(e) {}
|
|
1055
1187
|
}
|
|
@@ -1226,7 +1358,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
|
|
|
1226
1358
|
|
|
1227
1359
|
// Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
|
|
1228
1360
|
// Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
|
|
1229
|
-
// ext-apps sends ui/
|
|
1361
|
+
// ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
|
|
1230
1362
|
FrontMcpBridge.prototype.setSize = function(size) {
|
|
1231
1363
|
if (!this._adapter) return Promise.reject(new Error('Not initialized'));
|
|
1232
1364
|
if (!this._adapter.setSize) return Promise.resolve();
|
|
@@ -1328,7 +1460,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
|
|
|
1328
1460
|
target.classList.add('opacity-50', 'cursor-not-allowed');
|
|
1329
1461
|
|
|
1330
1462
|
// Add spinner for buttons
|
|
1331
|
-
var spinner = '<svg class="animate-spin -ml-1 mr-2 h-4 w-4 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
|
|
1463
|
+
var spinner = '<svg class="animate-spin -ml-1 mr-2 h-4 w-4 inline" style="width:1em;height:1em;vertical-align:-0.125em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
|
|
1332
1464
|
if (target.tagName === 'BUTTON') {
|
|
1333
1465
|
target.innerHTML = spinner + 'Loading...';
|
|
1334
1466
|
}
|
|
@@ -1409,7 +1541,22 @@ function minifyJS(code) {
|
|
|
1409
1541
|
if (code.length > MAX_MINIFY_CODE_LENGTH) {
|
|
1410
1542
|
return code;
|
|
1411
1543
|
}
|
|
1412
|
-
|
|
1544
|
+
const out = [];
|
|
1545
|
+
let inBlockComment = false;
|
|
1546
|
+
for (const raw of code.split("\n")) {
|
|
1547
|
+
const line = raw.trim();
|
|
1548
|
+
if (inBlockComment) {
|
|
1549
|
+
if (line.includes("*/")) inBlockComment = false;
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
1553
|
+
if (line.startsWith("/*")) {
|
|
1554
|
+
if (!line.includes("*/")) inBlockComment = true;
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1557
|
+
out.push(line);
|
|
1558
|
+
}
|
|
1559
|
+
return out.join("\n");
|
|
1413
1560
|
}
|
|
1414
1561
|
function generatePlatformBundle(platform, options = {}) {
|
|
1415
1562
|
const platformAdapters = {
|
|
@@ -3449,8 +3596,24 @@ function isFrontmcpUiResolvable(...candidatePaths) {
|
|
|
3449
3596
|
}
|
|
3450
3597
|
|
|
3451
3598
|
// libs/uipack/src/component/transpiler.ts
|
|
3599
|
+
function loadEsbuild(filename) {
|
|
3600
|
+
try {
|
|
3601
|
+
return require("esbuild");
|
|
3602
|
+
} catch (err) {
|
|
3603
|
+
if (!isMissingEsbuild(err)) throw err;
|
|
3604
|
+
const subject = filename ? `FileSource widget "${filename}"` : "A FileSource widget";
|
|
3605
|
+
throw new Error(
|
|
3606
|
+
`${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.`,
|
|
3607
|
+
{ cause: err }
|
|
3608
|
+
);
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
function isMissingEsbuild(err) {
|
|
3612
|
+
if (!(err instanceof Error)) return false;
|
|
3613
|
+
return err.code === "MODULE_NOT_FOUND" && /Cannot find module ['"]esbuild['"]/.test(err.message);
|
|
3614
|
+
}
|
|
3452
3615
|
function transpileReactSource(source, filename) {
|
|
3453
|
-
const esbuild =
|
|
3616
|
+
const esbuild = loadEsbuild(filename);
|
|
3454
3617
|
const loader = filename?.endsWith(".tsx") ? "tsx" : "jsx";
|
|
3455
3618
|
const result = esbuild.transformSync(source, {
|
|
3456
3619
|
loader,
|
|
@@ -3469,7 +3632,7 @@ function bundleFileSource(source, filename, resolveDir, componentName, options =
|
|
|
3469
3632
|
`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.`
|
|
3470
3633
|
);
|
|
3471
3634
|
}
|
|
3472
|
-
const esbuild =
|
|
3635
|
+
const esbuild = loadEsbuild(filename);
|
|
3473
3636
|
const mountCode = `
|
|
3474
3637
|
// --- Auto-generated mount ---
|
|
3475
3638
|
import { createElement as __h } from 'react';
|
|
@@ -3566,7 +3729,8 @@ if (__root) {
|
|
|
3566
3729
|
} catch (err) {
|
|
3567
3730
|
const message = err instanceof Error ? err.message : String(err);
|
|
3568
3731
|
throw new Error(
|
|
3569
|
-
`Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project
|
|
3732
|
+
`Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`,
|
|
3733
|
+
{ cause: err }
|
|
3570
3734
|
);
|
|
3571
3735
|
}
|
|
3572
3736
|
}
|
|
@@ -3628,6 +3792,37 @@ function resolveImportSource(source) {
|
|
|
3628
3792
|
peerDependencies: []
|
|
3629
3793
|
};
|
|
3630
3794
|
}
|
|
3795
|
+
var BUILD_DIR_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "out"]);
|
|
3796
|
+
function missingWidgetError(filePath, cause) {
|
|
3797
|
+
const sourceWidget = findSourceWidget(filePath);
|
|
3798
|
+
const error = new Error(
|
|
3799
|
+
`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\`.` : ""),
|
|
3800
|
+
{ cause }
|
|
3801
|
+
);
|
|
3802
|
+
error.code = "ENOENT";
|
|
3803
|
+
error.path = filePath;
|
|
3804
|
+
return error;
|
|
3805
|
+
}
|
|
3806
|
+
function findSourceWidget(filePath) {
|
|
3807
|
+
const path = require("path");
|
|
3808
|
+
const fs = require("fs");
|
|
3809
|
+
const parts = filePath.split(path.sep);
|
|
3810
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
3811
|
+
if (!BUILD_DIR_SEGMENTS.has(parts[i])) continue;
|
|
3812
|
+
const candidates = [[...parts.slice(0, i), "src", ...parts.slice(i + 1)]];
|
|
3813
|
+
if (parts.length - i > 2) candidates.push([...parts.slice(0, i), "src", ...parts.slice(i + 2)]);
|
|
3814
|
+
candidates.push([...parts.slice(0, i), ...parts.slice(i + 1)]);
|
|
3815
|
+
for (const candidate of candidates) {
|
|
3816
|
+
const candidatePath = candidate.join(path.sep);
|
|
3817
|
+
try {
|
|
3818
|
+
if (fs.existsSync(candidatePath)) return candidatePath;
|
|
3819
|
+
} catch {
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
return void 0;
|
|
3823
|
+
}
|
|
3824
|
+
return void 0;
|
|
3825
|
+
}
|
|
3631
3826
|
function resolveFileSource(source, options = {}) {
|
|
3632
3827
|
const path = require("path");
|
|
3633
3828
|
const ext = path.extname(source.file).toLowerCase();
|
|
@@ -3642,9 +3837,11 @@ function resolveFileSource(source, options = {}) {
|
|
|
3642
3837
|
const isNotFound = err?.code === "ENOENT";
|
|
3643
3838
|
if (isNotFound && wasRelative) {
|
|
3644
3839
|
throw new Error(
|
|
3645
|
-
`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)
|
|
3840
|
+
`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).`,
|
|
3841
|
+
{ cause: err }
|
|
3646
3842
|
);
|
|
3647
3843
|
}
|
|
3844
|
+
if (isNotFound) throw missingWidgetError(filePath, err);
|
|
3648
3845
|
throw err;
|
|
3649
3846
|
}
|
|
3650
3847
|
const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
|
|
@@ -4091,6 +4288,86 @@ function wrapDetectedContent(value) {
|
|
|
4091
4288
|
}
|
|
4092
4289
|
}
|
|
4093
4290
|
|
|
4291
|
+
// libs/uipack/src/adapters/markdown.ts
|
|
4292
|
+
var SAFE_HREF = /^(https?:|mailto:|\/|#)/i;
|
|
4293
|
+
function emphasis(text) {
|
|
4294
|
+
return text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
4295
|
+
}
|
|
4296
|
+
function inline(text) {
|
|
4297
|
+
const stash = [];
|
|
4298
|
+
const hold = (html2) => `\uE000${stash.push(html2) - 1}\uE000`;
|
|
4299
|
+
let out = escapeHtml(text.replace(/\uE000/g, "")).replace(
|
|
4300
|
+
/`([^`]+)`/g,
|
|
4301
|
+
(_m, code) => hold(`<code>${code}</code>`)
|
|
4302
|
+
);
|
|
4303
|
+
out = out.replace(
|
|
4304
|
+
/\[([^\]]+)\]\(([^)\s]+)\)/g,
|
|
4305
|
+
(_m, label, href) => SAFE_HREF.test(href) ? hold(`<a href="${href}" rel="noopener noreferrer">${emphasis(label)}</a>`) : label
|
|
4306
|
+
);
|
|
4307
|
+
out = emphasis(out);
|
|
4308
|
+
for (let i = 0; i < 2; i++) out = out.replace(/\uE000(\d+)\uE000/g, (_m, n) => stash[Number(n)]);
|
|
4309
|
+
return out;
|
|
4310
|
+
}
|
|
4311
|
+
function markdownToHtml(source) {
|
|
4312
|
+
const lines = source.replace(/\r\n?/g, "\n").split("\n");
|
|
4313
|
+
const blocks = [];
|
|
4314
|
+
let paragraph = [];
|
|
4315
|
+
let list;
|
|
4316
|
+
const flushParagraph = () => {
|
|
4317
|
+
if (paragraph.length > 0) blocks.push(`<p>${inline(paragraph.join(" "))}</p>`);
|
|
4318
|
+
paragraph = [];
|
|
4319
|
+
};
|
|
4320
|
+
const flushList = () => {
|
|
4321
|
+
if (list)
|
|
4322
|
+
blocks.push(`<${list.tag}>${list.items.map((item) => `<li>${inline(item)}</li>`).join("")}</${list.tag}>`);
|
|
4323
|
+
list = void 0;
|
|
4324
|
+
};
|
|
4325
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4326
|
+
const line = lines[i];
|
|
4327
|
+
const fence = /^\s*(`{3,})/.exec(line);
|
|
4328
|
+
if (fence) {
|
|
4329
|
+
flushParagraph();
|
|
4330
|
+
flushList();
|
|
4331
|
+
const fenceLength = fence[1].length;
|
|
4332
|
+
const isClosing = (l) => {
|
|
4333
|
+
const m = /^\s*(`{3,})\s*$/.exec(l);
|
|
4334
|
+
return m !== null && m[1].length >= fenceLength;
|
|
4335
|
+
};
|
|
4336
|
+
const code = [];
|
|
4337
|
+
for (i++; i < lines.length && !isClosing(lines[i]); i++) code.push(lines[i]);
|
|
4338
|
+
blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}
|
|
4339
|
+
</code></pre>`);
|
|
4340
|
+
continue;
|
|
4341
|
+
}
|
|
4342
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
4343
|
+
if (heading) {
|
|
4344
|
+
flushParagraph();
|
|
4345
|
+
flushList();
|
|
4346
|
+
blocks.push(`<h${heading[1].length}>${inline(heading[2].trim())}</h${heading[1].length}>`);
|
|
4347
|
+
continue;
|
|
4348
|
+
}
|
|
4349
|
+
const item = /^\s*([-*+]|\d+\.)\s+(.*)$/.exec(line);
|
|
4350
|
+
if (item) {
|
|
4351
|
+
flushParagraph();
|
|
4352
|
+
const tag = /\d/.test(item[1]) ? "ol" : "ul";
|
|
4353
|
+
if (list && list.tag !== tag) flushList();
|
|
4354
|
+
list ??= { tag, items: [] };
|
|
4355
|
+
list.items.push(item[2]);
|
|
4356
|
+
continue;
|
|
4357
|
+
}
|
|
4358
|
+
if (line.trim() === "") {
|
|
4359
|
+
flushParagraph();
|
|
4360
|
+
flushList();
|
|
4361
|
+
continue;
|
|
4362
|
+
}
|
|
4363
|
+
flushList();
|
|
4364
|
+
paragraph.push(line.trim());
|
|
4365
|
+
}
|
|
4366
|
+
flushParagraph();
|
|
4367
|
+
flushList();
|
|
4368
|
+
return blocks.join("\n");
|
|
4369
|
+
}
|
|
4370
|
+
|
|
4094
4371
|
// libs/uipack/src/adapters/type-detector.ts
|
|
4095
4372
|
function detectUIType(template) {
|
|
4096
4373
|
if (template === null || template === void 0) {
|
|
@@ -4124,9 +4401,15 @@ function detectUIType(template) {
|
|
|
4124
4401
|
}
|
|
4125
4402
|
|
|
4126
4403
|
// libs/uipack/src/adapters/template-renderer.ts
|
|
4127
|
-
function buildCspConfig(resolver) {
|
|
4404
|
+
function buildCspConfig(resolver, extra) {
|
|
4128
4405
|
const cspResourceDomains = ["https://esm.sh"];
|
|
4129
4406
|
const cspConnectDomains = ["https://esm.sh"];
|
|
4407
|
+
for (const domain of extra?.resourceDomains ?? []) {
|
|
4408
|
+
if (!cspResourceDomains.includes(domain)) cspResourceDomains.push(domain);
|
|
4409
|
+
}
|
|
4410
|
+
for (const domain of extra?.connectDomains ?? []) {
|
|
4411
|
+
if (!cspConnectDomains.includes(domain)) cspConnectDomains.push(domain);
|
|
4412
|
+
}
|
|
4130
4413
|
if (resolver && "overrides" in resolver) {
|
|
4131
4414
|
const overrides = resolver.overrides;
|
|
4132
4415
|
if (overrides) {
|
|
@@ -4166,6 +4449,14 @@ function renderUnwrappedResult(rawResult, options) {
|
|
|
4166
4449
|
}
|
|
4167
4450
|
return rawResult;
|
|
4168
4451
|
}
|
|
4452
|
+
function isReactElement(value) {
|
|
4453
|
+
return typeof value === "object" && value !== null && "$$typeof" in value;
|
|
4454
|
+
}
|
|
4455
|
+
function isDefinitelyReactComponent(template) {
|
|
4456
|
+
if (typeof template !== "function") return false;
|
|
4457
|
+
const proto = template.prototype;
|
|
4458
|
+
return typeof proto?.render === "function" || template["$$typeof"] !== void 0;
|
|
4459
|
+
}
|
|
4169
4460
|
function renderToolTemplate(options) {
|
|
4170
4461
|
const { toolName, input, output, template, resolver, platformType, sizing } = options;
|
|
4171
4462
|
const uiType = detectUIType(template);
|
|
@@ -4176,30 +4467,43 @@ function renderToolTemplate(options) {
|
|
|
4176
4467
|
output,
|
|
4177
4468
|
includeBridge: true,
|
|
4178
4469
|
resolver,
|
|
4179
|
-
sizing
|
|
4470
|
+
sizing,
|
|
4471
|
+
csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
|
|
4180
4472
|
};
|
|
4181
4473
|
let html2;
|
|
4182
4474
|
let hash = "";
|
|
4183
4475
|
let size = 0;
|
|
4184
4476
|
if (typeof template === "object" && template !== null && "file" in template) {
|
|
4185
|
-
const cspConfig = buildCspConfig(resolver);
|
|
4186
4477
|
const result = renderComponent(
|
|
4187
4478
|
{ source: template, inlineReact: resourceMode === "inline" },
|
|
4188
|
-
{ ...shellConfig, csp:
|
|
4479
|
+
{ ...shellConfig, csp: buildCspConfig(resolver, options.csp) }
|
|
4189
4480
|
);
|
|
4190
4481
|
html2 = result.html;
|
|
4191
4482
|
hash = result.hash;
|
|
4192
4483
|
size = result.size;
|
|
4193
4484
|
} else if (typeof template === "function") {
|
|
4194
|
-
|
|
4485
|
+
const helpers = createTemplateHelpers();
|
|
4486
|
+
const ctx = { input, output, helpers };
|
|
4487
|
+
let rawResult;
|
|
4488
|
+
let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
|
|
4489
|
+
if (isHtmlBuilder) {
|
|
4490
|
+
try {
|
|
4491
|
+
rawResult = template(ctx);
|
|
4492
|
+
isHtmlBuilder = !isReactElement(rawResult);
|
|
4493
|
+
} catch (error) {
|
|
4494
|
+
if (uiType !== "react") throw error;
|
|
4495
|
+
isHtmlBuilder = false;
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
if (!isHtmlBuilder) {
|
|
4499
|
+
(options.logger ?? console).warn(
|
|
4500
|
+
`[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.`
|
|
4501
|
+
);
|
|
4195
4502
|
const shellResult = buildShell('<div id="root"></div>', shellConfig);
|
|
4196
4503
|
html2 = shellResult.html;
|
|
4197
4504
|
hash = shellResult.hash;
|
|
4198
4505
|
size = shellResult.size;
|
|
4199
4506
|
} else {
|
|
4200
|
-
const helpers = createTemplateHelpers();
|
|
4201
|
-
const ctx = { input, output, helpers };
|
|
4202
|
-
const rawResult = template(ctx);
|
|
4203
4507
|
const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
|
|
4204
4508
|
if (wrapped) {
|
|
4205
4509
|
html2 = wrapped;
|
|
@@ -4211,7 +4515,7 @@ function renderToolTemplate(options) {
|
|
|
4211
4515
|
}
|
|
4212
4516
|
}
|
|
4213
4517
|
} else if (typeof template === "string") {
|
|
4214
|
-
const shellResult = buildShell(template, shellConfig);
|
|
4518
|
+
const shellResult = buildShell(uiType === "markdown" ? markdownToHtml(template) : template, shellConfig);
|
|
4215
4519
|
html2 = shellResult.html;
|
|
4216
4520
|
hash = shellResult.hash;
|
|
4217
4521
|
size = shellResult.size;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/uipack",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.7",
|
|
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.7",
|
|
65
|
+
"esbuild": ">=0.27.0 <1"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"esbuild": {
|
|
69
|
+
"optional": true
|
|
70
|
+
}
|
|
65
71
|
}
|
|
66
72
|
}
|