@frontmcp/uipack 1.8.1 → 1.8.3

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 CHANGED
@@ -125,17 +125,18 @@ const result = await fetcher.fetchBatch({
125
125
 
126
126
  ### Shell
127
127
 
128
- | Export | Type | Purpose |
129
- | ----------------------------------------------------- | -------- | --------------------------------------------------------------------- |
130
- | `buildShell(content, config)` | function | Main entry point -> `ShellResult` |
131
- | `buildCSPMetaTag(config?)` | function | Generate `<meta>` CSP tag |
132
- | `buildCSPDirectives(config?)` | function | Merge custom domains with defaults |
133
- | `buildDataInjectionScript(toolName, input?, output?)` | function | `<script>` setting `window.__mcp*` globals |
134
- | `createTemplateHelpers(toolName)` | function | `escapeHtml`, `formatDate`, `formatCurrency`, `uniqueId`, `jsonEmbed` |
135
- | `resolveShellTemplate(opts)` | function | Resolve custom shell from npm/URL/inline |
136
- | `applyShellTemplate(template, values)` | function | Apply values to resolved template |
137
- | `validateShellTemplate(template)` | function | Validate a custom shell template |
138
- | `DEFAULT_CDN_DOMAINS` | constant | jsDelivr, Cloudflare, Google Fonts |
128
+ | Export | Type | Purpose |
129
+ | ------------------------------------------------------------ | -------- | ----------------------------------------------------------------------------------------------- |
130
+ | `buildShell(content, config)` | function | Main entry point -> `ShellResult` |
131
+ | `buildCSPMetaTag(config?)` | function | Generate `<meta>` CSP tag |
132
+ | `buildCSPDirectives(config?)` | function | Merge custom domains with defaults |
133
+ | `buildDataInjectionScript(toolName, input?, output?)` | function | `<script>` setting `window.__mcp*` globals |
134
+ | `createTemplateHelpers(toolName)` | function | `html`, `trustedHtml`, `escapeHtml`, `formatDate`, `formatCurrency`, `uniqueId`, `jsonEmbed` |
135
+ | `` html`…` ``, `trustedHtml(markup)`, `isTrustedHtml(value)` | function | Build / mark / detect `TrustedHtml` template markup (interpolated values in `html` are escaped) |
136
+ | `resolveShellTemplate(opts)` | function | Resolve custom shell from npm/URL/inline |
137
+ | `applyShellTemplate(template, values)` | function | Apply values to resolved template |
138
+ | `validateShellTemplate(template)` | function | Validate a custom shell template |
139
+ | `DEFAULT_CDN_DOMAINS` | constant | jsDelivr, Cloudflare, Google Fonts |
139
140
 
140
141
  ### Component
141
142
 
package/adapters/index.js CHANGED
@@ -1857,6 +1857,29 @@ function validateShellTemplate(template) {
1857
1857
  };
1858
1858
  }
1859
1859
 
1860
+ // libs/uipack/src/shell/trusted-html.ts
1861
+ var TRUSTED_HTML = /* @__PURE__ */ Symbol.for("@frontmcp/uipack/trusted-html");
1862
+ function trustedHtml(markup) {
1863
+ const value = String(markup);
1864
+ return Object.freeze({ [TRUSTED_HTML]: value, toString: () => value });
1865
+ }
1866
+ function isTrustedHtml(value) {
1867
+ return typeof value === "object" && value !== null && typeof value[TRUSTED_HTML] === "string";
1868
+ }
1869
+ function interpolate(value) {
1870
+ if (isTrustedHtml(value)) return value[TRUSTED_HTML];
1871
+ if (Array.isArray(value)) return value.map(interpolate).join("");
1872
+ if (value === false) return "";
1873
+ return escapeHtml(value);
1874
+ }
1875
+ function html(strings, ...values) {
1876
+ let markup = strings[0];
1877
+ for (let index = 0; index < values.length; index++) {
1878
+ markup += interpolate(values[index]) + strings[index + 1];
1879
+ }
1880
+ return trustedHtml(markup);
1881
+ }
1882
+
1860
1883
  // libs/uipack/src/shell/data-injector.ts
1861
1884
  function hasSizing(sizing) {
1862
1885
  if (!sizing) return false;
@@ -1910,7 +1933,9 @@ function createTemplateHelpers() {
1910
1933
  uniqueId: (prefix = "mcp") => {
1911
1934
  return `${prefix}-${++_uniqueIdCounter}`;
1912
1935
  },
1913
- jsonEmbed: (data) => safeJsonForScript(data)
1936
+ jsonEmbed: (data) => safeJsonForScript(data),
1937
+ html,
1938
+ trustedHtml
1914
1939
  };
1915
1940
  }
1916
1941
 
@@ -1988,12 +2013,12 @@ function buildShell(content, config) {
1988
2013
  sizing
1989
2014
  });
