@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/esm/adapters/index.mjs
CHANGED
|
@@ -637,6 +637,23 @@ function __applySizingCss(sizing) {
|
|
|
637
637
|
}
|
|
638
638
|
}
|
|
639
639
|
|
|
640
|
+
function __measureHeight() {
|
|
641
|
+
// <html> at fit-content includes body margins and collapsed child margins;
|
|
642
|
+
// content overflowing a fixed-height body is added; a px max-height clamps.
|
|
643
|
+
var de = document.documentElement;
|
|
644
|
+
var body = document.body;
|
|
645
|
+
if (!de || !body) return 0;
|
|
646
|
+
var prev = de.style.height;
|
|
647
|
+
de.style.height = 'fit-content';
|
|
648
|
+
var dr = de.getBoundingClientRect();
|
|
649
|
+
var br = body.getBoundingClientRect();
|
|
650
|
+
var h = Math.max(dr.height, br.top + body.scrollHeight + Math.max(0, dr.bottom - br.bottom) - dr.top);
|
|
651
|
+
de.style.height = prev;
|
|
652
|
+
var maxHeight = getComputedStyle(de).maxHeight;
|
|
653
|
+
var mx = maxHeight && maxHeight.slice(-2) === 'px' ? parseFloat(maxHeight) : NaN;
|
|
654
|
+
return Math.ceil(isNaN(mx) ? h : Math.min(h, mx));
|
|
655
|
+
}
|
|
656
|
+
|
|
640
657
|
function __initAutoResize() {
|
|
641
658
|
if (typeof window === 'undefined') return;
|
|
642
659
|
// Idempotent: a re-injected IIFE must not stack observers.
|
|
@@ -659,23 +676,30 @@ function __initAutoResize() {
|
|
|
659
676
|
if (typeof ResizeObserver === 'undefined') return;
|
|
660
677
|
|
|
661
678
|
function startObserving() {
|
|
662
|
-
|
|
663
|
-
|
|
679
|
+
if (!document.body) return;
|
|
680
|
+
var targets = [document.documentElement, document.body];
|
|
681
|
+
var root = document.getElementById('root');
|
|
682
|
+
if (root) targets.push(root);
|
|
664
683
|
|
|
665
684
|
var rafId = null;
|
|
666
685
|
var lastReported = -1;
|
|
667
686
|
function report() {
|
|
668
687
|
rafId = null;
|
|
669
688
|
try {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
689
|
+
// Before the handshake an ext-apps host can't receive the report; the
|
|
690
|
+
// bridge:ready listener below sends it once the bridge is up.
|
|
691
|
+
if (window.FrontMcpBridge && window.FrontMcpBridge.initialized === false) return;
|
|
692
|
+
var height = __measureHeight();
|
|
693
|
+
var width = Math.ceil(document.documentElement.getBoundingClientRect().width);
|
|
673
694
|
if (height === lastReported || height <= 0) return;
|
|
674
695
|
lastReported = height;
|
|
675
696
|
var payload = { height: height, width: width };
|
|
676
697
|
if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
|
|
677
698
|
if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
|
|
678
|
-
window.FrontMcpBridge.setSize(payload).catch(function() {
|
|
699
|
+
window.FrontMcpBridge.setSize(payload).catch(function() {
|
|
700
|
+
// Forget a rejected report so the same height is sent again.
|
|
701
|
+
if (lastReported === height) lastReported = -1;
|
|
702
|
+
});
|
|
679
703
|
}
|
|
680
704
|
window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
|
|
681
705
|
} catch (e) {}
|
|
@@ -696,9 +720,17 @@ function __initAutoResize() {
|
|
|
696
720
|
window.__mcpResizeObserver.disconnect();
|
|
697
721
|
}
|
|
698
722
|
var ro = new ResizeObserver(function() { schedule(); });
|
|
699
|
-
ro.observe(
|
|
723
|
+
for (var i = 0; i < targets.length; i++) ro.observe(targets[i]);
|
|
700
724
|
window.__mcpResizeObserver = ro;
|
|
701
725
|
} catch (e) {}
|
|
726
|
+
// Report once the bridge is ready (the report scheduled below is skipped
|
|
727
|
+
// while the handshake is still pending).
|
|
728
|
+
var readySent = false;
|
|
729
|
+
window.addEventListener('bridge:ready', function() {
|
|
730
|
+
if (readySent) return;
|
|
731
|
+
readySent = true;
|
|
732
|
+
schedule();
|
|
733
|
+
});
|
|
702
734
|
// Report once on init so the host gets an initial measurement.
|
|
703
735
|
schedule();
|
|
704
736
|
}
|
|
@@ -746,12 +778,57 @@ function detectViewport() {
|
|
|
746
778
|
return undefined;
|
|
747
779
|
}
|
|
748
780
|
|
|
781
|
+
function __applyHostTheme(theme) {
|
|
782
|
+
// Follow a host-supplied theme: <meta name="color-scheme"> sets the frame's
|
|
783
|
+
// canvas and form controls (a widget's own :root color-scheme still wins),
|
|
784
|
+
// <html data-theme> is a styling hook. The OS fallback is never written.
|
|
785
|
+
if (theme !== 'light' && theme !== 'dark') return;
|
|
786
|
+
if (typeof document === 'undefined' || !document.documentElement) return;
|
|
787
|
+
try {
|
|
788
|
+
var meta = document.querySelector('meta[name="color-scheme"]');
|
|
789
|
+
var head = document.head || document.getElementsByTagName('head')[0];
|
|
790
|
+
if (!meta && head) {
|
|
791
|
+
meta = document.createElement('meta');
|
|
792
|
+
meta.setAttribute('name', 'color-scheme');
|
|
793
|
+
head.appendChild(meta);
|
|
794
|
+
}
|
|
795
|
+
if (meta) meta.setAttribute('content', theme);
|
|
796
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
797
|
+
} catch (e) {}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function extractToolOutput(params) {
|
|
801
|
+
// A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
|
|
802
|
+
if (!params) return undefined;
|
|
803
|
+
if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
|
|
804
|
+
var content = params.content;
|
|
805
|
+
if (Array.isArray(content)) {
|
|
806
|
+
for (var i = 0; i < content.length; i++) {
|
|
807
|
+
var item = content[i];
|
|
808
|
+
if (item && item.type === 'text' && typeof item.text === 'string') {
|
|
809
|
+
try { return JSON.parse(item.text); } catch (e) { return item.text; }
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return content;
|
|
814
|
+
}
|
|
815
|
+
|
|
749
816
|
function readInjectedData() {
|
|
750
817
|
var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
|
|
751
818
|
if (typeof window !== 'undefined') {
|
|
752
819
|
if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
|
|
753
820
|
if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
|
|
754
821
|
if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
|
|
822
|
+
// The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
|
|
823
|
+
// (toolOutput is the tool's structuredContent).
|
|
824
|
+
var oa = window.openai;
|
|
825
|
+
if (oa && typeof oa === 'object') {
|
|
826
|
+
if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
|
|
827
|
+
if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
|
|
828
|
+
if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
|
|
829
|
+
if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
755
832
|
}
|
|
756
833
|
return data;
|
|
757
834
|
}
|
|
@@ -800,7 +877,24 @@ var OpenAIAdapter = {
|
|
|
800
877
|
if (sdk.displayMode) {
|
|
801
878
|
context.hostContext.displayMode = sdk.displayMode;
|
|
802
879
|
}
|
|
803
|
-
//
|
|
880
|
+
// The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
|
|
881
|
+
window.addEventListener('openai:set_globals', function(event) {
|
|
882
|
+
var globals = (event && event.detail && event.detail.globals) || window.openai || {};
|
|
883
|
+
if (globals.toolInput !== undefined && globals.toolInput !== null) {
|
|
884
|
+
context.toolInput = globals.toolInput;
|
|
885
|
+
window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
|
|
886
|
+
}
|
|
887
|
+
if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
|
|
888
|
+
context.structuredContent = globals.toolOutput;
|
|
889
|
+
context.notifyToolResult(globals.toolOutput);
|
|
890
|
+
}
|
|
891
|
+
if (globals.theme && globals.theme !== context.hostContext.theme) {
|
|
892
|
+
context.notifyContextChange({ theme: globals.theme });
|
|
893
|
+
}
|
|
894
|
+
if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
|
|
895
|
+
context.notifyContextChange({ displayMode: globals.displayMode });
|
|
896
|
+
}
|
|
897
|
+
});
|
|
804
898
|
return Promise.resolve();
|
|
805
899
|
},
|
|
806
900
|
callTool: function(context, name, args) {
|
|
@@ -852,6 +946,8 @@ var ExtAppsAdapter = {
|
|
|
852
946
|
pendingRequests: {},
|
|
853
947
|
requestId: 0,
|
|
854
948
|
hostCapabilities: {},
|
|
949
|
+
handshakeSettled: false,
|
|
950
|
+
pendingSize: null,
|
|
855
951
|
canHandle: function() {
|
|
856
952
|
if (typeof window === 'undefined') return false;
|
|
857
953
|
if (window.parent === window) return false;
|
|
@@ -945,9 +1041,10 @@ var ExtAppsAdapter = {
|
|
|
945
1041
|
window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
|
|
946
1042
|
break;
|
|
947
1043
|
case 'ui/notifications/tool-result':
|
|
948
|
-
|
|
1044
|
+
var resultOutput = extractToolOutput(params);
|
|
1045
|
+
context.toolOutput = resultOutput;
|
|
949
1046
|
context.structuredContent = params.structuredContent;
|
|
950
|
-
context.notifyToolResult(
|
|
1047
|
+
context.notifyToolResult(resultOutput);
|
|
951
1048
|
window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
|
|
952
1049
|
break;
|
|
953
1050
|
case 'ui/notifications/host-context-changed':
|
|
@@ -1001,6 +1098,15 @@ var ExtAppsAdapter = {
|
|
|
1001
1098
|
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
|
|
1002
1099
|
});
|
|
1003
1100
|
},
|
|
1101
|
+
sendNotification: function(method, params) {
|
|
1102
|
+
// JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
|
|
1103
|
+
if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
1104
|
+
return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
|
|
1105
|
+
}
|
|
1106
|
+
var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
|
|
1107
|
+
window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
|
|
1108
|
+
return Promise.resolve();
|
|
1109
|
+
},
|
|
1004
1110
|
sendHandshake: function(context) {
|
|
1005
1111
|
// Send ui/initialize using '*' as target origin since TOFU hasn't
|
|
1006
1112
|
// been established yet. The response from the host will establish
|
|
@@ -1017,6 +1123,7 @@ var ExtAppsAdapter = {
|
|
|
1017
1123
|
var timeout = setTimeout(function() {
|
|
1018
1124
|
delete self.pendingRequests[id];
|
|
1019
1125
|
// Handshake timeout is non-fatal \u2014 notifications may still arrive
|
|
1126
|
+
self.settleHandshake();
|
|
1020
1127
|
resolve();
|
|
1021
1128
|
}, 10000);
|
|
1022
1129
|
|
|
@@ -1030,7 +1137,9 @@ var ExtAppsAdapter = {
|
|
|
1030
1137
|
supportsDisplayModes: true
|
|
1031
1138
|
});
|
|
1032
1139
|
if (result.hostContext) {
|
|
1033
|
-
|
|
1140
|
+
// Notify (not a silent merge) so onContextChange listeners and the
|
|
1141
|
+
// host theme see the handshake's context.
|
|
1142
|
+
context.notifyContextChange(result.hostContext);
|
|
1034
1143
|
}
|
|
1035
1144
|
// Send ui/notifications/initialized to tell the host the view is ready.
|
|
1036
1145
|
// Per MCP Apps spec, the host waits for this before sending tool-result.
|
|
@@ -1040,9 +1149,10 @@ var ExtAppsAdapter = {
|
|
|
1040
1149
|
method: 'ui/notifications/initialized',
|
|
1041
1150
|
params: {}
|
|
1042
1151
|
}, targetOrigin);
|
|
1152
|
+
self.settleHandshake();
|
|
1043
1153
|
resolve();
|
|
1044
1154
|
},
|
|
1045
|
-
reject: function(err) { resolve(); }, // Non-fatal
|
|
1155
|
+
reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
|
|
1046
1156
|
timeout: timeout
|
|
1047
1157
|
};
|
|
1048
1158
|
|
|
@@ -1056,6 +1166,18 @@ var ExtAppsAdapter = {
|
|
|
1056
1166
|
performHandshake: function(context) {
|
|
1057
1167
|
return this.sendHandshake(context);
|
|
1058
1168
|
},
|
|
1169
|
+
settleHandshake: function() {
|
|
1170
|
+
// Deliver the latest size requested while no host origin was trusted.
|
|
1171
|
+
this.handshakeSettled = true;
|
|
1172
|
+
var pending = this.pendingSize;
|
|
1173
|
+
if (!pending) return;
|
|
1174
|
+
this.pendingSize = null;
|
|
1175
|
+
this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
|
|
1176
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
|
|
1177
|
+
}, function(err) {
|
|
1178
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
|
|
1179
|
+
});
|
|
1180
|
+
},
|
|
1059
1181
|
callTool: function(context, name, args) {
|
|
1060
1182
|
if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
|
|
1061
1183
|
return Promise.reject(new Error('Server tool proxy not supported'));
|
|
@@ -1077,13 +1199,22 @@ var ExtAppsAdapter = {
|
|
|
1077
1199
|
return this.sendRequest('ui/setDisplayMode', { mode: mode });
|
|
1078
1200
|
},
|
|
1079
1201
|
setSize: function(context, size) {
|
|
1080
|
-
//
|
|
1081
|
-
//
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1202
|
+
// Standard ext-apps sizing: the view tells the host its dimensions with the
|
|
1203
|
+
// ui/notifications/size-changed notification ({ width?, height? } in px).
|
|
1204
|
+
var params = {};
|
|
1205
|
+
if (size && typeof size.width === 'number') params.width = size.width;
|
|
1206
|
+
if (size && typeof size.height === 'number') params.height = size.height;
|
|
1207
|
+
if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
1208
|
+
// No host origin yet: keep only the latest size and send it when the
|
|
1209
|
+
// handshake settles; every caller's promise follows that one request.
|
|
1210
|
+
var self = this;
|
|
1211
|
+
if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
|
|
1212
|
+
this.pendingSize.params = params;
|
|
1213
|
+
return new Promise(function(resolve, reject) {
|
|
1214
|
+
self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
return this.sendNotification('ui/notifications/size-changed', params);
|
|
1087
1218
|
},
|
|
1088
1219
|
requestClose: function(context) {
|
|
1089
1220
|
return this.sendRequest('ui/close', {});
|
|
@@ -1329,6 +1460,7 @@ function FrontMcpBridge() {
|
|
|
1329
1460
|
toolResultListeners: [],
|
|
1330
1461
|
notifyContextChange: function(changes) {
|
|
1331
1462
|
Object.assign(this.hostContext, changes);
|
|
1463
|
+
if (changes && changes.theme) __applyHostTheme(changes.theme);
|
|
1332
1464
|
for (var i = 0; i < this.contextListeners.length; i++) {
|
|
1333
1465
|
try { this.contextListeners[i](changes); } catch(e) {}
|
|
1334
1466
|
}
|
|
@@ -1505,7 +1637,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
|
|
|
1505
1637
|
|
|
1506
1638
|
// Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
|
|
1507
1639
|
// Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
|
|
1508
|
-
// ext-apps sends ui/
|
|
1640
|
+
// ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
|
|
1509
1641
|
FrontMcpBridge.prototype.setSize = function(size) {
|
|
1510
1642
|
if (!this._adapter) return Promise.reject(new Error('Not initialized'));
|
|
1511
1643
|
if (!this._adapter.setSize) return Promise.resolve();
|
|
@@ -1607,7 +1739,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
|
|
|
1607
1739
|
target.classList.add('opacity-50', 'cursor-not-allowed');
|
|
1608
1740
|
|
|
1609
1741
|
// Add spinner for buttons
|
|
1610
|
-
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>';
|
|
1742
|
+
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>';
|
|
1611
1743
|
if (target.tagName === 'BUTTON') {
|
|
1612
1744
|
target.innerHTML = spinner + 'Loading...';
|
|
1613
1745
|
}
|
|
@@ -1688,7 +1820,22 @@ function minifyJS(code) {
|
|
|
1688
1820
|
if (code.length > MAX_MINIFY_CODE_LENGTH) {
|
|
1689
1821
|
return code;
|
|
1690
1822
|
}
|
|
1691
|
-
|
|
1823
|
+
const out = [];
|
|
1824
|
+
let inBlockComment = false;
|
|
1825
|
+
for (const raw of code.split("\n")) {
|
|
1826
|
+
const line = raw.trim();
|
|
1827
|
+
if (inBlockComment) {
|
|
1828
|
+
if (line.includes("*/")) inBlockComment = false;
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
1832
|
+
if (line.startsWith("/*")) {
|
|
1833
|
+
if (!line.includes("*/")) inBlockComment = true;
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
out.push(line);
|
|
1837
|
+
}
|
|
1838
|
+
return out.join("\n");
|
|
1692
1839
|
}
|
|
1693
1840
|
function generatePlatformBundle(platform, options = {}) {
|
|
1694
1841
|
const platformAdapters = {
|
|
@@ -2113,8 +2260,24 @@ function isFrontmcpUiResolvable(...candidatePaths) {
|
|
|
2113
2260
|
}
|
|
2114
2261
|
|
|
2115
2262
|
// libs/uipack/src/component/transpiler.ts
|
|
2263
|
+
function loadEsbuild(filename) {
|
|
2264
|
+
try {
|
|
2265
|
+
return __require("esbuild");
|
|
2266
|
+
} catch (err) {
|
|
2267
|
+
if (!isMissingEsbuild(err)) throw err;
|
|
2268
|
+
const subject = filename ? `FileSource widget "${filename}"` : "A FileSource widget";
|
|
2269
|
+
throw new Error(
|
|
2270
|
+
`${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.`,
|
|
2271
|
+
{ cause: err }
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
function isMissingEsbuild(err) {
|
|
2276
|
+
if (!(err instanceof Error)) return false;
|
|
2277
|
+
return err.code === "MODULE_NOT_FOUND" && /Cannot find module ['"]esbuild['"]/.test(err.message);
|
|
2278
|
+
}
|
|
2116
2279
|
function transpileReactSource(source, filename) {
|
|
2117
|
-
const esbuild =
|
|
2280
|
+
const esbuild = loadEsbuild(filename);
|
|
2118
2281
|
const loader = filename?.endsWith(".tsx") ? "tsx" : "jsx";
|
|
2119
2282
|
const result = esbuild.transformSync(source, {
|
|
2120
2283
|
loader,
|
|
@@ -2133,7 +2296,7 @@ function bundleFileSource(source, filename, resolveDir, componentName, options =
|
|
|
2133
2296
|
`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.`
|
|
2134
2297
|
);
|
|
2135
2298
|
}
|
|
2136
|
-
const esbuild =
|
|
2299
|
+
const esbuild = loadEsbuild(filename);
|
|
2137
2300
|
const mountCode = `
|
|
2138
2301
|
// --- Auto-generated mount ---
|
|
2139
2302
|
import { createElement as __h } from 'react';
|
|
@@ -2230,7 +2393,8 @@ if (__root) {
|
|
|
2230
2393
|
} catch (err) {
|
|
2231
2394
|
const message = err instanceof Error ? err.message : String(err);
|
|
2232
2395
|
throw new Error(
|
|
2233
|
-
`Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project
|
|
2396
|
+
`Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`,
|
|
2397
|
+
{ cause: err }
|
|
2234
2398
|
);
|
|
2235
2399
|
}
|
|
2236
2400
|
}
|
|
@@ -2306,6 +2470,37 @@ function resolveImportSource(source) {
|
|
|
2306
2470
|
peerDependencies: []
|
|
2307
2471
|
};
|
|
2308
2472
|
}
|
|
2473
|
+
var BUILD_DIR_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "out"]);
|
|
2474
|
+
function missingWidgetError(filePath, cause) {
|
|
2475
|
+
const sourceWidget = findSourceWidget(filePath);
|
|
2476
|
+
const error = new Error(
|
|
2477
|
+
`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\`.` : ""),
|
|
2478
|
+
{ cause }
|
|
2479
|
+
);
|
|
2480
|
+
error.code = "ENOENT";
|
|
2481
|
+
error.path = filePath;
|
|
2482
|
+
return error;
|
|
2483
|
+
}
|
|
2484
|
+
function findSourceWidget(filePath) {
|
|
2485
|
+
const path = __require("path");
|
|
2486
|
+
const fs = __require("fs");
|
|
2487
|
+
const parts = filePath.split(path.sep);
|
|
2488
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
2489
|
+
if (!BUILD_DIR_SEGMENTS.has(parts[i])) continue;
|
|
2490
|
+
const candidates = [[...parts.slice(0, i), "src", ...parts.slice(i + 1)]];
|
|
2491
|
+
if (parts.length - i > 2) candidates.push([...parts.slice(0, i), "src", ...parts.slice(i + 2)]);
|
|
2492
|
+
candidates.push([...parts.slice(0, i), ...parts.slice(i + 1)]);
|
|
2493
|
+
for (const candidate of candidates) {
|
|
2494
|
+
const candidatePath = candidate.join(path.sep);
|
|
2495
|
+
try {
|
|
2496
|
+
if (fs.existsSync(candidatePath)) return candidatePath;
|
|
2497
|
+
} catch {
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
return void 0;
|
|
2501
|
+
}
|
|
2502
|
+
return void 0;
|
|
2503
|
+
}
|
|
2309
2504
|
function resolveFileSource(source, options = {}) {
|
|
2310
2505
|
const path = __require("path");
|
|
2311
2506
|
const ext = path.extname(source.file).toLowerCase();
|
|
@@ -2320,9 +2515,11 @@ function resolveFileSource(source, options = {}) {
|
|
|
2320
2515
|
const isNotFound = err?.code === "ENOENT";
|
|
2321
2516
|
if (isNotFound && wasRelative) {
|
|
2322
2517
|
throw new Error(
|
|
2323
|
-
`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)
|
|
2518
|
+
`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).`,
|
|
2519
|
+
{ cause: err }
|
|
2324
2520
|
);
|
|
2325
2521
|
}
|
|
2522
|
+
if (isNotFound) throw missingWidgetError(filePath, err);
|
|
2326
2523
|
throw err;
|
|
2327
2524
|
}
|
|
2328
2525
|
const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
|
|
@@ -2700,6 +2897,86 @@ function wrapDetectedContent(value) {
|
|
|
2700
2897
|
}
|
|
2701
2898
|
}
|
|
2702
2899
|
|
|
2900
|
+
// libs/uipack/src/adapters/markdown.ts
|
|
2901
|
+
var SAFE_HREF = /^(https?:|mailto:|\/|#)/i;
|
|
2902
|
+
function emphasis(text) {
|
|
2903
|
+
return text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
2904
|
+
}
|
|
2905
|
+
function inline(text) {
|
|
2906
|
+
const stash = [];
|
|
2907
|
+
const hold = (html2) => `\uE000${stash.push(html2) - 1}\uE000`;
|
|
2908
|
+
let out = escapeHtml(text.replace(/\uE000/g, "")).replace(
|
|
2909
|
+
/`([^`]+)`/g,
|
|
2910
|
+
(_m, code) => hold(`<code>${code}</code>`)
|
|
2911
|
+
);
|
|
2912
|
+
out = out.replace(
|
|
2913
|
+
/\[([^\]]+)\]\(([^)\s]+)\)/g,
|
|
2914
|
+
(_m, label, href) => SAFE_HREF.test(href) ? hold(`<a href="${href}" rel="noopener noreferrer">${emphasis(label)}</a>`) : label
|
|
2915
|
+
);
|
|
2916
|
+
out = emphasis(out);
|
|
2917
|
+
for (let i = 0; i < 2; i++) out = out.replace(/\uE000(\d+)\uE000/g, (_m, n) => stash[Number(n)]);
|
|
2918
|
+
return out;
|
|
2919
|
+
}
|
|
2920
|
+
function markdownToHtml(source) {
|
|
2921
|
+
const lines = source.replace(/\r\n?/g, "\n").split("\n");
|
|
2922
|
+
const blocks = [];
|
|
2923
|
+
let paragraph = [];
|
|
2924
|
+
let list;
|
|
2925
|
+
const flushParagraph = () => {
|
|
2926
|
+
if (paragraph.length > 0) blocks.push(`<p>${inline(paragraph.join(" "))}</p>`);
|
|
2927
|
+
paragraph = [];
|
|
2928
|
+
};
|
|
2929
|
+
const flushList = () => {
|
|
2930
|
+
if (list)
|
|
2931
|
+
blocks.push(`<${list.tag}>${list.items.map((item) => `<li>${inline(item)}</li>`).join("")}</${list.tag}>`);
|
|
2932
|
+
list = void 0;
|
|
2933
|
+
};
|
|
2934
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2935
|
+
const line = lines[i];
|
|
2936
|
+
const fence = /^\s*(`{3,})/.exec(line);
|
|
2937
|
+
if (fence) {
|
|
2938
|
+
flushParagraph();
|
|
2939
|
+
flushList();
|
|
2940
|
+
const fenceLength = fence[1].length;
|
|
2941
|
+
const isClosing = (l) => {
|
|
2942
|
+
const m = /^\s*(`{3,})\s*$/.exec(l);
|
|
2943
|
+
return m !== null && m[1].length >= fenceLength;
|
|
2944
|
+
};
|
|
2945
|
+
const code = [];
|
|
2946
|
+
for (i++; i < lines.length && !isClosing(lines[i]); i++) code.push(lines[i]);
|
|
2947
|
+
blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}
|
|
2948
|
+
</code></pre>`);
|
|
2949
|
+
continue;
|
|
2950
|
+
}
|
|
2951
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
2952
|
+
if (heading) {
|
|
2953
|
+
flushParagraph();
|
|
2954
|
+
flushList();
|
|
2955
|
+
blocks.push(`<h${heading[1].length}>${inline(heading[2].trim())}</h${heading[1].length}>`);
|
|
2956
|
+
continue;
|
|
2957
|
+
}
|
|
2958
|
+
const item = /^\s*([-*+]|\d+\.)\s+(.*)$/.exec(line);
|
|
2959
|
+
if (item) {
|
|
2960
|
+
flushParagraph();
|
|
2961
|
+
const tag = /\d/.test(item[1]) ? "ol" : "ul";
|
|
2962
|
+
if (list && list.tag !== tag) flushList();
|
|
2963
|
+
list ??= { tag, items: [] };
|
|
2964
|
+
list.items.push(item[2]);
|
|
2965
|
+
continue;
|
|
2966
|
+
}
|
|
2967
|
+
if (line.trim() === "") {
|
|
2968
|
+
flushParagraph();
|
|
2969
|
+
flushList();
|
|
2970
|
+
continue;
|
|
2971
|
+
}
|
|
2972
|
+
flushList();
|
|
2973
|
+
paragraph.push(line.trim());
|
|
2974
|
+
}
|
|
2975
|
+
flushParagraph();
|
|
2976
|
+
flushList();
|
|
2977
|
+
return blocks.join("\n");
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2703
2980
|
// libs/uipack/src/adapters/type-detector.ts
|
|
2704
2981
|
function detectUIType(template) {
|
|
2705
2982
|
if (template === null || template === void 0) {
|
|
@@ -2733,9 +3010,15 @@ function detectUIType(template) {
|
|
|
2733
3010
|
}
|
|
2734
3011
|
|
|
2735
3012
|
// libs/uipack/src/adapters/template-renderer.ts
|
|
2736
|
-
function buildCspConfig(resolver) {
|
|
3013
|
+
function buildCspConfig(resolver, extra) {
|
|
2737
3014
|
const cspResourceDomains = ["https://esm.sh"];
|
|
2738
3015
|
const cspConnectDomains = ["https://esm.sh"];
|
|
3016
|
+
for (const domain of extra?.resourceDomains ?? []) {
|
|
3017
|
+
if (!cspResourceDomains.includes(domain)) cspResourceDomains.push(domain);
|
|
3018
|
+
}
|
|
3019
|
+
for (const domain of extra?.connectDomains ?? []) {
|
|
3020
|
+
if (!cspConnectDomains.includes(domain)) cspConnectDomains.push(domain);
|
|
3021
|
+
}
|
|
2739
3022
|
if (resolver && "overrides" in resolver) {
|
|
2740
3023
|
const overrides = resolver.overrides;
|
|
2741
3024
|
if (overrides) {
|
|
@@ -2775,6 +3058,14 @@ function renderUnwrappedResult(rawResult, options) {
|
|
|
2775
3058
|
}
|
|
2776
3059
|
return rawResult;
|
|
2777
3060
|
}
|
|
3061
|
+
function isReactElement(value) {
|
|
3062
|
+
return typeof value === "object" && value !== null && "$$typeof" in value;
|
|
3063
|
+
}
|
|
3064
|
+
function isDefinitelyReactComponent(template) {
|
|
3065
|
+
if (typeof template !== "function") return false;
|
|
3066
|
+
const proto = template.prototype;
|
|
3067
|
+
return typeof proto?.render === "function" || template["$$typeof"] !== void 0;
|
|
3068
|
+
}
|
|
2778
3069
|
function renderToolTemplate(options) {
|
|
2779
3070
|
const { toolName, input, output, template, resolver, platformType, sizing } = options;
|
|
2780
3071
|
const uiType = detectUIType(template);
|
|
@@ -2785,30 +3076,43 @@ function renderToolTemplate(options) {
|
|
|
2785
3076
|
output,
|
|
2786
3077
|
includeBridge: true,
|
|
2787
3078
|
resolver,
|
|
2788
|
-
sizing
|
|
3079
|
+
sizing,
|
|
3080
|
+
csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
|
|
2789
3081
|
};
|
|
2790
3082
|
let html2;
|
|
2791
3083
|
let hash = "";
|
|
2792
3084
|
let size = 0;
|
|
2793
3085
|
if (typeof template === "object" && template !== null && "file" in template) {
|
|
2794
|
-
const cspConfig = buildCspConfig(resolver);
|
|
2795
3086
|
const result = renderComponent(
|
|
2796
3087
|
{ source: template, inlineReact: resourceMode === "inline" },
|
|
2797
|
-
{ ...shellConfig, csp:
|
|
3088
|
+
{ ...shellConfig, csp: buildCspConfig(resolver, options.csp) }
|
|
2798
3089
|
);
|
|
2799
3090
|
html2 = result.html;
|
|
2800
3091
|
hash = result.hash;
|
|
2801
3092
|
size = result.size;
|
|
2802
3093
|
} else if (typeof template === "function") {
|
|
2803
|
-
|
|
3094
|
+
const helpers = createTemplateHelpers();
|
|
3095
|
+
const ctx = { input, output, helpers };
|
|
3096
|
+
let rawResult;
|
|
3097
|
+
let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
|
|
3098
|
+
if (isHtmlBuilder) {
|
|
3099
|
+
try {
|
|
3100
|
+
rawResult = template(ctx);
|
|
3101
|
+
isHtmlBuilder = !isReactElement(rawResult);
|
|
3102
|
+
} catch (error) {
|
|
3103
|
+
if (uiType !== "react") throw error;
|
|
3104
|
+
isHtmlBuilder = false;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
if (!isHtmlBuilder) {
|
|
3108
|
+
(options.logger ?? console).warn(
|
|
3109
|
+
`[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.`
|
|
3110
|
+
);
|
|
2804
3111
|
const shellResult = buildShell('<div id="root"></div>', shellConfig);
|
|
2805
3112
|
html2 = shellResult.html;
|
|
2806
3113
|
hash = shellResult.hash;
|
|
2807
3114
|
size = shellResult.size;
|
|
2808
3115
|
} else {
|
|
2809
|
-
const helpers = createTemplateHelpers();
|
|
2810
|
-
const ctx = { input, output, helpers };
|
|
2811
|
-
const rawResult = template(ctx);
|
|
2812
3116
|
const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
|
|
2813
3117
|
if (wrapped) {
|
|
2814
3118
|
html2 = wrapped;
|
|
@@ -2820,7 +3124,7 @@ function renderToolTemplate(options) {
|
|
|
2820
3124
|
}
|
|
2821
3125
|
}
|
|
2822
3126
|
} else if (typeof template === "string") {
|
|
2823
|
-
const shellResult = buildShell(template, shellConfig);
|
|
3127
|
+
const shellResult = buildShell(uiType === "markdown" ? markdownToHtml(template) : template, shellConfig);
|
|
2824
3128
|
html2 = shellResult.html;
|
|
2825
3129
|
hash = shellResult.hash;
|
|
2826
3130
|
size = shellResult.size;
|