@frontmcp/uipack 1.8.5 → 1.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -358,6 +358,23 @@ function __applySizingCss(sizing) {
358
358
  }
359
359
  }
360
360
 
361
+ function __measureHeight() {
362
+ // <html> at fit-content includes body margins and collapsed child margins;
363
+ // content overflowing a fixed-height body is added; a px max-height clamps.
364
+ var de = document.documentElement;
365
+ var body = document.body;
366
+ if (!de || !body) return 0;
367
+ var prev = de.style.height;
368
+ de.style.height = 'fit-content';
369
+ var dr = de.getBoundingClientRect();
370
+ var br = body.getBoundingClientRect();
371
+ var h = Math.max(dr.height, br.top + body.scrollHeight + Math.max(0, dr.bottom - br.bottom) - dr.top);
372
+ de.style.height = prev;
373
+ var maxHeight = getComputedStyle(de).maxHeight;
374
+ var mx = maxHeight && maxHeight.slice(-2) === 'px' ? parseFloat(maxHeight) : NaN;
375
+ return Math.ceil(isNaN(mx) ? h : Math.min(h, mx));
376
+ }
377
+
361
378
  function __initAutoResize() {
362
379
  if (typeof window === 'undefined') return;
363
380
  // Idempotent: a re-injected IIFE must not stack observers.
@@ -380,23 +397,30 @@ function __initAutoResize() {
380
397
  if (typeof ResizeObserver === 'undefined') return;
381
398
 
382
399
  function startObserving() {
383
- var target = document.getElementById('root') || document.body;
384
- if (!target) return;
400
+ if (!document.body) return;
401
+ var targets = [document.documentElement, document.body];
402
+ var root = document.getElementById('root');
403
+ if (root) targets.push(root);
385
404
 
386
405
  var rafId = null;
387
406
  var lastReported = -1;
388
407
  function report() {
389
408
  rafId = null;
390
409
  try {
391
- var rect = target.getBoundingClientRect();
392
- var height = Math.ceil(rect.height);
393
- var width = Math.ceil(rect.width);
410
+ // Before the handshake an ext-apps host can't receive the report; the
411
+ // bridge:ready listener below sends it once the bridge is up.
412
+ if (window.FrontMcpBridge && window.FrontMcpBridge.initialized === false) return;
413
+ var height = __measureHeight();
414
+ var width = Math.ceil(document.documentElement.getBoundingClientRect().width);
394
415
  if (height === lastReported || height <= 0) return;
395
416
  lastReported = height;
396
417
  var payload = { height: height, width: width };
397
418
  if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
398
419
  if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
399
- window.FrontMcpBridge.setSize(payload).catch(function() {});
420
+ window.FrontMcpBridge.setSize(payload).catch(function() {
421
+ // Forget a rejected report so the same height is sent again.
422
+ if (lastReported === height) lastReported = -1;
423
+ });
400
424
  }
401
425
  window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
402
426
  } catch (e) {}
@@ -417,9 +441,17 @@ function __initAutoResize() {
417
441
  window.__mcpResizeObserver.disconnect();
418
442
  }
419
443
  var ro = new ResizeObserver(function() { schedule(); });
420
- ro.observe(target);
444
+ for (var i = 0; i < targets.length; i++) ro.observe(targets[i]);
421
445
  window.__mcpResizeObserver = ro;
422
446
  } catch (e) {}
447
+ // Report once the bridge is ready (the report scheduled below is skipped
448
+ // while the handshake is still pending).
449
+ var readySent = false;
450
+ window.addEventListener('bridge:ready', function() {
451
+ if (readySent) return;
452
+ readySent = true;
453
+ schedule();
454
+ });
423
455
  // Report once on init so the host gets an initial measurement.
424
456
  schedule();
425
457
  }
@@ -467,12 +499,57 @@ function detectViewport() {
467
499
  return undefined;
468
500
  }
469
501
 
502
+ function __applyHostTheme(theme) {
503
+ // Follow a host-supplied theme: <meta name="color-scheme"> sets the frame's
504
+ // canvas and form controls (a widget's own :root color-scheme still wins),
505
+ // <html data-theme> is a styling hook. The OS fallback is never written.
506
+ if (theme !== 'light' && theme !== 'dark') return;
507
+ if (typeof document === 'undefined' || !document.documentElement) return;
508
+ try {
509
+ var meta = document.querySelector('meta[name="color-scheme"]');
510
+ var head = document.head || document.getElementsByTagName('head')[0];
511
+ if (!meta && head) {
512
+ meta = document.createElement('meta');
513
+ meta.setAttribute('name', 'color-scheme');
514
+ head.appendChild(meta);
515
+ }
516
+ if (meta) meta.setAttribute('content', theme);
517
+ document.documentElement.setAttribute('data-theme', theme);
518
+ } catch (e) {}
519
+ }
520
+
521
+ function extractToolOutput(params) {
522
+ // A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
523
+ if (!params) return undefined;
524
+ if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
525
+ var content = params.content;
526
+ if (Array.isArray(content)) {
527
+ for (var i = 0; i < content.length; i++) {
528
+ var item = content[i];
529
+ if (item && item.type === 'text' && typeof item.text === 'string') {
530
+ try { return JSON.parse(item.text); } catch (e) { return item.text; }
531
+ }
532
+ }
533
+ }
534
+ return content;
535
+ }
536
+
470
537
  function readInjectedData() {
471
538
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
472
539
  if (typeof window !== 'undefined') {
473
540
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
474
541
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
475
542
  if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
543
+ // The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
544
+ // (toolOutput is the tool's structuredContent).
545
+ var oa = window.openai;
546
+ if (oa && typeof oa === 'object') {
547
+ if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
548
+ if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
549
+ if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
550
+ if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
551
+ }
552
+ }
476
553
  }
477
554
  return data;
478
555
  }