1990
2015
  if (!withShell) {
1991
- const html2 = `${dataScript}
2016
+ const html3 = `${dataScript}
1992
2017
  ${content}`;
1993
2018
  return {
1994
- html: html2,
1995
- hash: simpleHash(html2),
1996
- size: Buffer.byteLength(html2, "utf-8")
2019
+ html: html3,
2020
+ hash: simpleHash(html3),
2021
+ size: Buffer.byteLength(html3, "utf-8")
1997
2022
  };
1998
2023
  }
1999
2024
  if (customShell) {
@@ -2026,7 +2051,7 @@ ${content}`;
2026
2051
  const bridgeScript = generateBridgeIIFE({ minify: true });
2027
2052
  headParts.push(`<script>${bridgeScript}</script>`);
2028
2053
  }
2029
- const html = `<!DOCTYPE html>
2054
+ const html2 = `<!DOCTYPE html>
2030
2055
  <html lang="en">
2031
2056
  <head>
2032
2057
  ${headParts.map((p) => ` ${p}`).join("\n")}
@@ -2036,9 +2061,9 @@ ${content}
2036
2061
  </body>
2037
2062
  </html>`;
2038
2063
  return {
2039
- html,
2040
- hash: simpleHash(html),
2041
- size: Buffer.byteLength(html, "utf-8")
2064
+ html: html2,
2065
+ hash: simpleHash(html2),
2066
+ size: Buffer.byteLength(html2, "utf-8")
2042
2067
  };
2043
2068
  }
2044
2069
  function buildCustomShell(content, customShell, ctx) {
@@ -2071,7 +2096,7 @@ ${sizingStyle}` : dataScript;
2071
2096
  const bridgeScript = generateBridgeIIFE({ minify: true });
2072
2097
  bridgeHtml = `<script>${bridgeScript}</script>`;
2073
2098
  }
