@frontmcp/uipack 1.8.6 → 1.9.0
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 +224 -38
- 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 +81 -15
- package/component/index.js +100 -29
- package/esm/adapters/index.mjs +227 -38
- package/esm/bridge-runtime/index.mjs +84 -15
- package/esm/component/index.mjs +103 -29
- package/esm/index.mjs +227 -38
- package/esm/package.json +2 -2
- package/esm/resolver/index.mjs +3 -0
- package/esm/shell/index.mjs +100 -26
- package/esm/types/index.mjs +3 -0
- package/esm/typings/index.mjs +3 -0
- package/esm/utils/index.mjs +3 -0
- package/index.js +224 -38
- package/package.json +2 -2
- package/shell/csp.d.ts +6 -1
- package/shell/csp.d.ts.map +1 -1
- package/shell/index.js +97 -26
- package/types/ui-config.d.ts +1 -1
package/index.js
CHANGED
|
@@ -518,12 +518,38 @@ function __applyHostTheme(theme) {
|
|
|
518
518
|
} catch (e) {}
|
|
519
519
|
}
|
|
520
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
|
+
|
|
521
537
|
function readInjectedData() {
|
|
522
538
|
var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
|
|
523
539
|
if (typeof window !== 'undefined') {
|
|
524
540
|
if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
|
|
525
541
|
if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
|
|
526
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
|
+
}
|
|
527
553
|
}
|
|
528
554
|
return data;
|
|
529
555
|
}
|
|
@@ -572,7 +598,24 @@ var OpenAIAdapter = {
|
|
|
572
598
|
if (sdk.displayMode) {
|
|
573
599
|
context.hostContext.displayMode = sdk.displayMode;
|
|
574
600
|
}
|
|
575
|
-
//
|
|
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
|
+
});
|
|
576
619
|
return Promise.resolve();
|
|
577
620
|
},
|
|
578
621
|
callTool: function(context, name, args) {
|
|
@@ -719,9 +762,10 @@ var ExtAppsAdapter = {
|
|
|
719
762
|
window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
|
|
720
763
|
break;
|
|
721
764
|
case 'ui/notifications/tool-result':
|
|
722
|
-
|
|
765
|
+
var resultOutput = extractToolOutput(params);
|
|
766
|
+
context.toolOutput = resultOutput;
|
|
723
767
|
context.structuredContent = params.structuredContent;
|
|
724
|
-
context.notifyToolResult(
|
|
768
|
+
context.notifyToolResult(resultOutput);
|
|
725
769
|
window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
|
|
726
770
|
break;
|
|
727
771
|
case 'ui/notifications/host-context-changed':
|
|
@@ -775,6 +819,15 @@ var ExtAppsAdapter = {
|
|
|
775
819
|
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
|
|
776
820
|
});
|
|
777
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
|
+
},
|
|
778
831
|
sendHandshake: function(context) {
|
|
779
832
|
// Send ui/initialize using '*' as target origin since TOFU hasn't
|
|
780
833
|
// been established yet. The response from the host will establish
|
|
@@ -840,7 +893,7 @@ var ExtAppsAdapter = {
|
|
|
840
893
|
var pending = this.pendingSize;
|
|
841
894
|
if (!pending) return;
|
|
842
895
|
this.pendingSize = null;
|
|
843
|
-
this.
|
|
896
|
+
this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
|
|
844
897
|
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
|
|
845
898
|
}, function(err) {
|
|
846
899
|
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
|
|
@@ -867,13 +920,11 @@ var ExtAppsAdapter = {
|
|
|
867
920
|
return this.sendRequest('ui/setDisplayMode', { mode: mode });
|
|
868
921
|
},
|
|
869
922
|
setSize: function(context, size) {
|
|
870
|
-
//
|
|
871
|
-
//
|
|
872
|
-
var params = {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
aspectRatio: size && size.aspectRatio
|
|
876
|
-
};
|
|
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;
|
|
877
928
|
if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
878
929
|
// No host origin yet: keep only the latest size and send it when the
|
|
879
930
|
// handshake settles; every caller's promise follows that one request.
|
|
@@ -884,7 +935,7 @@ var ExtAppsAdapter = {
|
|
|
884
935
|
self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
|
|
885
936
|
});
|
|
886
937
|
}
|
|
887
|
-
return this.
|
|
938
|
+
return this.sendNotification('ui/notifications/size-changed', params);
|
|
888
939
|
},
|
|
889
940
|
requestClose: function(context) {
|
|
890
941
|
return this.sendRequest('ui/close', {});
|
|
@@ -1307,7 +1358,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
|
|
|
1307
1358
|
|
|
1308
1359
|
// Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
|
|
1309
1360
|
// Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
|
|
1310
|
-
// ext-apps sends ui/
|
|
1361
|
+
// ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
|
|
1311
1362
|
FrontMcpBridge.prototype.setSize = function(size) {
|
|
1312
1363
|
if (!this._adapter) return Promise.reject(new Error('Not initialized'));
|
|
1313
1364
|
if (!this._adapter.setSize) return Promise.resolve();
|
|
@@ -1409,7 +1460,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
|
|
|
1409
1460
|
target.classList.add('opacity-50', 'cursor-not-allowed');
|
|
1410
1461
|
|
|
1411
1462
|
// Add spinner for buttons
|
|
1412
|
-
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>';
|
|
1413
1464
|
if (target.tagName === 'BUTTON') {
|
|
1414
1465
|
target.innerHTML = spinner + 'Loading...';
|
|
1415
1466
|
}
|
|
@@ -1490,7 +1541,22 @@ function minifyJS(code) {
|
|
|
1490
1541
|
if (code.length > MAX_MINIFY_CODE_LENGTH) {
|
|
1491
1542
|
return code;
|
|
1492
1543
|
}
|
|
1493
|
-
|
|
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");
|
|
1494
1560
|
}
|
|
1495
1561
|
function generatePlatformBundle(platform, options = {}) {
|
|
1496
1562
|
const platformAdapters = {
|
|
@@ -3027,11 +3093,8 @@ function buildCSPDirectives(csp) {
|
|
|
3027
3093
|
directives.push(`img-src ${imgSources.join(" ")}`);
|
|
3028
3094
|
const fontSources = ["'self'", "data:", ...allResourceDomains];
|
|
3029
3095
|
directives.push(`font-src ${fontSources.join(" ")}`);
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
} else {
|
|
3033
|
-
directives.push(`connect-src ${allResourceDomains.join(" ")}`);
|
|
3034
|
-
}
|
|
3096
|
+
const connectSources = [.../* @__PURE__ */ new Set([...allResourceDomains, ...validConnectDomains])];
|
|
3097
|
+
directives.push(`connect-src ${connectSources.join(" ")}`);
|
|
3035
3098
|
directives.push("object-src 'self' data:");
|
|
3036
3099
|
return directives;
|
|
3037
3100
|
}
|
|
@@ -3040,26 +3103,34 @@ function buildCSPMetaTag(csp) {
|
|
|
3040
3103
|
const content = directives.join("; ");
|
|
3041
3104
|
return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
|
|
3042
3105
|
}
|
|
3106
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
3043
3107
|
function validateCSPDomain(domain) {
|
|
3044
|
-
if (domain
|
|
3045
|
-
|
|
3046
|
-
|
|
3108
|
+
if (typeof domain !== "string" || /[\s;,'"]/.test(domain)) return false;
|
|
3109
|
+
if (/[?#]/.test(domain)) return false;
|
|
3110
|
+
if (domain.includes("*")) {
|
|
3111
|
+
const wildcard = /^(?:https|wss):\/\/\*\.(.*)$/.exec(domain);
|
|
3112
|
+
return wildcard !== null && /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(wildcard[1]);
|
|
3047
3113
|
}
|
|
3048
3114
|
try {
|
|
3049
3115
|
const url = new URL(domain);
|
|
3050
|
-
|
|
3116
|
+
if (url.protocol === "https:" || url.protocol === "wss:") return true;
|
|
3117
|
+
return (url.protocol === "http:" || url.protocol === "ws:") && LOOPBACK_HOSTS.has(url.hostname);
|
|
3051
3118
|
} catch {
|
|
3052
3119
|
return false;
|
|
3053
3120
|
}
|
|
3054
3121
|
}
|
|
3122
|
+
var reportedInvalidDomains = /* @__PURE__ */ new Set();
|
|
3055
3123
|
function sanitizeCSPDomains(domains) {
|
|
3056
3124
|
if (!domains) return [];
|
|
3057
3125
|
const valid = [];
|
|
3058
3126
|
for (const domain of domains) {
|
|
3059
3127
|
if (validateCSPDomain(domain)) {
|
|
3060
3128
|
valid.push(domain);
|
|
3061
|
-
} else {
|
|
3062
|
-
|
|
3129
|
+
} else if (!reportedInvalidDomains.has(domain)) {
|
|
3130
|
+
reportedInvalidDomains.add(domain);
|
|
3131
|
+
console.warn(
|
|
3132
|
+
`Invalid CSP domain ignored: ${domain} (expected an https:// or wss:// origin, or http:// / ws:// on localhost)`
|
|
3133
|
+
);
|
|
3063
3134
|
}
|
|
3064
3135
|
}
|
|
3065
3136
|
return valid;
|
|
@@ -3596,8 +3667,8 @@ if (__root) {
|
|
|
3596
3667
|
configurable: true, enumerable: true
|
|
3597
3668
|
});
|
|
3598
3669
|
}
|
|
3599
|
-
// 2. Try injected data globals
|
|
3600
|
-
if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
|
|
3670
|
+
// 2. Try injected data globals (null: the page carries no call's data, e.g. compiled at startup)
|
|
3671
|
+
if (__hasData(window.__mcpToolOutput) && window.__mcpToolOutput !== null) { __render(window.__mcpToolOutput); }
|
|
3601
3672
|
// 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
|
|
3602
3673
|
var __bridge = window.FrontMcpBridge;
|
|
3603
3674
|
if (__bridge && typeof __bridge.onToolResult === 'function') {
|
|
@@ -3849,7 +3920,7 @@ createRoot(document.getElementById('root'))
|
|
|
3849
3920
|
return `${componentImport}
|
|
3850
3921
|
import React from 'react';
|
|
3851
3922
|
import { createRoot } from 'react-dom/client';
|
|
3852
|
-
const output = window.__mcpToolOutput;
|
|
3923
|
+
const output = window.__mcpToolOutput || {};
|
|
3853
3924
|
const props = ${propsCode};
|
|
3854
3925
|
createRoot(document.getElementById('root'))
|
|
3855
3926
|
.render(React.createElement(Component, props));`;
|
|
@@ -4222,6 +4293,86 @@ function wrapDetectedContent(value) {
|
|
|
4222
4293
|
}
|
|
4223
4294
|
}
|
|
4224
4295
|
|
|
4296
|
+
// libs/uipack/src/adapters/markdown.ts
|
|
4297
|
+
var SAFE_HREF = /^(https?:|mailto:|\/|#)/i;
|
|
4298
|
+
function emphasis(text) {
|
|
4299
|
+
return text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
4300
|
+
}
|
|
4301
|
+
function inline(text) {
|
|
4302
|
+
const stash = [];
|
|
4303
|
+
const hold = (html2) => `\uE000${stash.push(html2) - 1}\uE000`;
|
|
4304
|
+
let out = escapeHtml(text.replace(/\uE000/g, "")).replace(
|
|
4305
|
+
/`([^`]+)`/g,
|
|
4306
|
+
(_m, code) => hold(`<code>${code}</code>`)
|
|
4307
|
+
);
|
|
4308
|
+
out = out.replace(
|
|
4309
|
+
/\[([^\]]+)\]\(([^)\s]+)\)/g,
|
|
4310
|
+
(_m, label, href) => SAFE_HREF.test(href) ? hold(`<a href="${href}" rel="noopener noreferrer">${emphasis(label)}</a>`) : label
|
|
4311
|
+
);
|
|
4312
|
+
out = emphasis(out);
|
|
4313
|
+
for (let i = 0; i < 2; i++) out = out.replace(/\uE000(\d+)\uE000/g, (_m, n) => stash[Number(n)]);
|
|
4314
|
+
return out;
|
|
4315
|
+
}
|
|
4316
|
+
function markdownToHtml(source) {
|
|
4317
|
+
const lines = source.replace(/\r\n?/g, "\n").split("\n");
|
|
4318
|
+
const blocks = [];
|
|
4319
|
+
let paragraph = [];
|
|
4320
|
+
let list;
|
|
4321
|
+
const flushParagraph = () => {
|
|
4322
|
+
if (paragraph.length > 0) blocks.push(`<p>${inline(paragraph.join(" "))}</p>`);
|
|
4323
|
+
paragraph = [];
|
|
4324
|
+
};
|
|
4325
|
+
const flushList = () => {
|
|
4326
|
+
if (list)
|
|
4327
|
+
blocks.push(`<${list.tag}>${list.items.map((item) => `<li>${inline(item)}</li>`).join("")}</${list.tag}>`);
|
|
4328
|
+
list = void 0;
|
|
4329
|
+
};
|
|
4330
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4331
|
+
const line = lines[i];
|
|
4332
|
+
const fence = /^\s*(`{3,})/.exec(line);
|
|
4333
|
+
if (fence) {
|
|
4334
|
+
flushParagraph();
|
|
4335
|
+
flushList();
|
|
4336
|
+
const fenceLength = fence[1].length;
|
|
4337
|
+
const isClosing = (l) => {
|
|
4338
|
+
const m = /^\s*(`{3,})\s*$/.exec(l);
|
|
4339
|
+
return m !== null && m[1].length >= fenceLength;
|
|
4340
|
+
};
|
|
4341
|
+
const code = [];
|
|
4342
|
+
for (i++; i < lines.length && !isClosing(lines[i]); i++) code.push(lines[i]);
|
|
4343
|
+
blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}
|
|
4344
|
+
</code></pre>`);
|
|
4345
|
+
continue;
|
|
4346
|
+
}
|
|
4347
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
4348
|
+
if (heading) {
|
|
4349
|
+
flushParagraph();
|
|
4350
|
+
flushList();
|
|
4351
|
+
blocks.push(`<h${heading[1].length}>${inline(heading[2].trim())}</h${heading[1].length}>`);
|
|
4352
|
+
continue;
|
|
4353
|
+
}
|
|
4354
|
+
const item = /^\s*([-*+]|\d+\.)\s+(.*)$/.exec(line);
|
|
4355
|
+
if (item) {
|
|
4356
|
+
flushParagraph();
|
|
4357
|
+
const tag = /\d/.test(item[1]) ? "ol" : "ul";
|
|
4358
|
+
if (list && list.tag !== tag) flushList();
|
|
4359
|
+
list ??= { tag, items: [] };
|
|
4360
|
+
list.items.push(item[2]);
|
|
4361
|
+
continue;
|
|
4362
|
+
}
|
|
4363
|
+
if (line.trim() === "") {
|
|
4364
|
+
flushParagraph();
|
|
4365
|
+
flushList();
|
|
4366
|
+
continue;
|
|
4367
|
+
}
|
|
4368
|
+
flushList();
|
|
4369
|
+
paragraph.push(line.trim());
|
|
4370
|
+
}
|
|
4371
|
+
flushParagraph();
|
|
4372
|
+
flushList();
|
|
4373
|
+
return blocks.join("\n");
|
|
4374
|
+
}
|
|
4375
|
+
|
|
4225
4376
|
// libs/uipack/src/adapters/type-detector.ts
|
|
4226
4377
|
function detectUIType(template) {
|
|
4227
4378
|
if (template === null || template === void 0) {
|
|
@@ -4255,9 +4406,15 @@ function detectUIType(template) {
|
|
|
4255
4406
|
}
|
|
4256
4407
|
|
|
4257
4408
|
// libs/uipack/src/adapters/template-renderer.ts
|
|
4258
|
-
function buildCspConfig(resolver) {
|
|
4409
|
+
function buildCspConfig(resolver, extra) {
|
|
4259
4410
|
const cspResourceDomains = ["https://esm.sh"];
|
|
4260
4411
|
const cspConnectDomains = ["https://esm.sh"];
|
|
4412
|
+
for (const domain of extra?.resourceDomains ?? []) {
|
|
4413
|
+
if (!cspResourceDomains.includes(domain)) cspResourceDomains.push(domain);
|
|
4414
|
+
}
|
|
4415
|
+
for (const domain of extra?.connectDomains ?? []) {
|
|
4416
|
+
if (!cspConnectDomains.includes(domain)) cspConnectDomains.push(domain);
|
|
4417
|
+
}
|
|
4261
4418
|
if (resolver && "overrides" in resolver) {
|
|
4262
4419
|
const overrides = resolver.overrides;
|
|
4263
4420
|
if (overrides) {
|
|
@@ -4297,6 +4454,24 @@ function renderUnwrappedResult(rawResult, options) {
|
|
|
4297
4454
|
}
|
|
4298
4455
|
return rawResult;
|
|
4299
4456
|
}
|
|
4457
|
+
var warnedComponentReferences = /* @__PURE__ */ new WeakMap();
|
|
4458
|
+
function warnComponentReference(template, toolName, logger) {
|
|
4459
|
+
const warned = warnedComponentReferences.get(template) ?? /* @__PURE__ */ new Set();
|
|
4460
|
+
if (warned.has(toolName)) return;
|
|
4461
|
+
warned.add(toolName);
|
|
4462
|
+
warnedComponentReferences.set(template, warned);
|
|
4463
|
+
logger.warn(
|
|
4464
|
+
`[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.`
|
|
4465
|
+
);
|
|
4466
|
+
}
|
|
4467
|
+
function isReactElement(value) {
|
|
4468
|
+
return typeof value === "object" && value !== null && "$$typeof" in value;
|
|
4469
|
+
}
|
|
4470
|
+
function isDefinitelyReactComponent(template) {
|
|
4471
|
+
if (typeof template !== "function") return false;
|
|
4472
|
+
const proto = template.prototype;
|
|
4473
|
+
return typeof proto?.render === "function" || template["$$typeof"] !== void 0;
|
|
4474
|
+
}
|
|
4300
4475
|
function renderToolTemplate(options) {
|
|
4301
4476
|
const { toolName, input, output, template, resolver, platformType, sizing } = options;
|
|
4302
4477
|
const uiType = detectUIType(template);
|
|
@@ -4307,30 +4482,41 @@ function renderToolTemplate(options) {
|
|
|
4307
4482
|
output,
|
|
4308
4483
|
includeBridge: true,
|
|
4309
4484
|
resolver,
|
|
4310
|
-
sizing
|
|
4485
|
+
sizing,
|
|
4486
|
+
csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
|
|
4311
4487
|
};
|
|
4312
4488
|
let html2;
|
|
4313
4489
|
let hash = "";
|
|
4314
4490
|
let size = 0;
|
|
4315
4491
|
if (typeof template === "object" && template !== null && "file" in template) {
|
|
4316
|
-
const cspConfig = buildCspConfig(resolver);
|
|
4317
4492
|
const result = renderComponent(
|
|
4318
4493
|
{ source: template, inlineReact: resourceMode === "inline" },
|
|
4319
|
-
{ ...shellConfig, csp:
|
|
4494
|
+
{ ...shellConfig, csp: buildCspConfig(resolver, options.csp) }
|
|
4320
4495
|
);
|
|
4321
4496
|
html2 = result.html;
|
|
4322
4497
|
hash = result.hash;
|
|
4323
4498
|
size = result.size;
|
|
4324
4499
|
} else if (typeof template === "function") {
|
|
4325
|
-
|
|
4500
|
+
const helpers = createTemplateHelpers();
|
|
4501
|
+
const ctx = { input: input ?? {}, output: output ?? {}, helpers };
|
|
4502
|
+
let rawResult;
|
|
4503
|
+
let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
|
|
4504
|
+
if (isHtmlBuilder) {
|
|
4505
|
+
try {
|
|
4506
|
+
rawResult = template(ctx);
|
|
4507
|
+
isHtmlBuilder = !isReactElement(rawResult);
|
|
4508
|
+
} catch (error) {
|
|
4509
|
+
if (uiType !== "react") throw error;
|
|
4510
|
+
isHtmlBuilder = false;
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
if (!isHtmlBuilder) {
|
|
4514
|
+
warnComponentReference(template, toolName, options.logger ?? console);
|
|
4326
4515
|
const shellResult = buildShell('<div id="root"></div>', shellConfig);
|
|
4327
4516
|
html2 = shellResult.html;
|
|
4328
4517
|
hash = shellResult.hash;
|
|
4329
4518
|
size = shellResult.size;
|
|
4330
4519
|
} else {
|
|
4331
|
-
const helpers = createTemplateHelpers();
|
|
4332
|
-
const ctx = { input, output, helpers };
|
|
4333
|
-
const rawResult = template(ctx);
|
|
4334
4520
|
const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
|
|
4335
4521
|
if (wrapped) {
|
|
4336
4522
|
html2 = wrapped;
|
|
@@ -4342,7 +4528,7 @@ function renderToolTemplate(options) {
|
|
|
4342
4528
|
}
|
|
4343
4529
|
}
|
|
4344
4530
|
} else if (typeof template === "string") {
|
|
4345
|
-
const shellResult = buildShell(template, shellConfig);
|
|
4531
|
+
const shellResult = buildShell(uiType === "markdown" ? markdownToHtml(template) : template, shellConfig);
|
|
4346
4532
|
html2 = shellResult.html;
|
|
4347
4533
|
hash = shellResult.hash;
|
|
4348
4534
|
size = shellResult.size;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/uipack",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
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,7 +61,7 @@
|
|
|
61
61
|
"typescript": "^5.9.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@frontmcp/lazy-zod": "1.
|
|
64
|
+
"@frontmcp/lazy-zod": "1.9.0",
|
|
65
65
|
"esbuild": ">=0.27.0 <1"
|
|
66
66
|
},
|
|
67
67
|
"peerDependenciesMeta": {
|
package/shell/csp.d.ts
CHANGED
|
@@ -28,10 +28,15 @@ export declare function buildCSPDirectives(csp?: CSPConfig): string[];
|
|
|
28
28
|
export declare function buildCSPMetaTag(csp?: CSPConfig): string;
|
|
29
29
|
/**
|
|
30
30
|
* Validate CSP domain format.
|
|
31
|
+
*
|
|
32
|
+
* Accepts `https://` and `wss://` origins (a WebSocket API needs `wss://` in `connect-src`),
|
|
33
|
+
* their `https://*.` / `wss://*.` wildcard forms, and `http://` / `ws://` origins on a loopback
|
|
34
|
+
* host (`localhost`, `127.0.0.1`, `[::1]`) for local development. A value with a query or a
|
|
35
|
+
* fragment is rejected.
|
|
31
36
|
*/
|
|
32
37
|
export declare function validateCSPDomain(domain: string): boolean;
|
|
33
38
|
/**
|
|
34
|
-
* Filter
|
|
39
|
+
* Filter out invalid CSP domains, warning once per domain.
|
|
35
40
|
*/
|
|
36
41
|
export declare function sanitizeCSPDomains(domains: string[] | undefined): string[];
|
|
37
42
|
//# sourceMappingURL=csp.d.ts.map
|
package/shell/csp.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csp.d.ts","sourceRoot":"","sources":["../../src/shell/csp.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC;;GAEG;AACH,eAAO,MAAM,mBAAmB,sJAMtB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,sBAAsB,uPAQzB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,0BAA0B,4MAQ7B,CAAC;AAEX;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,CAAC,EAAE,SAAS,GAAG,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"csp.d.ts","sourceRoot":"","sources":["../../src/shell/csp.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC;;GAEG;AACH,eAAO,MAAM,mBAAmB,sJAMtB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,sBAAsB,uPAQzB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,0BAA0B,4MAQ7B,CAAC;AAEX;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,CAAC,EAAE,SAAS,GAAG,MAAM,EAAE,CA8B5D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,CAAC,EAAE,SAAS,GAAG,MAAM,CAIvD;AAKD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAkBzD;AAKD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,CAgB1E"}
|