@frontmcp/uipack 1.3.0 → 1.4.1

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.
Files changed (44) hide show
  1. package/adapters/index.js +1046 -698
  2. package/adapters/template-renderer.d.ts +14 -0
  3. package/adapters/template-renderer.d.ts.map +1 -1
  4. package/bridge-runtime/iife-generator.d.ts.map +1 -1
  5. package/bridge-runtime/index.js +149 -0
  6. package/component/index.d.ts +1 -0
  7. package/component/index.d.ts.map +1 -1
  8. package/component/index.js +468 -145
  9. package/component/loader.d.ts +21 -2
  10. package/component/loader.d.ts.map +1 -1
  11. package/component/renderer.d.ts +2 -2
  12. package/component/renderer.d.ts.map +1 -1
  13. package/component/transpiler.d.ts +16 -1
  14. package/component/transpiler.d.ts.map +1 -1
  15. package/component/types.d.ts +19 -0
  16. package/component/types.d.ts.map +1 -1
  17. package/component/ui-availability.d.ts +27 -0
  18. package/component/ui-availability.d.ts.map +1 -0
  19. package/esm/adapters/index.mjs +1046 -698
  20. package/esm/bridge-runtime/index.mjs +149 -0
  21. package/esm/component/index.mjs +468 -145
  22. package/esm/index.mjs +444 -109
  23. package/esm/package.json +2 -2
  24. package/esm/shell/index.mjs +420 -171
  25. package/index.d.ts +1 -1
  26. package/index.d.ts.map +1 -1
  27. package/index.js +445 -109
  28. package/package.json +2 -2
  29. package/shell/builder.d.ts.map +1 -1
  30. package/shell/data-injector.d.ts +27 -1
  31. package/shell/data-injector.d.ts.map +1 -1
  32. package/shell/index.d.ts +3 -2
  33. package/shell/index.d.ts.map +1 -1
  34. package/shell/index.js +423 -171
  35. package/shell/sizing-css.d.ts +27 -0
  36. package/shell/sizing-css.d.ts.map +1 -0
  37. package/shell/types.d.ts +102 -0
  38. package/shell/types.d.ts.map +1 -1
  39. package/types/index.d.ts +1 -1
  40. package/types/index.d.ts.map +1 -1
  41. package/types/ui-config.d.ts +105 -11
  42. package/types/ui-config.d.ts.map +1 -1
  43. package/types/ui-runtime.d.ts +23 -2
  44. package/types/ui-runtime.d.ts.map +1 -1
@@ -473,6 +473,29 @@ function safeJsonForScript(value) {
473
473
  }
474
474
  }
475
475
 
476
+ // libs/uipack/src/component/ui-availability.ts
477
+ function isFrontmcpUiResolvable(...candidatePaths) {
478
+ try {
479
+ const nodeFs = require("fs");
480
+ const nodePath = require("path");
481
+ const ORG = "@frontmcp";
482
+ const PKG = "ui";
483
+ for (const candidate of candidatePaths) {
484
+ let dir = candidate;
485
+ while (true) {
486
+ const pkgJson = nodePath.join(dir, "node_modules", ORG, PKG, "package.json");
487
+ if (nodeFs.existsSync(pkgJson)) return true;
488
+ const parent = nodePath.dirname(dir);
489
+ if (parent === dir) break;
490
+ dir = parent;
491
+ }
492
+ }
493
+ return false;
494
+ } catch {
495
+ return false;
496
+ }
497
+ }
498
+
476
499
  // libs/uipack/src/component/transpiler.ts
477
500
  function transpileReactSource(source, filename) {
478
501
  const esbuild = require("esbuild");
@@ -488,7 +511,12 @@ function transpileReactSource(source, filename) {
488
511
  });
489
512
  return result.code;
490
513
  }
