@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/esm/index.mjs CHANGED
@@ -238,6 +238,23 @@ function __applySizingCss(sizing) {
238
238
  }
239
239
  }
240
240
 
241
+ function __measureHeight() {
242
+ // <html> at fit-content includes body margins and collapsed child margins;
243
+ // content overflowing a fixed-height body is added; a px max-height clamps.
244
+ var de = document.documentElement;
245
+ var body = document.body;
246
+ if (!de || !body) return 0;
247
+ var prev = de.style.height;
248
+ de.style.height = 'fit-content';
249
+ var dr = de.getBoundingClientRect();
250
+ var br = body.getBoundingClientRect();
251
+ var h = Math.max(dr.height, br.top + body.scrollHeight + Math.max(0, dr.bottom - br.bottom) - dr.top);
252
+ de.style.height = prev;
253
+ var maxHeight = getComputedStyle(de).maxHeight;
254
+ var mx = maxHeight && maxHeight.slice(-2) === 'px' ? parseFloat(maxHeight) : NaN;
255
+ return Math.ceil(isNaN(mx) ? h : Math.min(h, mx));
256
+ }
257
+
241
258
  function __initAutoResize() {
242
259
  if (typeof window === 'undefined') return;
243
260
  // Idempotent: a re-injected IIFE must not stack observers.
@@ -260,23 +277,30 @@ function __initAutoResize() {
260
277
  if (typeof ResizeObserver === 'undefined') return;
261
278
 
262
279
  function startObserving() {
263
- var target = document.getElementById('root') || document.body;
264
- if (!target) return;
280
+ if (!document.body) return;
281
+ var targets = [document.documentElement, document.body];
282
+ var root = document.getElementById('root');
283
+ if (root) targets.push(root);
265
284
 
266
285
  var rafId = null;
267
286
  var lastReported = -1;
268
287
  function report() {
269
288
  rafId = null;
270
289
  try {
271
- var rect = target.getBoundingClientRect();
272
- var height = Math.ceil(rect.height);
273
- var width = Math.ceil(rect.width);
290
+ // Before the handshake an ext-apps host can't receive the report; the
291
+ // bridge:ready listener below sends it once the bridge is up.
292
+ if (window.FrontMcpBridge && window.FrontMcpBridge.initialized === false) return;
293
+ var height = __measureHeight();
294
+ var width = Math.ceil(document.documentElement.getBoundingClientRect().width);
274
295
  if (height === lastReported || height <= 0) return;
275
296
  lastReported = height;
276
297
  var payload = { height: height, width: width };
277
298
  if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
278
299
  if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
279
- window.FrontMcpBridge.setSize(payload).catch(function() {});
300
+ window.FrontMcpBridge.setSize(payload).catch(function() {
301
+ // Forget a rejected report so the same height is sent again.
302
+ if (lastReported === height) lastReported = -1;
303
+ });
280
304
  }
281
305
  window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
282
306
  } catch (e) {}
@@ -297,9 +321,17 @@ function __initAutoResize() {
297
321
  window.__mcpResizeObserver.disconnect();
298
322
  }
299
323
  var ro = new ResizeObserver(function() { schedule(); });
300
- ro.observe(target);
324
+ for (var i = 0; i < targets.length; i++) ro.observe(targets[i]);
301
325
  window.__mcpResizeObserver = ro;
302
326
  } catch (e) {}
327
+ // Report once the bridge is ready (the report scheduled below is skipped
328
+ // while the handshake is still pending).
329
+ var readySent = false;
330
+ window.addEventListener('bridge:ready', function() {
331
+ if (readySent) return;
332
+ readySent = true;
333
+ schedule();
334
+ });
303
335
  // Report once on init so the host gets an initial measurement.
304
336
  schedule();
305
337
  }
@@ -347,12 +379,57 @@ function detectViewport() {
347
379
  return undefined;
348
380
  }
349
381
 
382
+ function __applyHostTheme(theme) {
383
+ // Follow a host-supplied theme: <meta name="color-scheme"> sets the frame's
384
+ // canvas and form controls (a widget's own :root color-scheme still wins),
385
+ // <html data-theme> is a styling hook. The OS fallback is never written.
386
+ if (theme !== 'light' && theme !== 'dark') return;
387
+ if (typeof document === 'undefined' || !document.documentElement) return;
388
+ try {
389
+ var meta = document.querySelector('meta[name="color-scheme"]');
390
+ var head = document.head || document.getElementsByTagName('head')[0];
391
+ if (!meta && head) {
392
+ meta = document.createElement('meta');
393
+ meta.setAttribute('name', 'color-scheme');
394
+ head.appendChild(meta);
395
+ }
396
+ if (meta) meta.setAttribute('content', theme);
397
+ document.documentElement.setAttribute('data-theme', theme);
398
+ } catch (e) {}
399
+ }
400
+
401
+ function extractToolOutput(params) {
402
+ // A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
403
+ if (!params) return undefined;
404
+ if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
405
+ var content = params.content;
406
+ if (Array.isArray(content)) {
407
+ for (var i = 0; i < content.length; i++) {
408
+ var item = content[i];
409
+ if (item && item.type === 'text' && typeof item.text === 'string') {
410
+ try { return JSON.parse(item.text); } catch (e) { return item.text; }
411
+ }
412
+ }
413
+ }
414
+ return content;
415
+ }
416
+
350
417
  function readInjectedData() {
351
418
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
352
419
  if (typeof window !== 'undefined') {
353
420
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
354
421
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
355
422
  if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
423
+ // The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
424
+ // (toolOutput is the tool's structuredContent).
425
+ var oa = window.openai;
426
+ if (oa && typeof oa === 'object') {
427
+ if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
428
+ if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
429
+ if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
430
+ if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
431
+ }
432
+ }
356
433
  }
357
434
  return data;
358
435
  }
