@frontmcp/uipack 1.9.1 → 1.9.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.
@@ -11,7 +11,7 @@ export { MCP_APPS_MIME_TYPE, MCP_APPS_EXTENSION_ID } from './constants';
11
11
  export { resolveServingMode, type ResolveServingModeOptions, type ServingModeResult, type AdapterPlatformType, } from './serving-mode';
12
12
  export { buildToolResponseContent, type ToolResponseContent, type BuildToolResponseContentOptions, } from './response-builder';
13
13
  export { isUIRenderFailure, type UIRenderFailure } from './render-failure';
14
- export { renderToolTemplate, type RenderToolTemplateOptions, type RenderToolTemplateResult } from './template-renderer';
14
+ export { renderToolTemplate, warnIfComponentReference, type RenderToolTemplateOptions, type RenderToolTemplateResult, } from './template-renderer';
15
15
  export { detectContentType, type DetectedContentType } from './content-detector';
16
16
  export { buildChartHtml, buildMermaidHtml, buildPdfHtml, wrapDetectedContent } from './content-renderers';
17
17
  export { createDefaultBaseTemplate, type DefaultBaseTemplateOptions } from './base-template';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAKxE,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,EAC9B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AAKxB,OAAO,EACL,wBAAwB,EACxB,KAAK,mBAAmB,EACxB,KAAK,+BAA+B,GACrC,MAAM,oBAAoB,CAAC;AAK5B,OAAO,EAAE,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAK3E,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,KAAK,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAKxH,OAAO,EAAE,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAK1G,OAAO,EAAE,yBAAyB,EAAE,KAAK,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAK7F,OAAO,EAAE,qBAAqB,EAAE,KAAK,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAKzF,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAKxE,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,EAC9B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AAKxB,OAAO,EACL,wBAAwB,EACxB,KAAK,mBAAmB,EACxB,KAAK,+BAA+B,GACrC,MAAM,oBAAoB,CAAC;AAK5B,OAAO,EAAE,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAK3E,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,GAC9B,MAAM,qBAAqB,CAAC;AAK7B,OAAO,EAAE,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAK1G,OAAO,EAAE,yBAAyB,EAAE,KAAK,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAK7F,OAAO,EAAE,qBAAqB,EAAE,KAAK,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAKzF,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
package/adapters/index.js CHANGED
@@ -33,6 +33,7 @@ __export(adapters_exports, {
33
33
  isUIRenderFailure: () => isUIRenderFailure,
34
34
  renderToolTemplate: () => renderToolTemplate,
35
35
  resolveServingMode: () => resolveServingMode,
36
+ warnIfComponentReference: () => warnIfComponentReference,
36
37
  wrapDetectedContent: () => wrapDetectedContent
37
38
  });
38
39
  module.exports = __toCommonJS(adapters_exports);
@@ -3077,7 +3078,7 @@ function noticeStringResult(toolName, logger) {
3077
3078
  if (noticedStringResultTools.has(toolName)) return;
3078
3079
  noticedStringResultTools.add(toolName);
3079
3080
  logger.warn(
3080
- `[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}`
3081
+ `[frontmcp] The UI template of tool "${toolName}" returned a plain string containing markup. FrontMCP 1.9 HTML-escapes plain string results by default, so it is shown as text. Build the markup with ctx.helpers.html\`\u2026\` (interpolated values are escaped) or wrap safe markup with ctx.helpers.trustedHtml(). Set ui.escapeStringResults: false to render plain strings as markup, or true to silence this notice. See ${STRING_RESULT_DOCS}`
3081
3082
  );
3082
3083
  }
