@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.
@@ -482,8 +482,9 @@ function escapeHtml(str) {
482
482
  const s = String(str);
483
483
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
484
484
  }
485
- function escapeScriptClose(jsonString) {
486
- return jsonString.replace(/<\//g, "<\\/");
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 escapeScriptClose(jsonString);
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
- let result = template;
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
- return escapeScriptClose(JSON.stringify(data));
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 html2 = `${dataScript}
1984
+ const html3 = `${dataScript}
1959
1985
  ${content}`;
1960
1986
  return {
1961
- html: html2,
1962
- hash: simpleHash(html2),
1963
- size: Buffer.byteLength(html2, "utf-8")
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 html = `<!DOCTYPE html>
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(html),
2008
- size: Buffer.byteLength(html, "utf-8")
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 html = applyShellTemplate(template, {
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(html),
2051
- size: Buffer.byteLength(html, "utf-8")
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 html = source(input, output);
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 = ${JSON.stringify(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 html;
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
- html = result.html;
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
- html = shellResult.html;
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
- html = wrapped;
2814
+ html2 = wrapped;
2764
2815
  } else {
2765
- const textContent = typeof rawResult === "string" ? rawResult : `<pre>${JSON.stringify(rawResult, null, 2)}</pre>`;
2766
- const shellResult = buildShell(textContent, shellConfig);
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
- html = shellResult.html;
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
- html = shellResult.html;
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]: html,
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(html, "utf8"),
2849
+ size: size || Buffer.byteLength(html2, "utf8"),
2800
2850
  meta
2801
2851
  };
2802
2852
  }
@@ -421,8 +421,9 @@ function createEsmShResolver(options = {}) {
421
421
  }
422
422
 
423
423
  // libs/uipack/src/utils/index.ts
424
- function escapeScriptClose(jsonString) {
425
- return jsonString.replace(/<\//g, "<\\/");
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 escapeScriptClose(jsonString);
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
- let result = template;
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 escapeScriptClose(jsonString);
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
- let result = template;
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
- return escapeScriptClose(JSON.stringify(data));
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 html2 = `${dataScript}
3076
+ const html3 = `${dataScript}
3048
3077
  ${content}`;
3049
3078
  return {
3050
- html: html2,
3051
- hash: simpleHash(html2),
3052
- size: Buffer.byteLength(html2, "utf-8")
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 html = `<!DOCTYPE html>
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(html),
3097
- size: Buffer.byteLength(html, "utf-8")
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 html = applyShellTemplate(template, {
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(html),
3140
- size: Buffer.byteLength(html, "utf-8")
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 html = source(input, output);
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 = ${JSON.stringify(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 html;
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
- html = result.html;
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
- html = shellResult.html;
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
- html = wrapped;
4085
+ html2 = wrapped;
4032
4086
  } else {
4033
- const textContent = typeof rawResult === "string" ? rawResult : `<pre>${JSON.stringify(rawResult, null, 2)}</pre>`;
4034
- const shellResult = buildShell(textContent, shellConfig);
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
- html = shellResult.html;
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
- html = shellResult.html;
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]: html,
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(html, "utf8"),
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.0",
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.0"
64
+ "@frontmcp/lazy-zod": "1.8.2"
65
65
  }
66
66
  }