@@ -401,7 +478,24 @@ var OpenAIAdapter = {
401
478
  if (sdk.displayMode) {
402
479
  context.hostContext.displayMode = sdk.displayMode;
403
480
  }
404
- // Note: OpenAI SDK does not have an onContextChange equivalent
481
+ // The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
482
+ window.addEventListener('openai:set_globals', function(event) {
483
+ var globals = (event && event.detail && event.detail.globals) || window.openai || {};
484
+ if (globals.toolInput !== undefined && globals.toolInput !== null) {
485
+ context.toolInput = globals.toolInput;
486
+ window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
487
+ }
488
+ if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
489
+ context.structuredContent = globals.toolOutput;
490
+ context.notifyToolResult(globals.toolOutput);
491
+ }
492
+ if (globals.theme && globals.theme !== context.hostContext.theme) {
493
+ context.notifyContextChange({ theme: globals.theme });
494
+ }
495
+ if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
496
+ context.notifyContextChange({ displayMode: globals.displayMode });
497
+ }
498
+ });
405
499
  return Promise.resolve();
406
500
  },
407
501
  callTool: function(context, name, args) {
@@ -453,6 +547,8 @@ var ExtAppsAdapter = {
453
547
  pendingRequests: {},
454
548
  requestId: 0,
455
549
  hostCapabilities: {},
550
+ handshakeSettled: false,
551
+ pendingSize: null,
456
552
  canHandle: function() {
457
553
  if (typeof window === 'undefined') return false;
458
554
  if (window.parent === window) return false;
@@ -546,9 +642,10 @@ var ExtAppsAdapter = {
546
642
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
547
643
  break;
548
644
  case 'ui/notifications/tool-result':
549
- context.toolOutput = params.content;
645
+ var resultOutput = extractToolOutput(params);
646
+ context.toolOutput = resultOutput;
550
647
  context.structuredContent = params.structuredContent;
551
- context.notifyToolResult(params.content);
648
+ context.notifyToolResult(resultOutput);
552
649
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
553
650
  break;
554
651
  case 'ui/notifications/host-context-changed':
@@ -602,6 +699,15 @@ var ExtAppsAdapter = {
602
699
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
603
700
  });
604
701
  },
702
+ sendNotification: function(method, params) {
703
+ // JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
704
+ if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
705
+ return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
706
+ }
707
+ var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
708
+ window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
709
+ return Promise.resolve();
710
+ },
605
711
  sendHandshake: function(context) {
606
712
  // Send ui/initialize using '*' as target origin since TOFU hasn't
607
713
  // been established yet. The response from the host will establish
@@ -618,6 +724,7 @@ var ExtAppsAdapter = {
618
724
  var timeout = setTimeout(function() {
619
725
  delete self.pendingRequests[id];
620
726
  // Handshake timeout is non-fatal \u2014 notifications may still arrive
727
+ self.settleHandshake();
621
728
  resolve();
622
729
  }, 10000);
623
730
 
@@ -631,7 +738,9 @@ var ExtAppsAdapter = {
631
738
  supportsDisplayModes: true
632
739
  });
633
740
  if (result.hostContext) {
634
- Object.assign(context.hostContext, result.hostContext);
741
+ // Notify (not a silent merge) so onContextChange listeners and the
742
+ // host theme see the handshake's context.
743
+ context.notifyContextChange(result.hostContext);
635
744
  }
636
745
  // Send ui/notifications/initialized to tell the host the view is ready.
637
746
  // Per MCP Apps spec, the host waits for this before sending tool-result.
@@ -641,9 +750,10 @@ var ExtAppsAdapter = {
641
750
  method: 'ui/notifications/initialized',
642
751
  params: {}
643
752
  }, targetOrigin);
753
+ self.settleHandshake();
644
754
  resolve();
645
755
  },