3083
3084
  function renderUnwrappedResult(rawResult, options) {
@@ -3087,13 +3088,14 @@ function renderUnwrappedResult(rawResult, options) {
3087
3088
  if (typeof rawResult !== "string") {
3088
3089
  return `<pre>${escapeHtml(JSON.stringify(rawResult, null, 2))}</pre>`;
3089
3090
  }
3090
- if (detectContentType(rawResult) !== "html" || options.escapeStringResults === true) {
3091
- return escapeHtml(rawResult);
3091
+ const looksLikeMarkup = detectContentType(rawResult) === "html";
3092
+ if (looksLikeMarkup && options.escapeStringResults === false) {
3093
+ return rawResult;
3092
3094
  }
3093
- if (options.escapeStringResults === void 0) {
3095
+ if (looksLikeMarkup && options.escapeStringResults === void 0) {
3094
3096
  noticeStringResult(options.toolName, options.logger ?? console);
3095
3097
  }
3096
- return rawResult;
3098
+ return escapeHtml(rawResult);
3097
3099
  }
3098
3100
  var warnedComponentReferences = /* @__PURE__ */ new WeakMap();
3099
3101
  function warnComponentReference(template, toolName, logger) {
@@ -3105,7 +3107,18 @@ function warnComponentReference(template, toolName, logger) {
3105
3107
  `[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.`
3106
3108
  );
3107
3109
  }
3108
- function isReactElement(value) {
3110
+ var COMPILED_JSX_PATTERN = /\b(?:React\.createElement|_?jsx(?:s|DEV)?)\)?\s*\(/;
3111
+ function isComponentReference(template) {
3112
+ if (isReactTypedObject(template)) return true;
3113
+ if (typeof template !== "function" || detectUIType(template) !== "react") return false;
3114
+ return isDefinitelyReactComponent(template) || COMPILED_JSX_PATTERN.test(Function.prototype.toString.call(template));
3115
+ }
3116
+ function warnIfComponentReference(toolName, template, logger = console) {
3117
+ if (!isComponentReference(template)) return false;
3118
+ warnComponentReference(template, toolName, logger);
3119
+ return true;
3120
+ }
3121
+ function isReactTypedObject(value) {
3109
3122
  return typeof value === "object" && value !== null && "$$typeof" in value;
3110
3123
  }
3111
3124
  function isDefinitelyReactComponent(template) {
@@ -3145,7 +3158,7 @@ function renderToolTemplate(options) {
3145
3158
  if (isHtmlBuilder) {
3146
3159
  try {
3147
3160
  rawResult = template(ctx);
3148
- isHtmlBuilder = !isReactElement(rawResult);
3161
+ isHtmlBuilder = !isReactTypedObject(rawResult);
3149
3162
  } catch (error) {
3150
3163
  if (uiType !== "react") throw error;
3151
3164
  isHtmlBuilder = false;
@@ -3174,6 +3187,7 @@ function renderToolTemplate(options) {
3174
3187
  hash = shellResult.hash;
3175
3188
  size = shellResult.size;
3176
3189
  } else {
3190
+ if (isReactTypedObject(template)) warnComponentReference(template, toolName, options.logger ?? console);
3177
3191
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
3178
3192
  html2 = shellResult.html;
3179
3193
  hash = shellResult.hash;
@@ -3288,5 +3302,6 @@ function buildCDNInfoForUIType(uiType) {
3288
3302
  isUIRenderFailure,
3289
3303
  renderToolTemplate,
3290
3304
  resolveServingMode,
3305
+ warnIfComponentReference,
3291
3306
  wrapDetectedContent
3292
3307
  });
@@ -46,8 +46,9 @@ export interface RenderToolTemplateOptions {
46
46
  };
47
47
  /**
48
48
  * HTML-escape a plain string returned by a template function; results built with
49
- * `html` / `trustedHtml` stay markup. When unset, strings that look like HTML render as
50
- * markup and a one-time notice per tool is logged; `false` keeps that without the notice.
49
+ * `html` / `trustedHtml` stay markup. Unset is the 1.9 default: strings are escaped, and a
50
+ * one-time notice per tool is logged when one looked like markup. `true` escapes without the
51
+ * notice; `false` renders strings that look like HTML as markup.
51
52
  */
52
53
  escapeStringResults?: boolean;
53
54
  /** Receives the one-time string-result notice. Defaults to `console`. */
@@ -70,6 +71,15 @@ export interface RenderToolTemplateResult {
70
71
  /** Platform-specific metadata */
71
72
  meta: Record<string, unknown>;
72
73
  }
74
+ /**
75
+ * Warn, once per tool, that `template` is a React component reference the widget cannot bundle.
76
+ * The server calls this while it registers UI tools, so the warning appears at startup (#769).
77
+ *
78
+ * @returns Whether the template is a component reference
79
+ */
80
+ export declare function warnIfComponentReference(toolName: string, template: unknown, logger?: {
81
+ warn: (message: string) => void;
82
+ }): boolean;
73
83
  /**
74
84
  * Render a tool template into HTML.
75
85
  *
@@ -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;AAIxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAQnD;;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;;;OAGG;IACH,GAAG,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChE;;;;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;AA8FD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,CAoH/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;AAQnD;;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;;;OAGG;IACH,GAAG,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChE;;;;;OAKG;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;AAiGD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,OAAO,EACjB,MAAM,GAAE;IAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAAY,GACpD,OAAO,CAIT;AAeD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,CAqH/F"}
@@ -3048,7 +3048,7 @@ function noticeStringResult(toolName, logger) {
3048
3048
  if (noticedStringResultTools.has(toolName)) return;
3049
3049
  noticedStringResultTools.add(toolName);
3050
3050
  logger.warn(
3051
- `[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}`
3051
+ `[frontmcp] The UI template of tool "${toolName}" returned a plain string containing markup. FrontMCP 1.9 HTML-escapes plain string results by default, so it is shown as text. Build the markup with ctx.helpers.html\`\u2026\` (interpolated values are escaped) or wrap safe markup with ctx.helpers.trustedHtml(). Set ui.escapeStringResults: false to render plain strings as markup, or true to silence this notice. See ${STRING_RESULT_DOCS}`
3052
3052
  );
3053
3053
  }
3054
3054
  function renderUnwrappedResult(rawResult, options) {
@@ -3058,13 +3058,14 @@ function renderUnwrappedResult(rawResult, options) {
3058
3058
  if (typeof rawResult !== "string") {
3059
3059
  return `<pre>${escapeHtml(JSON.stringify(rawResult, null, 2))}</pre>`;
3060
3060
  }
3061
- if (detectContentType(rawResult) !== "html" || options.escapeStringResults === true) {
3062
- return escapeHtml(rawResult);
3061
+ const looksLikeMarkup = detectContentType(rawResult) === "html";
3062
+ if (looksLikeMarkup && options.escapeStringResults === false) {
3063
+ return rawResult;
3063
3064
  }
3064
- if (options.escapeStringResults === void 0) {
3065
+ if (looksLikeMarkup && options.escapeStringResults === void 0) {
3065
3066
  noticeStringResult(options.toolName, options.logger ?? console);
3066
3067
  }
3067
- return rawResult;
3068
+ return escapeHtml(rawResult);
3068
3069
  }
3069
3070
  var warnedComponentReferences = /* @__PURE__ */ new WeakMap();
3070
3071
  function warnComponentReference(template, toolName, logger) {
@@ -3076,7 +3077,18 @@ function warnComponentReference(template, toolName, logger) {
3076
3077
  `[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.`
3077
3078
  );
3078
3079
  }
3079
- function isReactElement(value) {
3080
+ var COMPILED_JSX_PATTERN = /\b(?:React\.createElement|_?jsx(?:s|DEV)?)\)?\s*\(/;
3081
+ function isComponentReference(template) {
3082
+ if (isReactTypedObject(template)) return true;
3083
+ if (typeof template !== "function" || detectUIType(template) !== "react") return false;
3084
+ return isDefinitelyReactComponent(template) || COMPILED_JSX_PATTERN.test(Function.prototype.toString.call(template));
3085
+ }
3086
+ function warnIfComponentReference(toolName, template, logger = console) {
3087
+ if (!isComponentReference(template)) return false;
3088
+ warnComponentReference(template, toolName, logger);
3089
+ return true;
3090
+ }
3091
+ function isReactTypedObject(value) {
3080
3092
  return typeof value === "object" && value !== null && "$$typeof" in value;
3081
3093
  }
3082
3094
  function isDefinitelyReactComponent(template) {
@@ -3116,7 +3128,7 @@ function renderToolTemplate(options) {
3116
3128
  if (isHtmlBuilder) {
3117
3129
  try {
3118
3130
  rawResult = template(ctx);
3119
- isHtmlBuilder = !isReactElement(rawResult);
3131
+ isHtmlBuilder = !isReactTypedObject(rawResult);
3120
3132
  } catch (error) {
3121
3133
  if (uiType !== "react") throw error;
3122
3134
  isHtmlBuilder = false;
@@ -3145,6 +3157,7 @@ function renderToolTemplate(options) {
3145
3157
  hash = shellResult.hash;
3146
3158
  size = shellResult.size;
3147
3159
  } else {
3160
+ if (isReactTypedObject(template)) warnComponentReference(template, toolName, options.logger ?? console);
3148
3161
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
3149
3162
  html2 = shellResult.html;
3150
3163
  hash = shellResult.hash;
@@ -3258,5 +3271,6 @@ export {
3258
3271
  isUIRenderFailure,
3259
3272
  renderToolTemplate,
3260
3273
  resolveServingMode,
3274
+ warnIfComponentReference,
3261
3275
  wrapDetectedContent
3262
3276
  };
package/esm/index.mjs CHANGED
@@ -4319,7 +4319,7 @@ function noticeStringResult(toolName, logger) {
4319
4319
  if (noticedStringResultTools.has(toolName)) return;
4320
4320
  noticedStringResultTools.add(toolName);
4321
4321
  logger.warn(
4322
- `[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}`
4322
+ `[frontmcp] The UI template of tool "${toolName}" returned a plain string containing markup. FrontMCP 1.9 HTML-escapes plain string results by default, so it is shown as text. Build the markup with ctx.helpers.html\`\u2026\` (interpolated values are escaped) or wrap safe markup with ctx.helpers.trustedHtml(). Set ui.escapeStringResults: false to render plain strings as markup, or true to silence this notice. See ${STRING_RESULT_DOCS}`
4323
4323
  );
4324
4324
  }
4325
4325
  function renderUnwrappedResult(rawResult, options) {
@@ -4329,13 +4329,14 @@ function renderUnwrappedResult(rawResult, options) {
4329
4329
  if (typeof rawResult !== "string") {
4330
4330
  return `<pre>${escapeHtml(JSON.stringify(rawResult, null, 2))}</pre>`;
4331
4331
  }
4332
- if (detectContentType(rawResult) !== "html" || options.escapeStringResults === true) {
4333
- return escapeHtml(rawResult);
4332
+ const looksLikeMarkup = detectContentType(rawResult) === "html";
4333
+ if (looksLikeMarkup && options.escapeStringResults === false) {
4334
+ return rawResult;
4334
4335
  }
4335
- if (options.escapeStringResults === void 0) {
4336
+ if (looksLikeMarkup && options.escapeStringResults === void 0) {
4336
4337
  noticeStringResult(options.toolName, options.logger ?? console);
4337
4338
  }
4338
- return rawResult;
4339
+ return escapeHtml(rawResult);
4339
4340
  }
4340
4341
  var warnedComponentReferences = /* @__PURE__ */ new WeakMap();
4341
4342
  function warnComponentReference(template, toolName, logger) {
@@ -4347,7 +4348,18 @@ function warnComponentReference(template, toolName, logger) {
4347
4348
  `[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.`
4348
4349
  );
4349
4350
  }
4350
- function isReactElement(value) {
4351
+ var COMPILED_JSX_PATTERN = /\b(?:React\.createElement|_?jsx(?:s|DEV)?)\)?\s*\(/;
4352
+ function isComponentReference(template) {
4353
+ if (isReactTypedObject(template)) return true;
4354
+ if (typeof template !== "function" || detectUIType(template) !== "react") return false;
4355
+ return isDefinitelyReactComponent(template) || COMPILED_JSX_PATTERN.test(Function.prototype.toString.call(template));
4356
+ }
4357
+ function warnIfComponentReference(toolName, template, logger = console) {
4358
+ if (!isComponentReference(template)) return false;
4359
+ warnComponentReference(template, toolName, logger);
4360
+ return true;
4361
+ }
4362
+ function isReactTypedObject(value) {
4351
4363
  return typeof value === "object" && value !== null && "$$typeof" in value;
4352
4364
  }
4353
4365
  function isDefinitelyReactComponent(template) {
@@ -4387,7 +4399,7 @@ function renderToolTemplate(options) {
4387
4399
  if (isHtmlBuilder) {
4388
4400
  try {
4389
4401
  rawResult = template(ctx);
4390
- isHtmlBuilder = !isReactElement(rawResult);
4402
+ isHtmlBuilder = !isReactTypedObject(rawResult);
4391
4403
  } catch (error) {
4392
4404
  if (uiType !== "react") throw error;
4393
4405
  isHtmlBuilder = false;
@@ -4416,6 +4428,7 @@ function renderToolTemplate(options) {
4416
4428
  hash = shellResult.hash;
4417
4429
  size = shellResult.size;
4418
4430
  } else {
4431
+ if (isReactTypedObject(template)) warnComponentReference(template, toolName, options.logger ?? console);
4419
4432
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
4420
4433
  html2 = shellResult.html;
4421
4434
  hash = shellResult.hash;
@@ -4617,5 +4630,6 @@ export {
4617
4630
  validateCSPDomain,
4618
4631
  validateImportMap,
4619
4632
  validateShellTemplate,
4633
+ warnIfComponentReference,
4620
4634
  wrapDetectedContent
4621
4635
  };
package/esm/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/uipack",
3
- "version": "1.9.1",
3
+ "version": "1.9.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,7 +61,7 @@
61
61
  "typescript": "^5.9.3"
62
62
  },
63
63
  "peerDependencies": {
64
- "@frontmcp/lazy-zod": "1.9.1",
64
+ "@frontmcp/lazy-zod": "1.9.2",
65
65
  "esbuild": ">=0.27.0 <1"
66
66
  },
67
67
  "peerDependenciesMeta": {
package/index.js CHANGED
@@ -121,6 +121,7 @@ __export(index_exports, {
121
121
  validateCSPDomain: () => validateCSPDomain,
122
122
  validateImportMap: () => validateImportMap,
123
123
  validateShellTemplate: () => validateShellTemplate,
124
+ warnIfComponentReference: () => warnIfComponentReference,
124
125
  wrapDetectedContent: () => wrapDetectedContent
125
126
  });
126
127
  module.exports = __toCommonJS(index_exports);
@@ -4436,7 +4437,7 @@ function noticeStringResult(toolName, logger) {
4436
4437
  if (noticedStringResultTools.has(toolName)) return;
4437
4438
  noticedStringResultTools.add(toolName);
4438
4439
  logger.warn(
4439
- `[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}`
4440
+ `[frontmcp] The UI template of tool "${toolName}" returned a plain string containing markup. FrontMCP 1.9 HTML-escapes plain string results by default, so it is shown as text. Build the markup with ctx.helpers.html\`\u2026\` (interpolated values are escaped) or wrap safe markup with ctx.helpers.trustedHtml(). Set ui.escapeStringResults: false to render plain strings as markup, or true to silence this notice. See ${STRING_RESULT_DOCS}`
4440
4441
  );
4441
4442
  }
4442
4443
  function renderUnwrappedResult(rawResult, options) {
@@ -4446,13 +4447,14 @@ function renderUnwrappedResult(rawResult, options) {
4446
4447
  if (typeof rawResult !== "string") {
4447
4448
  return `<pre>${escapeHtml(JSON.stringify(rawResult, null, 2))}</pre>`;
4448
4449
  }
4449
- if (detectContentType(rawResult) !== "html" || options.escapeStringResults === true) {
4450
- return escapeHtml(rawResult);
4450
+ const looksLikeMarkup = detectContentType(rawResult) === "html";
4451
+ if (looksLikeMarkup && options.escapeStringResults === false) {
4452
+ return rawResult;
4451
4453
  }
4452
- if (options.escapeStringResults === void 0) {
4454
+ if (looksLikeMarkup && options.escapeStringResults === void 0) {
4453
4455
  noticeStringResult(options.toolName, options.logger ?? console);
4454
4456
  }
4455
- return rawResult;
4457
+ return escapeHtml(rawResult);
4456
4458
  }
4457
4459
  var warnedComponentReferences = /* @__PURE__ */ new WeakMap();
4458
4460
  function warnComponentReference(template, toolName, logger) {
@@ -4464,7 +4466,18 @@ function warnComponentReference(template, toolName, logger) {
4464
4466
  `[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
4467
  );
4466
4468
  }
4467
- function isReactElement(value) {
4469
+ var COMPILED_JSX_PATTERN = /\b(?:React\.createElement|_?jsx(?:s|DEV)?)\)?\s*\(/;
4470
+ function isComponentReference(template) {
4471
+ if (isReactTypedObject(template)) return true;
4472
+ if (typeof template !== "function" || detectUIType(template) !== "react") return false;
4473
+ return isDefinitelyReactComponent(template) || COMPILED_JSX_PATTERN.test(Function.prototype.toString.call(template));
4474
+ }
4475
+ function warnIfComponentReference(toolName, template, logger = console) {
4476
+ if (!isComponentReference(template)) return false;
4477
+ warnComponentReference(template, toolName, logger);
4478
+ return true;
4479
+ }
4480
+ function isReactTypedObject(value) {
4468
4481
  return typeof value === "object" && value !== null && "$$typeof" in value;
4469
4482
  }
4470
4483
  function isDefinitelyReactComponent(template) {
@@ -4504,7 +4517,7 @@ function renderToolTemplate(options) {
4504
4517
  if (isHtmlBuilder) {
4505
4518
  try {
4506
4519
  rawResult = template(ctx);
4507
- isHtmlBuilder = !isReactElement(rawResult);
4520
+ isHtmlBuilder = !isReactTypedObject(rawResult);
4508
4521
  } catch (error) {
4509
4522
  if (uiType !== "react") throw error;
4510
4523
  isHtmlBuilder = false;
@@ -4533,6 +4546,7 @@ function renderToolTemplate(options) {
4533
4546
  hash = shellResult.hash;
4534
4547
  size = shellResult.size;
4535
4548
  } else {
4549
+ if (isReactTypedObject(template)) warnComponentReference(template, toolName, options.logger ?? console);
4536
4550
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
4537
4551
  html2 = shellResult.html;
4538
4552
  hash = shellResult.hash;
@@ -4735,5 +4749,6 @@ function buildCDNInfoForUIType(uiType) {
4735
4749
  validateCSPDomain,
4736
4750
  validateImportMap,
4737
4751
  validateShellTemplate,
4752
+ warnIfComponentReference,
4738
4753
  wrapDetectedContent
4739
4754
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/uipack",
3
- "version": "1.9.1",
3
+ "version": "1.9.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,7 +61,7 @@
61
61
  "typescript": "^5.9.3"
62
62
  },
63
63
  "peerDependencies": {
64
- "@frontmcp/lazy-zod": "1.9.1",
64
+ "@frontmcp/lazy-zod": "1.9.2",
65
65
  "esbuild": ">=0.27.0 <1"
66
66
  },
67
67
  "peerDependenciesMeta": {
@@ -2,8 +2,8 @@
2
2
  * Trusted Markup
3
3
  *
4
4
  * Explicit markup values for UI template functions. A template result built with {@link html}
5
- * or wrapped with {@link trustedHtml} is rendered as HTML; with `escapeStringResults` on, a plain
6
- * string result is escaped instead.
5
+ * or wrapped with {@link trustedHtml} is rendered as HTML; a plain string result is escaped instead
6
+ * (unless `escapeStringResults: false`).
7
7
  *
8
8
  * @packageDocumentation
9
9
  */
@@ -182,8 +182,8 @@ export interface TemplateHelpers {
182
182
  */
183
183
  html: (strings: TemplateStringsArray, ...values: unknown[]) => TrustedHtml;
184
184
  /**
185
- * Mark markup you produced or sanitized yourself as trusted, so it renders as HTML even when
186
- * `escapeStringResults` is on. Never pass raw tool output or user input.
185
+ * Mark markup you produced or sanitized yourself as trusted, so it renders as HTML although plain
186
+ * string results are escaped. Never pass raw tool output or user input.
187
187
  */
188
188
  trustedHtml: (markup: string) => TrustedHtml;
189
189
  }
@@ -349,11 +349,11 @@ export interface UITemplateConfig<In = unknown, Out = unknown> {
349
349
  /**
350
350
  * HTML-escape a plain string returned by a template function.
351
351
  *
352
- * - `true`: plain strings are escaped and shown as text; markup built with
353
- * `ctx.helpers.html` or wrapped with `ctx.helpers.trustedHtml` still renders as HTML.
354
- * - unset (1.8 default): strings that look like HTML render as markup, and a one-time
355
- * notice per tool recommends `html` because 1.9 will escape string results by default.
356
- * - `false`: keep rendering strings as markup without the notice.
352
+ * - unset (1.9 default): plain strings are escaped and shown as text, and a one-time notice per
353
+ * tool is logged when one looked like markup; markup built with `ctx.helpers.html` or wrapped
354
+ * with `ctx.helpers.trustedHtml` still renders as HTML.
355
+ * - `true`: the same, without the notice.
356
+ * - `false`: strings that look like HTML render as markup (the 1.8 behaviour).
357
357
  *
358
358
  * Overrides the server-wide `@FrontMcp({ ui: { escapeStringResults } })` default.
359
359
  * Static string templates (`template: '<div>…</div>'`) are author markup and never escaped.