491
- function bundleFileSource(source, filename, resolveDir, componentName) {
514
+ function bundleFileSource(source, filename, resolveDir, componentName, options = {}) {
515
+ if (!isFrontmcpUiResolvable(resolveDir, process.cwd())) {
516
+ throw new Error(
517
+ `FileSource widget "${filename}" requires the @frontmcp/ui package, which provides the React bridge mount that's injected at bundle time. Install it (e.g. \`npm install @frontmcp/ui\` or \`yarn add @frontmcp/ui\`) and try again.`
518
+ );
519
+ }
492
520
  const esbuild = require("esbuild");
493
521
  const mountCode = `
494
522
  // --- Auto-generated mount ---
@@ -570,7 +598,11 @@ if (__root) {
570
598
  format: "esm",
571
599
  target: "es2020",
572
600
  jsx: "automatic",
573
- external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
601
+ // When `bundleReact` is set (resourceMode: 'inline'), React itself is
602
+ // bundled — required for hosts that block external script execution
603
+ // such as Claude (#454). Otherwise React stays external and is loaded
604
+ // via the import map emitted by the renderer.
605
+ external: options.bundleReact ? [] : ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
574
606
  alias,
575
607
  define: { "process.env.NODE_ENV": '"production"' },
576
608
  platform: "browser",
@@ -582,7 +614,7 @@ if (__root) {
582
614
  } catch (err) {
583
615
  const message = err instanceof Error ? err.message : String(err);
584
616
  throw new Error(
585
- `Failed to bundle FileSource "${filename}": ${message}. Ensure workspace packages are built (e.g. nx build ui).`
617
+ `Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`
586
618
  );
587
619
  }
588
620
  }
@@ -600,6 +632,9 @@ var DEFAULT_META = {
600
632
  renderer: "auto"
601
633
  };
602
634
  function resolveUISource(source, options) {
635
+ if (options?.inlineReact && options?.transformOnly) {
636
+ throw new Error("resolveUISource: `inlineReact` and `transformOnly` are mutually exclusive \u2014 set at most one.");
637
+ }
603
638
  const resolver = options?.resolver ?? createEsmShResolver();
604
639
  if (isNpmSource(source)) {
605
640
  return resolveNpmSource(source, resolver);
@@ -608,7 +643,7 @@ function resolveUISource(source, options) {
608
643
  return resolveImportSource(source);
609
644
  }
610
645
  if (isFileSource(source)) {
611
- return resolveFileSource(source);
646
+ return resolveFileSource(source, { inlineReact: options?.inlineReact, transformOnly: options?.transformOnly });
612
647
  }
613
648
  if (isFunctionSource(source)) {
614
649
  return resolveFunctionSource(source, options?.input, options?.output);
@@ -641,15 +676,42 @@ function resolveImportSource(source) {
641
676
  peerDependencies: []
642
677
  };
643
678
  }
644
- function resolveFileSource(source) {
679
+ function resolveFileSource(source, options = {}) {
645
680
  const path = require("path");
646
681
  const ext = path.extname(source.file).toLowerCase();
647
682
  if (ext === ".tsx" || ext === ".jsx") {
648
683
  const fs = require("fs");
649
- const filePath = path.isAbsolute(source.file) ? source.file : path.resolve(process.cwd(), source.file);
650
- const rawSource = fs.readFileSync(filePath, "utf-8");
684
+ const wasRelative = !path.isAbsolute(source.file);
685
+ const filePath = wasRelative ? path.resolve(process.cwd(), source.file) : source.file;
686
+ let rawSource;
687
+ try {
688
+ rawSource = fs.readFileSync(filePath, "utf-8");
689
+ } catch (err) {
690
+ const isNotFound = err?.code === "ENOENT";
691
+ if (isNotFound && wasRelative) {
692
+ throw new Error(
693
+ `FileSource widget "${source.file}" not found at "${filePath}". Relative paths are resolved against process.cwd() ("${process.cwd()}"), not the tool file's directory. To anchor the path to the tool file, pass an absolute path \u2014 e.g. \`{ file: fileURLToPath(new URL('./widget.tsx', import.meta.url)) }\` from \`node:url\` (see issue #444).`
694
+ );
695
+ }
696
+ throw err;
697
+ }
651
698
  const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
652
- const bundled = bundleFileSource(rawSource, source.file, path.dirname(filePath), componentName);
699
+ if (options.transformOnly === true) {
700
+ const code = transpileReactSource(rawSource, path.basename(filePath));
701
+ const parsed2 = parseImports(code);
702
+ return {
703
+ mode: "module",
704
+ code,
705
+ imports: [...new Set(parsed2.externalImports.map((i) => i.specifier))],
706
+ exportName: componentName,
707
+ meta: { mcpAware: true, renderer: "react" },
708
+ peerDependencies: source.peerDependencies || ["react", "react-dom"],
709
+ bundled: false
710
+ };
711
+ }
712
+ const bundled = bundleFileSource(rawSource, source.file, path.dirname(filePath), componentName, {
713
+ bundleReact: options.inlineReact === true
714
+ });
653
715
  const parsed = parseImports(bundled.code);