646
- reject: function(err) { resolve(); }, // Non-fatal
756
+ reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
647
757
  timeout: timeout
648
758
  };
649
759
 
@@ -657,6 +767,18 @@ var ExtAppsAdapter = {
657
767
  performHandshake: function(context) {
658
768
  return this.sendHandshake(context);
659
769
  },
770
+ settleHandshake: function() {
771
+ // Deliver the latest size requested while no host origin was trusted.
772
+ this.handshakeSettled = true;
773
+ var pending = this.pendingSize;
774
+ if (!pending) return;
775
+ this.pendingSize = null;
776
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
777
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
778
+ }, function(err) {
779
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
780
+ });
781
+ },
660
782
  callTool: function(context, name, args) {
661
783
  if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
662
784
  return Promise.reject(new Error('Server tool proxy not supported'));
@@ -678,13 +800,22 @@ var ExtAppsAdapter = {
678
800
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
679
801
  },
680
802
  setSize: function(context, size) {
681
- // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
682
- // measured/desired widget dimensions to the host.
683
- return this.sendRequest('ui/setSize', {
684
- height: size && size.height,
685
- width: size && size.width,
686
- aspectRatio: size && size.aspectRatio
687
- });
803
+ // Standard ext-apps sizing: the view tells the host its dimensions with the
804
+ // ui/notifications/size-changed notification ({ width?, height? } in px).
805
+ var params = {};
806
+ if (size && typeof size.width === 'number') params.width = size.width;
807
+ if (size && typeof size.height === 'number') params.height = size.height;
808
+ if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
809
+ // No host origin yet: keep only the latest size and send it when the
810
+ // handshake settles; every caller's promise follows that one request.
811
+ var self = this;
812
+ if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
813
+ this.pendingSize.params = params;
814
+ return new Promise(function(resolve, reject) {
815
+ self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
816
+ });
817
+ }
818
+ return this.sendNotification('ui/notifications/size-changed', params);
688
819
  },