2074
- const html = applyShellTemplate(template, {
2099
+ const html2 = applyShellTemplate(template, {
2075
2100
  csp: cspTag,
2076
2101
  data: dataWithSizing,
2077
2102
  bridge: bridgeHtml,
@@ -2079,9 +2104,9 @@ ${sizingStyle}` : dataScript;
2079
2104
  title: ctx.title ? escapeHtmlForTag(ctx.title) : ""
2080
2105
  });
2081
2106
  return {
2082
- html,
2083
- hash: simpleHash(html),
2084
- size: Buffer.byteLength(html, "utf-8")
2107
+ html: html2,
2108
+ hash: simpleHash(html2),
2109
+ size: Buffer.byteLength(html2, "utf-8")
2085
2110
  };
2086
2111
  }
2087
2112
  function simpleHash(str) {
@@ -2378,10 +2403,10 @@ function resolveFileSource(source, options = {}) {
2378
2403
  };
2379
2404
  }
2380
2405
  function resolveFunctionSource(source, input, output) {
2381
- const html = source(input, output);
2406
+ const html2 = source(input, output);
2382
2407
  return {
2383
2408
  mode: "inline",
2384
- html,
2409
+ html: html2,
2385
2410
  exportName: "default",
2386
2411
  meta: { mcpAware: false, renderer: "html" },
2387
2412
  peerDependencies: []
@@ -2758,11 +2783,29 @@ function buildCspConfig(resolver) {
2758
2783
  }
2759
2784
  return { resourceDomains: cspResourceDomains, connectDomains: cspConnectDomains };
2760
2785
  }
2761
- function renderUnwrappedResult(rawResult) {
2786
+ var STRING_RESULT_DOCS = "https://docs.agentfront.dev/frontmcp/guides/building-tool-ui#trusted-markup";
2787
+ var noticedStringResultTools = /* @__PURE__ */ new Set();
2788
+ function noticeStringResult(toolName, logger) {
2789
+ if (noticedStringResultTools.has(toolName)) return;
2790
+ noticedStringResultTools.add(toolName);
2791
+ logger.warn(
2792
+ `[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}`
2793
+ );
2794
+ }
2795
+ function renderUnwrappedResult(rawResult, options) {
2796
+ if (isTrustedHtml(rawResult)) {
2797
+ return String(rawResult);
2798
+ }
2762
2799
  if (typeof rawResult !== "string") {
2763
2800
  return `<pre>${escapeHtml(JSON.stringify(rawResult, null, 2))}</pre>`;
2764
2801
  }
2765
- return detectContentType(rawResult) === "html" ? rawResult : escapeHtml(rawResult);
2802
+ if (detectContentType(rawResult) !== "html" || options.escapeStringResults === true) {
2803
+ return escapeHtml(rawResult);
2804
+ }
2805
+ if (options.escapeStringResults === void 0) {
2806
+ noticeStringResult(options.toolName, options.logger ?? console);
2807
+ }
2808
+ return rawResult;
2766
2809
  }
2767
2810
  function renderToolTemplate(options) {
2768
2811
  const { toolName, input, output, template, resolver, platformType, sizing } = options;
@@ -2776,7 +2819,7 @@ function renderToolTemplate(options) {
2776
2819
  resolver,
2777
2820
  sizing
2778
2821
  };
2779
- let html;
2822
+ let html2;
2780
2823
  let hash = "";
2781
2824
  let size = 0;
2782
2825
  if (typeof template === "object" && template !== null && "file" in template) {
@@ -2785,43 +2828,43 @@ function renderToolTemplate(options) {
2785
2828
  { source: template, inlineReact: resourceMode === "inline" },
2786
2829
  { ...shellConfig, csp: cspConfig }
2787
2830
  );
2788
- html = result.html;
2831
+ html2 = result.html;
2789
2832
  hash = result.hash;
2790
2833
  size = result.size;
2791
2834
  } else if (typeof template === "function") {
2792
2835
  if (uiType === "react") {
2793
2836
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
2794
- html = shellResult.html;
2837
+ html2 = shellResult.html;
2795
2838
  hash = shellResult.hash;
2796
2839
  size = shellResult.size;
2797
2840
  } else {
2798
2841
  const helpers = createTemplateHelpers();
2799
2842
  const ctx = { input, output, helpers };
2800
2843
  const rawResult = template(ctx);
2801
- const wrapped = wrapDetectedContent(rawResult);
2844
+ const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
2802
2845
  if (wrapped) {
2803
- html = wrapped;
2846
+ html2 = wrapped;
2804
2847
  } else {
2805
- const shellResult = buildShell(renderUnwrappedResult(rawResult), shellConfig);
2806
- html = shellResult.html;
2848
+ const shellResult = buildShell(renderUnwrappedResult(rawResult, options), shellConfig);
2849
+ html2 = shellResult.html;
2807
2850
  hash = shellResult.hash;
2808
2851
  size = shellResult.size;
2809
2852
  }
2810
2853
  }
2811
2854
  } else if (typeof template === "string") {
2812
2855
  const shellResult = buildShell(template, shellConfig);
2813
- html = shellResult.html;
2856
+ html2 = shellResult.html;
2814
2857
  hash = shellResult.hash;
2815
2858
  size = shellResult.size;
2816
2859
  } else {
2817
2860
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
2818
- html = shellResult.html;
2861
+ html2 = shellResult.html;
2819
2862
  hash = shellResult.hash;
2820
2863
  size = shellResult.size;
2821
2864
  }
2822
2865
  const htmlKey = "ui/html";
2823
2866
  const meta = {
2824
- [htmlKey]: html,
2867
+ [htmlKey]: html2,
2825
2868
  "ui/type": uiType,
2826
2869
  "ui/mimeType": MCP_APPS_MIME_TYPE
2827
2870
  };
@@ -2832,10 +2875,10 @@ function renderToolTemplate(options) {
2832
2875
  if (sizing.aspectRatio !== void 0) meta["ui/aspectRatio"] = sizing.aspectRatio;
2833
2876
  }
2834
2877
  return {
2835
- html,
2878
+ html: html2,
2836
2879
  uiType,
2837
2880
  hash,
2838
- size: size || Buffer.byteLength(html, "utf8"),
2881
+ size: size || Buffer.byteLength(html2, "utf8"),
2839
2882
  meta
2840
2883
  };
2841
2884
  }
@@ -36,6 +36,16 @@ export interface RenderToolTemplateOptions {
36
36
  * shell, and is mirrored onto the returned `meta` as `ui/preferredHeight` etc.
37
37
  */
38
38
  sizing?: WidgetSizing;
39
+ /**
40
+ * HTML-escape a plain string returned by a template function; results built with
41
+ * `html` / `trustedHtml` stay markup. When unset, strings that look like HTML render as
42
+ * markup and a one-time notice per tool is logged; `false` keeps that without the notice.
43
+ */
44
+ escapeStringResults?: boolean;
45
+ /** Receives the one-time string-result notice. Defaults to `console`. */
46
+ logger?: {
47
+ warn: (message: string) => void;
48
+ };
39
49
  }
40
50
  /**
41
51
  * Result of rendering a tool template.
@@ -57,7 +67,7 @@ export interface RenderToolTemplateResult {
57
67
  *
58
68
  * Supported template types:
59
69
  * - FileSource object `{ file: './widget.tsx' }` — bundled with esbuild, React loaded from esm.sh
60
- * - HTML template builder function `(ctx) => string`
70
+ * - HTML template builder function `(ctx) => string | TrustedHtml`
61
71
  * - Static HTML/MDX string
62
72
  *
63
73
  * React function references (`template: MyComponent`) are NOT supported for bundling.
@@ -1 +1 @@
1
- {"version":3,"file":"template-renderer.d.ts","sourceRoot":"","sources":["../../src/adapters/template-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAOnD;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,kBAAkB;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,qDAAqD;IACrD,QAAQ,EAAE,OAAO,CAAC;IAClB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAChC;;;;OAIG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAmCD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,CAsG/F"}
1
+ {"version":3,"file":"template-renderer.d.ts","sourceRoot":"","sources":["../../src/adapters/template-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAIxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAOnD;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,kBAAkB;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,qDAAqD;IACrD,QAAQ,EAAE,OAAO,CAAC;IAClB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAChC;;;;OAIG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,yEAAyE;IACzE,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CAC9C;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AA6DD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,CAsG/F"}
@@ -1825,6 +1825,29 @@ function validateShellTemplate(template) {
1825
1825
  };
1826
1826
  }
1827
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
+
1828
1851
  // libs/uipack/src/shell/data-injector.ts
1829
1852
  function hasSizing(sizing) {
1830
1853
  if (!sizing) return false;
@@ -1878,7 +1901,9 @@ function createTemplateHelpers() {
1878
1901
  uniqueId: (prefix = "mcp") => {
1879
1902
  return `${prefix}-${++_uniqueIdCounter}`;
1880
1903
  },
1881
- jsonEmbed: (data) => safeJsonForScript(data)
1904
+ jsonEmbed: (data) => safeJsonForScript(data),
1905
+ html,
1906
+ trustedHtml
1882
1907
  };
1883
1908
  }
1884
1909
 
@@ -1956,12 +1981,12 @@ function buildShell(content, config) {
1956
1981
  sizing
1957
1982
  });
1958
1983
  if (!withShell) {
1959
- const html2 = `${dataScript}
1984
+ const html3 = `${dataScript}
1960
1985
  ${content}`;
1961
1986
  return {
1962
- html: html2,
1963
- hash: simpleHash(html2),
1964
- size: Buffer.byteLength(html2, "utf-8")
1987
+ html: html3,
1988
+ hash: simpleHash(html3),
1989
+ size: Buffer.byteLength(html3, "utf-8")
1965
1990
  };
1966
1991
  }
1967
1992
  if (customShell) {
@@ -1994,7 +2019,7 @@ ${content}`;
1994
2019
  const bridgeScript = generateBridgeIIFE({ minify: true });
1995
2020
  headParts.push(`<script>${bridgeScript}</script>`);
1996
2021
  }
1997
- const html = `<!DOCTYPE html>
2022
+ const html2 = `<!DOCTYPE html>
1998
2023
  <html lang="en">
1999
2024
  <head>
2000
2025
  ${headParts.map((p) => ` ${p}`).join("\n")}
@@ -2004,9 +2029,9 @@ ${content}
2004
2029
  </body>
2005
2030
  </html>`;
2006
2031
  return {
2007
- html,
2008
- hash: simpleHash(html),
2009
- size: Buffer.byteLength(html, "utf-8")
2032
+ html: html2,
2033
+ hash: simpleHash(html2),
2034
+ size: Buffer.byteLength(html2, "utf-8")
2010
2035
  };
2011
2036
  }
2012
2037
  function buildCustomShell(content, customShell, ctx) {
@@ -2039,7 +2064,7 @@ ${sizingStyle}` : dataScript;
2039
2064
  const bridgeScript = generateBridgeIIFE({ minify: true });
2040
2065
  bridgeHtml = `<script>${bridgeScript}</script>`;
2041
2066
  }
2042
- const html = applyShellTemplate(template, {
2067
+ const html2 = applyShellTemplate(template, {
2043
2068
  csp: cspTag,
2044
2069
  data: dataWithSizing,
2045
2070
  bridge: bridgeHtml,
@@ -2047,9 +2072,9 @@ ${sizingStyle}` : dataScript;
2047
2072
  title: ctx.title ? escapeHtmlForTag(ctx.title) : ""
2048
2073
  });
2049
2074
  return {
2050
- html,
2051
- hash: simpleHash(html),
2052
- size: Buffer.byteLength(html, "utf-8")
2075
+ html: html2,
2076
+ hash: simpleHash(html2),
2077
+ size: Buffer.byteLength(html2, "utf-8")
2053
2078
  };
2054
2079
  }
2055
2080
  function simpleHash(str) {
@@ -2346,10 +2371,10 @@ function resolveFileSource(source, options = {}) {
2346
2371
  };
2347
2372
  }
2348
2373
  function resolveFunctionSource(source, input, output) {
2349
- const html = source(input, output);
2374
+ const html2 = source(input, output);
2350
2375
  return {
2351
2376
  mode: "inline",
2352
- html,
2377
+ html: html2,
2353
2378
  exportName: "default",
2354
2379
  meta: { mcpAware: false, renderer: "html" },
2355
2380
  peerDependencies: []
@@ -2726,11 +2751,29 @@ function buildCspConfig(resolver) {
2726
2751
  }
2727
2752
  return { resourceDomains: cspResourceDomains, connectDomains: cspConnectDomains };
2728
2753
  }
2729
- function renderUnwrappedResult(rawResult) {
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
+ }
2730
2767
  if (typeof rawResult !== "string") {
2731
2768
  return `<pre>${escapeHtml(JSON.stringify(rawResult, null, 2))}</pre>`;
2732
2769
  }
2733
- return detectContentType(rawResult) === "html" ? rawResult : escapeHtml(rawResult);
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;
2734
2777
  }
2735
2778
  function renderToolTemplate(options) {
2736
2779
  const { toolName, input, output, template, resolver, platformType, sizing } = options;
@@ -2744,7 +2787,7 @@ function renderToolTemplate(options) {
2744
2787
  resolver,
2745
2788
  sizing
2746
2789
  };
2747
- let html;
2790
+ let html2;
2748
2791
  let hash = "";
2749
2792
  let size = 0;
2750
2793
  if (typeof template === "object" && template !== null && "file" in template) {
@@ -2753,43 +2796,43 @@ function renderToolTemplate(options) {
2753
2796
  { source: template, inlineReact: resourceMode === "inline" },
2754
2797
  { ...shellConfig, csp: cspConfig }
2755
2798
  );
2756
- html = result.html;
2799
+ html2 = result.html;
2757
2800
  hash = result.hash;
2758
2801
  size = result.size;
2759
2802
  } else if (typeof template === "function") {
2760
2803
  if (uiType === "react") {
2761
2804
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
2762
- html = shellResult.html;
2805
+ html2 = shellResult.html;
2763
2806
  hash = shellResult.hash;
2764
2807
  size = shellResult.size;
2765
2808
  } else {
2766
2809
  const helpers = createTemplateHelpers();
2767
2810
  const ctx = { input, output, helpers };
2768
2811
  const rawResult = template(ctx);
2769
- const wrapped = wrapDetectedContent(rawResult);
2812
+ const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
2770
2813
  if (wrapped) {
2771
- html = wrapped;
2814
+ html2 = wrapped;
2772
2815
  } else {
2773
- const shellResult = buildShell(renderUnwrappedResult(rawResult), shellConfig);
2774
- html = shellResult.html;
2816
+ const shellResult = buildShell(renderUnwrappedResult(rawResult, options), shellConfig);
2817
+ html2 = shellResult.html;
2775
2818
  hash = shellResult.hash;
2776
2819
  size = shellResult.size;
2777
2820
  }
2778
2821
  }
2779
2822
  } else if (typeof template === "string") {
2780
2823
  const shellResult = buildShell(template, shellConfig);
2781
- html = shellResult.html;
2824
+ html2 = shellResult.html;
2782
2825
  hash = shellResult.hash;
2783
2826
  size = shellResult.size;
2784
2827
  } else {
2785
2828
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
2786
- html = shellResult.html;
2829
+ html2 = shellResult.html;
2787
2830
  hash = shellResult.hash;
2788
2831
  size = shellResult.size;
2789
2832
  }
2790
2833
  const htmlKey = "ui/html";
2791
2834
  const meta = {
2792
- [htmlKey]: html,
2835
+ [htmlKey]: html2,
2793
2836
  "ui/type": uiType,
2794
2837
  "ui/mimeType": MCP_APPS_MIME_TYPE
2795
2838
  };
@@ -2800,10 +2843,10 @@ function renderToolTemplate(options) {
2800
2843
  if (sizing.aspectRatio !== void 0) meta["ui/aspectRatio"] = sizing.aspectRatio;
2801
2844
  }
2802
2845
  return {
2803
- html,
2846
+ html: html2,
2804
2847
  uiType,
2805
2848
  hash,
2806
- size: size || Buffer.byteLength(html, "utf8"),
2849
+ size: size || Buffer.byteLength(html2, "utf8"),
2807
2850
  meta
2808
2851
  };
2809
2852
  }