@@ -521,7 +598,24 @@ var OpenAIAdapter = {
521
598
  if (sdk.displayMode) {
522
599
  context.hostContext.displayMode = sdk.displayMode;
523
600
  }
524
- // Note: OpenAI SDK does not have an onContextChange equivalent
601
+ // The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
602
+ window.addEventListener('openai:set_globals', function(event) {
603
+ var globals = (event && event.detail && event.detail.globals) || window.openai || {};
604
+ if (globals.toolInput !== undefined && globals.toolInput !== null) {
605
+ context.toolInput = globals.toolInput;
606
+ window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
607
+ }
608
+ if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
609
+ context.structuredContent = globals.toolOutput;
610
+ context.notifyToolResult(globals.toolOutput);
611
+ }
612
+ if (globals.theme && globals.theme !== context.hostContext.theme) {
613
+ context.notifyContextChange({ theme: globals.theme });
614
+ }
615
+ if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
616
+ context.notifyContextChange({ displayMode: globals.displayMode });
617
+ }
618
+ });
525
619
  return Promise.resolve();
526
620
  },
527
621
  callTool: function(context, name, args) {
@@ -573,6 +667,8 @@ var ExtAppsAdapter = {
573
667
  pendingRequests: {},
574
668
  requestId: 0,
575
669
  hostCapabilities: {},
670
+ handshakeSettled: false,
671
+ pendingSize: null,
576
672
  canHandle: function() {
577
673
  if (typeof window === 'undefined') return false;
578
674
  if (window.parent === window) return false;
@@ -666,9 +762,10 @@ var ExtAppsAdapter = {
666
762
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
667
763
  break;
668
764
  case 'ui/notifications/tool-result':
669
- context.toolOutput = params.content;
765
+ var resultOutput = extractToolOutput(params);
766
+ context.toolOutput = resultOutput;
670
767
  context.structuredContent = params.structuredContent;
671
- context.notifyToolResult(params.content);
768
+ context.notifyToolResult(resultOutput);
672
769
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
673
770
  break;
674
771
  case 'ui/notifications/host-context-changed':
@@ -722,6 +819,15 @@ var ExtAppsAdapter = {
722
819
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
723
820
  });
724
821
  },
822
+ sendNotification: function(method, params) {
823
+ // JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
824
+ if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
825
+ return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
826
+ }
827
+ var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
828
+ window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
829
+ return Promise.resolve();
830
+ },
725
831
  sendHandshake: function(context) {
726
832
  // Send ui/initialize using '*' as target origin since TOFU hasn't
727
833
  // been established yet. The response from the host will establish
@@ -738,6 +844,7 @@ var ExtAppsAdapter = {
738
844
  var timeout = setTimeout(function() {
739
845
  delete self.pendingRequests[id];
740
846
  // Handshake timeout is non-fatal \u2014 notifications may still arrive
847
+ self.settleHandshake();
741
848
  resolve();
742
849
  }, 10000);
743
850
 
@@ -751,7 +858,9 @@ var ExtAppsAdapter = {
751
858
  supportsDisplayModes: true
752
859
  });
753
860
  if (result.hostContext) {
754
- Object.assign(context.hostContext, result.hostContext);
861
+ // Notify (not a silent merge) so onContextChange listeners and the
862
+ // host theme see the handshake's context.
863
+ context.notifyContextChange(result.hostContext);
755
864
  }
756
865
  // Send ui/notifications/initialized to tell the host the view is ready.
757
866
  // Per MCP Apps spec, the host waits for this before sending tool-result.
@@ -761,9 +870,10 @@ var ExtAppsAdapter = {
761
870
  method: 'ui/notifications/initialized',
762
871
  params: {}
763
872
  }, targetOrigin);
873
+ self.settleHandshake();
764
874
  resolve();
765
875
  },
