@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
package/index.js CHANGED
@@ -46,6 +46,7 @@ __export(index_exports, {
46
46
  buildCSPDirectives: () => buildCSPDirectives,
47
47
  buildCSPMetaTag: () => buildCSPMetaTag,
48
48
  buildChartHtml: () => buildChartHtml,
49
+ buildCustomDataInjectionScript: () => buildCustomDataInjectionScript,
49
50
  buildDataInjectionScript: () => buildDataInjectionScript,
50
51
  buildMermaidHtml: () => buildMermaidHtml,
51
52
  buildPdfHtml: () => buildPdfHtml,
@@ -297,6 +298,8 @@ function generateBridgeIIFE(options = {}) {
297
298
  parts.push("});");
298
299
  parts.push("");
299
300
  parts.push("window.FrontMcpBridge = bridge;");
301
+ parts.push("");
302
+ parts.push(generateAutoResize());
300
303
  parts.push("function __showLoading() {");
301
304
  parts.push(' var root = document.getElementById("root");');
302
305
  parts.push(" if (root && !root.hasChildNodes()) {");
@@ -317,6 +320,112 @@ function generateBridgeIIFE(options = {}) {
317
320
  }
318
321
  return code;
319
322
  }
323
+ function generateAutoResize() {
324
+ return `
325
+ function __applySizingCss(sizing) {
326
+ if (typeof document === 'undefined' || !document.documentElement) return;
327
+ function toLen(v) { return typeof v === 'number' ? v + 'px' : v; }
328
+ var de = document.documentElement;
329
+ var body = document.body;
330
+ var root = document.getElementById('root');
331
+ if (sizing.preferredHeight != null) {
332
+ var ph = toLen(sizing.preferredHeight);
333
+ de.style.height = ph;
334
+ if (body) body.style.height = ph;
335
+ if (root && !root.style.minHeight) root.style.minHeight = ph;
336
+ }
337
+ if (sizing.minHeight != null) {
338
+ var mh = toLen(sizing.minHeight);
339
+ de.style.minHeight = mh;
340
+ if (body) body.style.minHeight = mh;
341
+ if (root) root.style.minHeight = mh;
342
+ }
343
+ if (sizing.maxHeight != null) {
344
+ var mx = toLen(sizing.maxHeight);
345
+ de.style.maxHeight = mx;
346
+ if (body) body.style.maxHeight = mx;
347
+ if (root) root.style.maxHeight = mx;
348
+ }
349
+ if (sizing.aspectRatio != null && root) {
350
+ root.style.aspectRatio = String(sizing.aspectRatio);
351
+ }
352
+ }
353
+
354
+ function __initAutoResize() {
355
+ if (typeof window === 'undefined') return;
356
+ // Idempotent: a re-injected IIFE must not stack observers.
357
+ if (window.__mcpAutoResizeInit) return;
358
+ window.__mcpAutoResizeInit = true;
359
+ var sizing = window.__mcpWidgetSizing;
360
+ if (!sizing || typeof sizing !== 'object') return;
361
+
362
+ // Apply CSS as a runtime fallback (the static <style> may be absent on some
363
+ // render paths). Wait for the body if it isn't ready yet.
364
+ function apply() { try { __applySizingCss(sizing); } catch (e) {} }
365
+ if (typeof document !== 'undefined' && document.readyState === 'loading') {
366
+ document.addEventListener('DOMContentLoaded', apply);
367
+ } else {
368
+ apply();
369
+ }
370
+
371
+ // Auto-resize defaults ON; opt out with autoResize:false.
372
+ if (sizing.autoResize === false) return;
373
+ if (typeof ResizeObserver === 'undefined') return;
374
+
375
+ function startObserving() {
376
+ var target = document.getElementById('root') || document.body;
377
+ if (!target) return;
378
+
379
+ var rafId = null;
380
+ var lastReported = -1;
381
+ function report() {
382
+ rafId = null;
383
+ try {
384
+ var rect = target.getBoundingClientRect();
385
+ var height = Math.ceil(rect.height);
386
+ var width = Math.ceil(rect.width);
387
+ if (height === lastReported || height <= 0) return;
388
+ lastReported = height;
389
+ var payload = { height: height, width: width };
390
+ if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
391
+ if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
392
+ window.FrontMcpBridge.setSize(payload).catch(function() {});
393
+ }
394
+ window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
395
+ } catch (e) {}
396
+ }
397
+ function schedule() {
398
+ // Debounce via rAF; fall back to setTimeout if rAF is unavailable.
399
+ if (rafId != null) return;
400
+ if (typeof requestAnimationFrame === 'function') {
401
+ rafId = requestAnimationFrame(report);
402
+ } else {
403
+ rafId = setTimeout(report, 100);
404
+ }
405
+ }
406
+
407
+ try {
408
+ // Disconnect any prior observer before creating a new one (no leaks/dupes).
409
+ if (window.__mcpResizeObserver && typeof window.__mcpResizeObserver.disconnect === 'function') {
410
+ window.__mcpResizeObserver.disconnect();
411
+ }
412
+ var ro = new ResizeObserver(function() { schedule(); });
413
+ ro.observe(target);
414
+ window.__mcpResizeObserver = ro;
415
+ } catch (e) {}
416
+ // Report once on init so the host gets an initial measurement.
417
+ schedule();
418
+ }
419
+
420
+ if (typeof document !== 'undefined' && document.readyState === 'loading') {
421
+ document.addEventListener('DOMContentLoaded', startObserving);
422
+ } else {
423
+ startObserving();
424
+ }
425
+ }
426
+ __initAutoResize();
427
+ `.trim();
428
+ }
320
429
  function generateContextDetection() {
321
430
  return `
322
431
  function detectTheme() {
@@ -424,6 +533,19 @@ var OpenAIAdapter = {
424
533
  requestDisplayMode: function(context, mode) {
425
534
  return Promise.resolve();
426
535
  },
536
+ setSize: function(context, size) {
537
+ // OpenAI Apps SDK measures DOM height itself; if a sizing API surfaces on
538
+ // window.openai, forward to it, otherwise no-op (CSS drives layout).
539
+ try {
540
+ if (window.openai && typeof window.openai.requestDisplayMode === 'function' && size && size.displayMode) {
541
+ window.openai.requestDisplayMode(size.displayMode);
542
+ }
543
+ if (window.openai && typeof window.openai.setWidgetHeight === 'function' && size && typeof size.height === 'number') {
544
+ window.openai.setWidgetHeight(size.height);
545
+ }
546
+ } catch (e) {}
547
+ return Promise.resolve();
548
+ },
427
549
  requestClose: function(context) {
428
550
  return Promise.resolve();
429
551
  }
@@ -668,6 +790,15 @@ var ExtAppsAdapter = {
668
790
  requestDisplayMode: function(context, mode) {
669
791
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
670
792
  },
793
+ setSize: function(context, size) {
794
+ // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
795
+ // measured/desired widget dimensions to the host.
796
+ return this.sendRequest('ui/setSize', {
797
+ height: size && size.height,
798
+ width: size && size.width,
799
+ aspectRatio: size && size.aspectRatio
800
+ });
801
+ },
671
802
  requestClose: function(context) {
672
803
  return this.sendRequest('ui/close', {});
673
804
  },
@@ -756,6 +887,10 @@ var ClaudeAdapter = {
756
887
  requestDisplayMode: function() {
757
888
  return Promise.resolve();
758
889
  },
890
+ setSize: function() {
891
+ // Claude measures the rendered DOM height itself \u2014 CSS-only, no reporting.
892
+ return Promise.resolve();
893
+ },
759
894
  requestClose: function() {
760
895
  return Promise.resolve();
761
896
  }
@@ -807,6 +942,9 @@ var GeminiAdapter = {
807
942
  requestDisplayMode: function() {
808
943
  return Promise.resolve();
809
944
  },
945
+ setSize: function() {
946
+ return Promise.resolve();
947
+ },
810
948
  requestClose: function() {
811
949
  return Promise.resolve();
812
950
  }
@@ -842,6 +980,9 @@ var GenericAdapter = {
842
980
  requestDisplayMode: function() {
843
981
  return Promise.resolve();
844
982
  },
983
+ setSize: function() {
984
+ return Promise.resolve();
985
+ },
845
986
  requestClose: function() {
846
987
  return Promise.resolve();
847
988
  }
@@ -1076,6 +1217,15 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1076
1217
  });
1077
1218
  };
1078
1219
 
1220
+ // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1221
+ // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1222
+ // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1223
+ FrontMcpBridge.prototype.setSize = function(size) {
1224
+ if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1225
+ if (!this._adapter.setSize) return Promise.resolve();
1226
+ return this._adapter.setSize(this._context, size || {});
1227
+ };
1228
+
1079
1229
  FrontMcpBridge.prototype.requestClose = function() {
1080
1230
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1081
1231
  return this._adapter.requestClose(this._context);
@@ -2830,9 +2980,61 @@ function escapeAttribute(str) {
2830
2980
  return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2831
2981
  }
2832
2982
 
2983
+ // libs/uipack/src/shell/custom-shell-types.ts
2984
+ var SHELL_PLACEHOLDER_NAMES = ["CSP", "DATA", "BRIDGE", "CONTENT", "TITLE"];
2985
+ var SHELL_PLACEHOLDERS = {
2986
+ CSP: "{{CSP}}",
2987
+ DATA: "{{DATA}}",
2988
+ BRIDGE: "{{BRIDGE}}",
2989
+ CONTENT: "{{CONTENT}}",
2990
+ TITLE: "{{TITLE}}"
2991
+ };
2992
+ var REQUIRED_PLACEHOLDERS = ["CONTENT"];
2993
+ var OPTIONAL_PLACEHOLDERS = ["CSP", "DATA", "BRIDGE", "TITLE"];
2994
+ function isInlineShellSource(source) {
2995
+ return typeof source === "object" && source !== null && "inline" in source;
2996
+ }
2997
+ function isUrlShellSource(source) {
2998
+ return typeof source === "object" && source !== null && "url" in source;
2999
+ }
3000
+ function isNpmShellSource(source) {
3001
+ return typeof source === "object" && source !== null && "npm" in source;
3002
+ }
3003
+
3004
+ // libs/uipack/src/shell/custom-shell-applier.ts
3005
+ function applyShellTemplate(template, values) {
3006
+ let result = template;
3007
+ result = result.replaceAll(SHELL_PLACEHOLDERS.CSP, values.csp);
3008
+ result = result.replaceAll(SHELL_PLACEHOLDERS.DATA, values.data);
3009
+ result = result.replaceAll(SHELL_PLACEHOLDERS.BRIDGE, values.bridge);
3010
+ result = result.replaceAll(SHELL_PLACEHOLDERS.CONTENT, values.content);
3011
+ result = result.replaceAll(SHELL_PLACEHOLDERS.TITLE, values.title);
3012
+ return result;
3013
+ }
3014
+
3015
+ // libs/uipack/src/shell/custom-shell-validator.ts
3016
+ function validateShellTemplate(template) {
3017
+ const found = {};
3018
+ for (const name of SHELL_PLACEHOLDER_NAMES) {
3019
+ found[name] = template.includes(SHELL_PLACEHOLDERS[name]);
3020
+ }
3021
+ const missingRequired = REQUIRED_PLACEHOLDERS.filter((name) => !found[name]);
3022
+ const missingOptional = OPTIONAL_PLACEHOLDERS.filter((name) => !found[name]);
3023
+ return {
3024
+ valid: missingRequired.length === 0,
3025
+ found,
3026
+ missingRequired,
3027
+ missingOptional
3028
+ };
3029
+ }
3030
+
2833
3031
  // libs/uipack/src/shell/data-injector.ts
3032
+ function hasSizing(sizing) {
3033
+ if (!sizing) return false;
3034
+ return sizing.preferredHeight !== void 0 || sizing.minHeight !== void 0 || sizing.maxHeight !== void 0 || sizing.aspectRatio !== void 0 || sizing.autoResize !== void 0;
3035
+ }
2834
3036
  function buildDataInjectionScript(options) {
2835
- const { toolName, input, output, structuredContent } = options;
3037
+ const { toolName, input, output, structuredContent, sizing } = options;
2836
3038
  const lines = [
2837
3039
  `window.__mcpAppsEnabled = true;`,
2838
3040
  `window.__mcpToolName = ${safeJsonForScript(toolName)};`,
@@ -2840,10 +3042,24 @@ function buildDataInjectionScript(options) {
2840
3042
  `window.__mcpToolOutput = ${safeJsonForScript(output ?? null)};`,
2841
3043
  `window.__mcpStructuredContent = ${safeJsonForScript(structuredContent ?? null)};`
2842
3044
  ];
3045
+ if (hasSizing(sizing)) {
3046
+ lines.push(`window.__mcpWidgetSizing = ${safeJsonForScript(sizing)};`);
3047
+ }
2843
3048
  return `<script>
2844
3049
  ${lines.join("\n")}
2845
3050
  </script>`;
2846
3051
  }
3052
+ function buildCustomDataInjectionScript(descriptor) {
3053
+ if (descriptor.script !== void 0) {
3054
+ return descriptor.script;
3055
+ }
3056
+ if (descriptor.globalKey !== void 0) {
3057
+ return `<script>window[${safeJsonForScript(descriptor.globalKey)}] = ${safeJsonForScript(
3058
+ descriptor.value ?? null
3059
+ )};</script>`;
3060
+ }
3061
+ return "";
3062
+ }
2847
3063
  var _uniqueIdCounter = 0;
2848
3064
  function createTemplateHelpers() {
2849
3065
  return {
@@ -2871,59 +3087,79 @@ function createTemplateHelpers() {
2871
3087
  };
2872
3088
  }
2873
3089
 
2874
- // libs/uipack/src/shell/custom-shell-types.ts
2875
- var SHELL_PLACEHOLDER_NAMES = ["CSP", "DATA", "BRIDGE", "CONTENT", "TITLE"];
2876
- var SHELL_PLACEHOLDERS = {
2877
- CSP: "{{CSP}}",
2878
- DATA: "{{DATA}}",
2879
- BRIDGE: "{{BRIDGE}}",
2880
- CONTENT: "{{CONTENT}}",
2881
- TITLE: "{{TITLE}}"
2882
- };
2883
- var REQUIRED_PLACEHOLDERS = ["CONTENT"];
2884
- var OPTIONAL_PLACEHOLDERS = ["CSP", "DATA", "BRIDGE", "TITLE"];
2885
- function isInlineShellSource(source) {
2886
- return typeof source === "object" && source !== null && "inline" in source;
3090
+ // libs/uipack/src/shell/sizing-css.ts
3091
+ function sanitizeCssValue(value) {
3092
+ return value.replace(/[<>{};]/g, "").trim();
2887
3093
  }
2888
- function isUrlShellSource(source) {
2889
- return typeof source === "object" && source !== null && "url" in source;
3094
+ function toCssLength(value) {
3095
+ return typeof value === "number" ? `${value}px` : sanitizeCssValue(value);
2890
3096
  }
2891
- function isNpmShellSource(source) {
2892
- return typeof source === "object" && source !== null && "npm" in source;
2893
- }
2894
-
2895
- // libs/uipack/src/shell/custom-shell-validator.ts
2896
- function validateShellTemplate(template) {
2897
- const found = {};
2898
- for (const name of SHELL_PLACEHOLDER_NAMES) {
2899
- found[name] = template.includes(SHELL_PLACEHOLDERS[name]);
2900
- }
2901
- const missingRequired = REQUIRED_PLACEHOLDERS.filter((name) => !found[name]);
2902
- const missingOptional = OPTIONAL_PLACEHOLDERS.filter((name) => !found[name]);
2903
- return {
2904
- valid: missingRequired.length === 0,
2905
- found,
2906
- missingRequired,
2907
- missingOptional
2908
- };
2909
- }
2910
-
2911
- // libs/uipack/src/shell/custom-shell-applier.ts
2912
- function applyShellTemplate(template, values) {
2913
- let result = template;
2914
- result = result.replaceAll(SHELL_PLACEHOLDERS.CSP, values.csp);
2915
- result = result.replaceAll(SHELL_PLACEHOLDERS.DATA, values.data);
2916
- result = result.replaceAll(SHELL_PLACEHOLDERS.BRIDGE, values.bridge);
2917
- result = result.replaceAll(SHELL_PLACEHOLDERS.CONTENT, values.content);
2918
- result = result.replaceAll(SHELL_PLACEHOLDERS.TITLE, values.title);
2919
- return result;
3097
+ function buildSizingStyleTag(sizing) {
3098
+ if (!hasSizing(sizing)) return "";
3099
+ const hasCssSizing = sizing.preferredHeight !== void 0 || sizing.minHeight !== void 0 || sizing.maxHeight !== void 0 || sizing.aspectRatio !== void 0;
3100
+ if (!hasCssSizing) return "";
3101
+ const rootRules = [];
3102
+ const docRules = ["margin: 0;"];
3103
+ if (sizing.preferredHeight !== void 0) {
3104
+ const h = toCssLength(sizing.preferredHeight);
3105
+ docRules.push(`height: ${h};`);
3106
+ rootRules.push(`min-height: ${h};`);
3107
+ }
3108
+ if (sizing.minHeight !== void 0) {
3109
+ const mh = toCssLength(sizing.minHeight);
3110
+ docRules.push(`min-height: ${mh};`);
3111
+ rootRules.push(`min-height: ${mh};`);
3112
+ }
3113
+ if (sizing.maxHeight !== void 0) {
3114
+ const mx = toCssLength(sizing.maxHeight);
3115
+ docRules.push(`max-height: ${mx};`);
3116
+ rootRules.push(`max-height: ${mx};`);
3117
+ }
3118
+ if (sizing.aspectRatio !== void 0) {
3119
+ const ar = typeof sizing.aspectRatio === "number" ? String(sizing.aspectRatio) : sanitizeCssValue(sizing.aspectRatio);
3120
+ if (ar) rootRules.push(`aspect-ratio: ${ar};`);
3121
+ }
3122
+ const parts = [`html, body { ${docRules.join(" ")} }`];
3123
+ if (rootRules.length > 0) {
3124
+ parts.push(`#root { ${rootRules.join(" ")} }`);
3125
+ }
3126
+ return `<style>${parts.join("\n")}</style>`;
2920
3127
  }
2921
3128
 
2922
3129
  // libs/uipack/src/shell/builder.ts
3130
+ function resolveDataInjectionScript(args) {
3131
+ if (args.dataInjection) {
3132
+ return buildCustomDataInjectionScript(args.dataInjection);
3133
+ }
3134
+ return buildDataInjectionScript({
3135
+ toolName: args.toolName,
3136
+ input: args.input,
3137
+ output: args.output,
3138
+ structuredContent: args.structuredContent,
3139
+ sizing: args.sizing
3140
+ });
3141
+ }
2923
3142
  function buildShell(content, config) {
2924
- const { toolName, csp, withShell = true, input, output, structuredContent, includeBridge = true, title } = config;
2925
- const { customShell } = config;
2926
- const dataScript = buildDataInjectionScript({ toolName, input, output, structuredContent });
3143
+ const {
3144
+ toolName,
3145
+ csp,
3146
+ withShell = true,
3147
+ input,
3148
+ output,
3149
+ structuredContent,
3150
+ includeBridge = true,
3151
+ title,
3152
+ sizing
3153
+ } = config;
3154
+ const { customShell, dataInjection } = config;
3155
+ const dataScript = resolveDataInjectionScript({
3156
+ dataInjection,
3157
+ toolName,
3158
+ input,
3159
+ output,
3160
+ structuredContent,
3161
+ sizing
3162
+ });
2927
3163
  if (!withShell) {
2928
3164
  const html2 = `${dataScript}
2929
3165
  ${content}`;
@@ -2941,7 +3177,9 @@ ${content}`;
2941
3177
  output,
2942
3178
  structuredContent,
2943
3179
  includeBridge,
2944
- title
3180
+ title,
3181
+ sizing,
3182
+ dataInjection
2945
3183
  });
2946
3184
  }
2947
3185
  const headParts = [
@@ -2953,6 +3191,10 @@ ${content}`;
2953
3191
  }
2954
3192
  headParts.push(buildCSPMetaTag(csp));
2955
3193
  headParts.push(dataScript);
3194
+ const sizingStyle = buildSizingStyleTag(sizing);
3195
+ if (sizingStyle) {
3196
+ headParts.push(sizingStyle);
3197
+ }
2956
3198
  if (includeBridge) {
2957
3199
  const bridgeScript = generateBridgeIIFE({ minify: true });
2958
3200
  headParts.push(`<script>${bridgeScript}</script>`);
@@ -2986,12 +3228,17 @@ function buildCustomShell(content, customShell, ctx) {
2986
3228
  template = customShell.template;
2987
3229
  }
2988
3230
  const cspTag = buildCSPMetaTag(ctx.csp);
2989
- const dataScript = buildDataInjectionScript({
3231
+ const dataScript = resolveDataInjectionScript({
3232
+ dataInjection: ctx.dataInjection,
2990
3233
  toolName: ctx.toolName,
2991
3234
  input: ctx.input,
2992
3235
  output: ctx.output,
2993
- structuredContent: ctx.structuredContent
3236
+ structuredContent: ctx.structuredContent,
3237
+ sizing: ctx.sizing
2994
3238
  });
3239
+ const sizingStyle = buildSizingStyleTag(ctx.sizing);
3240
+ const dataWithSizing = sizingStyle ? `${dataScript}
3241
+ ${sizingStyle}` : dataScript;
2995
3242
  let bridgeHtml = "";
2996
3243
  if (ctx.includeBridge) {
2997
3244
  const bridgeScript = generateBridgeIIFE({ minify: true });
@@ -2999,7 +3246,7 @@ function buildCustomShell(content, customShell, ctx) {
2999
3246
  }
3000
3247
  const html = applyShellTemplate(template, {
3001
3248
  csp: cspTag,
3002
- data: dataScript,
3249
+ data: dataWithSizing,
3003
3250
  bridge: bridgeHtml,
3004
3251
  content,
3005
3252
  title: ctx.title ? escapeHtmlForTag(ctx.title) : ""
@@ -3146,6 +3393,29 @@ function isFunctionSource(source) {
3146
3393
  }
3147
3394
  var FRONTMCP_META_KEY = "__frontmcp_meta";
3148
3395
 
3396
+ // libs/uipack/src/component/ui-availability.ts
3397
+ function isFrontmcpUiResolvable(...candidatePaths) {
3398
+ try {
3399
+ const nodeFs = require("fs");
3400
+ const nodePath = require("path");
3401
+ const ORG = "@frontmcp";
3402
+ const PKG = "ui";
3403
+ for (const candidate of candidatePaths) {
3404
+ let dir = candidate;
3405
+ while (true) {
3406
+ const pkgJson = nodePath.join(dir, "node_modules", ORG, PKG, "package.json");
3407
+ if (nodeFs.existsSync(pkgJson)) return true;
3408
+ const parent = nodePath.dirname(dir);
3409
+ if (parent === dir) break;
3410
+ dir = parent;
3411
+ }
3412
+ }
3413
+ return false;
3414
+ } catch {
3415
+ return false;
3416
+ }
3417
+ }
3418
+
3149
3419
  // libs/uipack/src/component/transpiler.ts
3150
3420
  function transpileReactSource(source, filename) {
3151
3421
  const esbuild = require("esbuild");
@@ -3161,7 +3431,12 @@ function transpileReactSource(source, filename) {
3161
3431
  });
3162
3432
  return result.code;
3163
3433
  }
3164
- function bundleFileSource(source, filename, resolveDir, componentName) {
3434
+ function bundleFileSource(source, filename, resolveDir, componentName, options = {}) {
3435
+ if (!isFrontmcpUiResolvable(resolveDir, process.cwd())) {
3436
+ throw new Error(
3437
+ `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.`
3438
+ );
3439
+ }
3165
3440
  const esbuild = require("esbuild");
3166
3441
  const mountCode = `
3167
3442
  // --- Auto-generated mount ---
@@ -3243,7 +3518,11 @@ if (__root) {
3243
3518
  format: "esm",
3244
3519
  target: "es2020",
3245
3520
  jsx: "automatic",
3246
- external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
3521
+ // When `bundleReact` is set (resourceMode: 'inline'), React itself is
3522
+ // bundled — required for hosts that block external script execution
3523
+ // such as Claude (#454). Otherwise React stays external and is loaded
3524
+ // via the import map emitted by the renderer.
3525
+ external: options.bundleReact ? [] : ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
3247
3526
  alias,
3248
3527
  define: { "process.env.NODE_ENV": '"production"' },
3249
3528
  platform: "browser",
@@ -3255,7 +3534,7 @@ if (__root) {
3255
3534
  } catch (err) {
3256
3535
  const message = err instanceof Error ? err.message : String(err);
3257
3536
  throw new Error(
3258
- `Failed to bundle FileSource "${filename}": ${message}. Ensure workspace packages are built (e.g. nx build ui).`
3537
+ `Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`
3259
3538
  );
3260
3539
  }
3261
3540
  }
@@ -3273,6 +3552,9 @@ var DEFAULT_META = {
3273
3552
  renderer: "auto"
3274
3553
  };
3275
3554
  function resolveUISource(source, options) {
3555
+ if (options?.inlineReact && options?.transformOnly) {
3556
+ throw new Error("resolveUISource: `inlineReact` and `transformOnly` are mutually exclusive \u2014 set at most one.");
3557
+ }
3276
3558
  const resolver = options?.resolver ?? createEsmShResolver();
3277
3559
  if (isNpmSource(source)) {
3278
3560
  return resolveNpmSource(source, resolver);
@@ -3281,7 +3563,7 @@ function resolveUISource(source, options) {
3281
3563
  return resolveImportSource(source);
3282
3564
  }
3283
3565
  if (isFileSource(source)) {
3284
- return resolveFileSource(source);
3566
+ return resolveFileSource(source, { inlineReact: options?.inlineReact, transformOnly: options?.transformOnly });
3285
3567
  }
3286
3568
  if (isFunctionSource(source)) {
3287
3569
  return resolveFunctionSource(source, options?.input, options?.output);
@@ -3314,15 +3596,42 @@ function resolveImportSource(source) {
3314
3596
  peerDependencies: []
3315
3597
  };
3316
3598
  }
3317
- function resolveFileSource(source) {
3599
+ function resolveFileSource(source, options = {}) {
3318
3600
  const path = require("path");
3319
3601
  const ext = path.extname(source.file).toLowerCase();
3320
3602
  if (ext === ".tsx" || ext === ".jsx") {
3321
3603
  const fs = require("fs");
3322
- const filePath = path.isAbsolute(source.file) ? source.file : path.resolve(process.cwd(), source.file);
3323
- const rawSource = fs.readFileSync(filePath, "utf-8");
3604
+ const wasRelative = !path.isAbsolute(source.file);
3605
+ const filePath = wasRelative ? path.resolve(process.cwd(), source.file) : source.file;
3606
+ let rawSource;
3607
+ try {
3608
+ rawSource = fs.readFileSync(filePath, "utf-8");
3609
+ } catch (err) {
3610
+ const isNotFound = err?.code === "ENOENT";
3611
+ if (isNotFound && wasRelative) {
3612
+ throw new Error(
3613
+ `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).`
3614
+ );
3615
+ }
3616
+ throw err;
3617
+ }
3324
3618
  const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
3325
- const bundled = bundleFileSource(rawSource, source.file, path.dirname(filePath), componentName);
3619
+ if (options.transformOnly === true) {
3620
+ const code = transpileReactSource(rawSource, path.basename(filePath));
3621
+ const parsed2 = parseImports(code);
3622
+ return {
3623
+ mode: "module",
3624
+ code,
3625
+ imports: [...new Set(parsed2.externalImports.map((i) => i.specifier))],
3626
+ exportName: componentName,
3627
+ meta: { mcpAware: true, renderer: "react" },
3628
+ peerDependencies: source.peerDependencies || ["react", "react-dom"],
3629
+ bundled: false
3630
+ };
3631
+ }
3632
+ const bundled = bundleFileSource(rawSource, source.file, path.dirname(filePath), componentName, {
3633
+ bundleReact: options.inlineReact === true
3634
+ });
3326
3635
  const parsed = parseImports(bundled.code);
3327
3636
  return {
3328
3637
  mode: "module",
@@ -3391,12 +3700,18 @@ function generateMappedPropsCode(mapping) {
3391
3700
  }
3392
3701
 
3393
3702
  // libs/uipack/src/component/renderer.ts
3703
+ var DEFAULT_WIDGET_MOUNT = {
3704
+ moduleSpecifier: "@frontmcp/ui/react",
3705
+ wrapperImportName: "McpBridgeProvider"
3706
+ };
3394
3707
  function renderComponent(config, shellConfig) {
3395
3708
  const resolver = shellConfig.resolver ?? createEsmShResolver();
3396
3709
  const resolved = resolveUISource(config.source, {
3397
3710
  resolver,
3398
3711
  input: shellConfig.input,
3399
- output: shellConfig.output
3712
+ output: shellConfig.output,
3713
+ inlineReact: config.inlineReact === true,
3714
+ transformOnly: config.transformOnly === true
3400
3715
  });
3401
3716
  const mergedShellConfig = {
3402
3717
  ...shellConfig,
@@ -3407,10 +3722,10 @@ function renderComponent(config, shellConfig) {
3407
3722
  if (resolved.mode === "inline") {
3408
3723
  return buildShell(resolved.html ?? "", mergedShellConfig);
3409
3724
  }
3410
- const content = buildModuleContent(resolved, resolver, config.props);
3725
+ const content = buildModuleContent(resolved, resolver, config.props, shellConfig.mount);
3411
3726
  return buildShell(content, mergedShellConfig);
3412
3727
  }
3413
- function buildModuleContent(resolved, resolver, propsMapping) {
3728
+ function buildModuleContent(resolved, resolver, propsMapping, mount = DEFAULT_WIDGET_MOUNT) {
3414
3729
  const parts = [];
3415
3730
  if (resolved.code && resolved.bundled) {
3416
3731
  const importEntries = {};
@@ -3434,14 +3749,16 @@ ${resolved.code}
3434
3749
  ...resolved.imports || [],
3435
3750
  "react-dom/client",
3436
3751
  // needed by mount script
3437
- "@frontmcp/ui/react",
3438
- // needed for McpBridgeProvider
3752
+ mount.moduleSpecifier,
3753
+ // mounter/provider package (e.g. @frontmcp/ui/react)
3439
3754
  // React subpath entries needed by esm.sh externalized modules:
3440
3755
  "react/jsx-runtime",
3441
- "react/jsx-dev-runtime",
3442
- "react-dom/server",
3443
- "react-dom/static"
3756
+ "react/jsx-dev-runtime"
3444
3757
  ]);
3758
+ if (mount === DEFAULT_WIDGET_MOUNT) {
3759
+ allSpecifiers.add("react-dom/server");
3760
+ allSpecifiers.add("react-dom/static");
3761
+ }
3445
3762
  const coreDeps = /* @__PURE__ */ new Set([
3446
3763
  "react",
3447
3764
  "react-dom",
@@ -3472,8 +3789,9 @@ ${resolved.code}
3472
3789
  const importMap = createImportMapFromResolved(importEntries);
3473
3790
  parts.push(generateImportMapScriptTag(importMap));
3474
3791
  }
3475
- parts.push('<div id="root"></div>');
3476
- const mountCode = generateInlineMountCode(resolved.exportName);
3792
+ const mountNodeId = mount.mountNodeId ?? "root";
3793
+ parts.push(`<div id="${mountNodeId}">${mount.mountNodeInnerHtml ?? ""}</div>`);
3794
+ const mountCode = generateInlineMountCode(resolved.exportName, mount);
3477
3795
  parts.push(`<script type="module">
3478
3796
  ${resolved.code}
3479
3797
  ${mountCode}
@@ -3510,15 +3828,21 @@ function addExternalParam(url, externals) {
3510
3828
  const sep = url.includes("?") ? "&" : "?";
3511
3829
  return `${url}${sep}external=${externals.join(",")}`;
3512
3830
  }
3513
- function generateInlineMountCode(componentName) {
3831
+ function generateInlineMountCode(componentName, mount) {
3832
+ if (mount.generate) {
3833
+ return mount.generate(componentName);
3834
+ }
3835
+ const wrapperName = mount.wrapperImportName ?? "McpBridgeProvider";
3836
+ const wrapperAlias = `__${wrapperName}`;
3837
+ const mountNodeId = mount.mountNodeId ?? "root";
3514
3838
  return `
3515
3839
  // --- Mount ---
3516
3840
  import { createRoot as __createRoot } from 'react-dom/client';
3517
- import { McpBridgeProvider as __McpBridgeProvider } from '@frontmcp/ui/react';
3518
- const __root = document.getElementById('root');
3841
+ import { ${wrapperName} as ${wrapperAlias} } from '${mount.moduleSpecifier}';
3842
+ const __root = document.getElementById('${mountNodeId}');
3519
3843
  if (__root) {
3520
3844
  __createRoot(__root).render(
3521
- React.createElement(__McpBridgeProvider, null,
3845
+ React.createElement(${wrapperAlias}, null,
3522
3846
  React.createElement(${componentName})
3523
3847
  )
3524
3848
  );
@@ -3594,38 +3918,6 @@ function isUIRenderFailure(result) {
3594
3918
  return typeof rec["reason"] === "string" && !("meta" in rec);
3595
3919
  }
3596
3920
 
3597
- // libs/uipack/src/adapters/type-detector.ts
3598
- function detectUIType(template) {
3599
- if (template === null || template === void 0) {
3600
- return "auto";
3601
- }
3602
- if (typeof template === "object" && template !== null && "file" in template) {
3603
- const file = template.file;
3604
- if (/\.(tsx|jsx)$/i.test(file)) return "react";
3605
- }
3606
- if (typeof template === "function") {
3607
- const proto = template.prototype;
3608
- if (proto && typeof proto.render === "function") {
3609
- return "react";
3610
- }
3611
- const asRecord = template;
3612
- if (asRecord["$$typeof"] !== void 0) {
3613
- return "react";
3614
- }
3615
- if (template.name && /^[A-Z]/.test(template.name)) {
3616
- return "react";
3617
- }
3618
- return "html";
3619
- }
3620
- if (typeof template === "string") {
3621
- if (template.includes("<") && template.includes(">")) {
3622
- return "html";
3623
- }
3624
- return "markdown";
3625
- }
3626
- return "auto";
3627
- }
3628
-
3629
3921
  // libs/uipack/src/adapters/content-detector.ts
3630
3922
  var CHART_TYPES = /* @__PURE__ */ new Set(["bar", "line", "pie", "area", "scatter", "doughnut", "radar", "polarArea", "bubble"]);
3631
3923
  var MERMAID_PREFIXES = [
@@ -3766,6 +4058,38 @@ function wrapDetectedContent(value) {
3766
4058
  }
3767
4059
  }
3768
4060
 
4061
+ // libs/uipack/src/adapters/type-detector.ts
4062
+ function detectUIType(template) {
4063
+ if (template === null || template === void 0) {
4064
+ return "auto";
4065
+ }
4066
+ if (typeof template === "object" && template !== null && "file" in template) {
4067
+ const file = template.file;
4068
+ if (/\.(tsx|jsx)$/i.test(file)) return "react";
4069
+ }
4070
+ if (typeof template === "function") {
4071
+ const proto = template.prototype;
4072
+ if (proto && typeof proto.render === "function") {
4073
+ return "react";
4074
+ }
4075
+ const asRecord = template;
4076
+ if (asRecord["$$typeof"] !== void 0) {
4077
+ return "react";
4078
+ }
4079
+ if (template.name && /^[A-Z]/.test(template.name)) {
4080
+ return "react";
4081
+ }
4082
+ return "html";
4083
+ }
4084
+ if (typeof template === "string") {
4085
+ if (template.includes("<") && template.includes(">")) {
4086
+ return "html";
4087
+ }
4088
+ return "markdown";
4089
+ }
4090
+ return "auto";
4091
+ }
4092
+
3769
4093
  // libs/uipack/src/adapters/template-renderer.ts
3770
4094
  function buildCspConfig(resolver) {
3771
4095
  const cspResourceDomains = ["https://esm.sh"];
@@ -3786,21 +4110,26 @@ function buildCspConfig(resolver) {
3786
4110
  return { resourceDomains: cspResourceDomains, connectDomains: cspConnectDomains };
3787
4111
  }
3788
4112
  function renderToolTemplate(options) {
3789
- const { toolName, input, output, template, resolver } = options;
4113
+ const { toolName, input, output, template, resolver, platformType, sizing } = options;
3790
4114
  const uiType = detectUIType(template);
4115
+ const resourceMode = options.resourceMode ?? (platformType === "claude" ? "inline" : "cdn");
3791
4116
  const shellConfig = {
3792
4117
  toolName,
3793
4118
  input,
3794
4119
  output,
3795
4120
  includeBridge: true,
3796
- resolver
4121
+ resolver,
4122
+ sizing
3797
4123
  };
3798
4124
  let html;
3799
4125
  let hash = "";
3800
4126
  let size = 0;
3801
4127
  if (typeof template === "object" && template !== null && "file" in template) {
3802
4128
  const cspConfig = buildCspConfig(resolver);
3803
- const result = renderComponent({ source: template }, { ...shellConfig, csp: cspConfig });
4129
+ const result = renderComponent(
4130
+ { source: template, inlineReact: resourceMode === "inline" },
4131
+ { ...shellConfig, csp: cspConfig }
4132
+ );
3804
4133
  html = result.html;
3805
4134
  hash = result.hash;
3806
4135
  size = result.size;
@@ -3842,6 +4171,12 @@ function renderToolTemplate(options) {
3842
4171
  "ui/type": uiType,
3843
4172
  "ui/mimeType": MCP_APPS_MIME_TYPE
3844
4173
  };
4174
+ if (hasSizing(sizing)) {
4175
+ if (sizing.preferredHeight !== void 0) meta["ui/preferredHeight"] = sizing.preferredHeight;
4176
+ if (sizing.minHeight !== void 0) meta["ui/minHeight"] = sizing.minHeight;
4177
+ if (sizing.maxHeight !== void 0) meta["ui/maxHeight"] = sizing.maxHeight;
4178
+ if (sizing.aspectRatio !== void 0) meta["ui/aspectRatio"] = sizing.aspectRatio;
4179
+ }
3845
4180
  return {
3846
4181
  html,
3847
4182
  uiType,
@@ -3947,6 +4282,7 @@ function buildCDNInfoForUIType(uiType) {
3947
4282
  buildCSPDirectives,
3948
4283
  buildCSPMetaTag,
3949
4284
  buildChartHtml,
4285
+ buildCustomDataInjectionScript,
3950
4286
  buildDataInjectionScript,
3951
4287
  buildMermaidHtml,
3952
4288
  buildPdfHtml,