@frontmcp/uipack 1.8.7 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/adapters/index.js CHANGED
@@ -1922,11 +1922,8 @@ function buildCSPDirectives(csp) {
1922
1922
  directives.push(`img-src ${imgSources.join(" ")}`);
1923
1923
  const fontSources = ["'self'", "data:", ...allResourceDomains];
1924
1924
  directives.push(`font-src ${fontSources.join(" ")}`);
1925
- if (validConnectDomains.length) {
1926
- directives.push(`connect-src ${validConnectDomains.join(" ")}`);
1927
- } else {
1928
- directives.push(`connect-src ${allResourceDomains.join(" ")}`);
1929
- }
1925
+ const connectSources = [.../* @__PURE__ */ new Set([...allResourceDomains, ...validConnectDomains])];
1926
+ directives.push(`connect-src ${connectSources.join(" ")}`);
1930
1927
  directives.push("object-src 'self' data:");
1931
1928
  return directives;
1932
1929
  }
@@ -1935,26 +1932,34 @@ function buildCSPMetaTag(csp) {
1935
1932
  const content = directives.join("; ");
1936
1933
  return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
1937
1934
  }
1935
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1938
1936
  function validateCSPDomain(domain) {
1939
- if (domain.startsWith("https://*.")) {
1940
- const rest = domain.slice(10);
1941
- return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$/.test(rest);
1937
+ if (typeof domain !== "string" || /[\s;,'"]/.test(domain)) return false;
1938
+ if (/[?#]/.test(domain)) return false;
1939
+ if (domain.includes("*")) {
1940
+ const wildcard = /^(?:https|wss):\/\/\*\.(.*)$/.exec(domain);
1941
+ return wildcard !== null && /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(wildcard[1]);
1942
1942
  }
1943
1943
  try {
1944
1944
  const url = new URL(domain);
1945
- return url.protocol === "https:";
1945
+ if (url.protocol === "https:" || url.protocol === "wss:") return true;
1946
+ return (url.protocol === "http:" || url.protocol === "ws:") && LOOPBACK_HOSTS.has(url.hostname);
1946
1947
  } catch {
1947
1948
  return false;
1948
1949
  }
1949
1950
  }
1951
+ var reportedInvalidDomains = /* @__PURE__ */ new Set();
1950
1952
  function sanitizeCSPDomains(domains) {
1951
1953
  if (!domains) return [];
1952
1954
  const valid = [];
1953
1955
  for (const domain of domains) {
1954
1956
  if (validateCSPDomain(domain)) {
1955
1957
  valid.push(domain);
1956
- } else {
1957
- console.warn(`Invalid CSP domain ignored: ${domain}`);
1958
+ } else if (!reportedInvalidDomains.has(domain)) {
1959
+ reportedInvalidDomains.add(domain);
1960
+ console.warn(
1961
+ `Invalid CSP domain ignored: ${domain} (expected an https:// or wss:// origin, or http:// / ws:// on localhost)`
1962
+ );
1958
1963
  }
1959
1964
  }
1960
1965
  return valid;
@@ -2358,8 +2363,8 @@ if (__root) {
2358
2363
  configurable: true, enumerable: true
2359
2364
  });
2360
2365
  }
2361
- // 2. Try injected data globals
2362
- if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
2366
+ // 2. Try injected data globals (null: the page carries no call's data, e.g. compiled at startup)
2367
+ if (__hasData(window.__mcpToolOutput) && window.__mcpToolOutput !== null) { __render(window.__mcpToolOutput); }
2363
2368
  // 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
2364
2369
  var __bridge = window.FrontMcpBridge;
2365
2370
  if (__bridge && typeof __bridge.onToolResult === 'function') {
@@ -2625,7 +2630,7 @@ createRoot(document.getElementById('root'))
2625
2630
  return `${componentImport}
2626
2631
  import React from 'react';
2627
2632
  import { createRoot } from 'react-dom/client';
2628
- const output = window.__mcpToolOutput;
2633
+ const output = window.__mcpToolOutput || {};
2629
2634
  const props = ${propsCode};
2630
2635
  createRoot(document.getElementById('root'))
2631
2636
  .render(React.createElement(Component, props));`;
@@ -3090,6 +3095,16 @@ function renderUnwrappedResult(rawResult, options) {
3090
3095
  }
3091
3096
  return rawResult;
3092
3097
  }
3098
+ var warnedComponentReferences = /* @__PURE__ */ new WeakMap();
3099
+ function warnComponentReference(template, toolName, logger) {
3100
+ const warned = warnedComponentReferences.get(template) ?? /* @__PURE__ */ new Set();
3101
+ if (warned.has(toolName)) return;
3102
+ warned.add(toolName);
3103
+ warnedComponentReferences.set(template, warned);
3104
+ logger.warn(
3105
+ `[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
+ );
3107
+ }
3093
3108
  function isReactElement(value) {
3094
3109
  return typeof value === "object" && value !== null && "$$typeof" in value;
3095
3110
  }
@@ -3124,7 +3139,7 @@ function renderToolTemplate(options) {
3124
3139
  size = result.size;
3125
3140
  } else if (typeof template === "function") {
3126
3141
  const helpers = createTemplateHelpers();
3127
- const ctx = { input, output, helpers };
3142
+ const ctx = { input: input ?? {}, output: output ?? {}, helpers };
3128
3143
  let rawResult;
3129
3144
  let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
3130
3145
  if (isHtmlBuilder) {
@@ -3137,9 +3152,7 @@ function renderToolTemplate(options) {
3137
3152
  }
3138
3153
  }
3139
3154
  if (!isHtmlBuilder) {
3140
- (options.logger ?? console).warn(
3141
- `[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.`
3142
- );
3155
+ warnComponentReference(template, toolName, options.logger ?? console);
3143
3156
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
3144
3157
  html2 = shellResult.html;
3145
3158
  hash = shellResult.hash;
@@ -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;AA+ED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,CAsH/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;;;;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"}
@@ -564,8 +564,8 @@ if (__root) {
564
564
  configurable: true, enumerable: true
565
565
  });
566
566
  }
567
- // 2. Try injected data globals
568
- if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
567
+ // 2. Try injected data globals (null: the page carries no call's data, e.g. compiled at startup)
568
+ if (__hasData(window.__mcpToolOutput) && window.__mcpToolOutput !== null) { __render(window.__mcpToolOutput); }
569
569
  // 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
570
570
  var __bridge = window.FrontMcpBridge;
571
571
  if (__bridge && typeof __bridge.onToolResult === 'function') {
@@ -817,7 +817,7 @@ createRoot(document.getElementById('root'))
817
817
  return `${componentImport}
818
818
  import React from 'react';
819
819
  import { createRoot } from 'react-dom/client';
820
- const output = window.__mcpToolOutput;
820
+ const output = window.__mcpToolOutput || {};
821
821
  const props = ${propsCode};
822
822
  createRoot(document.getElementById('root'))
823
823
  .render(React.createElement(Component, props));`;
@@ -2214,11 +2214,8 @@ function buildCSPDirectives(csp) {
2214
2214
  directives.push(`img-src ${imgSources.join(" ")}`);
2215
2215
  const fontSources = ["'self'", "data:", ...allResourceDomains];
2216
2216
  directives.push(`font-src ${fontSources.join(" ")}`);
2217
- if (validConnectDomains.length) {
2218
- directives.push(`connect-src ${validConnectDomains.join(" ")}`);
2219
- } else {
2220
- directives.push(`connect-src ${allResourceDomains.join(" ")}`);
2221
- }
2217
+ const connectSources = [.../* @__PURE__ */ new Set([...allResourceDomains, ...validConnectDomains])];
2218
+ directives.push(`connect-src ${connectSources.join(" ")}`);
2222
2219
  directives.push("object-src 'self' data:");
2223
2220
  return directives;
2224
2221
  }
@@ -2227,26 +2224,34 @@ function buildCSPMetaTag(csp) {
2227
2224
  const content = directives.join("; ");
2228
2225
  return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
2229
2226
  }
2227
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
2230
2228
  function validateCSPDomain(domain) {
2231
- if (domain.startsWith("https://*.")) {
2232
- const rest = domain.slice(10);
2233
- return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$/.test(rest);
2229
+ if (typeof domain !== "string" || /[\s;,'"]/.test(domain)) return false;
2230
+ if (/[?#]/.test(domain)) return false;
2231
+ if (domain.includes("*")) {
2232
+ const wildcard = /^(?:https|wss):\/\/\*\.(.*)$/.exec(domain);
2233
+ return wildcard !== null && /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(wildcard[1]);
2234
2234
  }
2235
2235
  try {
2236
2236
  const url = new URL(domain);
2237
- return url.protocol === "https:";
2237
+ if (url.protocol === "https:" || url.protocol === "wss:") return true;
2238
+ return (url.protocol === "http:" || url.protocol === "ws:") && LOOPBACK_HOSTS.has(url.hostname);
2238
2239
  } catch {
2239
2240
  return false;
2240
2241
  }
2241
2242
  }
2243
+ var reportedInvalidDomains = /* @__PURE__ */ new Set();
2242
2244
  function sanitizeCSPDomains(domains) {
2243
2245
  if (!domains) return [];
2244
2246
  const valid = [];
2245
2247
  for (const domain of domains) {
2246
2248
  if (validateCSPDomain(domain)) {
2247
2249
  valid.push(domain);
2248
- } else {
2249
- console.warn(`Invalid CSP domain ignored: ${domain}`);
2250
+ } else if (!reportedInvalidDomains.has(domain)) {
2251
+ reportedInvalidDomains.add(domain);
2252
+ console.warn(
2253
+ `Invalid CSP domain ignored: ${domain} (expected an https:// or wss:// origin, or http:// / ws:// on localhost)`
2254
+ );
2250
2255
  }
2251
2256
  }
2252
2257
  return valid;
@@ -1,3 +1,6 @@
1
+ var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
1
4
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
5
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
6
  }) : x)(function(x) {
@@ -1890,11 +1893,8 @@ function buildCSPDirectives(csp) {
1890
1893
  directives.push(`img-src ${imgSources.join(" ")}`);
1891
1894
  const fontSources = ["'self'", "data:", ...allResourceDomains];
1892
1895
  directives.push(`font-src ${fontSources.join(" ")}`);
1893
- if (validConnectDomains.length) {
1894
- directives.push(`connect-src ${validConnectDomains.join(" ")}`);
1895
- } else {
1896
- directives.push(`connect-src ${allResourceDomains.join(" ")}`);
1897
- }
1896
+ const connectSources = [.../* @__PURE__ */ new Set([...allResourceDomains, ...validConnectDomains])];
1897
+ directives.push(`connect-src ${connectSources.join(" ")}`);
1898
1898
  directives.push("object-src 'self' data:");
1899
1899
  return directives;
1900
1900
  }
@@ -1903,26 +1903,34 @@ function buildCSPMetaTag(csp) {
1903
1903
  const content = directives.join("; ");
1904
1904
  return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
1905
1905
  }
1906
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1906
1907
  function validateCSPDomain(domain) {
1907
- if (domain.startsWith("https://*.")) {
1908
- const rest = domain.slice(10);
1909
- return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$/.test(rest);
1908
+ if (typeof domain !== "string" || /[\s;,'"]/.test(domain)) return false;
1909
+ if (/[?#]/.test(domain)) return false;
1910
+ if (domain.includes("*")) {
1911
+ const wildcard = /^(?:https|wss):\/\/\*\.(.*)$/.exec(domain);
1912
+ return wildcard !== null && /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(wildcard[1]);
1910
1913
  }
1911
1914
  try {
1912
1915
  const url = new URL(domain);
1913
- return url.protocol === "https:";
1916
+ if (url.protocol === "https:" || url.protocol === "wss:") return true;
1917
+ return (url.protocol === "http:" || url.protocol === "ws:") && LOOPBACK_HOSTS.has(url.hostname);
1914
1918
  } catch {
1915
1919
  return false;
1916
1920
  }
1917
1921
  }
1922
+ var reportedInvalidDomains = /* @__PURE__ */ new Set();
1918
1923
  function sanitizeCSPDomains(domains) {
1919
1924
  if (!domains) return [];
1920
1925
  const valid = [];
1921
1926
  for (const domain of domains) {
1922
1927
  if (validateCSPDomain(domain)) {
1923
1928
  valid.push(domain);
1924
- } else {
1925
- console.warn(`Invalid CSP domain ignored: ${domain}`);
1929
+ } else if (!reportedInvalidDomains.has(domain)) {
1930
+ reportedInvalidDomains.add(domain);
1931
+ console.warn(
1932
+ `Invalid CSP domain ignored: ${domain} (expected an https:// or wss:// origin, or http:// / ws:// on localhost)`
1933
+ );
1926
1934
  }
1927
1935
  }
1928
1936
  return valid;
@@ -2326,8 +2334,8 @@ if (__root) {
2326
2334
  configurable: true, enumerable: true
2327
2335
  });
2328
2336
  }
2329
- // 2. Try injected data globals
2330
- if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
2337
+ // 2. Try injected data globals (null: the page carries no call's data, e.g. compiled at startup)
2338
+ if (__hasData(window.__mcpToolOutput) && window.__mcpToolOutput !== null) { __render(window.__mcpToolOutput); }
2331
2339
  // 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
2332
2340
  var __bridge = window.FrontMcpBridge;
2333
2341
  if (__bridge && typeof __bridge.onToolResult === 'function') {
@@ -2593,7 +2601,7 @@ createRoot(document.getElementById('root'))
2593
2601
  return `${componentImport}
2594
2602
  import React from 'react';
2595
2603
  import { createRoot } from 'react-dom/client';
2596
- const output = window.__mcpToolOutput;
2604
+ const output = window.__mcpToolOutput || {};
2597
2605
  const props = ${propsCode};
2598
2606
  createRoot(document.getElementById('root'))
2599
2607
  .render(React.createElement(Component, props));`;
@@ -3058,6 +3066,16 @@ function renderUnwrappedResult(rawResult, options) {
3058
3066
  }
3059
3067
  return rawResult;
3060
3068
  }
3069
+ var warnedComponentReferences = /* @__PURE__ */ new WeakMap();
3070
+ function warnComponentReference(template, toolName, logger) {
3071
+ const warned = warnedComponentReferences.get(template) ?? /* @__PURE__ */ new Set();
3072
+ if (warned.has(toolName)) return;
3073
+ warned.add(toolName);
3074
+ warnedComponentReferences.set(template, warned);
3075
+ logger.warn(
3076
+ `[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
+ }
3061
3079
  function isReactElement(value) {
3062
3080
  return typeof value === "object" && value !== null && "$$typeof" in value;
3063
3081
  }
@@ -3092,7 +3110,7 @@ function renderToolTemplate(options) {
3092
3110
  size = result.size;
3093
3111
  } else if (typeof template === "function") {
3094
3112
  const helpers = createTemplateHelpers();
3095
- const ctx = { input, output, helpers };
3113
+ const ctx = { input: input ?? {}, output: output ?? {}, helpers };
3096
3114
  let rawResult;
3097
3115
  let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
3098
3116
  if (isHtmlBuilder) {
@@ -3105,9 +3123,7 @@ function renderToolTemplate(options) {
3105
3123
  }
3106
3124
  }
3107
3125
  if (!isHtmlBuilder) {
3108
- (options.logger ?? console).warn(
3109
- `[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.`
3110
- );
3126
+ warnComponentReference(template, toolName, options.logger ?? console);
3111
3127
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
3112
3128
  html2 = shellResult.html;
3113
3129
  hash = shellResult.hash;
@@ -1,3 +1,6 @@
1
+ var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
1
4
  // libs/uipack/src/bridge-runtime/iife-generator.ts
2
5
  function generateBridgeIIFE(options = {}) {
3
6
  const { debug = false, trustedOrigins = [], minify = false } = options;
@@ -1,3 +1,6 @@
1
+ var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
1
4
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
5
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
6
  }) : x)(function(x) {
@@ -535,8 +538,8 @@ if (__root) {
535
538
  configurable: true, enumerable: true
536
539
  });
537
540
  }
538
- // 2. Try injected data globals
539
- if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
541
+ // 2. Try injected data globals (null: the page carries no call's data, e.g. compiled at startup)
542
+ if (__hasData(window.__mcpToolOutput) && window.__mcpToolOutput !== null) { __render(window.__mcpToolOutput); }
540
543
  // 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
541
544
  var __bridge = window.FrontMcpBridge;
542
545
  if (__bridge && typeof __bridge.onToolResult === 'function') {
@@ -788,7 +791,7 @@ createRoot(document.getElementById('root'))
788
791
  return `${componentImport}
789
792
  import React from 'react';
790
793
  import { createRoot } from 'react-dom/client';
791
- const output = window.__mcpToolOutput;
794
+ const output = window.__mcpToolOutput || {};
792
795
  const props = ${propsCode};
793
796
  createRoot(document.getElementById('root'))
794
797
  .render(React.createElement(Component, props));`;
@@ -2185,11 +2188,8 @@ function buildCSPDirectives(csp) {
2185
2188
  directives.push(`img-src ${imgSources.join(" ")}`);
2186
2189
  const fontSources = ["'self'", "data:", ...allResourceDomains];
2187
2190
  directives.push(`font-src ${fontSources.join(" ")}`);
2188
- if (validConnectDomains.length) {
2189
- directives.push(`connect-src ${validConnectDomains.join(" ")}`);
2190
- } else {
2191
- directives.push(`connect-src ${allResourceDomains.join(" ")}`);
2192
- }
2191
+ const connectSources = [.../* @__PURE__ */ new Set([...allResourceDomains, ...validConnectDomains])];
2192
+ directives.push(`connect-src ${connectSources.join(" ")}`);
2193
2193
  directives.push("object-src 'self' data:");
2194
2194
  return directives;
2195
2195
  }
@@ -2198,26 +2198,34 @@ function buildCSPMetaTag(csp) {
2198
2198
  const content = directives.join("; ");
2199
2199
  return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
2200
2200
  }
2201
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
2201
2202
  function validateCSPDomain(domain) {
2202
- if (domain.startsWith("https://*.")) {
2203
- const rest = domain.slice(10);
2204
- return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$/.test(rest);
2203
+ if (typeof domain !== "string" || /[\s;,'"]/.test(domain)) return false;
2204
+ if (/[?#]/.test(domain)) return false;
2205
+ if (domain.includes("*")) {
2206
+ const wildcard = /^(?:https|wss):\/\/\*\.(.*)$/.exec(domain);
2207
+ return wildcard !== null && /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(wildcard[1]);
2205
2208
  }
2206
2209
  try {
2207
2210
  const url = new URL(domain);
2208
- return url.protocol === "https:";
2211
+ if (url.protocol === "https:" || url.protocol === "wss:") return true;
2212
+ return (url.protocol === "http:" || url.protocol === "ws:") && LOOPBACK_HOSTS.has(url.hostname);
2209
2213
  } catch {
2210
2214
  return false;
2211
2215
  }
2212
2216
  }
2217
+ var reportedInvalidDomains = /* @__PURE__ */ new Set();
2213
2218
  function sanitizeCSPDomains(domains) {
2214
2219
  if (!domains) return [];
2215
2220
  const valid = [];
2216
2221
  for (const domain of domains) {
2217
2222
  if (validateCSPDomain(domain)) {
2218
2223
  valid.push(domain);
2219
- } else {
2220
- console.warn(`Invalid CSP domain ignored: ${domain}`);
2224
+ } else if (!reportedInvalidDomains.has(domain)) {
2225
+ reportedInvalidDomains.add(domain);
2226
+ console.warn(
2227
+ `Invalid CSP domain ignored: ${domain} (expected an https:// or wss:// origin, or http:// / ws:// on localhost)`
2228
+ );
2221
2229
  }
2222
2230
  }
2223
2231
  return valid;
package/esm/index.mjs CHANGED
@@ -1,3 +1,6 @@
1
+ var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
1
4
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
5
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
6
  }) : x)(function(x) {
@@ -2973,11 +2976,8 @@ function buildCSPDirectives(csp) {
2973
2976
  directives.push(`img-src ${imgSources.join(" ")}`);
2974
2977
  const fontSources = ["'self'", "data:", ...allResourceDomains];
2975
2978
  directives.push(`font-src ${fontSources.join(" ")}`);
2976
- if (validConnectDomains.length) {
2977
- directives.push(`connect-src ${validConnectDomains.join(" ")}`);
2978
- } else {
2979
- directives.push(`connect-src ${allResourceDomains.join(" ")}`);
2980
- }
2979
+ const connectSources = [.../* @__PURE__ */ new Set([...allResourceDomains, ...validConnectDomains])];
2980
+ directives.push(`connect-src ${connectSources.join(" ")}`);
2981
2981
  directives.push("object-src 'self' data:");
2982
2982
  return directives;
2983
2983
  }
@@ -2986,26 +2986,34 @@ function buildCSPMetaTag(csp) {
2986
2986
  const content = directives.join("; ");
2987
2987
  return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
2988
2988
  }
2989
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
2989
2990
  function validateCSPDomain(domain) {
2990
- if (domain.startsWith("https://*.")) {
2991
- const rest = domain.slice(10);
2992
- return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$/.test(rest);
2991
+ if (typeof domain !== "string" || /[\s;,'"]/.test(domain)) return false;
2992
+ if (/[?#]/.test(domain)) return false;
2993
+ if (domain.includes("*")) {
2994
+ const wildcard = /^(?:https|wss):\/\/\*\.(.*)$/.exec(domain);
2995
+ return wildcard !== null && /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(wildcard[1]);
2993
2996
  }
2994
2997
  try {
2995
2998
  const url = new URL(domain);
2996
- return url.protocol === "https:";
2999
+ if (url.protocol === "https:" || url.protocol === "wss:") return true;
3000
+ return (url.protocol === "http:" || url.protocol === "ws:") && LOOPBACK_HOSTS.has(url.hostname);
2997
3001
  } catch {
2998
3002
  return false;
2999
3003
  }
3000
3004
  }
3005
+ var reportedInvalidDomains = /* @__PURE__ */ new Set();
3001
3006
  function sanitizeCSPDomains(domains) {
3002
3007
  if (!domains) return [];
3003
3008
  const valid = [];
3004
3009
  for (const domain of domains) {
3005
3010
  if (validateCSPDomain(domain)) {
3006
3011
  valid.push(domain);
3007
- } else {
3008
- console.warn(`Invalid CSP domain ignored: ${domain}`);
3012
+ } else if (!reportedInvalidDomains.has(domain)) {
3013
+ reportedInvalidDomains.add(domain);
3014
+ console.warn(
3015
+ `Invalid CSP domain ignored: ${domain} (expected an https:// or wss:// origin, or http:// / ws:// on localhost)`
3016
+ );
3009
3017
  }
3010
3018
  }
3011
3019
  return valid;
@@ -3542,8 +3550,8 @@ if (__root) {
3542
3550
  configurable: true, enumerable: true
3543
3551
  });
3544
3552
  }
3545
- // 2. Try injected data globals
3546
- if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
3553
+ // 2. Try injected data globals (null: the page carries no call's data, e.g. compiled at startup)
3554
+ if (__hasData(window.__mcpToolOutput) && window.__mcpToolOutput !== null) { __render(window.__mcpToolOutput); }
3547
3555
  // 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
3548
3556
  var __bridge = window.FrontMcpBridge;
3549
3557
  if (__bridge && typeof __bridge.onToolResult === 'function') {
@@ -3795,7 +3803,7 @@ createRoot(document.getElementById('root'))
3795
3803
  return `${componentImport}
3796
3804
  import React from 'react';
3797
3805
  import { createRoot } from 'react-dom/client';
3798
- const output = window.__mcpToolOutput;
3806
+ const output = window.__mcpToolOutput || {};
3799
3807
  const props = ${propsCode};
3800
3808
  createRoot(document.getElementById('root'))
3801
3809
  .render(React.createElement(Component, props));`;
@@ -4329,6 +4337,16 @@ function renderUnwrappedResult(rawResult, options) {
4329
4337
  }
4330
4338
  return rawResult;
4331
4339
  }
4340
+ var warnedComponentReferences = /* @__PURE__ */ new WeakMap();
4341
+ function warnComponentReference(template, toolName, logger) {
4342
+ const warned = warnedComponentReferences.get(template) ?? /* @__PURE__ */ new Set();
4343
+ if (warned.has(toolName)) return;
4344
+ warned.add(toolName);
4345
+ warnedComponentReferences.set(template, warned);
4346
+ logger.warn(
4347
+ `[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
+ }
4332
4350
  function isReactElement(value) {
4333
4351
  return typeof value === "object" && value !== null && "$$typeof" in value;
4334
4352
  }
@@ -4363,7 +4381,7 @@ function renderToolTemplate(options) {
4363
4381
  size = result.size;
4364
4382
  } else if (typeof template === "function") {
4365
4383
  const helpers = createTemplateHelpers();
4366
- const ctx = { input, output, helpers };
4384
+ const ctx = { input: input ?? {}, output: output ?? {}, helpers };
4367
4385
  let rawResult;
4368
4386
  let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
4369
4387
  if (isHtmlBuilder) {
@@ -4376,9 +4394,7 @@ function renderToolTemplate(options) {
4376
4394
  }
4377
4395
  }
4378
4396
  if (!isHtmlBuilder) {
4379
- (options.logger ?? console).warn(
4380
- `[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.`
4381
- );
4397
+ warnComponentReference(template, toolName, options.logger ?? console);
4382
4398
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
4383
4399
  html2 = shellResult.html;
4384
4400
  hash = shellResult.hash;
package/esm/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/uipack",
3
- "version": "1.8.7",
3
+ "version": "1.9.0",
4
4
  "description": "FrontMCP UIpack - HTML shell builder, pluggable import resolver, and NPM component loader for MCP UI (React-free core)",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "homepage": "https://docs.agentfront.dev",
@@ -61,7 +61,7 @@
61
61
  "typescript": "^5.9.3"
62
62
  },
63
63
  "peerDependencies": {
64
- "@frontmcp/lazy-zod": "1.8.7",
64
+ "@frontmcp/lazy-zod": "1.9.0",
65
65
  "esbuild": ">=0.27.0 <1"
66
66
  },
67
67
  "peerDependenciesMeta": {
@@ -1,3 +1,6 @@
1
+ var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
1
4
  // libs/uipack/src/resolver/cdn-registry.ts
2
5
  var DEFAULT_CDN_REGISTRY = {
3
6
  react: {
@@ -1,3 +1,6 @@
1
+ var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
1
4
  // libs/uipack/src/bridge-runtime/iife-generator.ts
2
5
  function generateBridgeIIFE(options = {}) {
3
6
  const { debug = false, trustedOrigins = [], minify = false } = options;
@@ -1367,11 +1370,8 @@ function buildCSPDirectives(csp) {
1367
1370
  directives.push(`img-src ${imgSources.join(" ")}`);
1368
1371
  const fontSources = ["'self'", "data:", ...allResourceDomains];
1369
1372
  directives.push(`font-src ${fontSources.join(" ")}`);
1370
- if (validConnectDomains.length) {
1371
- directives.push(`connect-src ${validConnectDomains.join(" ")}`);
1372
- } else {
1373
- directives.push(`connect-src ${allResourceDomains.join(" ")}`);
1374
- }
1373
+ const connectSources = [.../* @__PURE__ */ new Set([...allResourceDomains, ...validConnectDomains])];
1374
+ directives.push(`connect-src ${connectSources.join(" ")}`);
1375
1375
  directives.push("object-src 'self' data:");
1376
1376
  return directives;
1377
1377
  }
@@ -1380,26 +1380,34 @@ function buildCSPMetaTag(csp) {
1380
1380
  const content = directives.join("; ");
1381
1381
  return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
1382
1382
  }
1383
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1383
1384
  function validateCSPDomain(domain) {
1384
- if (domain.startsWith("https://*.")) {
1385
- const rest = domain.slice(10);
1386
- return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$/.test(rest);
1385
+ if (typeof domain !== "string" || /[\s;,'"]/.test(domain)) return false;
1386
+ if (/[?#]/.test(domain)) return false;
1387
+ if (domain.includes("*")) {
1388
+ const wildcard = /^(?:https|wss):\/\/\*\.(.*)$/.exec(domain);
1389
+ return wildcard !== null && /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(wildcard[1]);
1387
1390
  }
1388
1391
  try {
1389
1392
  const url = new URL(domain);
1390
- return url.protocol === "https:";
1393
+ if (url.protocol === "https:" || url.protocol === "wss:") return true;
1394
+ return (url.protocol === "http:" || url.protocol === "ws:") && LOOPBACK_HOSTS.has(url.hostname);
1391
1395
  } catch {
1392
1396
  return false;
1393
1397
  }
1394
1398
  }
1399
+ var reportedInvalidDomains = /* @__PURE__ */ new Set();
1395
1400
  function sanitizeCSPDomains(domains) {
1396
1401
  if (!domains) return [];
1397
1402
  const valid = [];
1398
1403
  for (const domain of domains) {
1399
1404
  if (validateCSPDomain(domain)) {
1400
1405
  valid.push(domain);
1401
- } else {
1402
- console.warn(`Invalid CSP domain ignored: ${domain}`);
1406
+ } else if (!reportedInvalidDomains.has(domain)) {
1407
+ reportedInvalidDomains.add(domain);
1408
+ console.warn(
1409
+ `Invalid CSP domain ignored: ${domain} (expected an https:// or wss:// origin, or http:// / ws:// on localhost)`
1410
+ );
1403
1411
  }
1404
1412
  }
1405
1413
  return valid;
@@ -1,3 +1,6 @@
1
+ var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
1
4
  // libs/uipack/src/types/ui-runtime.ts
2
5
  function isUIType(value) {
3
6
  return typeof value === "string" && ["html", "react", "mdx", "markdown", "auto"].includes(value);
@@ -1,3 +1,6 @@
1
+ var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
1
4
  // libs/uipack/src/typings/types.ts
2
5
  var DEFAULT_ALLOWED_PACKAGES = ["react", "react-dom", "react/jsx-runtime", "zod", "@frontmcp/*"];
3
6
  var DEFAULT_TYPE_FETCHER_OPTIONS = {
@@ -1,3 +1,6 @@
1
+ var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
1
4
  // libs/uipack/src/utils/index.ts
2
5
  function safeStringify(value, space) {
3
6
  const seen = /* @__PURE__ */ new WeakSet();
package/index.js CHANGED
@@ -3093,11 +3093,8 @@ function buildCSPDirectives(csp) {
3093
3093
  directives.push(`img-src ${imgSources.join(" ")}`);
3094
3094
  const fontSources = ["'self'", "data:", ...allResourceDomains];
3095
3095
  directives.push(`font-src ${fontSources.join(" ")}`);
3096
- if (validConnectDomains.length) {
3097
- directives.push(`connect-src ${validConnectDomains.join(" ")}`);
3098
- } else {
3099
- directives.push(`connect-src ${allResourceDomains.join(" ")}`);
3100
- }
3096
+ const connectSources = [.../* @__PURE__ */ new Set([...allResourceDomains, ...validConnectDomains])];
3097
+ directives.push(`connect-src ${connectSources.join(" ")}`);
3101
3098
  directives.push("object-src 'self' data:");
3102
3099
  return directives;
3103
3100
  }
@@ -3106,26 +3103,34 @@ function buildCSPMetaTag(csp) {
3106
3103
  const content = directives.join("; ");
3107
3104
  return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
3108
3105
  }
3106
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
3109
3107
  function validateCSPDomain(domain) {
3110
- if (domain.startsWith("https://*.")) {
3111
- const rest = domain.slice(10);
3112
- return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$/.test(rest);
3108
+ if (typeof domain !== "string" || /[\s;,'"]/.test(domain)) return false;
3109
+ if (/[?#]/.test(domain)) return false;
3110
+ if (domain.includes("*")) {
3111
+ const wildcard = /^(?:https|wss):\/\/\*\.(.*)$/.exec(domain);
3112
+ return wildcard !== null && /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(wildcard[1]);
3113
3113
  }
3114
3114
  try {
3115
3115
  const url = new URL(domain);
3116
- return url.protocol === "https:";
3116
+ if (url.protocol === "https:" || url.protocol === "wss:") return true;
3117
+ return (url.protocol === "http:" || url.protocol === "ws:") && LOOPBACK_HOSTS.has(url.hostname);
3117
3118
  } catch {
3118
3119
  return false;
3119
3120
  }
3120
3121
  }
3122
+ var reportedInvalidDomains = /* @__PURE__ */ new Set();
3121
3123
  function sanitizeCSPDomains(domains) {
3122
3124
  if (!domains) return [];
3123
3125
  const valid = [];
3124
3126
  for (const domain of domains) {
3125
3127
  if (validateCSPDomain(domain)) {
3126
3128
  valid.push(domain);
3127
- } else {
3128
- console.warn(`Invalid CSP domain ignored: ${domain}`);
3129
+ } else if (!reportedInvalidDomains.has(domain)) {
3130
+ reportedInvalidDomains.add(domain);
3131
+ console.warn(
3132
+ `Invalid CSP domain ignored: ${domain} (expected an https:// or wss:// origin, or http:// / ws:// on localhost)`
3133
+ );
3129
3134
  }
3130
3135
  }
3131
3136
  return valid;
@@ -3662,8 +3667,8 @@ if (__root) {
3662
3667
  configurable: true, enumerable: true
3663
3668
  });
3664
3669
  }
3665
- // 2. Try injected data globals
3666
- if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
3670
+ // 2. Try injected data globals (null: the page carries no call's data, e.g. compiled at startup)
3671
+ if (__hasData(window.__mcpToolOutput) && window.__mcpToolOutput !== null) { __render(window.__mcpToolOutput); }
3667
3672
  // 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
3668
3673
  var __bridge = window.FrontMcpBridge;
3669
3674
  if (__bridge && typeof __bridge.onToolResult === 'function') {
@@ -3915,7 +3920,7 @@ createRoot(document.getElementById('root'))
3915
3920
  return `${componentImport}
3916
3921
  import React from 'react';
3917
3922
  import { createRoot } from 'react-dom/client';
3918
- const output = window.__mcpToolOutput;
3923
+ const output = window.__mcpToolOutput || {};
3919
3924
  const props = ${propsCode};
3920
3925
  createRoot(document.getElementById('root'))
3921
3926
  .render(React.createElement(Component, props));`;
@@ -4449,6 +4454,16 @@ function renderUnwrappedResult(rawResult, options) {
4449
4454
  }
4450
4455
  return rawResult;
4451
4456
  }
4457
+ var warnedComponentReferences = /* @__PURE__ */ new WeakMap();
4458
+ function warnComponentReference(template, toolName, logger) {
4459
+ const warned = warnedComponentReferences.get(template) ?? /* @__PURE__ */ new Set();
4460
+ if (warned.has(toolName)) return;
4461
+ warned.add(toolName);
4462
+ warnedComponentReferences.set(template, warned);
4463
+ logger.warn(
4464
+ `[FrontMCP] Tool "${toolName}": \`ui.template\` is a React component reference, which cannot be bundled (there is no source file to compile), so the widget would render an empty page. Use \`template: { file: './widget.tsx' }\` instead.`
4465
+ );
4466
+ }
4452
4467
  function isReactElement(value) {
4453
4468
  return typeof value === "object" && value !== null && "$$typeof" in value;
4454
4469
  }
@@ -4483,7 +4498,7 @@ function renderToolTemplate(options) {
4483
4498
  size = result.size;
4484
4499
  } else if (typeof template === "function") {
4485
4500
  const helpers = createTemplateHelpers();
4486
- const ctx = { input, output, helpers };
4501
+ const ctx = { input: input ?? {}, output: output ?? {}, helpers };
4487
4502
  let rawResult;
4488
4503
  let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
4489
4504
  if (isHtmlBuilder) {
@@ -4496,9 +4511,7 @@ function renderToolTemplate(options) {
4496
4511
  }
4497
4512
  }
4498
4513
  if (!isHtmlBuilder) {
4499
- (options.logger ?? console).warn(
4500
- `[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.`
4501
- );
4514
+ warnComponentReference(template, toolName, options.logger ?? console);
4502
4515
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
4503
4516
  html2 = shellResult.html;
4504
4517
  hash = shellResult.hash;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/uipack",
3
- "version": "1.8.7",
3
+ "version": "1.9.0",
4
4
  "description": "FrontMCP UIpack - HTML shell builder, pluggable import resolver, and NPM component loader for MCP UI (React-free core)",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "homepage": "https://docs.agentfront.dev",
@@ -61,7 +61,7 @@
61
61
  "typescript": "^5.9.3"
62
62
  },
63
63
  "peerDependencies": {
64
- "@frontmcp/lazy-zod": "1.8.7",
64
+ "@frontmcp/lazy-zod": "1.9.0",
65
65
  "esbuild": ">=0.27.0 <1"
66
66
  },
67
67
  "peerDependenciesMeta": {
package/shell/csp.d.ts CHANGED
@@ -28,10 +28,15 @@ export declare function buildCSPDirectives(csp?: CSPConfig): string[];
28
28
  export declare function buildCSPMetaTag(csp?: CSPConfig): string;
29
29
  /**
30
30
  * Validate CSP domain format.
31
+ *
32
+ * Accepts `https://` and `wss://` origins (a WebSocket API needs `wss://` in `connect-src`),
33
+ * their `https://*.` / `wss://*.` wildcard forms, and `http://` / `ws://` origins on a loopback
34
+ * host (`localhost`, `127.0.0.1`, `[::1]`) for local development. A value with a query or a
35
+ * fragment is rejected.
31
36
  */
32
37
  export declare function validateCSPDomain(domain: string): boolean;
33
38
  /**
34
- * Filter and warn about invalid CSP domains.
39
+ * Filter out invalid CSP domains, warning once per domain.
35
40
  */
36
41
  export declare function sanitizeCSPDomains(domains: string[] | undefined): string[];
37
42
  //# sourceMappingURL=csp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"csp.d.ts","sourceRoot":"","sources":["../../src/shell/csp.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC;;GAEG;AACH,eAAO,MAAM,mBAAmB,sJAMtB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,sBAAsB,uPAQzB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,0BAA0B,4MAQ7B,CAAC;AAEX;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,CAAC,EAAE,SAAS,GAAG,MAAM,EAAE,CA+B5D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,CAAC,EAAE,SAAS,GAAG,MAAM,CAIvD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAYzD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,CAa1E"}
1
+ {"version":3,"file":"csp.d.ts","sourceRoot":"","sources":["../../src/shell/csp.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC;;GAEG;AACH,eAAO,MAAM,mBAAmB,sJAMtB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,sBAAsB,uPAQzB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,0BAA0B,4MAQ7B,CAAC;AAEX;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,CAAC,EAAE,SAAS,GAAG,MAAM,EAAE,CA8B5D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,CAAC,EAAE,SAAS,GAAG,MAAM,CAIvD;AAKD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAkBzD;AAKD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,CAgB1E"}
package/shell/index.js CHANGED
@@ -1419,11 +1419,8 @@ function buildCSPDirectives(csp) {
1419
1419
  directives.push(`img-src ${imgSources.join(" ")}`);
1420
1420
  const fontSources = ["'self'", "data:", ...allResourceDomains];
1421
1421
  directives.push(`font-src ${fontSources.join(" ")}`);
1422
- if (validConnectDomains.length) {
1423
- directives.push(`connect-src ${validConnectDomains.join(" ")}`);
1424
- } else {
1425
- directives.push(`connect-src ${allResourceDomains.join(" ")}`);
1426
- }
1422
+ const connectSources = [.../* @__PURE__ */ new Set([...allResourceDomains, ...validConnectDomains])];
1423
+ directives.push(`connect-src ${connectSources.join(" ")}`);
1427
1424
  directives.push("object-src 'self' data:");
1428
1425
  return directives;
1429
1426
  }
@@ -1432,26 +1429,34 @@ function buildCSPMetaTag(csp) {
1432
1429
  const content = directives.join("; ");
1433
1430
  return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
1434
1431
  }
1432
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1435
1433
  function validateCSPDomain(domain) {
1436
- if (domain.startsWith("https://*.")) {
1437
- const rest = domain.slice(10);
1438
- return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$/.test(rest);
1434
+ if (typeof domain !== "string" || /[\s;,'"]/.test(domain)) return false;
1435
+ if (/[?#]/.test(domain)) return false;
1436
+ if (domain.includes("*")) {
1437
+ const wildcard = /^(?:https|wss):\/\/\*\.(.*)$/.exec(domain);
1438
+ return wildcard !== null && /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(wildcard[1]);
1439
1439
  }
1440
1440
  try {
1441
1441
  const url = new URL(domain);
1442
- return url.protocol === "https:";
1442
+ if (url.protocol === "https:" || url.protocol === "wss:") return true;
1443
+ return (url.protocol === "http:" || url.protocol === "ws:") && LOOPBACK_HOSTS.has(url.hostname);
1443
1444
  } catch {
1444
1445
  return false;
1445
1446
  }
1446
1447
  }
1448
+ var reportedInvalidDomains = /* @__PURE__ */ new Set();
1447
1449
  function sanitizeCSPDomains(domains) {
1448
1450
  if (!domains) return [];
1449
1451
  const valid = [];
1450
1452
  for (const domain of domains) {
1451
1453
  if (validateCSPDomain(domain)) {
1452
1454
  valid.push(domain);
1453
- } else {
1454
- console.warn(`Invalid CSP domain ignored: ${domain}`);
1455
+ } else if (!reportedInvalidDomains.has(domain)) {
1456
+ reportedInvalidDomains.add(domain);
1457
+ console.warn(
1458
+ `Invalid CSP domain ignored: ${domain} (expected an https:// or wss:// origin, or http:// / ws:// on localhost)`
1459
+ );
1455
1460
  }
1456
1461
  }
1457
1462
  return valid;