766
- reject: function(err) { resolve(); }, // Non-fatal
876
+ reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
767
877
  timeout: timeout
768
878
  };
769
879
 
@@ -777,6 +887,18 @@ var ExtAppsAdapter = {
777
887
  performHandshake: function(context) {
778
888
  return this.sendHandshake(context);
779
889
  },
890
+ settleHandshake: function() {
891
+ // Deliver the latest size requested while no host origin was trusted.
892
+ this.handshakeSettled = true;
893
+ var pending = this.pendingSize;
894
+ if (!pending) return;
895
+ this.pendingSize = null;
896
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
897
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
898
+ }, function(err) {
899
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
900
+ });
901
+ },
780
902
  callTool: function(context, name, args) {
781
903
  if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
782
904
  return Promise.reject(new Error('Server tool proxy not supported'));
@@ -798,13 +920,22 @@ var ExtAppsAdapter = {
798
920
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
799
921
  },
800
922
  setSize: function(context, size) {
801
- // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
802
- // measured/desired widget dimensions to the host.
803
- return this.sendRequest('ui/setSize', {
804
- height: size && size.height,
805
- width: size && size.width,
806
- aspectRatio: size && size.aspectRatio
807
- });
923
+ // Standard ext-apps sizing: the view tells the host its dimensions with the
924
+ // ui/notifications/size-changed notification ({ width?, height? } in px).
925
+ var params = {};
926
+ if (size && typeof size.width === 'number') params.width = size.width;
927
+ if (size && typeof size.height === 'number') params.height = size.height;
928
+ if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
929
+ // No host origin yet: keep only the latest size and send it when the
930
+ // handshake settles; every caller's promise follows that one request.
931
+ var self = this;
932
+ if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
933
+ this.pendingSize.params = params;
934
+ return new Promise(function(resolve, reject) {
935
+ self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
936
+ });
937
+ }
938
+ return this.sendNotification('ui/notifications/size-changed', params);
808
939
  },