689
820
  requestClose: function(context) {
690
821
  return this.sendRequest('ui/close', {});
@@ -930,6 +1061,7 @@ function FrontMcpBridge() {
930
1061
  toolResultListeners: [],
931
1062
  notifyContextChange: function(changes) {
932
1063
  Object.assign(this.hostContext, changes);
1064
+ if (changes && changes.theme) __applyHostTheme(changes.theme);
933
1065
  for (var i = 0; i < this.contextListeners.length; i++) {
934
1066
  try { this.contextListeners[i](changes); } catch(e) {}
935
1067
  }
@@ -1106,7 +1238,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1106
1238
 
1107
1239
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1108
1240
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1109
- // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1241
+ // ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
1110
1242
  FrontMcpBridge.prototype.setSize = function(size) {
1111
1243
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1112
1244
  if (!this._adapter.setSize) return Promise.resolve();
@@ -1208,7 +1340,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
1208
1340
  target.classList.add('opacity-50', 'cursor-not-allowed');
1209
1341
 
1210
1342
  // Add spinner for buttons
1211
- 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>';
1343
+ 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>';
1212
1344
  if (target.tagName === 'BUTTON') {
1213
1345
  target.innerHTML = spinner + 'Loading...';
1214
1346
  }
@@ -1289,7 +1421,22 @@ function minifyJS(code) {
1289
1421
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
1290
1422
  return code;
1291
1423
  }
1292
- return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\s+/g, " ").replace(/\s*([{};,:()[\]])\s*/g, "$1").replace(/;\}/g, "}").trim();
1424
+ const out = [];
1425
+ let inBlockComment = false;
1426
+ for (const raw of code.split("\n")) {
1427
+ const line = raw.trim();
1428
+ if (inBlockComment) {
1429
+ if (line.includes("*/")) inBlockComment = false;
1430
+ continue;
1431
+ }
1432
+ if (line === "" || line.startsWith("//")) continue;
1433
+ if (line.startsWith("/*")) {
1434
+ if (!line.includes("*/")) inBlockComment = true;
1435
+ continue;
1436
+ }
1437
+ out.push(line);
1438
+ }
1439
+ return out.join("\n");
1293
1440
  }
1294
1441
  function generatePlatformBundle(platform, options = {}) {
1295
1442
  const platformAdapters = {
@@ -3329,8 +3476,24 @@ function isFrontmcpUiResolvable(...candidatePaths) {
3329
3476
  }
3330
3477
 
3331
3478
  // libs/uipack/src/component/transpiler.ts
3479
+ function loadEsbuild(filename) {
3480
+ try {
3481
+ return __require("esbuild");
3482
+ } catch (err) {
3483
+ if (!isMissingEsbuild(err)) throw err;
3484
+ const subject = filename ? `FileSource widget "${filename}"` : "A FileSource widget";
3485
+ throw new Error(
3486
+ `${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.`,
3487
+ { cause: err }
3488
+ );
3489
+ }
3490
+ }
3491
+ function isMissingEsbuild(err) {
3492
+ if (!(err instanceof Error)) return false;
3493
+ return err.code === "MODULE_NOT_FOUND" && /Cannot find module ['"]esbuild['"]/.test(err.message);
3494
+ }
3332
3495
  function transpileReactSource(source, filename) {
3333
- const esbuild = __require("esbuild");
3496
+ const esbuild = loadEsbuild(filename);
3334
3497
  const loader = filename?.endsWith(".tsx") ? "tsx" : "jsx";
3335
3498
  const result = esbuild.transformSync(source, {
3336
3499
  loader,
@@ -3349,7 +3512,7 @@ function bundleFileSource(source, filename, resolveDir, componentName, options =
3349
3512
  `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.`
3350
3513
  );
3351
3514
  }
3352
- const esbuild = __require("esbuild");
3515
+ const esbuild = loadEsbuild(filename);
3353
3516
  const mountCode = `
3354
3517
  // --- Auto-generated mount ---
3355
3518
  import { createElement as __h } from 'react';
@@ -3446,7 +3609,8 @@ if (__root) {
3446
3609
  } catch (err) {
3447
3610
  const message = err instanceof Error ? err.message : String(err);
3448
3611
  throw new Error(
3449
- `Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`
3612
+ `Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`,
3613
+ { cause: err }
3450
3614
  );
3451
3615
  }
3452
3616
  }
@@ -3508,6 +3672,37 @@ function resolveImportSource(source) {
3508
3672
  peerDependencies: []
3509
3673
  };
3510
3674
  }
3675
+ var BUILD_DIR_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "out"]);
3676
+ function missingWidgetError(filePath, cause) {
3677
+ const sourceWidget = findSourceWidget(filePath);
3678
+ const error = new Error(
3679
+ `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\`.` : ""),
3680
+ { cause }
3681
+ );
3682
+ error.code = "ENOENT";
3683
+ error.path = filePath;
3684
+ return error;
3685
+ }
3686
+ function findSourceWidget(filePath) {
3687
+ const path = __require("path");
3688
+ const fs = __require("fs");
3689
+ const parts = filePath.split(path.sep);
3690
+ for (let i = parts.length - 2; i >= 0; i--) {
3691
+ if (!BUILD_DIR_SEGMENTS.has(parts[i])) continue;
3692
+ const candidates = [[...parts.slice(0, i), "src", ...parts.slice(i + 1)]];
3693
+ if (parts.length - i > 2) candidates.push([...parts.slice(0, i), "src", ...parts.slice(i + 2)]);
3694
+ candidates.push([...parts.slice(0, i), ...parts.slice(i + 1)]);
3695
+ for (const candidate of candidates) {
3696
+ const candidatePath = candidate.join(path.sep);
3697
+ try {
3698
+ if (fs.existsSync(candidatePath)) return candidatePath;
3699
+ } catch {
3700
+ }
3701
+ }
3702
+ return void 0;
3703
+ }
3704
+ return void 0;
3705
+ }
3511
3706
  function resolveFileSource(source, options = {}) {
3512
3707
  const path = __require("path");
3513
3708
  const ext = path.extname(source.file).toLowerCase();
@@ -3522,9 +3717,11 @@ function resolveFileSource(source, options = {}) {
3522
3717
  const isNotFound = err?.code === "ENOENT";
3523
3718
  if (isNotFound && wasRelative) {
3524
3719
  throw new Error(
3525
- `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).`
3720
+ `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).`,
3721
+ { cause: err }
3526
3722
  );
3527
3723
  }
3724
+ if (isNotFound) throw missingWidgetError(filePath, err);
3528
3725
  throw err;
3529
3726
  }
3530
3727
  const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
@@ -3971,6 +4168,86 @@ function wrapDetectedContent(value) {
3971
4168
  }
3972
4169
  }
3973
4170
 
4171
+ // libs/uipack/src/adapters/markdown.ts
4172
+ var SAFE_HREF = /^(https?:|mailto:|\/|#)/i;
4173
+ function emphasis(text) {
4174
+ return text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
4175
+ }
4176
+ function inline(text) {
4177
+ const stash = [];
4178
+ const hold = (html2) => `\uE000${stash.push(html2) - 1}\uE000`;
4179
+ let out = escapeHtml(text.replace(/\uE000/g, "")).replace(
4180
+ /`([^`]+)`/g,
4181
+ (_m, code) => hold(`<code>${code}</code>`)
4182
+ );
4183
+ out = out.replace(
4184
+ /\[([^\]]+)\]\(([^)\s]+)\)/g,
4185
+ (_m, label, href) => SAFE_HREF.test(href) ? hold(`<a href="${href}" rel="noopener noreferrer">${emphasis(label)}</a>`) : label
4186
+ );
4187
+ out = emphasis(out);
4188
+ for (let i = 0; i < 2; i++) out = out.replace(/\uE000(\d+)\uE000/g, (_m, n) => stash[Number(n)]);
4189
+ return out;
4190
+ }
4191
+ function markdownToHtml(source) {
4192
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
4193
+ const blocks = [];
4194
+ let paragraph = [];
4195
+ let list;
4196
+ const flushParagraph = () => {
4197
+ if (paragraph.length > 0) blocks.push(`<p>${inline(paragraph.join(" "))}</p>`);
4198
+ paragraph = [];
4199
+ };
4200
+ const flushList = () => {
4201
+ if (list)
4202
+ blocks.push(`<${list.tag}>${list.items.map((item) => `<li>${inline(item)}</li>`).join("")}</${list.tag}>`);
4203
+ list = void 0;
4204
+ };
4205
+ for (let i = 0; i < lines.length; i++) {
4206
+ const line = lines[i];
4207
+ const fence = /^\s*(`{3,})/.exec(line);
4208
+ if (fence) {
4209
+ flushParagraph();
4210
+ flushList();
4211
+ const fenceLength = fence[1].length;
4212
+ const isClosing = (l) => {
4213
+ const m = /^\s*(`{3,})\s*$/.exec(l);
4214
+ return m !== null && m[1].length >= fenceLength;
4215
+ };
4216
+ const code = [];
4217
+ for (i++; i < lines.length && !isClosing(lines[i]); i++) code.push(lines[i]);
4218
+ blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}
4219
+ </code></pre>`);
4220
+ continue;
4221
+ }
4222
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
4223
+ if (heading) {
4224
+ flushParagraph();
4225
+ flushList();
4226
+ blocks.push(`<h${heading[1].length}>${inline(heading[2].trim())}</h${heading[1].length}>`);
4227
+ continue;
4228
+ }
4229
+ const item = /^\s*([-*+]|\d+\.)\s+(.*)$/.exec(line);
4230
+ if (item) {
4231
+ flushParagraph();
4232
+ const tag = /\d/.test(item[1]) ? "ol" : "ul";
4233
+ if (list && list.tag !== tag) flushList();
4234
+ list ??= { tag, items: [] };
4235
+ list.items.push(item[2]);
4236
+ continue;
4237
+ }
4238
+ if (line.trim() === "") {
4239
+ flushParagraph();
4240
+ flushList();
4241
+ continue;
4242
+ }
4243
+ flushList();
4244
+ paragraph.push(line.trim());
4245
+ }
4246
+ flushParagraph();
4247
+ flushList();
4248
+ return blocks.join("\n");
4249
+ }
4250
+
3974
4251
  // libs/uipack/src/adapters/type-detector.ts
3975
4252
  function detectUIType(template) {
3976
4253
  if (template === null || template === void 0) {
@@ -4004,9 +4281,15 @@ function detectUIType(template) {
4004
4281
  }
4005
4282
 
4006
4283
  // libs/uipack/src/adapters/template-renderer.ts
4007
- function buildCspConfig(resolver) {
4284
+ function buildCspConfig(resolver, extra) {
4008
4285
  const cspResourceDomains = ["https://esm.sh"];
4009
4286
  const cspConnectDomains = ["https://esm.sh"];
4287
+ for (const domain of extra?.resourceDomains ?? []) {
4288
+ if (!cspResourceDomains.includes(domain)) cspResourceDomains.push(domain);
4289
+ }
4290
+ for (const domain of extra?.connectDomains ?? []) {
4291
+ if (!cspConnectDomains.includes(domain)) cspConnectDomains.push(domain);
4292
+ }
4010
4293
  if (resolver && "overrides" in resolver) {
4011
4294
  const overrides = resolver.overrides;
4012
4295
  if (overrides) {
@@ -4046,6 +4329,14 @@ function renderUnwrappedResult(rawResult, options) {
4046
4329
  }
4047
4330
  return rawResult;
4048
4331
  }
4332
+ function isReactElement(value) {
4333
+ return typeof value === "object" && value !== null && "$$typeof" in value;
4334
+ }
4335
+ function isDefinitelyReactComponent(template) {
4336
+ if (typeof template !== "function") return false;
4337
+ const proto = template.prototype;
4338
+ return typeof proto?.render === "function" || template["$$typeof"] !== void 0;
4339
+ }
4049
4340
  function renderToolTemplate(options) {
4050
4341
  const { toolName, input, output, template, resolver, platformType, sizing } = options;
4051
4342
  const uiType = detectUIType(template);
@@ -4056,30 +4347,43 @@ function renderToolTemplate(options) {
4056
4347
  output,
4057
4348
  includeBridge: true,
4058
4349
  resolver,
4059
- sizing
4350
+ sizing,
4351
+ csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
4060
4352
  };
4061
4353
  let html2;
4062
4354
  let hash = "";
4063
4355
  let size = 0;
4064
4356
  if (typeof template === "object" && template !== null && "file" in template) {
4065
- const cspConfig = buildCspConfig(resolver);
4066
4357
  const result = renderComponent(
4067
4358
  { source: template, inlineReact: resourceMode === "inline" },
4068
- { ...shellConfig, csp: cspConfig }
4359
+ { ...shellConfig, csp: buildCspConfig(resolver, options.csp) }
4069
4360
  );
4070
4361
  html2 = result.html;
4071
4362
  hash = result.hash;
4072
4363
  size = result.size;
4073
4364
  } else if (typeof template === "function") {
4074
- if (uiType === "react") {
4365
+ const helpers = createTemplateHelpers();
4366
+ const ctx = { input, output, helpers };
4367
+ let rawResult;
4368
+ let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
4369
+ if (isHtmlBuilder) {
4370
+ try {
4371
+ rawResult = template(ctx);
4372
+ isHtmlBuilder = !isReactElement(rawResult);
4373
+ } catch (error) {
4374
+ if (uiType !== "react") throw error;
4375
+ isHtmlBuilder = false;
4376
+ }
4377
+ }
4378
+ if (!isHtmlBuilder) {
4379
+ (options.logger ?? console).warn(
4380
+ `[FrontMCP] Tool "${toolName}": \`ui.template\` is a React component reference, which cannot be bundled (there is no source file to compile), so the widget would render an empty page. Use \`template: { file: './widget.tsx' }\` instead.`
4381
+ );
4075
4382
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
4076
4383
  html2 = shellResult.html;
4077
4384
  hash = shellResult.hash;
4078
4385
  size = shellResult.size;
4079
4386
  } else {
4080
- const helpers = createTemplateHelpers();
4081
- const ctx = { input, output, helpers };
4082
- const rawResult = template(ctx);
4083
4387
  const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
4084
4388
  if (wrapped) {
4085
4389
  html2 = wrapped;
@@ -4091,7 +4395,7 @@ function renderToolTemplate(options) {
4091
4395
  }
4092
4396
  }
4093
4397
  } else if (typeof template === "string") {
4094
- const shellResult = buildShell(template, shellConfig);
4398
+ const shellResult = buildShell(uiType === "markdown" ? markdownToHtml(template) : template, shellConfig);
4095
4399
  html2 = shellResult.html;
4096
4400
  hash = shellResult.hash;
4097
4401
  size = shellResult.size;
package/esm/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
  }