@frontmcp/uipack 1.8.0 → 1.8.2
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/README.md +12 -11
- package/adapters/content-detector.d.ts.map +1 -1
- package/adapters/index.js +92 -42
- package/adapters/template-renderer.d.ts +11 -1
- package/adapters/template-renderer.d.ts.map +1 -1
- package/component/index.js +13 -10
- package/esm/adapters/index.mjs +92 -42
- package/esm/component/index.mjs +13 -10
- package/esm/index.mjs +96 -40
- package/esm/package.json +2 -2
- package/esm/shell/index.mjs +54 -25
- package/esm/utils/index.mjs +5 -1
- package/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js +99 -40
- package/package.json +2 -2
- package/shell/custom-shell-applier.d.ts +7 -6
- package/shell/custom-shell-applier.d.ts.map +1 -1
- package/shell/data-injector.d.ts +5 -0
- package/shell/data-injector.d.ts.map +1 -1
- package/shell/index.d.ts +1 -0
- package/shell/index.d.ts.map +1 -1
- package/shell/index.js +57 -25
- package/shell/trusted-html.d.ts +40 -0
- package/shell/trusted-html.d.ts.map +1 -0
- package/types/ui-config.d.ts +30 -2
- package/types/ui-config.d.ts.map +1 -1
- package/utils/index.d.ts +7 -0
- package/utils/index.d.ts.map +1 -1
- package/utils/index.js +5 -1
package/esm/adapters/index.mjs
CHANGED
|
@@ -482,8 +482,9 @@ function escapeHtml(str) {
|
|
|
482
482
|
const s = String(str);
|
|
483
483
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
484
484
|
}
|
|
485
|
-
|
|
486
|
-
|
|
485
|
+
var SCRIPT_UNSAFE_CHARACTERS = /[<>&\u2028\u2029]/g;
|
|
486
|
+
function toUnicodeEscape(character) {
|
|
487
|
+
return `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
|
487
488
|
}
|
|
488
489
|
function safeJsonForScript(value) {
|
|
489
490
|
if (value === void 0) {
|
|
@@ -499,7 +500,7 @@ function safeJsonForScript(value) {
|
|
|
499
500
|
if (jsonString === void 0) {
|
|
500
501
|
return "null";
|
|
501
502
|
}
|
|
502
|
-
return
|
|
503
|
+
return jsonString.replace(SCRIPT_UNSAFE_CHARACTERS, toUnicodeEscape);
|
|
503
504
|
} catch {
|
|
504
505
|
return '{"error":"Value could not be serialized"}';
|
|
505
506
|
}
|
|
@@ -1796,14 +1797,16 @@ var REQUIRED_PLACEHOLDERS = ["CONTENT"];
|
|
|
1796
1797
|
var OPTIONAL_PLACEHOLDERS = ["CSP", "DATA", "BRIDGE", "TITLE"];
|
|
1797
1798
|
|
|
1798
1799
|
// libs/uipack/src/shell/custom-shell-applier.ts
|
|
1800
|
+
var PLACEHOLDER_PATTERN = new RegExp(`\\{\\{(${SHELL_PLACEHOLDER_NAMES.join("|")})\\}\\}`, "g");
|
|
1801
|
+
var VALUE_KEYS = {
|
|
1802
|
+
CSP: "csp",
|
|
1803
|
+
DATA: "data",
|
|
1804
|
+
BRIDGE: "bridge",
|
|
1805
|
+
CONTENT: "content",
|
|
1806
|
+
TITLE: "title"
|
|
1807
|
+
};
|
|
1799
1808
|
function applyShellTemplate(template, values) {
|
|
1800
|
-
|
|
1801
|
-
result = result.replaceAll(SHELL_PLACEHOLDERS.CSP, values.csp);
|
|
1802
|
-
result = result.replaceAll(SHELL_PLACEHOLDERS.DATA, values.data);
|
|
1803
|
-
result = result.replaceAll(SHELL_PLACEHOLDERS.BRIDGE, values.bridge);
|
|
1804
|
-
result = result.replaceAll(SHELL_PLACEHOLDERS.CONTENT, values.content);
|
|
1805
|
-
result = result.replaceAll(SHELL_PLACEHOLDERS.TITLE, values.title);
|
|
1806
|
-
return result;
|
|
1809
|
+
return template.replace(PLACEHOLDER_PATTERN, (_token, name) => values[VALUE_KEYS[name]]);
|
|
1807
1810
|
}
|
|
1808
1811
|
|
|
1809
1812
|
// libs/uipack/src/shell/custom-shell-validator.ts
|
|
@@ -1822,6 +1825,29 @@ function validateShellTemplate(template) {
|
|
|
1822
1825
|
};
|
|
1823
1826
|
}
|
|
1824
1827
|
|
|
1828
|
+
// libs/uipack/src/shell/trusted-html.ts
|
|
1829
|
+
var TRUSTED_HTML = /* @__PURE__ */ Symbol.for("@frontmcp/uipack/trusted-html");
|
|
1830
|
+
function trustedHtml(markup) {
|
|
1831
|
+
const value = String(markup);
|
|
1832
|
+
return Object.freeze({ [TRUSTED_HTML]: value, toString: () => value });
|
|
1833
|
+
}
|
|
1834
|
+
function isTrustedHtml(value) {
|
|
1835
|
+
return typeof value === "object" && value !== null && typeof value[TRUSTED_HTML] === "string";
|
|
1836
|
+
}
|
|
1837
|
+
function interpolate(value) {
|
|
1838
|
+
if (isTrustedHtml(value)) return value[TRUSTED_HTML];
|
|
1839
|
+
if (Array.isArray(value)) return value.map(interpolate).join("");
|
|
1840
|
+
if (value === false) return "";
|
|
1841
|
+
return escapeHtml(value);
|
|
1842
|
+
}
|
|
1843
|
+
function html(strings, ...values) {
|
|
1844
|
+
let markup = strings[0];
|
|
1845
|
+
for (let index = 0; index < values.length; index++) {
|
|
1846
|
+
markup += interpolate(values[index]) + strings[index + 1];
|
|
1847
|
+
}
|
|
1848
|
+
return trustedHtml(markup);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1825
1851
|
// libs/uipack/src/shell/data-injector.ts
|
|
1826
1852
|
function hasSizing(sizing) {
|
|
1827
1853
|
if (!sizing) return false;
|
|
@@ -1875,9 +1901,9 @@ function createTemplateHelpers() {
|
|
|
1875
1901
|
uniqueId: (prefix = "mcp") => {
|
|
1876
1902
|
return `${prefix}-${++_uniqueIdCounter}`;
|
|
1877
1903
|
},
|
|
1878
|
-
jsonEmbed: (data) =>
|
|
1879
|
-
|
|
1880
|
-
|
|
1904
|
+
jsonEmbed: (data) => safeJsonForScript(data),
|
|
1905
|
+
html,
|
|
1906
|
+
trustedHtml
|
|
1881
1907
|
};
|
|
1882
1908
|
}
|
|
1883
1909
|
|
|
@@ -1955,12 +1981,12 @@ function buildShell(content, config) {
|
|
|
1955
1981
|
sizing
|
|
1956
1982
|
});
|
|
1957
1983
|
if (!withShell) {
|
|
1958
|
-
const
|
|
1984
|
+
const html3 = `${dataScript}
|
|
1959
1985
|
${content}`;
|
|
1960
1986
|
return {
|
|
1961
|
-
html:
|
|
1962
|
-
hash: simpleHash(
|
|
1963
|
-
size: Buffer.byteLength(
|
|
1987
|
+
html: html3,
|
|
1988
|
+
hash: simpleHash(html3),
|
|
1989
|
+
size: Buffer.byteLength(html3, "utf-8")
|
|
1964
1990
|
};
|
|
1965
1991
|
}
|
|
1966
1992
|
if (customShell) {
|
|
@@ -1993,7 +2019,7 @@ ${content}`;
|
|
|
1993
2019
|
const bridgeScript = generateBridgeIIFE({ minify: true });
|
|
1994
2020
|
headParts.push(`<script>${bridgeScript}</script>`);
|
|
1995
2021
|
}
|
|
1996
|
-
const
|
|
2022
|
+
const html2 = `<!DOCTYPE html>
|
|
1997
2023
|
<html lang="en">
|
|
1998
2024
|
<head>
|
|
1999
2025
|
${headParts.map((p) => ` ${p}`).join("\n")}
|
|
@@ -2003,9 +2029,9 @@ ${content}
|
|
|
2003
2029
|
</body>
|
|
2004
2030
|
</html>`;
|
|
2005
2031
|
return {
|
|
2006
|
-
html,
|
|
2007
|
-
hash: simpleHash(
|
|
2008
|
-
size: Buffer.byteLength(
|
|
2032
|
+
html: html2,
|
|
2033
|
+
hash: simpleHash(html2),
|
|
2034
|
+
size: Buffer.byteLength(html2, "utf-8")
|
|
2009
2035
|
};
|
|
2010
2036
|
}
|
|
2011
2037
|
function buildCustomShell(content, customShell, ctx) {
|
|
@@ -2038,7 +2064,7 @@ ${sizingStyle}` : dataScript;
|
|
|
2038
2064
|
const bridgeScript = generateBridgeIIFE({ minify: true });
|
|
2039
2065
|
bridgeHtml = `<script>${bridgeScript}</script>`;
|
|
2040
2066
|
}
|
|
2041
|
-
const
|
|
2067
|
+
const html2 = applyShellTemplate(template, {
|
|
2042
2068
|
csp: cspTag,
|
|
2043
2069
|
data: dataWithSizing,
|
|
2044
2070
|
bridge: bridgeHtml,
|
|
@@ -2046,9 +2072,9 @@ ${sizingStyle}` : dataScript;
|
|
|
2046
2072
|
title: ctx.title ? escapeHtmlForTag(ctx.title) : ""
|
|
2047
2073
|
});
|
|
2048
2074
|
return {
|
|
2049
|
-
html,
|
|
2050
|
-
hash: simpleHash(
|
|
2051
|
-
size: Buffer.byteLength(
|
|
2075
|
+
html: html2,
|
|
2076
|
+
hash: simpleHash(html2),
|
|
2077
|
+
size: Buffer.byteLength(html2, "utf-8")
|
|
2052
2078
|
};
|
|
2053
2079
|
}
|
|
2054
2080
|
function simpleHash(str) {
|
|
@@ -2345,10 +2371,10 @@ function resolveFileSource(source, options = {}) {
|
|
|
2345
2371
|
};
|
|
2346
2372
|
}
|
|
2347
2373
|
function resolveFunctionSource(source, input, output) {
|
|
2348
|
-
const
|
|
2374
|
+
const html2 = source(input, output);
|
|
2349
2375
|
return {
|
|
2350
2376
|
mode: "inline",
|
|
2351
|
-
html,
|
|
2377
|
+
html: html2,
|
|
2352
2378
|
exportName: "default",
|
|
2353
2379
|
meta: { mcpAware: false, renderer: "html" },
|
|
2354
2380
|
peerDependencies: []
|
|
@@ -2553,6 +2579,7 @@ var MERMAID_PREFIXES = [
|
|
|
2553
2579
|
"xychart"
|
|
2554
2580
|
];
|
|
2555
2581
|
var PDF_MAGIC = "JVBERi";
|
|
2582
|
+
var BASE64_PATTERN = /^[A-Za-z0-9+/=\r\n]+$/;
|
|
2556
2583
|
function detectContentType(value) {
|
|
2557
2584
|
if (value === null || value === void 0) {
|
|
2558
2585
|
return "text";
|
|
@@ -2567,7 +2594,7 @@ function detectContentType(value) {
|
|
|
2567
2594
|
if (typeof value === "string") {
|
|
2568
2595
|
const trimmed = value.trimStart();
|
|
2569
2596
|
if (trimmed.startsWith(PDF_MAGIC)) {
|
|
2570
|
-
return "pdf";
|
|
2597
|
+
return BASE64_PATTERN.test(trimmed.trimEnd()) ? "pdf" : "text";
|
|
2571
2598
|
}
|
|
2572
2599
|
for (const prefix of MERMAID_PREFIXES) {
|
|
2573
2600
|
if (trimmed.startsWith(prefix)) {
|
|
@@ -2629,7 +2656,7 @@ function buildPdfHtml(base64) {
|
|
|
2629
2656
|
import * as pdfjsLib from 'https://esm.sh/pdfjs-dist@4/build/pdf.min.mjs';
|
|
2630
2657
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://esm.sh/pdfjs-dist@4/build/pdf.worker.min.mjs';
|
|
2631
2658
|
|
|
2632
|
-
const base64 = ${
|
|
2659
|
+
const base64 = ${safeJsonForScript(base64)};
|
|
2633
2660
|
const binaryStr = atob(base64);
|
|
2634
2661
|
const bytes = new Uint8Array(binaryStr.length);
|
|
2635
2662
|
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
|
|
@@ -2724,6 +2751,30 @@ function buildCspConfig(resolver) {
|
|
|
2724
2751
|
}
|
|
2725
2752
|
return { resourceDomains: cspResourceDomains, connectDomains: cspConnectDomains };
|
|
2726
2753
|
}
|
|
2754
|
+
var STRING_RESULT_DOCS = "https://docs.agentfront.dev/frontmcp/guides/building-tool-ui#trusted-markup";
|
|
2755
|
+
var noticedStringResultTools = /* @__PURE__ */ new Set();
|
|
2756
|
+
function noticeStringResult(toolName, logger) {
|
|
2757
|
+
if (noticedStringResultTools.has(toolName)) return;
|
|
2758
|
+
noticedStringResultTools.add(toolName);
|
|
2759
|
+
logger.warn(
|
|
2760
|
+
`[frontmcp] The UI template of tool "${toolName}" returned a plain string containing markup, which is rendered as HTML. FrontMCP 1.9 will HTML-escape plain string results by default. Build the markup with ctx.helpers.html\`\u2026\` (interpolated values are escaped) or wrap safe markup with ctx.helpers.trustedHtml(), then set ui.escapeStringResults: true (or escapeStringResults: false to keep the current behaviour without this notice). See ${STRING_RESULT_DOCS}`
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
function renderUnwrappedResult(rawResult, options) {
|
|
2764
|
+
if (isTrustedHtml(rawResult)) {
|
|
2765
|
+
return String(rawResult);
|
|
2766
|
+
}
|
|
2767
|
+
if (typeof rawResult !== "string") {
|
|
2768
|
+
return `<pre>${escapeHtml(JSON.stringify(rawResult, null, 2))}</pre>`;
|
|
2769
|
+
}
|
|
2770
|
+
if (detectContentType(rawResult) !== "html" || options.escapeStringResults === true) {
|
|
2771
|
+
return escapeHtml(rawResult);
|
|
2772
|
+
}
|
|
2773
|
+
if (options.escapeStringResults === void 0) {
|
|
2774
|
+
noticeStringResult(options.toolName, options.logger ?? console);
|
|
2775
|
+
}
|
|
2776
|
+
return rawResult;
|
|
2777
|
+
}
|
|
2727
2778
|
function renderToolTemplate(options) {
|
|
2728
2779
|
const { toolName, input, output, template, resolver, platformType, sizing } = options;
|
|
2729
2780
|
const uiType = detectUIType(template);
|
|
@@ -2736,7 +2787,7 @@ function renderToolTemplate(options) {
|
|
|
2736
2787
|
resolver,
|
|
2737
2788
|
sizing
|
|
2738
2789
|
};
|
|
2739
|
-
let
|
|
2790
|
+
let html2;
|
|
2740
2791
|
let hash = "";
|
|
2741
2792
|
let size = 0;
|
|
2742
2793
|
if (typeof template === "object" && template !== null && "file" in template) {
|
|
@@ -2745,44 +2796,43 @@ function renderToolTemplate(options) {
|
|
|
2745
2796
|
{ source: template, inlineReact: resourceMode === "inline" },
|
|
2746
2797
|
{ ...shellConfig, csp: cspConfig }
|
|
2747
2798
|
);
|
|
2748
|
-
|
|
2799
|
+
html2 = result.html;
|
|
2749
2800
|
hash = result.hash;
|
|
2750
2801
|
size = result.size;
|
|
2751
2802
|
} else if (typeof template === "function") {
|
|
2752
2803
|
if (uiType === "react") {
|
|
2753
2804
|
const shellResult = buildShell('<div id="root"></div>', shellConfig);
|
|
2754
|
-
|
|
2805
|
+
html2 = shellResult.html;
|
|
2755
2806
|
hash = shellResult.hash;
|
|
2756
2807
|
size = shellResult.size;
|
|
2757
2808
|
} else {
|
|
2758
2809
|
const helpers = createTemplateHelpers();
|
|
2759
2810
|
const ctx = { input, output, helpers };
|
|
2760
2811
|
const rawResult = template(ctx);
|
|
2761
|
-
const wrapped = wrapDetectedContent(rawResult);
|
|
2812
|
+
const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
|
|
2762
2813
|
if (wrapped) {
|
|
2763
|
-
|
|
2814
|
+
html2 = wrapped;
|
|
2764
2815
|
} else {
|
|
2765
|
-
const
|
|
2766
|
-
|
|
2767
|
-
html = shellResult.html;
|
|
2816
|
+
const shellResult = buildShell(renderUnwrappedResult(rawResult, options), shellConfig);
|
|
2817
|
+
html2 = shellResult.html;
|
|
2768
2818
|
hash = shellResult.hash;
|
|
2769
2819
|
size = shellResult.size;
|
|
2770
2820
|
}
|
|
2771
2821
|
}
|
|
2772
2822
|
} else if (typeof template === "string") {
|
|
2773
2823
|
const shellResult = buildShell(template, shellConfig);
|
|
2774
|
-
|
|
2824
|
+
html2 = shellResult.html;
|
|
2775
2825
|
hash = shellResult.hash;
|
|
2776
2826
|
size = shellResult.size;
|
|
2777
2827
|
} else {
|
|
2778
2828
|
const shellResult = buildShell('<div id="root"></div>', shellConfig);
|
|
2779
|
-
|
|
2829
|
+
html2 = shellResult.html;
|
|
2780
2830
|
hash = shellResult.hash;
|
|
2781
2831
|
size = shellResult.size;
|
|
2782
2832
|
}
|
|
2783
2833
|
const htmlKey = "ui/html";
|
|
2784
2834
|
const meta = {
|
|
2785
|
-
[htmlKey]:
|
|
2835
|
+
[htmlKey]: html2,
|
|
2786
2836
|
"ui/type": uiType,
|
|
2787
2837
|
"ui/mimeType": MCP_APPS_MIME_TYPE
|
|
2788
2838
|
};
|
|
@@ -2793,10 +2843,10 @@ function renderToolTemplate(options) {
|
|
|
2793
2843
|
if (sizing.aspectRatio !== void 0) meta["ui/aspectRatio"] = sizing.aspectRatio;
|
|
2794
2844
|
}
|
|
2795
2845
|
return {
|
|
2796
|
-
html,
|
|
2846
|
+
html: html2,
|
|
2797
2847
|
uiType,
|
|
2798
2848
|
hash,
|
|
2799
|
-
size: size || Buffer.byteLength(
|
|
2849
|
+
size: size || Buffer.byteLength(html2, "utf8"),
|
|
2800
2850
|
meta
|
|
2801
2851
|
};
|
|
2802
2852
|
}
|
package/esm/component/index.mjs
CHANGED
|
@@ -421,8 +421,9 @@ function createEsmShResolver(options = {}) {
|
|
|
421
421
|
}
|
|
422
422
|
|
|
423
423
|
// libs/uipack/src/utils/index.ts
|
|
424
|
-
|
|
425
|
-
|
|
424
|
+
var SCRIPT_UNSAFE_CHARACTERS = /[<>&\u2028\u2029]/g;
|
|
425
|
+
function toUnicodeEscape(character) {
|
|
426
|
+
return `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
|
426
427
|
}
|
|
427
428
|
function safeJsonForScript(value) {
|
|
428
429
|
if (value === void 0) {
|
|
@@ -438,7 +439,7 @@ function safeJsonForScript(value) {
|
|
|
438
439
|
if (jsonString === void 0) {
|
|
439
440
|
return "null";
|
|
440
441
|
}
|
|
441
|
-
return
|
|
442
|
+
return jsonString.replace(SCRIPT_UNSAFE_CHARACTERS, toUnicodeEscape);
|
|
442
443
|
} catch {
|
|
443
444
|
return '{"error":"Value could not be serialized"}';
|
|
444
445
|
}
|
|
@@ -2041,14 +2042,16 @@ var REQUIRED_PLACEHOLDERS = ["CONTENT"];
|
|
|
2041
2042
|
var OPTIONAL_PLACEHOLDERS = ["CSP", "DATA", "BRIDGE", "TITLE"];
|
|
2042
2043
|
|
|
2043
2044
|
// libs/uipack/src/shell/custom-shell-applier.ts
|
|
2045
|
+
var PLACEHOLDER_PATTERN = new RegExp(`\\{\\{(${SHELL_PLACEHOLDER_NAMES.join("|")})\\}\\}`, "g");
|
|
2046
|
+
var VALUE_KEYS = {
|
|
2047
|
+
CSP: "csp",
|
|
2048
|
+
DATA: "data",
|
|
2049
|
+
BRIDGE: "bridge",
|
|
2050
|
+
CONTENT: "content",
|
|
2051
|
+
TITLE: "title"
|
|
2052
|
+
};
|
|
2044
2053
|
function applyShellTemplate(template, values) {
|
|
2045
|
-
|
|
2046
|
-
result = result.replaceAll(SHELL_PLACEHOLDERS.CSP, values.csp);
|
|
2047
|
-
result = result.replaceAll(SHELL_PLACEHOLDERS.DATA, values.data);
|
|
2048
|
-
result = result.replaceAll(SHELL_PLACEHOLDERS.BRIDGE, values.bridge);
|
|
2049
|
-
result = result.replaceAll(SHELL_PLACEHOLDERS.CONTENT, values.content);
|
|
2050
|
-
result = result.replaceAll(SHELL_PLACEHOLDERS.TITLE, values.title);
|
|
2051
|
-
return result;
|
|
2054
|
+
return template.replace(PLACEHOLDER_PATTERN, (_token, name) => values[VALUE_KEYS[name]]);
|
|
2052
2055
|
}
|
|
2053
2056
|
|
|
2054
2057
|
// libs/uipack/src/shell/custom-shell-validator.ts
|
package/esm/index.mjs
CHANGED
|
@@ -107,6 +107,10 @@ function escapeJsString(str) {
|
|
|
107
107
|
function escapeScriptClose(jsonString) {
|
|
108
108
|
return jsonString.replace(/<\//g, "<\\/");
|
|
109
109
|
}
|
|
110
|
+
var SCRIPT_UNSAFE_CHARACTERS = /[<>&\u2028\u2029]/g;
|
|
111
|
+
function toUnicodeEscape(character) {
|
|
112
|
+
return `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
|
113
|
+
}
|
|
110
114
|
function safeJsonForScript(value) {
|
|
111
115
|
if (value === void 0) {
|
|
112
116
|
return "null";
|
|
@@ -121,7 +125,7 @@ function safeJsonForScript(value) {
|
|
|
121
125
|
if (jsonString === void 0) {
|
|
122
126
|
return "null";
|
|
123
127
|
}
|
|
124
|
-
return
|
|
128
|
+
return jsonString.replace(SCRIPT_UNSAFE_CHARACTERS, toUnicodeEscape);
|
|
125
129
|
} catch {
|
|
126
130
|
return '{"error":"Value could not be serialized"}';
|
|
127
131
|
}
|
|
@@ -2885,14 +2889,16 @@ function isNpmShellSource(source) {
|
|
|
2885
2889
|
}
|
|
2886
2890
|
|
|
2887
2891
|
// libs/uipack/src/shell/custom-shell-applier.ts
|
|
2892
|
+
var PLACEHOLDER_PATTERN = new RegExp(`\\{\\{(${SHELL_PLACEHOLDER_NAMES.join("|")})\\}\\}`, "g");
|
|
2893
|
+
var VALUE_KEYS = {
|
|
2894
|
+
CSP: "csp",
|
|
2895
|
+
DATA: "data",
|
|
2896
|
+
BRIDGE: "bridge",
|
|
2897
|
+
CONTENT: "content",
|
|
2898
|
+
TITLE: "title"
|
|
2899
|
+
};
|
|
2888
2900
|
function applyShellTemplate(template, values) {
|
|
2889
|
-
|
|
2890
|
-
result = result.replaceAll(SHELL_PLACEHOLDERS.CSP, values.csp);
|
|
2891
|
-
result = result.replaceAll(SHELL_PLACEHOLDERS.DATA, values.data);
|
|
2892
|
-
result = result.replaceAll(SHELL_PLACEHOLDERS.BRIDGE, values.bridge);
|
|
2893
|
-
result = result.replaceAll(SHELL_PLACEHOLDERS.CONTENT, values.content);
|
|
2894
|
-
result = result.replaceAll(SHELL_PLACEHOLDERS.TITLE, values.title);
|
|
2895
|
-
return result;
|
|
2901
|
+
return template.replace(PLACEHOLDER_PATTERN, (_token, name) => values[VALUE_KEYS[name]]);
|
|
2896
2902
|
}
|
|
2897
2903
|
|
|
2898
2904
|
// libs/uipack/src/shell/custom-shell-validator.ts
|
|
@@ -2911,6 +2917,29 @@ function validateShellTemplate(template) {
|
|
|
2911
2917
|
};
|
|
2912
2918
|
}
|
|
2913
2919
|
|
|
2920
|
+
// libs/uipack/src/shell/trusted-html.ts
|
|
2921
|
+
var TRUSTED_HTML = /* @__PURE__ */ Symbol.for("@frontmcp/uipack/trusted-html");
|
|
2922
|
+
function trustedHtml(markup) {
|
|
2923
|
+
const value = String(markup);
|
|
2924
|
+
return Object.freeze({ [TRUSTED_HTML]: value, toString: () => value });
|
|
2925
|
+
}
|
|
2926
|
+
function isTrustedHtml(value) {
|
|
2927
|
+
return typeof value === "object" && value !== null && typeof value[TRUSTED_HTML] === "string";
|
|
2928
|
+
}
|
|
2929
|
+
function interpolate(value) {
|
|
2930
|
+
if (isTrustedHtml(value)) return value[TRUSTED_HTML];
|
|
2931
|
+
if (Array.isArray(value)) return value.map(interpolate).join("");
|
|
2932
|
+
if (value === false) return "";
|
|
2933
|
+
return escapeHtml(value);
|
|
2934
|
+
}
|
|
2935
|
+
function html(strings, ...values) {
|
|
2936
|
+
let markup = strings[0];
|
|
2937
|
+
for (let index = 0; index < values.length; index++) {
|
|
2938
|
+
markup += interpolate(values[index]) + strings[index + 1];
|
|
2939
|
+
}
|
|
2940
|
+
return trustedHtml(markup);
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2914
2943
|
// libs/uipack/src/shell/data-injector.ts
|
|
2915
2944
|
function hasSizing(sizing) {
|
|
2916
2945
|
if (!sizing) return false;
|
|
@@ -2964,9 +2993,9 @@ function createTemplateHelpers() {
|
|
|
2964
2993
|
uniqueId: (prefix = "mcp") => {
|
|
2965
2994
|
return `${prefix}-${++_uniqueIdCounter}`;
|
|
2966
2995
|
},
|
|
2967
|
-
jsonEmbed: (data) =>
|
|
2968
|
-
|
|
2969
|
-
|
|
2996
|
+
jsonEmbed: (data) => safeJsonForScript(data),
|
|
2997
|
+
html,
|
|
2998
|
+
trustedHtml
|
|
2970
2999
|
};
|
|
2971
3000
|
}
|
|
2972
3001
|
|
|
@@ -3044,12 +3073,12 @@ function buildShell(content, config) {
|
|
|
3044
3073
|
sizing
|
|
3045
3074
|
});
|
|
3046
3075
|
if (!withShell) {
|
|
3047
|
-
const
|
|
3076
|
+
const html3 = `${dataScript}
|
|
3048
3077
|
${content}`;
|
|
3049
3078
|
return {
|
|
3050
|
-
html:
|
|
3051
|
-
hash: simpleHash(
|
|
3052
|
-
size: Buffer.byteLength(
|
|
3079
|
+
html: html3,
|
|
3080
|
+
hash: simpleHash(html3),
|
|
3081
|
+
size: Buffer.byteLength(html3, "utf-8")
|
|
3053
3082
|
};
|
|
3054
3083
|
}
|
|
3055
3084
|
if (customShell) {
|
|
@@ -3082,7 +3111,7 @@ ${content}`;
|
|
|
3082
3111
|
const bridgeScript = generateBridgeIIFE({ minify: true });
|
|
3083
3112
|
headParts.push(`<script>${bridgeScript}</script>`);
|
|
3084
3113
|
}
|
|
3085
|
-
const
|
|
3114
|
+
const html2 = `<!DOCTYPE html>
|
|
3086
3115
|
<html lang="en">
|
|
3087
3116
|
<head>
|
|
3088
3117
|
${headParts.map((p) => ` ${p}`).join("\n")}
|
|
@@ -3092,9 +3121,9 @@ ${content}
|
|
|
3092
3121
|
</body>
|
|
3093
3122
|
</html>`;
|
|
3094
3123
|
return {
|
|
3095
|
-
html,
|
|
3096
|
-
hash: simpleHash(
|
|
3097
|
-
size: Buffer.byteLength(
|
|
3124
|
+
html: html2,
|
|
3125
|
+
hash: simpleHash(html2),
|
|
3126
|
+
size: Buffer.byteLength(html2, "utf-8")
|
|
3098
3127
|
};
|
|
3099
3128
|
}
|
|
3100
3129
|
function buildCustomShell(content, customShell, ctx) {
|
|
@@ -3127,7 +3156,7 @@ ${sizingStyle}` : dataScript;
|
|
|
3127
3156
|
const bridgeScript = generateBridgeIIFE({ minify: true });
|
|
3128
3157
|
bridgeHtml = `<script>${bridgeScript}</script>`;
|
|
3129
3158
|
}
|
|
3130
|
-
const
|
|
3159
|
+
const html2 = applyShellTemplate(template, {
|
|
3131
3160
|
csp: cspTag,
|
|
3132
3161
|
data: dataWithSizing,
|
|
3133
3162
|
bridge: bridgeHtml,
|
|
@@ -3135,9 +3164,9 @@ ${sizingStyle}` : dataScript;
|
|
|
3135
3164
|
title: ctx.title ? escapeHtmlForTag(ctx.title) : ""
|
|
3136
3165
|
});
|
|
3137
3166
|
return {
|
|
3138
|
-
html,
|
|
3139
|
-
hash: simpleHash(
|
|
3140
|
-
size: Buffer.byteLength(
|
|
3167
|
+
html: html2,
|
|
3168
|
+
hash: simpleHash(html2),
|
|
3169
|
+
size: Buffer.byteLength(html2, "utf-8")
|
|
3141
3170
|
};
|
|
3142
3171
|
}
|
|
3143
3172
|
function simpleHash(str) {
|
|
@@ -3544,10 +3573,10 @@ function resolveFileSource(source, options = {}) {
|
|
|
3544
3573
|
};
|
|
3545
3574
|
}
|
|
3546
3575
|
function resolveFunctionSource(source, input, output) {
|
|
3547
|
-
const
|
|
3576
|
+
const html2 = source(input, output);
|
|
3548
3577
|
return {
|
|
3549
3578
|
mode: "inline",
|
|
3550
|
-
html,
|
|
3579
|
+
html: html2,
|
|
3551
3580
|
exportName: "default",
|
|
3552
3581
|
meta: { mcpAware: false, renderer: "html" },
|
|
3553
3582
|
peerDependencies: []
|
|
@@ -3821,6 +3850,7 @@ var MERMAID_PREFIXES = [
|
|
|
3821
3850
|
"xychart"
|
|
3822
3851
|
];
|
|
3823
3852
|
var PDF_MAGIC = "JVBERi";
|
|
3853
|
+
var BASE64_PATTERN = /^[A-Za-z0-9+/=\r\n]+$/;
|
|
3824
3854
|
function detectContentType(value) {
|
|
3825
3855
|
if (value === null || value === void 0) {
|
|
3826
3856
|
return "text";
|
|
@@ -3835,7 +3865,7 @@ function detectContentType(value) {
|
|
|
3835
3865
|
if (typeof value === "string") {
|
|
3836
3866
|
const trimmed = value.trimStart();
|
|
3837
3867
|
if (trimmed.startsWith(PDF_MAGIC)) {
|
|
3838
|
-
return "pdf";
|
|
3868
|
+
return BASE64_PATTERN.test(trimmed.trimEnd()) ? "pdf" : "text";
|
|
3839
3869
|
}
|
|
3840
3870
|
for (const prefix of MERMAID_PREFIXES) {
|
|
3841
3871
|
if (trimmed.startsWith(prefix)) {
|
|
@@ -3897,7 +3927,7 @@ function buildPdfHtml(base64) {
|
|
|
3897
3927
|
import * as pdfjsLib from 'https://esm.sh/pdfjs-dist@4/build/pdf.min.mjs';
|
|
3898
3928
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://esm.sh/pdfjs-dist@4/build/pdf.worker.min.mjs';
|
|
3899
3929
|
|
|
3900
|
-
const base64 = ${
|
|
3930
|
+
const base64 = ${safeJsonForScript(base64)};
|
|
3901
3931
|
const binaryStr = atob(base64);
|
|
3902
3932
|
const bytes = new Uint8Array(binaryStr.length);
|
|
3903
3933
|
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
|
|
@@ -3992,6 +4022,30 @@ function buildCspConfig(resolver) {
|
|
|
3992
4022
|
}
|
|
3993
4023
|
return { resourceDomains: cspResourceDomains, connectDomains: cspConnectDomains };
|
|
3994
4024
|
}
|
|
4025
|
+
var STRING_RESULT_DOCS = "https://docs.agentfront.dev/frontmcp/guides/building-tool-ui#trusted-markup";
|
|
4026
|
+
var noticedStringResultTools = /* @__PURE__ */ new Set();
|
|
4027
|
+
function noticeStringResult(toolName, logger) {
|
|
4028
|
+
if (noticedStringResultTools.has(toolName)) return;
|
|
4029
|
+
noticedStringResultTools.add(toolName);
|
|
4030
|
+
logger.warn(
|
|
4031
|
+
`[frontmcp] The UI template of tool "${toolName}" returned a plain string containing markup, which is rendered as HTML. FrontMCP 1.9 will HTML-escape plain string results by default. Build the markup with ctx.helpers.html\`\u2026\` (interpolated values are escaped) or wrap safe markup with ctx.helpers.trustedHtml(), then set ui.escapeStringResults: true (or escapeStringResults: false to keep the current behaviour without this notice). See ${STRING_RESULT_DOCS}`
|
|
4032
|
+
);
|
|
4033
|
+
}
|
|
4034
|
+
function renderUnwrappedResult(rawResult, options) {
|
|
4035
|
+
if (isTrustedHtml(rawResult)) {
|
|
4036
|
+
return String(rawResult);
|
|
4037
|
+
}
|
|
4038
|
+
if (typeof rawResult !== "string") {
|
|
4039
|
+
return `<pre>${escapeHtml(JSON.stringify(rawResult, null, 2))}</pre>`;
|
|
4040
|
+
}
|
|
4041
|
+
if (detectContentType(rawResult) !== "html" || options.escapeStringResults === true) {
|
|
4042
|
+
return escapeHtml(rawResult);
|
|
4043
|
+
}
|
|
4044
|
+
if (options.escapeStringResults === void 0) {
|
|
4045
|
+
noticeStringResult(options.toolName, options.logger ?? console);
|
|
4046
|
+
}
|
|
4047
|
+
return rawResult;
|
|
4048
|
+
}
|
|
3995
4049
|
function renderToolTemplate(options) {
|
|
3996
4050
|
const { toolName, input, output, template, resolver, platformType, sizing } = options;
|
|
3997
4051
|
const uiType = detectUIType(template);
|
|
@@ -4004,7 +4058,7 @@ function renderToolTemplate(options) {
|
|
|
4004
4058
|
resolver,
|
|
4005
4059
|
sizing
|
|
4006
4060
|
};
|
|
4007
|
-
let
|
|
4061
|
+
let html2;
|
|
4008
4062
|
let hash = "";
|
|
4009
4063
|
let size = 0;
|
|
4010
4064
|
if (typeof template === "object" && template !== null && "file" in template) {
|
|
@@ -4013,44 +4067,43 @@ function renderToolTemplate(options) {
|
|
|
4013
4067
|
{ source: template, inlineReact: resourceMode === "inline" },
|
|
4014
4068
|
{ ...shellConfig, csp: cspConfig }
|
|
4015
4069
|
);
|
|
4016
|
-
|
|
4070
|
+
html2 = result.html;
|
|
4017
4071
|
hash = result.hash;
|
|
4018
4072
|
size = result.size;
|
|
4019
4073
|
} else if (typeof template === "function") {
|
|
4020
4074
|
if (uiType === "react") {
|
|
4021
4075
|
const shellResult = buildShell('<div id="root"></div>', shellConfig);
|
|
4022
|
-
|
|
4076
|
+
html2 = shellResult.html;
|
|
4023
4077
|
hash = shellResult.hash;
|
|
4024
4078
|
size = shellResult.size;
|
|
4025
4079
|
} else {
|
|
4026
4080
|
const helpers = createTemplateHelpers();
|
|
4027
4081
|
const ctx = { input, output, helpers };
|
|
4028
4082
|
const rawResult = template(ctx);
|
|
4029
|
-
const wrapped = wrapDetectedContent(rawResult);
|
|
4083
|
+
const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
|
|
4030
4084
|
if (wrapped) {
|
|
4031
|
-
|
|
4085
|
+
html2 = wrapped;
|
|
4032
4086
|
} else {
|
|
4033
|
-
const
|
|
4034
|
-
|
|
4035
|
-
html = shellResult.html;
|
|
4087
|
+
const shellResult = buildShell(renderUnwrappedResult(rawResult, options), shellConfig);
|
|
4088
|
+
html2 = shellResult.html;
|
|
4036
4089
|
hash = shellResult.hash;
|
|
4037
4090
|
size = shellResult.size;
|
|
4038
4091
|
}
|
|
4039
4092
|
}
|
|
4040
4093
|
} else if (typeof template === "string") {
|
|
4041
4094
|
const shellResult = buildShell(template, shellConfig);
|
|
4042
|
-
|
|
4095
|
+
html2 = shellResult.html;
|
|
4043
4096
|
hash = shellResult.hash;
|
|
4044
4097
|
size = shellResult.size;
|
|
4045
4098
|
} else {
|
|
4046
4099
|
const shellResult = buildShell('<div id="root"></div>', shellConfig);
|
|
4047
|
-
|
|
4100
|
+
html2 = shellResult.html;
|
|
4048
4101
|
hash = shellResult.hash;
|
|
4049
4102
|
size = shellResult.size;
|
|
4050
4103
|
}
|
|
4051
4104
|
const htmlKey = "ui/html";
|
|
4052
4105
|
const meta = {
|
|
4053
|
-
[htmlKey]:
|
|
4106
|
+
[htmlKey]: html2,
|
|
4054
4107
|
"ui/type": uiType,
|
|
4055
4108
|
"ui/mimeType": MCP_APPS_MIME_TYPE
|
|
4056
4109
|
};
|
|
@@ -4061,10 +4114,10 @@ function renderToolTemplate(options) {
|
|
|
4061
4114
|
if (sizing.aspectRatio !== void 0) meta["ui/aspectRatio"] = sizing.aspectRatio;
|
|
4062
4115
|
}
|
|
4063
4116
|
return {
|
|
4064
|
-
html,
|
|
4117
|
+
html: html2,
|
|
4065
4118
|
uiType,
|
|
4066
4119
|
hash,
|
|
4067
|
-
size: size || Buffer.byteLength(
|
|
4120
|
+
size: size || Buffer.byteLength(html2, "utf8"),
|
|
4068
4121
|
meta
|
|
4069
4122
|
};
|
|
4070
4123
|
}
|
|
@@ -4207,6 +4260,7 @@ export {
|
|
|
4207
4260
|
getRegisteredPackages,
|
|
4208
4261
|
getSubpathFromSpecifier,
|
|
4209
4262
|
globalTypeCache,
|
|
4263
|
+
html,
|
|
4210
4264
|
isBundlingMode,
|
|
4211
4265
|
isDisplayMode,
|
|
4212
4266
|
isFileSource,
|
|
@@ -4219,6 +4273,7 @@ export {
|
|
|
4219
4273
|
isPackageRegistered,
|
|
4220
4274
|
isRelativeImport,
|
|
4221
4275
|
isResourceMode,
|
|
4276
|
+
isTrustedHtml,
|
|
4222
4277
|
isUIRenderFailure,
|
|
4223
4278
|
isUIType,
|
|
4224
4279
|
isUrlShellSource,
|
|
@@ -4238,6 +4293,7 @@ export {
|
|
|
4238
4293
|
safeStringify,
|
|
4239
4294
|
sanitizeCSPDomains,
|
|
4240
4295
|
transpileReactSource,
|
|
4296
|
+
trustedHtml,
|
|
4241
4297
|
validateCSPDomain,
|
|
4242
4298
|
validateImportMap,
|
|
4243
4299
|
validateShellTemplate,
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/uipack",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.2",
|
|
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,6 @@
|
|
|
61
61
|
"typescript": "^5.9.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@frontmcp/lazy-zod": "1.8.
|
|
64
|
+
"@frontmcp/lazy-zod": "1.8.2"
|
|
65
65
|
}
|
|
66
66
|
}
|