809
940
  requestClose: function(context) {
810
941
  return this.sendRequest('ui/close', {});
@@ -1050,6 +1181,7 @@ function FrontMcpBridge() {
1050
1181
  toolResultListeners: [],
1051
1182
  notifyContextChange: function(changes) {
1052
1183
  Object.assign(this.hostContext, changes);
1184
+ if (changes && changes.theme) __applyHostTheme(changes.theme);
1053
1185
  for (var i = 0; i < this.contextListeners.length; i++) {
1054
1186
  try { this.contextListeners[i](changes); } catch(e) {}
1055
1187
  }
@@ -1226,7 +1358,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1226
1358
 
1227
1359
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1228
1360
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1229
- // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1361
+ // ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
1230
1362
  FrontMcpBridge.prototype.setSize = function(size) {
1231
1363
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1232
1364
  if (!this._adapter.setSize) return Promise.resolve();
@@ -1328,7 +1460,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
1328
1460
  target.classList.add('opacity-50', 'cursor-not-allowed');
1329
1461
 
1330
1462
  // Add spinner for buttons
1331
- var spinner = '<svg class="animate-spin -ml-1 mr-2 h-4 w-4 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
1463
+ var spinner = '<svg class="animate-spin -ml-1 mr-2 h-4 w-4 inline" style="width:1em;height:1em;vertical-align:-0.125em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
1332
1464
  if (target.tagName === 'BUTTON') {
1333
1465
  target.innerHTML = spinner + 'Loading...';
1334
1466
  }
@@ -1409,7 +1541,22 @@ function minifyJS(code) {
1409
1541
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
1410
1542
  return code;
1411
1543
  }
1412
- return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\s+/g, " ").replace(/\s*([{};,:()[\]])\s*/g, "$1").replace(/;\}/g, "}").trim();
1544
+ const out = [];
1545
+ let inBlockComment = false;
1546
+ for (const raw of code.split("\n")) {
1547
+ const line = raw.trim();
1548
+ if (inBlockComment) {
1549
+ if (line.includes("*/")) inBlockComment = false;
1550
+ continue;
1551
+ }
1552
+ if (line === "" || line.startsWith("//")) continue;
1553
+ if (line.startsWith("/*")) {
1554
+ if (!line.includes("*/")) inBlockComment = true;
1555
+ continue;
1556
+ }
1557
+ out.push(line);
1558
+ }
1559
+ return out.join("\n");
1413
1560
  }
1414
1561
  function generatePlatformBundle(platform, options = {}) {
1415
1562
  const platformAdapters = {
@@ -3449,8 +3596,24 @@ function isFrontmcpUiResolvable(...candidatePaths) {
3449
3596
  }
3450
3597
 
3451
3598
  // libs/uipack/src/component/transpiler.ts
3599
+ function loadEsbuild(filename) {
3600
+ try {
3601
+ return require("esbuild");
3602
+ } catch (err) {
3603
+ if (!isMissingEsbuild(err)) throw err;
3604
+ const subject = filename ? `FileSource widget "${filename}"` : "A FileSource widget";
3605
+ throw new Error(
3606
+ `${subject} needs esbuild, which is not installed. @frontmcp/uipack loads esbuild on demand to bundle FileSource widgets (.tsx/.jsx) when the tool is called, so it must be installed where the server runs: \`npm install esbuild\` (in "dependencies", not "devDependencies"). Projects created with \`frontmcp create\` already get it through the \`frontmcp\` package.`,
3607
+ { cause: err }
3608
+ );
3609
+ }
3610
+ }
3611
+ function isMissingEsbuild(err) {
3612
+ if (!(err instanceof Error)) return false;
3613
+ return err.code === "MODULE_NOT_FOUND" && /Cannot find module ['"]esbuild['"]/.test(err.message);
3614
+ }
3452
3615
  function transpileReactSource(source, filename) {
3453
- const esbuild = require("esbuild");
3616
+ const esbuild = loadEsbuild(filename);
3454
3617
  const loader = filename?.endsWith(".tsx") ? "tsx" : "jsx";
3455
3618
  const result = esbuild.transformSync(source, {
3456
3619
  loader,
@@ -3469,7 +3632,7 @@ function bundleFileSource(source, filename, resolveDir, componentName, options =
3469
3632
  `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.`
3470
3633
  );
3471
3634
  }
3472
- const esbuild = require("esbuild");
3635
+ const esbuild = loadEsbuild(filename);
3473
3636
  const mountCode = `
3474
3637
  // --- Auto-generated mount ---
3475
3638
  import { createElement as __h } from 'react';
@@ -3566,7 +3729,8 @@ if (__root) {
3566
3729
  } catch (err) {
3567
3730
  const message = err instanceof Error ? err.message : String(err);
3568
3731
  throw new Error(
3569
- `Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`
3732
+ `Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`,
3733
+ { cause: err }
3570
3734
  );
3571
3735
  }
3572
3736
  }
@@ -3628,6 +3792,37 @@ function resolveImportSource(source) {
3628
3792
  peerDependencies: []
3629
3793
  };
3630
3794
  }
3795
+ var BUILD_DIR_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "out"]);
3796
+ function missingWidgetError(filePath, cause) {
3797
+ const sourceWidget = findSourceWidget(filePath);
3798
+ const error = new Error(
3799
+ `FileSource widget not found (ENOENT): no file at "${filePath}". The widget source is read from this path when the tool is called, so it has to exist where the compiled tool runs \u2014 a path built from \`__dirname\` or \`import.meta.url\` points into the build output once the tool is compiled. tsc does not copy \`*.widget.tsx\` / \`*.widget.jsx\` files into its output; \`frontmcp build\` does, and any other build needs a step that copies them next to the compiled tool.` + (sourceWidget ? ` The source widget exists at "${sourceWidget}" \u2014 copy it to "${filePath}", or build with \`frontmcp build\`.` : ""),
3800
+ { cause }
3801
+ );
3802
+ error.code = "ENOENT";
3803
+ error.path = filePath;
3804
+ return error;
3805
+ }
3806
+ function findSourceWidget(filePath) {
3807
+ const path = require("path");
3808
+ const fs = require("fs");
3809
+ const parts = filePath.split(path.sep);
3810
+ for (let i = parts.length - 2; i >= 0; i--) {
3811
+ if (!BUILD_DIR_SEGMENTS.has(parts[i])) continue;
3812
+ const candidates = [[...parts.slice(0, i), "src", ...parts.slice(i + 1)]];
3813
+ if (parts.length - i > 2) candidates.push([...parts.slice(0, i), "src", ...parts.slice(i + 2)]);
3814
+ candidates.push([...parts.slice(0, i), ...parts.slice(i + 1)]);
3815
+ for (const candidate of candidates) {
3816
+ const candidatePath = candidate.join(path.sep);
3817
+ try {
3818
+ if (fs.existsSync(candidatePath)) return candidatePath;
3819
+ } catch {
3820
+ }
3821
+ }
3822
+ return void 0;
3823
+ }
3824
+ return void 0;
3825
+ }
3631
3826
  function resolveFileSource(source, options = {}) {
3632
3827
  const path = require("path");
3633
3828
  const ext = path.extname(source.file).toLowerCase();
@@ -3642,9 +3837,11 @@ function resolveFileSource(source, options = {}) {
3642
3837
  const isNotFound = err?.code === "ENOENT";
3643
3838
  if (isNotFound && wasRelative) {
3644
3839
  throw new Error(
3645
- `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).`
3840
+ `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).`,
3841
+ { cause: err }
3646
3842
  );
3647
3843
  }
3844
+ if (isNotFound) throw missingWidgetError(filePath, err);
3648
3845
  throw err;
3649
3846
  }
3650
3847
  const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
@@ -4091,6 +4288,86 @@ function wrapDetectedContent(value) {
4091
4288
  }
4092
4289
  }
4093
4290
 
4291
+ // libs/uipack/src/adapters/markdown.ts
4292
+ var SAFE_HREF = /^(https?:|mailto:|\/|#)/i;
4293
+ function emphasis(text) {
4294
+ return text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
4295
+ }
4296
+ function inline(text) {
4297
+ const stash = [];
4298
+ const hold = (html2) => `\uE000${stash.push(html2) - 1}\uE000`;
4299
+ let out = escapeHtml(text.replace(/\uE000/g, "")).replace(
4300
+ /`([^`]+)`/g,
4301
+ (_m, code) => hold(`<code>${code}</code>`)
4302
+ );
4303
+ out = out.replace(
4304
+ /\[([^\]]+)\]\(([^)\s]+)\)/g,
4305
+ (_m, label, href) => SAFE_HREF.test(href) ? hold(`<a href="${href}" rel="noopener noreferrer">${emphasis(label)}</a>`) : label
4306
+ );
4307
+ out = emphasis(out);
4308
+ for (let i = 0; i < 2; i++) out = out.replace(/\uE000(\d+)\uE000/g, (_m, n) => stash[Number(n)]);
4309
+ return out;
4310
+ }
4311
+ function markdownToHtml(source) {
4312
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
4313
+ const blocks = [];
4314
+ let paragraph = [];
4315
+ let list;
4316
+ const flushParagraph = () => {
4317
+ if (paragraph.length > 0) blocks.push(`<p>${inline(paragraph.join(" "))}</p>`);
4318
+ paragraph = [];
4319
+ };
4320
+ const flushList = () => {
4321
+ if (list)
4322
+ blocks.push(`<${list.tag}>${list.items.map((item) => `<li>${inline(item)}</li>`).join("")}</${list.tag}>`);
4323
+ list = void 0;
4324
+ };
4325
+ for (let i = 0; i < lines.length; i++) {
4326
+ const line = lines[i];
4327
+ const fence = /^\s*(`{3,})/.exec(line);
4328
+ if (fence) {
4329
+ flushParagraph();
4330
+ flushList();
4331
+ const fenceLength = fence[1].length;
4332
+ const isClosing = (l) => {
4333
+ const m = /^\s*(`{3,})\s*$/.exec(l);
4334
+ return m !== null && m[1].length >= fenceLength;
4335
+ };
4336
+ const code = [];
4337
+ for (i++; i < lines.length && !isClosing(lines[i]); i++) code.push(lines[i]);
4338
+ blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}
4339
+ </code></pre>`);
4340
+ continue;
4341
+ }
4342
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
4343
+ if (heading) {
4344
+ flushParagraph();
4345
+ flushList();
4346
+ blocks.push(`<h${heading[1].length}>${inline(heading[2].trim())}</h${heading[1].length}>`);
4347
+ continue;
4348
+ }
4349
+ const item = /^\s*([-*+]|\d+\.)\s+(.*)$/.exec(line);
4350
+ if (item) {
4351
+ flushParagraph();
4352
+ const tag = /\d/.test(item[1]) ? "ol" : "ul";
4353
+ if (list && list.tag !== tag) flushList();
4354
+ list ??= { tag, items: [] };
4355
+ list.items.push(item[2]);
4356
+ continue;
4357
+ }
4358
+ if (line.trim() === "") {
4359
+ flushParagraph();
4360
+ flushList();
4361
+ continue;
4362
+ }
4363
+ flushList();
4364
+ paragraph.push(line.trim());
4365
+ }
4366
+ flushParagraph();
4367
+ flushList();
4368
+ return blocks.join("\n");
4369
+ }
4370
+
4094
4371
  // libs/uipack/src/adapters/type-detector.ts
4095
4372
  function detectUIType(template) {
4096
4373
  if (template === null || template === void 0) {
@@ -4124,9 +4401,15 @@ function detectUIType(template) {
4124
4401
  }
4125
4402
 
4126
4403
  // libs/uipack/src/adapters/template-renderer.ts
4127
- function buildCspConfig(resolver) {
4404
+ function buildCspConfig(resolver, extra) {
4128
4405
  const cspResourceDomains = ["https://esm.sh"];
4129
4406
  const cspConnectDomains = ["https://esm.sh"];
4407
+ for (const domain of extra?.resourceDomains ?? []) {
4408
+ if (!cspResourceDomains.includes(domain)) cspResourceDomains.push(domain);
4409
+ }
4410
+ for (const domain of extra?.connectDomains ?? []) {
4411
+ if (!cspConnectDomains.includes(domain)) cspConnectDomains.push(domain);
4412
+ }
4130
4413
  if (resolver && "overrides" in resolver) {
4131
4414
  const overrides = resolver.overrides;
4132
4415
  if (overrides) {
@@ -4166,6 +4449,14 @@ function renderUnwrappedResult(rawResult, options) {
4166
4449
  }
4167
4450
  return rawResult;
4168
4451
  }
4452
+ function isReactElement(value) {
4453
+ return typeof value === "object" && value !== null && "$$typeof" in value;
4454
+ }
4455
+ function isDefinitelyReactComponent(template) {
4456
+ if (typeof template !== "function") return false;
4457
+ const proto = template.prototype;
4458
+ return typeof proto?.render === "function" || template["$$typeof"] !== void 0;
4459
+ }
4169
4460
  function renderToolTemplate(options) {
4170
4461
  const { toolName, input, output, template, resolver, platformType, sizing } = options;
4171
4462
  const uiType = detectUIType(template);
@@ -4176,30 +4467,43 @@ function renderToolTemplate(options) {
4176
4467
  output,
4177
4468
  includeBridge: true,
4178
4469
  resolver,
4179
- sizing
4470
+ sizing,
4471
+ csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
4180
4472
  };
4181
4473
  let html2;
4182
4474
  let hash = "";
4183
4475
  let size = 0;
4184
4476
  if (typeof template === "object" && template !== null && "file" in template) {
4185
- const cspConfig = buildCspConfig(resolver);
4186
4477
  const result = renderComponent(
4187
4478
  { source: template, inlineReact: resourceMode === "inline" },
4188
- { ...shellConfig, csp: cspConfig }
4479
+ { ...shellConfig, csp: buildCspConfig(resolver, options.csp) }
4189
4480
  );
4190
4481
  html2 = result.html;
4191
4482
  hash = result.hash;
4192
4483
  size = result.size;
4193
4484
  } else if (typeof template === "function") {
4194
- if (uiType === "react") {
4485
+ const helpers = createTemplateHelpers();
4486
+ const ctx = { input, output, helpers };
4487
+ let rawResult;
4488
+ let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
4489
+ if (isHtmlBuilder) {
4490
+ try {
4491
+ rawResult = template(ctx);
4492
+ isHtmlBuilder = !isReactElement(rawResult);
4493
+ } catch (error) {
4494
+ if (uiType !== "react") throw error;
4495
+ isHtmlBuilder = false;
4496
+ }
4497
+ }
4498
+ if (!isHtmlBuilder) {
4499
+ (options.logger ?? console).warn(
4500
+ `[FrontMCP] Tool "${toolName}": \`ui.template\` is a React component reference, which cannot be bundled (there is no source file to compile), so the widget would render an empty page. Use \`template: { file: './widget.tsx' }\` instead.`
4501
+ );
4195
4502
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
4196
4503
  html2 = shellResult.html;
4197
4504
  hash = shellResult.hash;
4198
4505
  size = shellResult.size;
4199
4506
  } else {
4200
- const helpers = createTemplateHelpers();
4201
- const ctx = { input, output, helpers };
4202
- const rawResult = template(ctx);
4203
4507
  const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
4204
4508
  if (wrapped) {
4205
4509
  html2 = wrapped;
@@ -4211,7 +4515,7 @@ function renderToolTemplate(options) {
4211
4515
  }
4212
4516
  }
4213
4517
  } else if (typeof template === "string") {
4214
- const shellResult = buildShell(template, shellConfig);
4518
+ const shellResult = buildShell(uiType === "markdown" ? markdownToHtml(template) : template, shellConfig);
4215
4519
  html2 = shellResult.html;
4216
4520
  hash = shellResult.hash;
4217
4521
  size = shellResult.size;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/uipack",
3
- "version": "1.8.5",
3
+ "version": "1.8.7",
4
4
  "description": "FrontMCP UIpack - HTML shell builder, pluggable import resolver, and NPM component loader for MCP UI (React-free core)",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "homepage": "https://docs.agentfront.dev",
@@ -61,6 +61,12 @@
61
61
  "typescript": "^5.9.3"
62
62
  },
63
63
  "peerDependencies": {
64
- "@frontmcp/lazy-zod": "1.8.5"
64
+ "@frontmcp/lazy-zod": "1.8.7",
65
+ "esbuild": ">=0.27.0 <1"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "esbuild": {
69
+ "optional": true
70
+ }
65
71
  }
66
72
  }