@frontmcp/uipack 1.3.0 → 1.4.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.
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
package/esm/index.mjs CHANGED
@@ -181,6 +181,8 @@ function generateBridgeIIFE(options = {}) {
181
181
  parts.push("});");
182
182
  parts.push("");
183
183
  parts.push("window.FrontMcpBridge = bridge;");
184
+ parts.push("");
185
+ parts.push(generateAutoResize());
184
186
  parts.push("function __showLoading() {");
185
187
  parts.push(' var root = document.getElementById("root");');
186
188
  parts.push(" if (root && !root.hasChildNodes()) {");
@@ -201,6 +203,112 @@ function generateBridgeIIFE(options = {}) {
201
203
  }
202
204
  return code;
203
205
  }
206
+ function generateAutoResize() {
207
+ return `
208
+ function __applySizingCss(sizing) {
209
+ if (typeof document === 'undefined' || !document.documentElement) return;
210
+ function toLen(v) { return typeof v === 'number' ? v + 'px' : v; }
211
+ var de = document.documentElement;
212
+ var body = document.body;
213
+ var root = document.getElementById('root');
214
+ if (sizing.preferredHeight != null) {
215
+ var ph = toLen(sizing.preferredHeight);
216
+ de.style.height = ph;
217
+ if (body) body.style.height = ph;
218
+ if (root && !root.style.minHeight) root.style.minHeight = ph;
219
+ }
220
+ if (sizing.minHeight != null) {
221
+ var mh = toLen(sizing.minHeight);
222
+ de.style.minHeight = mh;
223
+ if (body) body.style.minHeight = mh;
224
+ if (root) root.style.minHeight = mh;
225
+ }
226
+ if (sizing.maxHeight != null) {
227
+ var mx = toLen(sizing.maxHeight);
228
+ de.style.maxHeight = mx;
229
+ if (body) body.style.maxHeight = mx;
230
+ if (root) root.style.maxHeight = mx;
231
+ }
232
+ if (sizing.aspectRatio != null && root) {
233
+ root.style.aspectRatio = String(sizing.aspectRatio);
234
+ }
235
+ }
236
+
237
+ function __initAutoResize() {
238
+ if (typeof window === 'undefined') return;
239
+ // Idempotent: a re-injected IIFE must not stack observers.
240
+ if (window.__mcpAutoResizeInit) return;
241
+ window.__mcpAutoResizeInit = true;
242
+ var sizing = window.__mcpWidgetSizing;
243
+ if (!sizing || typeof sizing !== 'object') return;
244
+
245
+ // Apply CSS as a runtime fallback (the static <style> may be absent on some
246
+ // render paths). Wait for the body if it isn't ready yet.
247
+ function apply() { try { __applySizingCss(sizing); } catch (e) {} }
248
+ if (typeof document !== 'undefined' && document.readyState === 'loading') {
249
+ document.addEventListener('DOMContentLoaded', apply);
250
+ } else {
251
+ apply();
252
+ }
253
+
254
+ // Auto-resize defaults ON; opt out with autoResize:false.
255
+ if (sizing.autoResize === false) return;
256
+ if (typeof ResizeObserver === 'undefined') return;
257
+
258
+ function startObserving() {
259
+ var target = document.getElementById('root') || document.body;
260
+ if (!target) return;
261
+
262
+ var rafId = null;
263
+ var lastReported = -1;
264
+ function report() {
265
+ rafId = null;
266
+ try {
267
+ var rect = target.getBoundingClientRect();
268
+ var height = Math.ceil(rect.height);
269
+ var width = Math.ceil(rect.width);
270
+ if (height === lastReported || height <= 0) return;
271
+ lastReported = height;
272
+ var payload = { height: height, width: width };
273
+ if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
274
+ if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
275
+ window.FrontMcpBridge.setSize(payload).catch(function() {});
276
+ }
277
+ window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
278
+ } catch (e) {}
279
+ }
280
+ function schedule() {
281
+ // Debounce via rAF; fall back to setTimeout if rAF is unavailable.
282
+ if (rafId != null) return;
283
+ if (typeof requestAnimationFrame === 'function') {
284
+ rafId = requestAnimationFrame(report);
285
+ } else {
286
+ rafId = setTimeout(report, 100);
287
+ }
288
+ }
289
+
290
+ try {
291
+ // Disconnect any prior observer before creating a new one (no leaks/dupes).
292
+ if (window.__mcpResizeObserver && typeof window.__mcpResizeObserver.disconnect === 'function') {
293
+ window.__mcpResizeObserver.disconnect();
294
+ }
295
+ var ro = new ResizeObserver(function() { schedule(); });
296
+ ro.observe(target);
297
+ window.__mcpResizeObserver = ro;
298
+ } catch (e) {}
299
+ // Report once on init so the host gets an initial measurement.
300
+ schedule();
301
+ }
302
+
303
+ if (typeof document !== 'undefined' && document.readyState === 'loading') {
304
+ document.addEventListener('DOMContentLoaded', startObserving);
305
+ } else {
306
+ startObserving();
307
+ }
308
+ }
309
+ __initAutoResize();
310
+ `.trim();
311
+ }
204
312
  function generateContextDetection() {
205
313
  return `
206
314
  function detectTheme() {
@@ -308,6 +416,19 @@ var OpenAIAdapter = {
308
416
  requestDisplayMode: function(context, mode) {
309
417
  return Promise.resolve();
310
418
  },
419
+ setSize: function(context, size) {
420
+ // OpenAI Apps SDK measures DOM height itself; if a sizing API surfaces on
421
+ // window.openai, forward to it, otherwise no-op (CSS drives layout).
422
+ try {
423
+ if (window.openai && typeof window.openai.requestDisplayMode === 'function' && size && size.displayMode) {
424
+ window.openai.requestDisplayMode(size.displayMode);
425
+ }
426
+ if (window.openai && typeof window.openai.setWidgetHeight === 'function' && size && typeof size.height === 'number') {
427
+ window.openai.setWidgetHeight(size.height);
428
+ }
429
+ } catch (e) {}
430
+ return Promise.resolve();
431
+ },
311
432
  requestClose: function(context) {
312
433
  return Promise.resolve();
313
434
  }
@@ -552,6 +673,15 @@ var ExtAppsAdapter = {
552
673
  requestDisplayMode: function(context, mode) {
553
674
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
554
675
  },
676
+ setSize: function(context, size) {
677
+ // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
678
+ // measured/desired widget dimensions to the host.
679
+ return this.sendRequest('ui/setSize', {
680
+ height: size && size.height,
681
+ width: size && size.width,
682
+ aspectRatio: size && size.aspectRatio
683
+ });
684
+ },
555
685
  requestClose: function(context) {
556
686
  return this.sendRequest('ui/close', {});
557
687
  },
@@ -640,6 +770,10 @@ var ClaudeAdapter = {
640
770
  requestDisplayMode: function() {
641
771
  return Promise.resolve();
642
772
  },
773
+ setSize: function() {
774
+ // Claude measures the rendered DOM height itself \u2014 CSS-only, no reporting.
775
+ return Promise.resolve();
776
+ },
643
777
  requestClose: function() {
644
778
  return Promise.resolve();
645
779
  }
@@ -691,6 +825,9 @@ var GeminiAdapter = {
691
825
  requestDisplayMode: function() {
692
826
  return Promise.resolve();
693
827
  },
828
+ setSize: function() {
829
+ return Promise.resolve();
830
+ },
694
831
  requestClose: function() {
695
832
  return Promise.resolve();
696
833
  }
@@ -726,6 +863,9 @@ var GenericAdapter = {
726
863
  requestDisplayMode: function() {
727
864
  return Promise.resolve();
728
865
  },
866
+ setSize: function() {
867
+ return Promise.resolve();
868
+ },
729
869
  requestClose: function() {
730
870
  return Promise.resolve();
731
871
  }
@@ -960,6 +1100,15 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
960
1100
  });
961
1101
  };
962
1102
 
1103
+ // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1104
+ // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1105
+ // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1106
+ FrontMcpBridge.prototype.setSize = function(size) {
1107
+ if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1108
+ if (!this._adapter.setSize) return Promise.resolve();
1109
+ return this._adapter.setSize(this._context, size || {});
1110
+ };
1111
+
963
1112
  FrontMcpBridge.prototype.requestClose = function() {
964
1113
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
965
1114
  return this._adapter.requestClose(this._context);
@@ -2714,9 +2863,61 @@ function escapeAttribute(str) {
2714
2863
  return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2715
2864
  }
2716
2865
 
2866
+ // libs/uipack/src/shell/custom-shell-types.ts
2867
+ var SHELL_PLACEHOLDER_NAMES = ["CSP", "DATA", "BRIDGE", "CONTENT", "TITLE"];
2868
+ var SHELL_PLACEHOLDERS = {
2869
+ CSP: "{{CSP}}",
2870
+ DATA: "{{DATA}}",
2871
+ BRIDGE: "{{BRIDGE}}",
2872
+ CONTENT: "{{CONTENT}}",
2873
+ TITLE: "{{TITLE}}"
2874
+ };
2875
+ var REQUIRED_PLACEHOLDERS = ["CONTENT"];
2876
+ var OPTIONAL_PLACEHOLDERS = ["CSP", "DATA", "BRIDGE", "TITLE"];
2877
+ function isInlineShellSource(source) {
2878
+ return typeof source === "object" && source !== null && "inline" in source;
2879
+ }
2880
+ function isUrlShellSource(source) {
2881
+ return typeof source === "object" && source !== null && "url" in source;
2882
+ }
2883
+ function isNpmShellSource(source) {
2884
+ return typeof source === "object" && source !== null && "npm" in source;
2885
+ }
2886
+
2887
+ // libs/uipack/src/shell/custom-shell-applier.ts
2888
+ function applyShellTemplate(template, values) {
2889
+ let result = template;
2890
+ result = result.replaceAll(SHELL_PLACEHOLDERS.CSP, values.csp);
2891
+ result = result.replaceAll(SHELL_PLACEHOLDERS.DATA, values.data);
2892
+ result = result.replaceAll(SHELL_PLACEHOLDERS.BRIDGE, values.bridge);
2893
+ result = result.replaceAll(SHELL_PLACEHOLDERS.CONTENT, values.content);
2894
+ result = result.replaceAll(SHELL_PLACEHOLDERS.TITLE, values.title);
2895
+ return result;
2896
+ }
2897
+
2898
+ // libs/uipack/src/shell/custom-shell-validator.ts
2899
+ function validateShellTemplate(template) {
2900
+ const found = {};
2901
+ for (const name of SHELL_PLACEHOLDER_NAMES) {
2902
+ found[name] = template.includes(SHELL_PLACEHOLDERS[name]);
2903
+ }
2904
+ const missingRequired = REQUIRED_PLACEHOLDERS.filter((name) => !found[name]);
2905
+ const missingOptional = OPTIONAL_PLACEHOLDERS.filter((name) => !found[name]);
2906
+ return {
2907
+ valid: missingRequired.length === 0,
2908
+ found,
2909
+ missingRequired,
2910
+ missingOptional
2911
+ };
2912
+ }
2913
+
2717
2914
  // libs/uipack/src/shell/data-injector.ts
2915
+ function hasSizing(sizing) {
2916
+ if (!sizing) return false;
2917
+ return sizing.preferredHeight !== void 0 || sizing.minHeight !== void 0 || sizing.maxHeight !== void 0 || sizing.aspectRatio !== void 0 || sizing.autoResize !== void 0;
2918
+ }
2718
2919
  function buildDataInjectionScript(options) {
2719
- const { toolName, input, output, structuredContent } = options;
2920
+ const { toolName, input, output, structuredContent, sizing } = options;
2720
2921
  const lines = [
2721
2922
  `window.__mcpAppsEnabled = true;`,
2722
2923
  `window.__mcpToolName = ${safeJsonForScript(toolName)};`,
@@ -2724,10 +2925,24 @@ function buildDataInjectionScript(options) {
2724
2925
  `window.__mcpToolOutput = ${safeJsonForScript(output ?? null)};`,
2725
2926
  `window.__mcpStructuredContent = ${safeJsonForScript(structuredContent ?? null)};`
2726
2927
  ];
2928
+ if (hasSizing(sizing)) {
2929
+ lines.push(`window.__mcpWidgetSizing = ${safeJsonForScript(sizing)};`);
2930
+ }
2727
2931
  return `<script>
2728
2932
  ${lines.join("\n")}
2729
2933
  </script>`;
2730
2934
  }
2935
+ function buildCustomDataInjectionScript(descriptor) {
2936
+ if (descriptor.script !== void 0) {
2937
+ return descriptor.script;
2938
+ }
2939
+ if (descriptor.globalKey !== void 0) {
2940
+ return `<script>window[${safeJsonForScript(descriptor.globalKey)}] = ${safeJsonForScript(
2941
+ descriptor.value ?? null
2942
+ )};</script>`;
2943
+ }
2944
+ return "";
2945
+ }
2731
2946
  var _uniqueIdCounter = 0;
2732
2947
  function createTemplateHelpers() {
2733
2948
  return {
@@ -2755,59 +2970,79 @@ function createTemplateHelpers() {
2755
2970
  };
2756
2971
  }
2757
2972
 
2758
- // libs/uipack/src/shell/custom-shell-types.ts
2759
- var SHELL_PLACEHOLDER_NAMES = ["CSP", "DATA", "BRIDGE", "CONTENT", "TITLE"];
2760
- var SHELL_PLACEHOLDERS = {
2761
- CSP: "{{CSP}}",
2762
- DATA: "{{DATA}}",
2763
- BRIDGE: "{{BRIDGE}}",
2764
- CONTENT: "{{CONTENT}}",
2765
- TITLE: "{{TITLE}}"
2766
- };
2767
- var REQUIRED_PLACEHOLDERS = ["CONTENT"];
2768
- var OPTIONAL_PLACEHOLDERS = ["CSP", "DATA", "BRIDGE", "TITLE"];
2769
- function isInlineShellSource(source) {
2770
- return typeof source === "object" && source !== null && "inline" in source;
2973
+ // libs/uipack/src/shell/sizing-css.ts
2974
+ function sanitizeCssValue(value) {
2975
+ return value.replace(/[<>{};]/g, "").trim();
2771
2976
  }
2772
- function isUrlShellSource(source) {
2773
- return typeof source === "object" && source !== null && "url" in source;
2977
+ function toCssLength(value) {
2978
+ return typeof value === "number" ? `${value}px` : sanitizeCssValue(value);
2774
2979
  }
2775
- function isNpmShellSource(source) {
2776
- return typeof source === "object" && source !== null && "npm" in source;
2777
- }
2778
-
2779
- // libs/uipack/src/shell/custom-shell-validator.ts
2780
- function validateShellTemplate(template) {
2781
- const found = {};
2782
- for (const name of SHELL_PLACEHOLDER_NAMES) {
2783
- found[name] = template.includes(SHELL_PLACEHOLDERS[name]);
2784
- }
2785
- const missingRequired = REQUIRED_PLACEHOLDERS.filter((name) => !found[name]);
2786
- const missingOptional = OPTIONAL_PLACEHOLDERS.filter((name) => !found[name]);
2787
- return {
2788
- valid: missingRequired.length === 0,
2789
- found,
2790
- missingRequired,
2791
- missingOptional
2792
- };
2793
- }
2794
-
2795
- // libs/uipack/src/shell/custom-shell-applier.ts
2796
- function applyShellTemplate(template, values) {
2797
- let result = template;
2798
- result = result.replaceAll(SHELL_PLACEHOLDERS.CSP, values.csp);
2799
- result = result.replaceAll(SHELL_PLACEHOLDERS.DATA, values.data);
2800
- result = result.replaceAll(SHELL_PLACEHOLDERS.BRIDGE, values.bridge);
2801
- result = result.replaceAll(SHELL_PLACEHOLDERS.CONTENT, values.content);
2802
- result = result.replaceAll(SHELL_PLACEHOLDERS.TITLE, values.title);
2803
- return result;
2980
+ function buildSizingStyleTag(sizing) {
2981
+ if (!hasSizing(sizing)) return "";
2982
+ const hasCssSizing = sizing.preferredHeight !== void 0 || sizing.minHeight !== void 0 || sizing.maxHeight !== void 0 || sizing.aspectRatio !== void 0;
2983
+ if (!hasCssSizing) return "";
2984
+ const rootRules = [];
2985
+ const docRules = ["margin: 0;"];
2986
+ if (sizing.preferredHeight !== void 0) {
2987
+ const h = toCssLength(sizing.preferredHeight);
2988
+ docRules.push(`height: ${h};`);
2989
+ rootRules.push(`min-height: ${h};`);
2990
+ }
2991
+ if (sizing.minHeight !== void 0) {
2992
+ const mh = toCssLength(sizing.minHeight);
2993
+ docRules.push(`min-height: ${mh};`);
2994
+ rootRules.push(`min-height: ${mh};`);
2995
+ }
2996
+ if (sizing.maxHeight !== void 0) {
2997
+ const mx = toCssLength(sizing.maxHeight);
2998
+ docRules.push(`max-height: ${mx};`);
2999
+ rootRules.push(`max-height: ${mx};`);
3000
+ }
3001
+ if (sizing.aspectRatio !== void 0) {
3002
+ const ar = typeof sizing.aspectRatio === "number" ? String(sizing.aspectRatio) : sanitizeCssValue(sizing.aspectRatio);
3003
+ if (ar) rootRules.push(`aspect-ratio: ${ar};`);
3004
+ }
3005
+ const parts = [`html, body { ${docRules.join(" ")} }`];
3006
+ if (rootRules.length > 0) {
3007
+ parts.push(`#root { ${rootRules.join(" ")} }`);
3008
+ }
3009
+ return `<style>${parts.join("\n")}</style>`;
2804
3010
  }
