@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/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,12 +379,57 @@ 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
|
+
|
|
401
|
+
function extractToolOutput(params) {
|
|
402
|
+
// A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
|
|
403
|
+
if (!params) return undefined;
|
|
404
|
+
if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
|
|
405
|
+
var content = params.content;
|
|
406
|
+
if (Array.isArray(content)) {
|
|
407
|
+
for (var i = 0; i < content.length; i++) {
|
|
408
|
+
var item = content[i];
|
|
409
|
+
if (item && item.type === 'text' && typeof item.text === 'string') {
|
|
410
|
+
try { return JSON.parse(item.text); } catch (e) { return item.text; }
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return content;
|
|
415
|
+
}
|
|
416
|
+
|
|
350
417
|
function readInjectedData() {
|
|
351
418
|
var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
|
|
352
419
|
if (typeof window !== 'undefined') {
|
|
353
420
|
if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
|
|
354
421
|
if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
|
|
355
422
|
if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
|
|
423
|
+
// The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
|
|
424
|
+
// (toolOutput is the tool's structuredContent).
|
|
425
|
+
var oa = window.openai;
|
|
426
|
+
if (oa && typeof oa === 'object') {
|
|
427
|
+
if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
|
|
428
|
+
if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
|
|
429
|
+
if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
|
|
430
|
+
if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
356
433
|
}
|
|
357
434
|
return data;
|
|
358
435
|
}
|
|
@@ -401,7 +478,24 @@ var OpenAIAdapter = {
|
|
|
401
478
|
if (sdk.displayMode) {
|
|
402
479
|
context.hostContext.displayMode = sdk.displayMode;
|
|
403
480
|
}
|
|
404
|
-
//
|
|
481
|
+
// The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
|
|
482
|
+
window.addEventListener('openai:set_globals', function(event) {
|
|
483
|
+
var globals = (event && event.detail && event.detail.globals) || window.openai || {};
|
|
484
|
+
if (globals.toolInput !== undefined && globals.toolInput !== null) {
|
|
485
|
+
context.toolInput = globals.toolInput;
|
|
486
|
+
window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
|
|
487
|
+
}
|
|
488
|
+
if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
|
|
489
|
+
context.structuredContent = globals.toolOutput;
|
|
490
|
+
context.notifyToolResult(globals.toolOutput);
|
|
491
|
+
}
|
|
492
|
+
if (globals.theme && globals.theme !== context.hostContext.theme) {
|
|
493
|
+
context.notifyContextChange({ theme: globals.theme });
|
|
494
|
+
}
|
|
495
|
+
if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
|
|
496
|
+
context.notifyContextChange({ displayMode: globals.displayMode });
|
|
497
|
+
}
|
|
498
|
+
});
|
|
405
499
|
return Promise.resolve();
|
|
406
500
|
},
|
|
407
501
|
callTool: function(context, name, args) {
|
|
@@ -453,6 +547,8 @@ var ExtAppsAdapter = {
|
|
|
453
547
|
pendingRequests: {},
|
|
454
548
|
requestId: 0,
|
|
455
549
|
hostCapabilities: {},
|
|
550
|
+
handshakeSettled: false,
|
|
551
|
+
pendingSize: null,
|
|
456
552
|
canHandle: function() {
|
|
457
553
|
if (typeof window === 'undefined') return false;
|
|
458
554
|
if (window.parent === window) return false;
|
|
@@ -546,9 +642,10 @@ var ExtAppsAdapter = {
|
|
|
546
642
|
window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
|
|
547
643
|
break;
|
|
548
644
|
case 'ui/notifications/tool-result':
|
|
549
|
-
|
|
645
|
+
var resultOutput = extractToolOutput(params);
|
|
646
|
+
context.toolOutput = resultOutput;
|
|
550
647
|
context.structuredContent = params.structuredContent;
|
|
551
|
-
context.notifyToolResult(
|
|
648
|
+
context.notifyToolResult(resultOutput);
|
|
552
649
|
window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
|
|
553
650
|
break;
|
|
554
651
|
case 'ui/notifications/host-context-changed':
|
|
@@ -602,6 +699,15 @@ var ExtAppsAdapter = {
|
|
|
602
699
|
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
|
|
603
700
|
});
|
|
604
701
|
},
|
|
702
|
+
sendNotification: function(method, params) {
|
|
703
|
+
// JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
|
|
704
|
+
if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
705
|
+
return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
|
|
706
|
+
}
|
|
707
|
+
var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
|
|
708
|
+
window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
|
|
709
|
+
return Promise.resolve();
|
|
710
|
+
},
|
|
605
711
|
sendHandshake: function(context) {
|
|
606
712
|
// Send ui/initialize using '*' as target origin since TOFU hasn't
|
|
607
713
|
// been established yet. The response from the host will establish
|
|
@@ -618,6 +724,7 @@ var ExtAppsAdapter = {
|
|
|
618
724
|
var timeout = setTimeout(function() {
|
|
619
725
|
delete self.pendingRequests[id];
|
|
620
726
|
// Handshake timeout is non-fatal \u2014 notifications may still arrive
|
|
727
|
+
self.settleHandshake();
|
|
621
728
|
resolve();
|
|
622
729
|
}, 10000);
|
|
623
730
|
|
|
@@ -631,7 +738,9 @@ var ExtAppsAdapter = {
|
|
|
631
738
|
supportsDisplayModes: true
|
|
632
739
|
});
|
|
633
740
|
if (result.hostContext) {
|
|
634
|
-
|
|
741
|
+
// Notify (not a silent merge) so onContextChange listeners and the
|
|
742
|
+
// host theme see the handshake's context.
|
|
743
|
+
context.notifyContextChange(result.hostContext);
|
|
635
744
|
}
|
|
636
745
|
// Send ui/notifications/initialized to tell the host the view is ready.
|
|
637
746
|
// Per MCP Apps spec, the host waits for this before sending tool-result.
|
|
@@ -641,9 +750,10 @@ var ExtAppsAdapter = {
|
|
|
641
750
|
method: 'ui/notifications/initialized',
|
|
642
751
|
params: {}
|
|
643
752
|
}, targetOrigin);
|
|
753
|
+
self.settleHandshake();
|
|
644
754
|
resolve();
|
|
645
755
|
},
|
|
646
|
-
reject: function(err) { resolve(); }, // Non-fatal
|
|
756
|
+
reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
|
|
647
757
|
timeout: timeout
|
|
648
758
|
};
|
|
649
759
|
|
|
@@ -657,6 +767,18 @@ var ExtAppsAdapter = {
|
|
|
657
767
|
performHandshake: function(context) {
|
|
658
768
|
return this.sendHandshake(context);
|
|
659
769
|
},
|
|
770
|
+
settleHandshake: function() {
|
|
771
|
+
// Deliver the latest size requested while no host origin was trusted.
|
|
772
|
+
this.handshakeSettled = true;
|
|
773
|
+
var pending = this.pendingSize;
|
|
774
|
+
if (!pending) return;
|
|
775
|
+
this.pendingSize = null;
|
|
776
|
+
this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
|
|
777
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
|
|
778
|
+
}, function(err) {
|
|
779
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
|
|
780
|
+
});
|
|
781
|
+
},
|
|
660
782
|
callTool: function(context, name, args) {
|
|
661
783
|
if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
|
|
662
784
|
return Promise.reject(new Error('Server tool proxy not supported'));
|
|
@@ -678,13 +800,22 @@ var ExtAppsAdapter = {
|
|
|
678
800
|
return this.sendRequest('ui/setDisplayMode', { mode: mode });
|
|
679
801
|
},
|
|
680
802
|
setSize: function(context, size) {
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
803
|
+
// Standard ext-apps sizing: the view tells the host its dimensions with the
|
|
804
|
+
// ui/notifications/size-changed notification ({ width?, height? } in px).
|
|
805
|
+
var params = {};
|
|
806
|
+
if (size && typeof size.width === 'number') params.width = size.width;
|
|
807
|
+
if (size && typeof size.height === 'number') params.height = size.height;
|
|
808
|
+
if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
809
|
+
// No host origin yet: keep only the latest size and send it when the
|
|
810
|
+
// handshake settles; every caller's promise follows that one request.
|
|
811
|
+
var self = this;
|
|
812
|
+
if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
|
|
813
|
+
this.pendingSize.params = params;
|
|
814
|
+
return new Promise(function(resolve, reject) {
|
|
815
|
+
self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
return this.sendNotification('ui/notifications/size-changed', params);
|
|
688
819
|
},
|
|
689
820
|
requestClose: function(context) {
|
|
690
821
|
return this.sendRequest('ui/close', {});
|
|
@@ -930,6 +1061,7 @@ function FrontMcpBridge() {
|
|
|
930
1061
|
toolResultListeners: [],
|
|
931
1062
|
notifyContextChange: function(changes) {
|
|
932
1063
|
Object.assign(this.hostContext, changes);
|
|
1064
|
+
if (changes && changes.theme) __applyHostTheme(changes.theme);
|
|
933
1065
|
for (var i = 0; i < this.contextListeners.length; i++) {
|
|
934
1066
|
try { this.contextListeners[i](changes); } catch(e) {}
|
|
935
1067
|
}
|
|
@@ -1106,7 +1238,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
|
|
|
1106
1238
|
|
|
1107
1239
|
// Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
|
|
1108
1240
|
// Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
|
|
1109
|
-
// ext-apps sends ui/
|
|
1241
|
+
// ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
|
|
1110
1242
|
FrontMcpBridge.prototype.setSize = function(size) {
|
|
1111
1243
|
if (!this._adapter) return Promise.reject(new Error('Not initialized'));
|
|
1112
1244
|
if (!this._adapter.setSize) return Promise.resolve();
|
|
@@ -1208,7 +1340,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
|
|
|
1208
1340
|
target.classList.add('opacity-50', 'cursor-not-allowed');
|
|
1209
1341
|
|
|
1210
1342
|
// Add spinner for buttons
|
|
1211
|
-
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>';
|
|
1343
|
+
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>';
|
|
1212
1344
|
if (target.tagName === 'BUTTON') {
|
|
1213
1345
|
target.innerHTML = spinner + 'Loading...';
|
|
1214
1346
|
}
|
|
@@ -1289,7 +1421,22 @@ function minifyJS(code) {
|
|
|
1289
1421
|
if (code.length > MAX_MINIFY_CODE_LENGTH) {
|
|
1290
1422
|
return code;
|
|
1291
1423
|
}
|
|
1292
|
-
|
|
1424
|
+
const out = [];
|
|
1425
|
+
let inBlockComment = false;
|
|
1426
|
+
for (const raw of code.split("\n")) {
|
|
1427
|
+
const line = raw.trim();
|
|
1428
|
+
if (inBlockComment) {
|
|
1429
|
+
if (line.includes("*/")) inBlockComment = false;
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
1433
|
+
if (line.startsWith("/*")) {
|
|
1434
|
+
if (!line.includes("*/")) inBlockComment = true;
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
out.push(line);
|
|
1438
|
+
}
|
|
1439
|
+
return out.join("\n");
|
|
1293
1440
|
}
|
|
1294
1441
|
function generatePlatformBundle(platform, options = {}) {
|
|
1295
1442
|
const platformAdapters = {
|
|
@@ -3329,8 +3476,24 @@ function isFrontmcpUiResolvable(...candidatePaths) {
|
|
|
3329
3476
|
}
|
|
3330
3477
|
|
|
3331
3478
|
// libs/uipack/src/component/transpiler.ts
|
|
3479
|
+
function loadEsbuild(filename) {
|
|
3480
|
+
try {
|
|
3481
|
+
return __require("esbuild");
|
|
3482
|
+
} catch (err) {
|
|
3483
|
+
if (!isMissingEsbuild(err)) throw err;
|
|
3484
|
+
const subject = filename ? `FileSource widget "${filename}"` : "A FileSource widget";
|
|
3485
|
+
throw new Error(
|
|
3486
|
+
`${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.`,
|
|
3487
|
+
{ cause: err }
|
|
3488
|
+
);
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
function isMissingEsbuild(err) {
|
|
3492
|
+
if (!(err instanceof Error)) return false;
|
|
3493
|
+
return err.code === "MODULE_NOT_FOUND" && /Cannot find module ['"]esbuild['"]/.test(err.message);
|
|
3494
|
+
}
|
|
3332
3495
|
function transpileReactSource(source, filename) {
|
|
3333
|
-
const esbuild =
|
|
3496
|
+
const esbuild = loadEsbuild(filename);
|
|
3334
3497
|
const loader = filename?.endsWith(".tsx") ? "tsx" : "jsx";
|
|
3335
3498
|
const result = esbuild.transformSync(source, {
|
|
3336
3499
|
loader,
|
|
@@ -3349,7 +3512,7 @@ function bundleFileSource(source, filename, resolveDir, componentName, options =
|
|
|
3349
3512
|
`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
3513
|
);
|
|
3351
3514
|
}
|
|
3352
|
-
const esbuild =
|
|
3515
|
+
const esbuild = loadEsbuild(filename);
|
|
3353
3516
|
const mountCode = `
|
|
3354
3517
|
// --- Auto-generated mount ---
|
|
3355
3518
|
import { createElement as __h } from 'react';
|
|
@@ -3446,7 +3609,8 @@ if (__root) {
|
|
|
3446
3609
|
} catch (err) {
|
|
3447
3610
|
const message = err instanceof Error ? err.message : String(err);
|
|
3448
3611
|
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
|
|
3612
|
+
`Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`,
|
|
3613
|
+
{ cause: err }
|
|
3450
3614
|
);
|
|
3451
3615
|
}
|
|
3452
3616
|
}
|
|
@@ -3508,6 +3672,37 @@ function resolveImportSource(source) {
|
|
|
3508
3672
|
peerDependencies: []
|
|
3509
3673
|
};
|
|
3510
3674
|
}
|
|
3675
|
+
var BUILD_DIR_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "out"]);
|
|
3676
|
+
function missingWidgetError(filePath, cause) {
|
|
3677
|
+
const sourceWidget = findSourceWidget(filePath);
|
|
3678
|
+
const error = new Error(
|
|
3679
|
+
`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\`.` : ""),
|
|
3680
|
+
{ cause }
|
|
3681
|
+
);
|
|
3682
|
+
error.code = "ENOENT";
|
|
3683
|
+
error.path = filePath;
|
|
3684
|
+
return error;
|
|
3685
|
+
}
|
|
3686
|
+
function findSourceWidget(filePath) {
|
|
3687
|
+
const path = __require("path");
|
|
3688
|
+
const fs = __require("fs");
|
|
3689
|
+
const parts = filePath.split(path.sep);
|
|
3690
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
3691
|
+
if (!BUILD_DIR_SEGMENTS.has(parts[i])) continue;
|
|
3692
|
+
const candidates = [[...parts.slice(0, i), "src", ...parts.slice(i + 1)]];
|
|
3693
|
+
if (parts.length - i > 2) candidates.push([...parts.slice(0, i), "src", ...parts.slice(i + 2)]);
|
|
3694
|
+
candidates.push([...parts.slice(0, i), ...parts.slice(i + 1)]);
|
|
3695
|
+
for (const candidate of candidates) {
|
|
3696
|
+
const candidatePath = candidate.join(path.sep);
|
|
3697
|
+
try {
|
|
3698
|
+
if (fs.existsSync(candidatePath)) return candidatePath;
|
|
3699
|
+
} catch {
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
return void 0;
|
|
3703
|
+
}
|
|
3704
|
+
return void 0;
|
|
3705
|
+
}
|
|
3511
3706
|
function resolveFileSource(source, options = {}) {
|
|
3512
3707
|
const path = __require("path");
|
|
3513
3708
|
const ext = path.extname(source.file).toLowerCase();
|
|
@@ -3522,9 +3717,11 @@ function resolveFileSource(source, options = {}) {
|
|
|
3522
3717
|
const isNotFound = err?.code === "ENOENT";
|
|
3523
3718
|
if (isNotFound && wasRelative) {
|
|
3524
3719
|
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)
|
|
3720
|
+
`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).`,
|
|
3721
|
+
{ cause: err }
|
|
3526
3722
|
);
|
|
3527
3723
|
}
|
|
3724
|
+
if (isNotFound) throw missingWidgetError(filePath, err);
|
|
3528
3725
|
throw err;
|
|
3529
3726
|
}
|
|
3530
3727
|
const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
|
|
@@ -3971,6 +4168,86 @@ function wrapDetectedContent(value) {
|
|
|
3971
4168
|
}
|
|
3972
4169
|
}
|
|
3973
4170
|
|
|
4171
|
+
// libs/uipack/src/adapters/markdown.ts
|
|
4172
|
+
var SAFE_HREF = /^(https?:|mailto:|\/|#)/i;
|
|
4173
|
+
function emphasis(text) {
|
|
4174
|
+
return text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
4175
|
+
}
|
|
4176
|
+
function inline(text) {
|
|
4177
|
+
const stash = [];
|
|
4178
|
+
const hold = (html2) => `\uE000${stash.push(html2) - 1}\uE000`;
|
|
4179
|
+
let out = escapeHtml(text.replace(/\uE000/g, "")).replace(
|
|
4180
|
+
/`([^`]+)`/g,
|
|
4181
|
+
(_m, code) => hold(`<code>${code}</code>`)
|
|
4182
|
+
);
|
|
4183
|
+
out = out.replace(
|
|
4184
|
+
/\[([^\]]+)\]\(([^)\s]+)\)/g,
|
|
4185
|
+
(_m, label, href) => SAFE_HREF.test(href) ? hold(`<a href="${href}" rel="noopener noreferrer">${emphasis(label)}</a>`) : label
|
|
4186
|
+
);
|
|
4187
|
+
out = emphasis(out);
|
|
4188
|
+
for (let i = 0; i < 2; i++) out = out.replace(/\uE000(\d+)\uE000/g, (_m, n) => stash[Number(n)]);
|
|
4189
|
+
return out;
|
|
4190
|
+
}
|
|
4191
|
+
function markdownToHtml(source) {
|
|
4192
|
+
const lines = source.replace(/\r\n?/g, "\n").split("\n");
|
|
4193
|
+
const blocks = [];
|
|
4194
|
+
let paragraph = [];
|
|
4195
|
+
let list;
|
|
4196
|
+
const flushParagraph = () => {
|
|
4197
|
+
if (paragraph.length > 0) blocks.push(`<p>${inline(paragraph.join(" "))}</p>`);
|
|
4198
|
+
paragraph = [];
|
|
4199
|
+
};
|
|
4200
|
+
const flushList = () => {
|
|
4201
|
+
if (list)
|
|
4202
|
+
blocks.push(`<${list.tag}>${list.items.map((item) => `<li>${inline(item)}</li>`).join("")}</${list.tag}>`);
|
|
4203
|
+
list = void 0;
|
|
4204
|
+
};
|
|
4205
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4206
|
+
const line = lines[i];
|
|
4207
|
+
const fence = /^\s*(`{3,})/.exec(line);
|
|
4208
|
+
if (fence) {
|
|
4209
|
+
flushParagraph();
|
|
4210
|
+
flushList();
|
|
4211
|
+
const fenceLength = fence[1].length;
|
|
4212
|
+
const isClosing = (l) => {
|
|
4213
|
+
const m = /^\s*(`{3,})\s*$/.exec(l);
|
|
4214
|
+
return m !== null && m[1].length >= fenceLength;
|
|
4215
|
+
};
|
|
4216
|
+
const code = [];
|
|
4217
|
+
for (i++; i < lines.length && !isClosing(lines[i]); i++) code.push(lines[i]);
|
|
4218
|
+
blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}
|
|
4219
|
+
</code></pre>`);
|
|
4220
|
+
continue;
|
|
4221
|
+
}
|
|
4222
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
4223
|
+
if (heading) {
|
|
4224
|
+
flushParagraph();
|
|
4225
|
+
flushList();
|
|
4226
|
+
blocks.push(`<h${heading[1].length}>${inline(heading[2].trim())}</h${heading[1].length}>`);
|
|
4227
|
+
continue;
|
|
4228
|
+
}
|
|
4229
|
+
const item = /^\s*([-*+]|\d+\.)\s+(.*)$/.exec(line);
|
|
4230
|
+
if (item) {
|
|
4231
|
+
flushParagraph();
|
|
4232
|
+
const tag = /\d/.test(item[1]) ? "ol" : "ul";
|
|
4233
|
+
if (list && list.tag !== tag) flushList();
|
|
4234
|
+
list ??= { tag, items: [] };
|
|
4235
|
+
list.items.push(item[2]);
|
|
4236
|
+
continue;
|
|
4237
|
+
}
|
|
4238
|
+
if (line.trim() === "") {
|
|
4239
|
+
flushParagraph();
|
|
4240
|
+
flushList();
|
|
4241
|
+
continue;
|
|
4242
|
+
}
|
|
4243
|
+
flushList();
|
|
4244
|
+
paragraph.push(line.trim());
|
|
4245
|
+
}
|
|
4246
|
+
flushParagraph();
|
|
4247
|
+
flushList();
|
|
4248
|
+
return blocks.join("\n");
|
|
4249
|
+
}
|
|
4250
|
+
|
|
3974
4251
|
// libs/uipack/src/adapters/type-detector.ts
|
|
3975
4252
|
function detectUIType(template) {
|
|
3976
4253
|
if (template === null || template === void 0) {
|
|
@@ -4004,9 +4281,15 @@ function detectUIType(template) {
|
|
|
4004
4281
|
}
|
|
4005
4282
|
|
|
4006
4283
|
// libs/uipack/src/adapters/template-renderer.ts
|
|
4007
|
-
function buildCspConfig(resolver) {
|
|
4284
|
+
function buildCspConfig(resolver, extra) {
|
|
4008
4285
|
const cspResourceDomains = ["https://esm.sh"];
|
|
4009
4286
|
const cspConnectDomains = ["https://esm.sh"];
|
|
4287
|
+
for (const domain of extra?.resourceDomains ?? []) {
|
|
4288
|
+
if (!cspResourceDomains.includes(domain)) cspResourceDomains.push(domain);
|
|
4289
|
+
}
|
|
4290
|
+
for (const domain of extra?.connectDomains ?? []) {
|
|
4291
|
+
if (!cspConnectDomains.includes(domain)) cspConnectDomains.push(domain);
|
|
4292
|
+
}
|
|
4010
4293
|
if (resolver && "overrides" in resolver) {
|
|
4011
4294
|
const overrides = resolver.overrides;
|
|
4012
4295
|
if (overrides) {
|
|
@@ -4046,6 +4329,14 @@ function renderUnwrappedResult(rawResult, options) {
|
|
|
4046
4329
|
}
|
|
4047
4330
|
return rawResult;
|
|
4048
4331
|
}
|
|
4332
|
+
function isReactElement(value) {
|
|
4333
|
+
return typeof value === "object" && value !== null && "$$typeof" in value;
|
|
4334
|
+
}
|
|
4335
|
+
function isDefinitelyReactComponent(template) {
|
|
4336
|
+
if (typeof template !== "function") return false;
|
|
4337
|
+
const proto = template.prototype;
|
|
4338
|
+
return typeof proto?.render === "function" || template["$$typeof"] !== void 0;
|
|
4339
|
+
}
|
|
4049
4340
|
function renderToolTemplate(options) {
|
|
4050
4341
|
const { toolName, input, output, template, resolver, platformType, sizing } = options;
|
|
4051
4342
|
const uiType = detectUIType(template);
|
|
@@ -4056,30 +4347,43 @@ function renderToolTemplate(options) {
|
|
|
4056
4347
|
output,
|
|
4057
4348
|
includeBridge: true,
|
|
4058
4349
|
resolver,
|
|
4059
|
-
sizing
|
|
4350
|
+
sizing,
|
|
4351
|
+
csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
|
|
4060
4352
|
};
|
|
4061
4353
|
let html2;
|
|
4062
4354
|
let hash = "";
|
|
4063
4355
|
let size = 0;
|
|
4064
4356
|
if (typeof template === "object" && template !== null && "file" in template) {
|
|
4065
|
-
const cspConfig = buildCspConfig(resolver);
|
|
4066
4357
|
const result = renderComponent(
|
|
4067
4358
|
{ source: template, inlineReact: resourceMode === "inline" },
|
|
4068
|
-
{ ...shellConfig, csp:
|
|
4359
|
+
{ ...shellConfig, csp: buildCspConfig(resolver, options.csp) }
|
|
4069
4360
|
);
|
|
4070
4361
|
html2 = result.html;
|
|
4071
4362
|
hash = result.hash;
|
|
4072
4363
|
size = result.size;
|
|
4073
4364
|
} else if (typeof template === "function") {
|
|
4074
|
-
|
|
4365
|
+
const helpers = createTemplateHelpers();
|
|
4366
|
+
const ctx = { input, output, helpers };
|
|
4367
|
+
let rawResult;
|
|
4368
|
+
let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
|
|
4369
|
+
if (isHtmlBuilder) {
|
|
4370
|
+
try {
|
|
4371
|
+
rawResult = template(ctx);
|
|
4372
|
+
isHtmlBuilder = !isReactElement(rawResult);
|
|
4373
|
+
} catch (error) {
|
|
4374
|
+
if (uiType !== "react") throw error;
|
|
4375
|
+
isHtmlBuilder = false;
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
if (!isHtmlBuilder) {
|
|
4379
|
+
(options.logger ?? console).warn(
|
|
4380
|
+
`[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.`
|
|
4381
|
+
);
|
|
4075
4382
|
const shellResult = buildShell('<div id="root"></div>', shellConfig);
|
|
4076
4383
|
html2 = shellResult.html;
|
|
4077
4384
|
hash = shellResult.hash;
|
|
4078
4385
|
size = shellResult.size;
|
|
4079
4386
|
} else {
|
|
4080
|
-
const helpers = createTemplateHelpers();
|
|
4081
|
-
const ctx = { input, output, helpers };
|
|
4082
|
-
const rawResult = template(ctx);
|
|
4083
4387
|
const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
|
|
4084
4388
|
if (wrapped) {
|
|
4085
4389
|
html2 = wrapped;
|
|
@@ -4091,7 +4395,7 @@ function renderToolTemplate(options) {
|
|
|
4091
4395
|
}
|
|
4092
4396
|
}
|
|
4093
4397
|
} else if (typeof template === "string") {
|
|
4094
|
-
const shellResult = buildShell(template, shellConfig);
|
|
4398
|
+
const shellResult = buildShell(uiType === "markdown" ? markdownToHtml(template) : template, shellConfig);
|
|
4095
4399
|
html2 = shellResult.html;
|
|
4096
4400
|
hash = shellResult.hash;
|
|
4097
4401
|
size = shellResult.size;
|
package/esm/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
|
}
|