654
716
  return {
655
717
  mode: "module",
@@ -717,92 +779,27 @@ function generateMappedPropsCode(mapping) {
717
779
  return `({ ${entries} })`;
718
780
  }
719
781
 
720
- // libs/uipack/src/shell/csp.ts
721
- var DEFAULT_CDN_DOMAINS = [
722
- "https://cdn.jsdelivr.net",
723
- "https://cdnjs.cloudflare.com",
724
- "https://fonts.googleapis.com",
725
- "https://fonts.gstatic.com",
726
- "https://esm.sh"
727
- ];
728
- var DEFAULT_CSP_DIRECTIVES = [
729
- "default-src 'none'",
730
- `script-src 'self' 'unsafe-inline' ${DEFAULT_CDN_DOMAINS.join(" ")}`,
731
- `style-src 'self' 'unsafe-inline' ${DEFAULT_CDN_DOMAINS.join(" ")}`,
732
- `img-src 'self' data: ${DEFAULT_CDN_DOMAINS.join(" ")}`,
733
- `font-src 'self' data: ${DEFAULT_CDN_DOMAINS.join(" ")}`,
734
- `connect-src ${DEFAULT_CDN_DOMAINS.join(" ")}`,
735
- "object-src 'self' data:"
736
- ];
737
- function buildCSPDirectives(csp) {
738
- if (!csp) {
739
- return [...DEFAULT_CSP_DIRECTIVES];
740
- }
741
- const validResourceDomains = sanitizeCSPDomains(csp.resourceDomains);
742
- const validConnectDomains = sanitizeCSPDomains(csp.connectDomains);
743
- const allResourceDomains = [.../* @__PURE__ */ new Set([...DEFAULT_CDN_DOMAINS, ...validResourceDomains])];
744
- const directives = [
745
- "default-src 'none'",
746
- `script-src 'self' 'unsafe-inline' ${allResourceDomains.join(" ")}`,
747
- `style-src 'self' 'unsafe-inline' ${allResourceDomains.join(" ")}`
748
- ];
749
- const imgSources = ["'self'", "data:", ...allResourceDomains];
750
- directives.push(`img-src ${imgSources.join(" ")}`);
751
- const fontSources = ["'self'", "data:", ...allResourceDomains];
752
- directives.push(`font-src ${fontSources.join(" ")}`);
753
- if (validConnectDomains.length) {
754
- directives.push(`connect-src ${validConnectDomains.join(" ")}`);
755
- } else {
756
- directives.push(`connect-src ${allResourceDomains.join(" ")}`);
757
- }
758
- directives.push("object-src 'self' data:");
759
- return directives;
760
- }
761
- function buildCSPMetaTag(csp) {
762
- const directives = buildCSPDirectives(csp);
763
- const content = directives.join("; ");
764
- return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
765
- }
766
- function validateCSPDomain(domain) {
767
- if (domain.startsWith("https://*.")) {
768
- const rest = domain.slice(10);
769
- return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$/.test(rest);
770
- }
771
- try {
772
- const url = new URL(domain);
773
- return url.protocol === "https:";
774
- } catch {
775
- return false;
776
- }
777
- }
778
- function sanitizeCSPDomains(domains) {
779
- if (!domains) return [];
780
- const valid = [];
781
- for (const domain of domains) {
782
- if (validateCSPDomain(domain)) {
783
- valid.push(domain);
784
- } else {
785
- console.warn(`Invalid CSP domain ignored: ${domain}`);
782
+ // libs/uipack/src/resolver/import-map.ts
783
+ function createImportMapFromResolved(resolved) {
784
+ const imports = {};
785
+ const integrity = {};
786
+ for (const [specifier, res] of Object.entries(resolved)) {
787
+ if (res.type === "url") {
788
+ imports[specifier] = res.value;
789
+ if (res.integrity) {
790
+ integrity[res.value] = res.integrity;
791
+ }
786
792
  }
787
793
  }
788
- return valid;
789
- }
790
- function escapeAttribute(str) {
791
- return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
794
+ return {
795
+ imports,
796
+ integrity: Object.keys(integrity).length > 0 ? integrity : void 0
797
+ };
792
798
  }
793
-
794
- // libs/uipack/src/shell/data-injector.ts
795
- function buildDataInjectionScript(options) {
796
- const { toolName, input, output, structuredContent } = options;
797
- const lines = [
798
- `window.__mcpAppsEnabled = true;`,
799
- `window.__mcpToolName = ${safeJsonForScript(toolName)};`,
800
- `window.__mcpToolInput = ${safeJsonForScript(input ?? null)};`,
801
- `window.__mcpToolOutput = ${safeJsonForScript(output ?? null)};`,
802
- `window.__mcpStructuredContent = ${safeJsonForScript(structuredContent ?? null)};`
803
- ];
804
- return `<script>
805
- ${lines.join("\n")}
799
+ function generateImportMapScriptTag(map) {
800
+ const json = JSON.stringify(map, null, 2).replace(/<\//g, "<\\/");
801
+ return `<script type="importmap">
802
+ ${json}
806
803
  </script>`;
807
804
  }
808
805
 
@@ -860,6 +857,8 @@ function generateBridgeIIFE(options = {}) {
860
857
  parts.push("});");
861
858
  parts.push("");
862
859
  parts.push("window.FrontMcpBridge = bridge;");
860
+ parts.push("");
861
+ parts.push(generateAutoResize());
863
862
  parts.push("function __showLoading() {");
864
863
  parts.push(' var root = document.getElementById("root");');
865
864
  parts.push(" if (root && !root.hasChildNodes()) {");
@@ -880,6 +879,112 @@ function generateBridgeIIFE(options = {}) {
880
879
  }
881
880
  return code;
882
881
  }
882
+ function generateAutoResize() {
883
+ return `
884
+ function __applySizingCss(sizing) {
885
+ if (typeof document === 'undefined' || !document.documentElement) return;
886
+ function toLen(v) { return typeof v === 'number' ? v + 'px' : v; }
887
+ var de = document.documentElement;
888
+ var body = document.body;
889
+ var root = document.getElementById('root');
890
+ if (sizing.preferredHeight != null) {
891
+ var ph = toLen(sizing.preferredHeight);
892
+ de.style.height = ph;
893
+ if (body) body.style.height = ph;
894
+ if (root && !root.style.minHeight) root.style.minHeight = ph;
895
+ }
896
+ if (sizing.minHeight != null) {
897
+ var mh = toLen(sizing.minHeight);
898
+ de.style.minHeight = mh;
899
+ if (body) body.style.minHeight = mh;
900
+ if (root) root.style.minHeight = mh;
901
+ }
902
+ if (sizing.maxHeight != null) {
903
+ var mx = toLen(sizing.maxHeight);
904
+ de.style.maxHeight = mx;
905
+ if (body) body.style.maxHeight = mx;
906
+ if (root) root.style.maxHeight = mx;
907
+ }
908
+ if (sizing.aspectRatio != null && root) {
909
+ root.style.aspectRatio = String(sizing.aspectRatio);
910
+ }
911
+ }
912
+
913
+ function __initAutoResize() {
914
+ if (typeof window === 'undefined') return;
915
+ // Idempotent: a re-injected IIFE must not stack observers.
916
+ if (window.__mcpAutoResizeInit) return;
917
+ window.__mcpAutoResizeInit = true;
918
+ var sizing = window.__mcpWidgetSizing;
919
+ if (!sizing || typeof sizing !== 'object') return;
920
+
921
+ // Apply CSS as a runtime fallback (the static <style> may be absent on some
922
+ // render paths). Wait for the body if it isn't ready yet.
923
+ function apply() { try { __applySizingCss(sizing); } catch (e) {} }
924
+ if (typeof document !== 'undefined' && document.readyState === 'loading') {
925
+ document.addEventListener('DOMContentLoaded', apply);
926
+ } else {
927
+ apply();
928
+ }
929
+
930
+ // Auto-resize defaults ON; opt out with autoResize:false.
931
+ if (sizing.autoResize === false) return;
932
+ if (typeof ResizeObserver === 'undefined') return;
933
+
934
+ function startObserving() {
935
+ var target = document.getElementById('root') || document.body;
936
+ if (!target) return;
937
+
938
+ var rafId = null;
939
+ var lastReported = -1;
940
+ function report() {
941
+ rafId = null;
942
+ try {
943
+ var rect = target.getBoundingClientRect();
944
+ var height = Math.ceil(rect.height);
945
+ var width = Math.ceil(rect.width);
946
+ if (height === lastReported || height <= 0) return;
947
+ lastReported = height;
948
+ var payload = { height: height, width: width };
949
+ if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
950
+ if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
951
+ window.FrontMcpBridge.setSize(payload).catch(function() {});
952
+ }
953
+ window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
954
+ } catch (e) {}
955
+ }
956
+ function schedule() {
957
+ // Debounce via rAF; fall back to setTimeout if rAF is unavailable.
958
+ if (rafId != null) return;
959
+ if (typeof requestAnimationFrame === 'function') {
960
+ rafId = requestAnimationFrame(report);
961
+ } else {
962
+ rafId = setTimeout(report, 100);
963
+ }
964
+ }
965
+
966
+ try {
967
+ // Disconnect any prior observer before creating a new one (no leaks/dupes).
968
+ if (window.__mcpResizeObserver && typeof window.__mcpResizeObserver.disconnect === 'function') {
969
+ window.__mcpResizeObserver.disconnect();
970
+ }
971
+ var ro = new ResizeObserver(function() { schedule(); });
972
+ ro.observe(target);
973
+ window.__mcpResizeObserver = ro;
974
+ } catch (e) {}
975
+ // Report once on init so the host gets an initial measurement.
976
+ schedule();
977
+ }
978
+
979
+ if (typeof document !== 'undefined' && document.readyState === 'loading') {
980
+ document.addEventListener('DOMContentLoaded', startObserving);
981
+ } else {
982
+ startObserving();
983
+ }
984
+ }
985
+ __initAutoResize();
986
+ `.trim();
987
+ }
883
988
  function generateContextDetection() {
884
989
  return `
885
990
  function detectTheme() {
@@ -987,6 +1092,19 @@ var OpenAIAdapter = {
987
1092
  requestDisplayMode: function(context, mode) {
988
1093
  return Promise.resolve();
989
1094
  },
1095
+ setSize: function(context, size) {
1096
+ // OpenAI Apps SDK measures DOM height itself; if a sizing API surfaces on
1097
+ // window.openai, forward to it, otherwise no-op (CSS drives layout).
1098
+ try {
1099
+ if (window.openai && typeof window.openai.requestDisplayMode === 'function' && size && size.displayMode) {
1100
+ window.openai.requestDisplayMode(size.displayMode);
1101
+ }
1102
+ if (window.openai && typeof window.openai.setWidgetHeight === 'function' && size && typeof size.height === 'number') {
1103
+ window.openai.setWidgetHeight(size.height);
1104
+ }
1105
+ } catch (e) {}
1106
+ return Promise.resolve();
1107
+ },
990
1108
  requestClose: function(context) {
991
1109
  return Promise.resolve();
992
1110
  }
@@ -1231,6 +1349,15 @@ var ExtAppsAdapter = {
1231
1349
  requestDisplayMode: function(context, mode) {
1232
1350
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
1233
1351
  },
1352
+ setSize: function(context, size) {
1353
+ // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
1354
+ // measured/desired widget dimensions to the host.
1355
+ return this.sendRequest('ui/setSize', {
1356
+ height: size && size.height,
1357
+ width: size && size.width,
1358
+ aspectRatio: size && size.aspectRatio
1359
+ });
1360
+ },
1234
1361
  requestClose: function(context) {
1235
1362
  return this.sendRequest('ui/close', {});
1236
1363
  },
@@ -1319,6 +1446,10 @@ var ClaudeAdapter = {
1319
1446
  requestDisplayMode: function() {
1320
1447
  return Promise.resolve();
1321
1448
  },
1449
+ setSize: function() {
1450
+ // Claude measures the rendered DOM height itself \u2014 CSS-only, no reporting.
1451
+ return Promise.resolve();
1452
+ },
1322
1453
  requestClose: function() {
1323
1454
  return Promise.resolve();
1324
1455
  }
@@ -1370,6 +1501,9 @@ var GeminiAdapter = {
1370
1501
  requestDisplayMode: function() {
1371
1502
  return Promise.resolve();
1372
1503
  },
1504
+ setSize: function() {
1505
+ return Promise.resolve();
1506
+ },
1373
1507
  requestClose: function() {
1374
1508
  return Promise.resolve();
1375
1509
  }
@@ -1405,6 +1539,9 @@ var GenericAdapter = {
1405
1539
  requestDisplayMode: function() {
1406
1540
  return Promise.resolve();
1407
1541
  },
1542
+ setSize: function() {
1543
+ return Promise.resolve();
1544
+ },
1408
1545
  requestClose: function() {
1409
1546
  return Promise.resolve();
1410
1547
  }
@@ -1639,6 +1776,15 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1639
1776
  });
1640
1777
  };
1641
1778
 
1779
+ // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1780
+ // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1781
+ // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1782
+ FrontMcpBridge.prototype.setSize = function(size) {
1783
+ if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1784
+ if (!this._adapter.setSize) return Promise.resolve();
1785
+ return this._adapter.setSize(this._context, size || {});
1786
+ };
1787
+
1642
1788
  FrontMcpBridge.prototype.requestClose = function() {
1643
1789
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1644
1790
  return this._adapter.requestClose(this._context);
@@ -1837,6 +1983,80 @@ var BRIDGE_SCRIPT_TAGS = {
1837
1983
  gemini: `<script>${generatePlatformBundle("gemini")}</script>`
1838
1984
  };
1839
1985
 
1986
+ // libs/uipack/src/shell/csp.ts
1987
+ var DEFAULT_CDN_DOMAINS = [
1988
+ "https://cdn.jsdelivr.net",
1989
+ "https://cdnjs.cloudflare.com",
1990
+ "https://fonts.googleapis.com",
1991
+ "https://fonts.gstatic.com",
1992
+ "https://esm.sh"
1993
+ ];
1994
+ var DEFAULT_CSP_DIRECTIVES = [
1995
+ "default-src 'none'",
1996
+ `script-src 'self' 'unsafe-inline' ${DEFAULT_CDN_DOMAINS.join(" ")}`,
1997
+ `style-src 'self' 'unsafe-inline' ${DEFAULT_CDN_DOMAINS.join(" ")}`,
1998
+ `img-src 'self' data: ${DEFAULT_CDN_DOMAINS.join(" ")}`,
1999
+ `font-src 'self' data: ${DEFAULT_CDN_DOMAINS.join(" ")}`,
2000
+ `connect-src ${DEFAULT_CDN_DOMAINS.join(" ")}`,
2001
+ "object-src 'self' data:"
2002
+ ];
2003
+ function buildCSPDirectives(csp) {
2004
+ if (!csp) {
2005
+ return [...DEFAULT_CSP_DIRECTIVES];
2006
+ }
2007
+ const validResourceDomains = sanitizeCSPDomains(csp.resourceDomains);
2008
+ const validConnectDomains = sanitizeCSPDomains(csp.connectDomains);
2009
+ const allResourceDomains = [.../* @__PURE__ */ new Set([...DEFAULT_CDN_DOMAINS, ...validResourceDomains])];
2010
+ const directives = [
2011
+ "default-src 'none'",
2012
+ `script-src 'self' 'unsafe-inline' ${allResourceDomains.join(" ")}`,
2013
+ `style-src 'self' 'unsafe-inline' ${allResourceDomains.join(" ")}`
2014
+ ];
2015
+ const imgSources = ["'self'", "data:", ...allResourceDomains];
2016
+ directives.push(`img-src ${imgSources.join(" ")}`);
2017
+ const fontSources = ["'self'", "data:", ...allResourceDomains];
2018
+ directives.push(`font-src ${fontSources.join(" ")}`);
2019
+ if (validConnectDomains.length) {
2020
+ directives.push(`connect-src ${validConnectDomains.join(" ")}`);
2021
+ } else {
2022
+ directives.push(`connect-src ${allResourceDomains.join(" ")}`);
2023
+ }
2024
+ directives.push("object-src 'self' data:");
2025
+ return directives;
2026
+ }
2027
+ function buildCSPMetaTag(csp) {
2028
+ const directives = buildCSPDirectives(csp);
2029
+ const content = directives.join("; ");
2030
+ return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
2031
+ }
2032
+ function validateCSPDomain(domain) {
2033
+ if (domain.startsWith("https://*.")) {
2034
+ const rest = domain.slice(10);
2035
+ return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$/.test(rest);
2036
+ }
2037
+ try {
2038
+ const url = new URL(domain);
2039
+ return url.protocol === "https:";
2040
+ } catch {
2041
+ return false;
2042
+ }
2043
+ }
2044
+ function sanitizeCSPDomains(domains) {
2045
+ if (!domains) return [];
2046
+ const valid = [];
2047
+ for (const domain of domains) {
2048
+ if (validateCSPDomain(domain)) {
2049
+ valid.push(domain);
2050
+ } else {
2051
+ console.warn(`Invalid CSP domain ignored: ${domain}`);
2052
+ }
2053
+ }
2054
+ return valid;
2055
+ }
2056
+ function escapeAttribute(str) {
2057
+ return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2058
+ }
2059
+
1840
2060
  // libs/uipack/src/shell/custom-shell-types.ts
1841
2061
  var SHELL_PLACEHOLDER_NAMES = ["CSP", "DATA", "BRIDGE", "CONTENT", "TITLE"];
1842
2062
  var SHELL_PLACEHOLDERS = {
@@ -1849,6 +2069,17 @@ var SHELL_PLACEHOLDERS = {
1849
2069
  var REQUIRED_PLACEHOLDERS = ["CONTENT"];
1850
2070
  var OPTIONAL_PLACEHOLDERS = ["CSP", "DATA", "BRIDGE", "TITLE"];
1851
2071
 
2072
+ // libs/uipack/src/shell/custom-shell-applier.ts
2073
+ function applyShellTemplate(template, values) {
2074
+ let result = template;
2075
+ result = result.replaceAll(SHELL_PLACEHOLDERS.CSP, values.csp);
2076
+ result = result.replaceAll(SHELL_PLACEHOLDERS.DATA, values.data);
2077
+ result = result.replaceAll(SHELL_PLACEHOLDERS.BRIDGE, values.bridge);
2078
+ result = result.replaceAll(SHELL_PLACEHOLDERS.CONTENT, values.content);
2079
+ result = result.replaceAll(SHELL_PLACEHOLDERS.TITLE, values.title);
2080
+ return result;
2081
+ }
2082
+
1852
2083
  // libs/uipack/src/shell/custom-shell-validator.ts
1853
2084
  function validateShellTemplate(template) {
1854
2085
  const found = {};
@@ -1865,22 +2096,112 @@ function validateShellTemplate(template) {
1865
2096
  };
1866
2097
  }
1867
2098
 
1868
- // libs/uipack/src/shell/custom-shell-applier.ts
1869
- function applyShellTemplate(template, values) {
1870
- let result = template;
1871
- result = result.replaceAll(SHELL_PLACEHOLDERS.CSP, values.csp);
1872
- result = result.replaceAll(SHELL_PLACEHOLDERS.DATA, values.data);
1873
- result = result.replaceAll(SHELL_PLACEHOLDERS.BRIDGE, values.bridge);
1874
- result = result.replaceAll(SHELL_PLACEHOLDERS.CONTENT, values.content);
1875
- result = result.replaceAll(SHELL_PLACEHOLDERS.TITLE, values.title);
1876
- return result;
2099
+ // libs/uipack/src/shell/data-injector.ts
2100
+ function hasSizing(sizing) {
2101
+ if (!sizing) return false;
2102
+ return sizing.preferredHeight !== void 0 || sizing.minHeight !== void 0 || sizing.maxHeight !== void 0 || sizing.aspectRatio !== void 0 || sizing.autoResize !== void 0;
2103
+ }
2104
+ function buildDataInjectionScript(options) {
2105
+ const { toolName, input, output, structuredContent, sizing } = options;
2106
+ const lines = [
2107
+ `window.__mcpAppsEnabled = true;`,
2108
+ `window.__mcpToolName = ${safeJsonForScript(toolName)};`,
2109
+ `window.__mcpToolInput = ${safeJsonForScript(input ?? null)};`,
2110
+ `window.__mcpToolOutput = ${safeJsonForScript(output ?? null)};`,
2111
+ `window.__mcpStructuredContent = ${safeJsonForScript(structuredContent ?? null)};`
2112
+ ];
2113
+ if (hasSizing(sizing)) {
2114
+ lines.push(`window.__mcpWidgetSizing = ${safeJsonForScript(sizing)};`);
2115
+ }
2116
+ return `<script>
2117
+ ${lines.join("\n")}
2118
+ </script>`;
2119
+ }
2120
+ function buildCustomDataInjectionScript(descriptor) {
2121
+ if (descriptor.script !== void 0) {
2122
+ return descriptor.script;
2123
+ }
2124
+ if (descriptor.globalKey !== void 0) {
2125
+ return `<script>window[${safeJsonForScript(descriptor.globalKey)}] = ${safeJsonForScript(
2126
+ descriptor.value ?? null
2127
+ )};</script>`;
2128
+ }
2129
+ return "";
2130
+ }
2131
+
2132
+ // libs/uipack/src/shell/sizing-css.ts
2133
+ function sanitizeCssValue(value) {
2134
+ return value.replace(/[<>{};]/g, "").trim();
2135
+ }
2136
+ function toCssLength(value) {
2137
+ return typeof value === "number" ? `${value}px` : sanitizeCssValue(value);
2138
+ }
2139
+ function buildSizingStyleTag(sizing) {
2140
+ if (!hasSizing(sizing)) return "";
2141
+ const hasCssSizing = sizing.preferredHeight !== void 0 || sizing.minHeight !== void 0 || sizing.maxHeight !== void 0 || sizing.aspectRatio !== void 0;
2142
+ if (!hasCssSizing) return "";
2143
+ const rootRules = [];
2144
+ const docRules = ["margin: 0;"];
2145
+ if (sizing.preferredHeight !== void 0) {
2146
+ const h = toCssLength(sizing.preferredHeight);
2147
+ docRules.push(`height: ${h};`);
2148
+ rootRules.push(`min-height: ${h};`);
2149
+ }
2150
+ if (sizing.minHeight !== void 0) {
2151
+ const mh = toCssLength(sizing.minHeight);
2152
+ docRules.push(`min-height: ${mh};`);
2153
+ rootRules.push(`min-height: ${mh};`);
2154
+ }
2155
+ if (sizing.maxHeight !== void 0) {
2156
+ const mx = toCssLength(sizing.maxHeight);
2157
+ docRules.push(`max-height: ${mx};`);
2158
+ rootRules.push(`max-height: ${mx};`);
2159
+ }
2160
+ if (sizing.aspectRatio !== void 0) {
2161
+ const ar = typeof sizing.aspectRatio === "number" ? String(sizing.aspectRatio) : sanitizeCssValue(sizing.aspectRatio);
2162
+ if (ar) rootRules.push(`aspect-ratio: ${ar};`);
2163
+ }
2164
+ const parts = [`html, body { ${docRules.join(" ")} }`];
2165
+ if (rootRules.length > 0) {
2166
+ parts.push(`#root { ${rootRules.join(" ")} }`);
2167
+ }
2168
+ return `<style>${parts.join("\n")}</style>`;
1877
2169
  }
1878
2170
 
1879
2171
  // libs/uipack/src/shell/builder.ts
2172
+ function resolveDataInjectionScript(args) {
2173
+ if (args.dataInjection) {
2174
+ return buildCustomDataInjectionScript(args.dataInjection);
2175
+ }
2176
+ return buildDataInjectionScript({
2177
+ toolName: args.toolName,
2178
+ input: args.input,
2179
+ output: args.output,
2180
+ structuredContent: args.structuredContent,
2181
+ sizing: args.sizing
2182
+ });
2183
+ }
1880
2184
  function buildShell(content, config) {
1881
- const { toolName, csp, withShell = true, input, output, structuredContent, includeBridge = true, title } = config;
1882
- const { customShell } = config;
1883
- const dataScript = buildDataInjectionScript({ toolName, input, output, structuredContent });
2185
+ const {
2186
+ toolName,
2187
+ csp,
2188
+ withShell = true,
2189
+ input,
2190
+ output,
2191
+ structuredContent,
2192
+ includeBridge = true,
2193
+ title,
2194
+ sizing
2195
+ } = config;
2196
+ const { customShell, dataInjection } = config;
2197
+ const dataScript = resolveDataInjectionScript({
2198
+ dataInjection,
2199
+ toolName,
2200
+ input,
2201
+ output,
2202
+ structuredContent,
2203
+ sizing
2204
+ });
1884
2205
  if (!withShell) {
1885
2206
  const html2 = `${dataScript}
1886
2207
  ${content}`;
@@ -1898,7 +2219,9 @@ ${content}`;
1898
2219
  output,
1899
2220
  structuredContent,
1900
2221
  includeBridge,
1901
- title
2222
+ title,
2223
+ sizing,
2224
+ dataInjection
1902
2225
  });
1903
2226
  }
1904
2227
  const headParts = [
@@ -1910,6 +2233,10 @@ ${content}`;
1910
2233
  }
1911
2234
  headParts.push(buildCSPMetaTag(csp));
1912
2235
  headParts.push(dataScript);
2236
+ const sizingStyle = buildSizingStyleTag(sizing);
2237
+ if (sizingStyle) {
2238
+ headParts.push(sizingStyle);
2239
+ }
1913
2240
  if (includeBridge) {
1914
2241
  const bridgeScript = generateBridgeIIFE({ minify: true });
1915
2242
  headParts.push(`<script>${bridgeScript}</script>`);
@@ -1943,12 +2270,17 @@ function buildCustomShell(content, customShell, ctx) {
1943
2270
  template = customShell.template;
1944
2271
  }
1945
2272
  const cspTag = buildCSPMetaTag(ctx.csp);
1946
- const dataScript = buildDataInjectionScript({
2273
+ const dataScript = resolveDataInjectionScript({
2274
+ dataInjection: ctx.dataInjection,
1947
2275
  toolName: ctx.toolName,
1948
2276
  input: ctx.input,
1949
2277
  output: ctx.output,
1950
- structuredContent: ctx.structuredContent
2278
+ structuredContent: ctx.structuredContent,
2279
+ sizing: ctx.sizing
1951
2280
  });
2281
+ const sizingStyle = buildSizingStyleTag(ctx.sizing);
2282
+ const dataWithSizing = sizingStyle ? `${dataScript}
2283
+ ${sizingStyle}` : dataScript;
1952
2284
  let bridgeHtml = "";
1953
2285
  if (ctx.includeBridge) {
1954
2286
  const bridgeScript = generateBridgeIIFE({ minify: true });
@@ -1956,7 +2288,7 @@ function buildCustomShell(content, customShell, ctx) {
1956
2288
  }
1957
2289
  const html = applyShellTemplate(template, {
1958
2290
  csp: cspTag,
1959
- data: dataScript,
2291
+ data: dataWithSizing,
1960
2292
  bridge: bridgeHtml,
1961
2293
  content,
1962
2294
  title: ctx.title ? escapeHtmlForTag(ctx.title) : ""
@@ -1979,37 +2311,19 @@ function escapeHtmlForTag(str) {
1979
2311
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1980
2312
  }
1981
2313
 
1982
- // libs/uipack/src/resolver/import-map.ts
1983
- function createImportMapFromResolved(resolved) {
1984
- const imports = {};
1985
- const integrity = {};
1986
- for (const [specifier, res] of Object.entries(resolved)) {
1987
- if (res.type === "url") {
1988
- imports[specifier] = res.value;
1989
- if (res.integrity) {
1990
- integrity[res.value] = res.integrity;
1991
- }
1992
- }
1993
- }
1994
- return {
1995
- imports,
1996
- integrity: Object.keys(integrity).length > 0 ? integrity : void 0
1997
- };
1998
- }
1999
- function generateImportMapScriptTag(map) {
2000
- const json = JSON.stringify(map, null, 2).replace(/<\//g, "<\\/");
2001
- return `<script type="importmap">
2002
- ${json}
2003
- </script>`;
2004
- }
2005
-
2006
2314
  // libs/uipack/src/component/renderer.ts
2315
+ var DEFAULT_WIDGET_MOUNT = {
2316
+ moduleSpecifier: "@frontmcp/ui/react",
2317
+ wrapperImportName: "McpBridgeProvider"
2318
+ };
2007
2319
  function renderComponent(config, shellConfig) {
2008
2320
  const resolver = shellConfig.resolver ?? createEsmShResolver();
2009
2321
  const resolved = resolveUISource(config.source, {
2010
2322
  resolver,
2011
2323
  input: shellConfig.input,
2012
- output: shellConfig.output
2324
+ output: shellConfig.output,
2325
+ inlineReact: config.inlineReact === true,
2326
+ transformOnly: config.transformOnly === true
2013
2327
  });
2014
2328
  const mergedShellConfig = {
2015
2329
  ...shellConfig,
@@ -2020,10 +2334,10 @@ function renderComponent(config, shellConfig) {
2020
2334
  if (resolved.mode === "inline") {
2021
2335
  return buildShell(resolved.html ?? "", mergedShellConfig);
2022
2336
  }
2023
- const content = buildModuleContent(resolved, resolver, config.props);
2337
+ const content = buildModuleContent(resolved, resolver, config.props, shellConfig.mount);
2024
2338
  return buildShell(content, mergedShellConfig);
2025
2339
  }
2026
- function buildModuleContent(resolved, resolver, propsMapping) {
2340
+ function buildModuleContent(resolved, resolver, propsMapping, mount = DEFAULT_WIDGET_MOUNT) {
2027
2341
  const parts = [];
2028
2342
  if (resolved.code && resolved.bundled) {
2029
2343
  const importEntries = {};
@@ -2047,14 +2361,16 @@ ${resolved.code}
2047
2361
  ...resolved.imports || [],
2048
2362
  "react-dom/client",
2049
2363
  // needed by mount script
2050
- "@frontmcp/ui/react",
2051
- // needed for McpBridgeProvider
2364
+ mount.moduleSpecifier,
2365
+ // mounter/provider package (e.g. @frontmcp/ui/react)
2052
2366
  // React subpath entries needed by esm.sh externalized modules:
2053
2367
  "react/jsx-runtime",
2054
- "react/jsx-dev-runtime",
2055
- "react-dom/server",
2056
- "react-dom/static"
2368
+ "react/jsx-dev-runtime"
2057
2369
  ]);
2370
+ if (mount === DEFAULT_WIDGET_MOUNT) {
2371
+ allSpecifiers.add("react-dom/server");
2372
+ allSpecifiers.add("react-dom/static");
2373
+ }
2058
2374
  const coreDeps = /* @__PURE__ */ new Set([
2059
2375
  "react",
2060
2376
  "react-dom",
@@ -2085,8 +2401,9 @@ ${resolved.code}
2085
2401
  const importMap = createImportMapFromResolved(importEntries);
2086
2402
  parts.push(generateImportMapScriptTag(importMap));
2087
2403
  }
2088
- parts.push('<div id="root"></div>');
2089
- const mountCode = generateInlineMountCode(resolved.exportName);
2404
+ const mountNodeId = mount.mountNodeId ?? "root";
2405
+ parts.push(`<div id="${mountNodeId}">${mount.mountNodeInnerHtml ?? ""}</div>`);
2406
+ const mountCode = generateInlineMountCode(resolved.exportName, mount);
2090
2407
  parts.push(`<script type="module">
2091
2408
  ${resolved.code}
2092
2409
  ${mountCode}
@@ -2123,15 +2440,21 @@ function addExternalParam(url, externals) {
2123
2440
  const sep = url.includes("?") ? "&" : "?";
2124
2441
  return `${url}${sep}external=${externals.join(",")}`;
2125
2442
  }
2126
- function generateInlineMountCode(componentName) {
2443
+ function generateInlineMountCode(componentName, mount) {
2444
+ if (mount.generate) {
2445
+ return mount.generate(componentName);
2446
+ }
2447
+ const wrapperName = mount.wrapperImportName ?? "McpBridgeProvider";
2448
+ const wrapperAlias = `__${wrapperName}`;
2449
+ const mountNodeId = mount.mountNodeId ?? "root";
2127
2450
  return `
2128
2451
  // --- Mount ---
2129
2452
  import { createRoot as __createRoot } from 'react-dom/client';
2130
- import { McpBridgeProvider as __McpBridgeProvider } from '@frontmcp/ui/react';
2131
- const __root = document.getElementById('root');
2453
+ import { ${wrapperName} as ${wrapperAlias} } from '${mount.moduleSpecifier}';
2454
+ const __root = document.getElementById('${mountNodeId}');
2132
2455
  if (__root) {
2133
2456
  __createRoot(__root).render(
2134
- React.createElement(__McpBridgeProvider, null,
2457
+ React.createElement(${wrapperAlias}, null,
2135
2458
  React.createElement(${componentName})
2136
2459
  )
2137
2460
  );