2805
3011
 
2806
3012
  // libs/uipack/src/shell/builder.ts
3013
+ function resolveDataInjectionScript(args) {
3014
+ if (args.dataInjection) {
3015
+ return buildCustomDataInjectionScript(args.dataInjection);
3016
+ }
3017
+ return buildDataInjectionScript({
3018
+ toolName: args.toolName,
3019
+ input: args.input,
3020
+ output: args.output,
3021
+ structuredContent: args.structuredContent,
3022
+ sizing: args.sizing
3023
+ });
3024
+ }
2807
3025
  function buildShell(content, config) {
2808
- const { toolName, csp, withShell = true, input, output, structuredContent, includeBridge = true, title } = config;
2809
- const { customShell } = config;
2810
- const dataScript = buildDataInjectionScript({ toolName, input, output, structuredContent });
3026
+ const {
3027
+ toolName,
3028
+ csp,
3029
+ withShell = true,
3030
+ input,
3031
+ output,
3032
+ structuredContent,
3033
+ includeBridge = true,
3034
+ title,
3035
+ sizing
3036
+ } = config;
3037
+ const { customShell, dataInjection } = config;
3038
+ const dataScript = resolveDataInjectionScript({
3039
+ dataInjection,
3040
+ toolName,
3041
+ input,
3042
+ output,
3043
+ structuredContent,
3044
+ sizing
3045
+ });
2811
3046
  if (!withShell) {
2812
3047
  const html2 = `${dataScript}
2813
3048
  ${content}`;
@@ -2825,7 +3060,9 @@ ${content}`;
2825
3060
  output,
2826
3061
  structuredContent,
2827
3062
  includeBridge,
2828
- title
3063
+ title,
3064
+ sizing,
3065
+ dataInjection
2829
3066
  });
2830
3067
  }
2831
3068
  const headParts = [
@@ -2837,6 +3074,10 @@ ${content}`;
2837
3074
  }
2838
3075
  headParts.push(buildCSPMetaTag(csp));
2839
3076
  headParts.push(dataScript);
3077
+ const sizingStyle = buildSizingStyleTag(sizing);
3078
+ if (sizingStyle) {
3079
+ headParts.push(sizingStyle);
3080
+ }
2840
3081
  if (includeBridge) {
2841
3082
  const bridgeScript = generateBridgeIIFE({ minify: true });
2842
3083
  headParts.push(`<script>${bridgeScript}</script>`);
@@ -2870,12 +3111,17 @@ function buildCustomShell(content, customShell, ctx) {
2870
3111
  template = customShell.template;
2871
3112
  }
2872
3113
  const cspTag = buildCSPMetaTag(ctx.csp);
2873
- const dataScript = buildDataInjectionScript({
3114
+ const dataScript = resolveDataInjectionScript({
3115
+ dataInjection: ctx.dataInjection,
2874
3116
  toolName: ctx.toolName,
2875
3117
  input: ctx.input,
2876
3118
  output: ctx.output,
2877
- structuredContent: ctx.structuredContent
3119
+ structuredContent: ctx.structuredContent,
3120
+ sizing: ctx.sizing
2878
3121
  });
3122
+ const sizingStyle = buildSizingStyleTag(ctx.sizing);
3123
+ const dataWithSizing = sizingStyle ? `${dataScript}
3124
+ ${sizingStyle}` : dataScript;
2879
3125
  let bridgeHtml = "";
2880
3126
  if (ctx.includeBridge) {
2881
3127
  const bridgeScript = generateBridgeIIFE({ minify: true });
@@ -2883,7 +3129,7 @@ function buildCustomShell(content, customShell, ctx) {
2883
3129
  }
2884
3130
  const html = applyShellTemplate(template, {
2885
3131
  csp: cspTag,
2886
- data: dataScript,
3132
+ data: dataWithSizing,
2887
3133
  bridge: bridgeHtml,
2888
3134
  content,
2889
3135
  title: ctx.title ? escapeHtmlForTag(ctx.title) : ""
@@ -3030,6 +3276,29 @@ function isFunctionSource(source) {
3030
3276
  }
3031
3277
  var FRONTMCP_META_KEY = "__frontmcp_meta";
3032
3278
 
3279
+ // libs/uipack/src/component/ui-availability.ts
3280
+ function isFrontmcpUiResolvable(...candidatePaths) {
3281
+ try {
3282
+ const nodeFs = __require("fs");
3283
+ const nodePath = __require("path");
3284
+ const ORG = "@frontmcp";
3285
+ const PKG = "ui";
3286
+ for (const candidate of candidatePaths) {
3287
+ let dir = candidate;
3288
+ while (true) {
3289
+ const pkgJson = nodePath.join(dir, "node_modules", ORG, PKG, "package.json");
3290
+ if (nodeFs.existsSync(pkgJson)) return true;
3291
+ const parent = nodePath.dirname(dir);
3292
+ if (parent === dir) break;
3293
+ dir = parent;
3294
+ }
3295
+ }
3296
+ return false;
3297
+ } catch {
3298
+ return false;
3299
+ }
3300
+ }
3301
+
3033
3302
  // libs/uipack/src/component/transpiler.ts
3034
3303
  function transpileReactSource(source, filename) {
3035
3304
  const esbuild = __require("esbuild");
@@ -3045,7 +3314,12 @@ function transpileReactSource(source, filename) {
3045
3314
  });
3046
3315
  return result.code;
3047
3316
  }
3048
- function bundleFileSource(source, filename, resolveDir, componentName) {
3317
+ function bundleFileSource(source, filename, resolveDir, componentName, options = {}) {
3318
+ if (!isFrontmcpUiResolvable(resolveDir, process.cwd())) {
3319
+ throw new Error(
3320
+ `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.`
3321
+ );
3322
+ }
3049
3323
  const esbuild = __require("esbuild");
3050
3324
  const mountCode = `
3051
3325
  // --- Auto-generated mount ---
@@ -3127,7 +3401,11 @@ if (__root) {
3127
3401
  format: "esm",
3128
3402
  target: "es2020",
3129
3403
  jsx: "automatic",
3130
- external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
3404
+ // When `bundleReact` is set (resourceMode: 'inline'), React itself is
3405
+ // bundled — required for hosts that block external script execution
3406
+ // such as Claude (#454). Otherwise React stays external and is loaded
3407
+ // via the import map emitted by the renderer.
3408
+ external: options.bundleReact ? [] : ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
3131
3409
  alias,
3132
3410
  define: { "process.env.NODE_ENV": '"production"' },
3133
3411
  platform: "browser",
@@ -3139,7 +3417,7 @@ if (__root) {
3139
3417
  } catch (err) {
3140
3418
  const message = err instanceof Error ? err.message : String(err);
3141
3419
  throw new Error(
3142
- `Failed to bundle FileSource "${filename}": ${message}. Ensure workspace packages are built (e.g. nx build ui).`
3420
+ `Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`
3143
3421
  );
3144
3422
  }
3145
3423
  }
@@ -3157,6 +3435,9 @@ var DEFAULT_META = {
3157
3435
  renderer: "auto"
3158
3436
  };
3159
3437
  function resolveUISource(source, options) {
3438
+ if (options?.inlineReact && options?.transformOnly) {
3439
+ throw new Error("resolveUISource: `inlineReact` and `transformOnly` are mutually exclusive \u2014 set at most one.");
3440
+ }
3160
3441
  const resolver = options?.resolver ?? createEsmShResolver();
3161
3442
  if (isNpmSource(source)) {
3162
3443
  return resolveNpmSource(source, resolver);
@@ -3165,7 +3446,7 @@ function resolveUISource(source, options) {
3165
3446
  return resolveImportSource(source);
3166
3447
  }
3167
3448
  if (isFileSource(source)) {
3168
- return resolveFileSource(source);
3449
+ return resolveFileSource(source, { inlineReact: options?.inlineReact, transformOnly: options?.transformOnly });
3169
3450
  }
3170
3451
  if (isFunctionSource(source)) {
3171
3452
  return resolveFunctionSource(source, options?.input, options?.output);
@@ -3198,15 +3479,42 @@ function resolveImportSource(source) {
3198
3479
  peerDependencies: []
3199
3480
  };
3200
3481
  }
3201
- function resolveFileSource(source) {
3482
+ function resolveFileSource(source, options = {}) {
3202
3483
  const path = __require("path");
3203
3484
  const ext = path.extname(source.file).toLowerCase();
3204
3485
  if (ext === ".tsx" || ext === ".jsx") {
3205
3486
  const fs = __require("fs");
3206
- const filePath = path.isAbsolute(source.file) ? source.file : path.resolve(process.cwd(), source.file);
3207
- const rawSource = fs.readFileSync(filePath, "utf-8");
3487
+ const wasRelative = !path.isAbsolute(source.file);
3488
+ const filePath = wasRelative ? path.resolve(process.cwd(), source.file) : source.file;
3489
+ let rawSource;
3490
+ try {
3491
+ rawSource = fs.readFileSync(filePath, "utf-8");
3492
+ } catch (err) {
3493
+ const isNotFound = err?.code === "ENOENT";
3494
+ if (isNotFound && wasRelative) {
3495
+ throw new Error(
3496
+ `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).`
3497
+ );
3498
+ }
3499
+ throw err;
3500
+ }
3208
3501
  const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
3209
- const bundled = bundleFileSource(rawSource, source.file, path.dirname(filePath), componentName);
3502
+ if (options.transformOnly === true) {
3503
+ const code = transpileReactSource(rawSource, path.basename(filePath));
3504
+ const parsed2 = parseImports(code);
3505
+ return {
3506
+ mode: "module",
3507
+ code,
3508
+ imports: [...new Set(parsed2.externalImports.map((i) => i.specifier))],
3509
+ exportName: componentName,
3510
+ meta: { mcpAware: true, renderer: "react" },
3511
+ peerDependencies: source.peerDependencies || ["react", "react-dom"],
3512
+ bundled: false
3513
+ };
3514
+ }
3515
+ const bundled = bundleFileSource(rawSource, source.file, path.dirname(filePath), componentName, {
3516
+ bundleReact: options.inlineReact === true
3517
+ });
3210
3518
  const parsed = parseImports(bundled.code);
3211
3519
  return {
3212
3520
  mode: "module",
@@ -3275,12 +3583,18 @@ function generateMappedPropsCode(mapping) {
3275
3583
  }
3276
3584
 
3277
3585
  // libs/uipack/src/component/renderer.ts
3586
+ var DEFAULT_WIDGET_MOUNT = {
3587
+ moduleSpecifier: "@frontmcp/ui/react",
3588
+ wrapperImportName: "McpBridgeProvider"
3589
+ };
3278
3590
  function renderComponent(config, shellConfig) {
3279
3591
  const resolver = shellConfig.resolver ?? createEsmShResolver();
3280
3592
  const resolved = resolveUISource(config.source, {
3281
3593
  resolver,
3282
3594
  input: shellConfig.input,
3283
- output: shellConfig.output
3595
+ output: shellConfig.output,
3596
+ inlineReact: config.inlineReact === true,
3597
+ transformOnly: config.transformOnly === true
3284
3598
  });
3285
3599
  const mergedShellConfig = {
3286
3600
  ...shellConfig,
@@ -3291,10 +3605,10 @@ function renderComponent(config, shellConfig) {
3291
3605
  if (resolved.mode === "inline") {
3292
3606
  return buildShell(resolved.html ?? "", mergedShellConfig);
3293
3607
  }
3294
- const content = buildModuleContent(resolved, resolver, config.props);
3608
+ const content = buildModuleContent(resolved, resolver, config.props, shellConfig.mount);
3295
3609
  return buildShell(content, mergedShellConfig);
3296
3610
  }
3297
- function buildModuleContent(resolved, resolver, propsMapping) {
3611
+ function buildModuleContent(resolved, resolver, propsMapping, mount = DEFAULT_WIDGET_MOUNT) {
3298
3612
  const parts = [];
3299
3613
  if (resolved.code && resolved.bundled) {
3300
3614
  const importEntries = {};
@@ -3318,14 +3632,16 @@ ${resolved.code}
3318
3632
  ...resolved.imports || [],
3319
3633
  "react-dom/client",
3320
3634
  // needed by mount script
3321
- "@frontmcp/ui/react",
3322
- // needed for McpBridgeProvider
3635
+ mount.moduleSpecifier,
3636
+ // mounter/provider package (e.g. @frontmcp/ui/react)
3323
3637
  // React subpath entries needed by esm.sh externalized modules:
3324
3638
  "react/jsx-runtime",
3325
- "react/jsx-dev-runtime",
3326
- "react-dom/server",
3327
- "react-dom/static"
3639
+ "react/jsx-dev-runtime"
3328
3640
  ]);
3641
+ if (mount === DEFAULT_WIDGET_MOUNT) {
3642
+ allSpecifiers.add("react-dom/server");
3643
+ allSpecifiers.add("react-dom/static");
3644
+ }
3329
3645
  const coreDeps = /* @__PURE__ */ new Set([
3330
3646
  "react",
3331
3647
  "react-dom",
@@ -3356,8 +3672,9 @@ ${resolved.code}
3356
3672
  const importMap = createImportMapFromResolved(importEntries);
3357
3673
  parts.push(generateImportMapScriptTag(importMap));
3358
3674
  }
3359
- parts.push('<div id="root"></div>');
3360
- const mountCode = generateInlineMountCode(resolved.exportName);
3675
+ const mountNodeId = mount.mountNodeId ?? "root";
3676
+ parts.push(`<div id="${mountNodeId}">${mount.mountNodeInnerHtml ?? ""}</div>`);
3677
+ const mountCode = generateInlineMountCode(resolved.exportName, mount);
3361
3678
  parts.push(`<script type="module">
3362
3679
  ${resolved.code}
3363
3680
  ${mountCode}
@@ -3394,15 +3711,21 @@ function addExternalParam(url, externals) {
3394
3711
  const sep = url.includes("?") ? "&" : "?";
3395
3712
  return `${url}${sep}external=${externals.join(",")}`;
3396
3713
  }
3397
- function generateInlineMountCode(componentName) {
3714
+ function generateInlineMountCode(componentName, mount) {
3715
+ if (mount.generate) {
3716
+ return mount.generate(componentName);
3717
+ }
3718
+ const wrapperName = mount.wrapperImportName ?? "McpBridgeProvider";
3719
+ const wrapperAlias = `__${wrapperName}`;
3720
+ const mountNodeId = mount.mountNodeId ?? "root";
3398
3721
  return `
3399
3722
  // --- Mount ---
3400
3723
  import { createRoot as __createRoot } from 'react-dom/client';
3401
- import { McpBridgeProvider as __McpBridgeProvider } from '@frontmcp/ui/react';
3402
- const __root = document.getElementById('root');
3724
+ import { ${wrapperName} as ${wrapperAlias} } from '${mount.moduleSpecifier}';
3725
+ const __root = document.getElementById('${mountNodeId}');
3403
3726
  if (__root) {
3404
3727
  __createRoot(__root).render(
3405
- React.createElement(__McpBridgeProvider, null,
3728
+ React.createElement(${wrapperAlias}, null,
3406
3729
  React.createElement(${componentName})
3407
3730
  )
3408
3731
  );
@@ -3478,38 +3801,6 @@ function isUIRenderFailure(result) {
3478
3801
  return typeof rec["reason"] === "string" && !("meta" in rec);
3479
3802
  }
3480
3803
 
3481
- // libs/uipack/src/adapters/type-detector.ts
3482
- function detectUIType(template) {
3483
- if (template === null || template === void 0) {
3484
- return "auto";
3485
- }
3486
- if (typeof template === "object" && template !== null && "file" in template) {
3487
- const file = template.file;
3488
- if (/\.(tsx|jsx)$/i.test(file)) return "react";
3489
- }
3490
- if (typeof template === "function") {
3491
- const proto = template.prototype;
3492
- if (proto && typeof proto.render === "function") {
3493
- return "react";
3494
- }
3495
- const asRecord = template;
3496
- if (asRecord["$$typeof"] !== void 0) {
3497
- return "react";
3498
- }
3499
- if (template.name && /^[A-Z]/.test(template.name)) {
3500
- return "react";
3501
- }
3502
- return "html";
3503
- }
3504
- if (typeof template === "string") {
3505
- if (template.includes("<") && template.includes(">")) {
3506
- return "html";
3507
- }
3508
- return "markdown";
3509
- }
3510
- return "auto";
3511
- }
3512
-
3513
3804
  // libs/uipack/src/adapters/content-detector.ts
3514
3805
  var CHART_TYPES = /* @__PURE__ */ new Set(["bar", "line", "pie", "area", "scatter", "doughnut", "radar", "polarArea", "bubble"]);
3515
3806
  var MERMAID_PREFIXES = [
@@ -3650,6 +3941,38 @@ function wrapDetectedContent(value) {
3650
3941
  }
3651
3942
  }
3652
3943
 
3944
+ // libs/uipack/src/adapters/type-detector.ts
3945
+ function detectUIType(template) {
3946
+ if (template === null || template === void 0) {
3947
+ return "auto";
3948
+ }
3949
+ if (typeof template === "object" && template !== null && "file" in template) {
3950
+ const file = template.file;
3951
+ if (/\.(tsx|jsx)$/i.test(file)) return "react";
3952
+ }
3953
+ if (typeof template === "function") {
3954
+ const proto = template.prototype;
3955
+ if (proto && typeof proto.render === "function") {
3956
+ return "react";
3957
+ }
3958
+ const asRecord = template;
3959
+ if (asRecord["$$typeof"] !== void 0) {
3960
+ return "react";
3961
+ }
3962
+ if (template.name && /^[A-Z]/.test(template.name)) {
3963
+ return "react";
3964
+ }
3965
+ return "html";
3966
+ }
3967
+ if (typeof template === "string") {
3968
+ if (template.includes("<") && template.includes(">")) {
3969
+ return "html";
3970
+ }
3971
+ return "markdown";
3972
+ }
3973
+ return "auto";
3974
+ }
3975
+
3653
3976
  // libs/uipack/src/adapters/template-renderer.ts
3654
3977
  function buildCspConfig(resolver) {
3655
3978
  const cspResourceDomains = ["https://esm.sh"];
@@ -3670,21 +3993,26 @@ function buildCspConfig(resolver) {
3670
3993
  return { resourceDomains: cspResourceDomains, connectDomains: cspConnectDomains };
3671
3994
  }
3672
3995
  function renderToolTemplate(options) {
3673
- const { toolName, input, output, template, resolver } = options;
3996
+ const { toolName, input, output, template, resolver, platformType, sizing } = options;
3674
3997
  const uiType = detectUIType(template);
3998
+ const resourceMode = options.resourceMode ?? (platformType === "claude" ? "inline" : "cdn");
3675
3999
  const shellConfig = {
3676
4000
  toolName,
3677
4001
  input,
3678
4002
  output,
3679
4003
  includeBridge: true,
3680
- resolver
4004
+ resolver,
4005
+ sizing
3681
4006
  };
3682
4007
  let html;
3683
4008
  let hash = "";
3684
4009
  let size = 0;
3685
4010
  if (typeof template === "object" && template !== null && "file" in template) {
3686
4011
  const cspConfig = buildCspConfig(resolver);
3687
- const result = renderComponent({ source: template }, { ...shellConfig, csp: cspConfig });
4012
+ const result = renderComponent(
4013
+ { source: template, inlineReact: resourceMode === "inline" },
4014
+ { ...shellConfig, csp: cspConfig }
4015
+ );
3688
4016
  html = result.html;
3689
4017
  hash = result.hash;
3690
4018
  size = result.size;
@@ -3726,6 +4054,12 @@ function renderToolTemplate(options) {
3726
4054
  "ui/type": uiType,
3727
4055
  "ui/mimeType": MCP_APPS_MIME_TYPE
3728
4056
  };
4057
+ if (hasSizing(sizing)) {
4058
+ if (sizing.preferredHeight !== void 0) meta["ui/preferredHeight"] = sizing.preferredHeight;
4059
+ if (sizing.minHeight !== void 0) meta["ui/minHeight"] = sizing.minHeight;
4060
+ if (sizing.maxHeight !== void 0) meta["ui/maxHeight"] = sizing.maxHeight;
4061
+ if (sizing.aspectRatio !== void 0) meta["ui/aspectRatio"] = sizing.aspectRatio;
4062
+ }
3729
4063
  return {
3730
4064
  html,
3731
4065
  uiType,
@@ -3830,6 +4164,7 @@ export {
3830
4164
  buildCSPDirectives,
3831
4165
  buildCSPMetaTag,
3832
4166
  buildChartHtml,
4167
+ buildCustomDataInjectionScript,
3833
4168
  buildDataInjectionScript,
3834
4169
  buildMermaidHtml,
3835
4170
  buildPdfHtml,