@frontmcp/uipack 1.0.0-beta.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -492,22 +492,71 @@ function bundleFileSource(source, filename, resolveDir, componentName) {
492
492
  const esbuild = require("esbuild");
493
493
  const mountCode = `
494
494
  // --- Auto-generated mount ---
495
+ import { createElement as __h } from 'react';
495
496
  import { createRoot } from 'react-dom/client';
496
497
  import { McpBridgeProvider } from '@frontmcp/ui/react';
497
- import React from 'react';
498
- const __root = document.getElementById('root');
498
+ var __root = document.getElementById('root');
499
499
  if (__root) {
500
- createRoot(__root).render(
501
- React.createElement(McpBridgeProvider, null,
502
- React.createElement(${componentName})
503
- )
504
- );
500
+ var __reactRoot = createRoot(__root);
501
+ function __hasData(v) { return v !== undefined; }
502
+ function __render(output) {
503
+ __reactRoot.render(
504
+ __h(McpBridgeProvider, null,
505
+ __h(${componentName}, { output: output !== undefined ? output : null, input: window.__mcpToolInput, loading: !__hasData(output) })
506
+ )
507
+ );
508
+ }
509
+ // Render immediately (component shows loading state until data arrives)
510
+ __render(undefined);
511
+ // 1. Try OpenAI SDK (toolOutput set synchronously or after load)
512
+ if (typeof window !== 'undefined') {
513
+ if (!window.openai) window.openai = {};
514
+ var __cur = window.openai.toolOutput;
515
+ if (__hasData(__cur)) { __render(__cur); }
516
+ Object.defineProperty(window.openai, 'toolOutput', {
517
+ get: function() { return __cur; },
518
+ set: function(v) { __cur = v; __render(v); },
519
+ configurable: true, enumerable: true
520
+ });
521
+ }
522
+ // 2. Try injected data globals
523
+ if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
524
+ // 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
525
+ var __bridge = window.FrontMcpBridge;
526
+ if (__bridge && typeof __bridge.onToolResult === 'function') {
527
+ __bridge.onToolResult(function(data) { __render(data); });
528
+ } else {
529
+ // 4. Fallback: listen for tool:result CustomEvent (standalone / MCP Inspector)
530
+ window.addEventListener('tool:result', function(e) {
531
+ var d = e.detail;
532
+ if (d) __render(d.structuredContent !== undefined ? d.structuredContent : d.content !== undefined ? d.content : d);
533
+ });
534
+ }
505
535
  }`;
506
536
  const loader = {
507
537
  ".tsx": "tsx",
508
538
  ".jsx": "jsx"
509
539
  };
510
540
  const stdinLoader = filename.endsWith(".tsx") ? "tsx" : "jsx";
541
+ const alias = {};
542
+ try {
543
+ const nodePath = require("path");
544
+ const nodeFs = require("fs");
545
+ const candidates = [
546
+ nodePath.join(process.cwd(), "node_modules", "@frontmcp", "ui", "dist", "esm"),
547
+ nodePath.join(resolveDir, "node_modules", "@frontmcp", "ui", "dist", "esm")
548
+ ];
549
+ for (const uiEsmBase of candidates) {
550
+ if (!nodeFs.existsSync(uiEsmBase)) continue;
551
+ const subpaths = ["components", "react", "theme", "bridge", "runtime"];
552
+ for (const sub of subpaths) {
553
+ const mjs = nodePath.join(uiEsmBase, sub, "index.mjs");
554
+ if (nodeFs.existsSync(mjs)) alias[`@frontmcp/ui/${sub}`] = mjs;
555
+ }
556
+ if (Object.keys(alias).length > 0) break;
557
+ }
558
+ } catch {
559
+ }
511
560
  try {
512
561
  const result = esbuild.buildSync({
513
562
  stdin: {
@@ -520,10 +569,9 @@ if (__root) {
520
569
  write: false,
521
570
  format: "esm",
522
571
  target: "es2020",
523
- jsx: "transform",
524
- jsxFactory: "React.createElement",
525
- jsxFragment: "React.Fragment",
526
- external: ["react", "react-dom"],
572
+ jsx: "automatic",
573
+ external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
574
+ alias,
527
575
  define: { "process.env.NODE_ENV": '"production"' },
528
576
  platform: "browser",
529
577
  treeShaking: true,
@@ -747,6 +795,7 @@ function escapeAttribute(str) {
747
795
  function buildDataInjectionScript(options) {
748
796
  const { toolName, input, output, structuredContent } = options;
749
797
  const lines = [
798
+ `window.__mcpAppsEnabled = true;`,
750
799
  `window.__mcpToolName = ${safeJsonForScript(toolName)};`,
751
800
  `window.__mcpToolInput = ${safeJsonForScript(input ?? null)};`,
752
801
  `window.__mcpToolOutput = ${safeJsonForScript(output ?? null)};`,
@@ -811,6 +860,19 @@ function generateBridgeIIFE(options = {}) {
811
860
  parts.push("});");
812
861
  parts.push("");
813
862
  parts.push("window.FrontMcpBridge = bridge;");
863
+ parts.push("function __showLoading() {");
864
+ parts.push(' var root = document.getElementById("root");');
865
+ parts.push(" if (root && !root.hasChildNodes()) {");
866
+ parts.push(
867
+ ` root.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;padding:2rem;color:#6b7280;font-family:system-ui,sans-serif"><div style="text-align:center"><div style="width:24px;height:24px;border:2px solid #e5e7eb;border-top-color:#3b82f6;border-radius:50%;animation:__spin 0.6s linear infinite;margin:0 auto 12px"></div><div style="font-size:0.875rem">Loading widget...</div></div></div><style>@keyframes __spin{to{transform:rotate(360deg)}}</style>';`
868
+ );
869
+ parts.push(" }");
870
+ parts.push("}");
871
+ parts.push('if (document.readyState === "loading") {');
872
+ parts.push(' document.addEventListener("DOMContentLoaded", __showLoading);');
873
+ parts.push("} else {");
874
+ parts.push(" __showLoading();");
875
+ parts.push("}");
814
876
  parts.push("})();");
815
877
  const code = parts.join("\n");
816
878
  if (minify) {
@@ -987,7 +1049,20 @@ var ExtAppsAdapter = {
987
1049
  self.handleMessage(context, event);
988
1050
  });
989
1051
 
990
- return self.performHandshake(context);
1052
+ // Defer handshake until after the document is fully loaded.
1053
+ // During document.write() (used by the sandbox proxy), postMessage
1054
+ // from the inner iframe may not reach the parent. Waiting for DOMContentLoaded
1055
+ // or using setTimeout ensures the iframe is fully attached.
1056
+ return new Promise(function(resolve) {
1057
+ function doHandshake() {
1058
+ self.sendHandshake(context).then(resolve, resolve);
1059
+ }
1060
+ if (document.readyState === 'loading') {
1061
+ document.addEventListener('DOMContentLoaded', doHandshake);
1062
+ } else {
1063
+ setTimeout(doHandshake, 0);
1064
+ }
1065
+ });
991
1066
  },
992
1067
  handleMessage: function(context, event) {
993
1068
  if (!this.isOriginTrusted(event.origin)) return;
@@ -1081,32 +1156,67 @@ var ExtAppsAdapter = {
1081
1156
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
1082
1157
  });
1083
1158
  },
1084
- performHandshake: function(context) {
1159
+ sendHandshake: function(context) {
1160
+ // Send ui/initialize using '*' as target origin since TOFU hasn't
1161
+ // been established yet. The response from the host will establish
1162
+ // TOFU trust via handleMessage \u2192 isOriginTrusted.
1085
1163
  var self = this;
1164
+ var id = ++this.requestId;
1086
1165
  var params = {
1087
1166
  appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
1088
1167
  appCapabilities: { tools: { listChanged: false } },
1089
1168
  protocolVersion: '2024-11-05'
1090
1169
  };
1091
1170
 
1092
- return this.sendRequest('ui/initialize', params).then(function(result) {
1093
- self.hostCapabilities = result.hostCapabilities || {};
1094
- self.capabilities = Object.assign({}, self.capabilities, {
1095
- canCallTools: Boolean(self.hostCapabilities.serverToolProxy),
1096
- canSendMessages: true,
1097
- canOpenLinks: Boolean(self.hostCapabilities.openLink),
1098
- supportsDisplayModes: true
1099
- });
1100
- if (result.hostContext) {
1101
- Object.assign(context.hostContext, result.hostContext);
1102
- }
1171
+ return new Promise(function(resolve, reject) {
1172
+ var timeout = setTimeout(function() {
1173
+ delete self.pendingRequests[id];
1174
+ // Handshake timeout is non-fatal \u2014 notifications may still arrive
1175
+ resolve();
1176
+ }, 10000);
1177
+
1178
+ self.pendingRequests[id] = {
1179
+ resolve: function(result) {
1180
+ self.hostCapabilities = result.hostCapabilities || {};
1181
+ self.capabilities = Object.assign({}, self.capabilities, {
1182
+ canCallTools: Boolean(self.hostCapabilities.serverTools || self.hostCapabilities.serverToolProxy),
1183
+ canSendMessages: true,
1184
+ canOpenLinks: Boolean(self.hostCapabilities.openLinks || self.hostCapabilities.openLink),
1185
+ supportsDisplayModes: true
1186
+ });
1187
+ if (result.hostContext) {
1188
+ Object.assign(context.hostContext, result.hostContext);
1189
+ }
1190
+ // Send ui/notifications/initialized to tell the host the view is ready.
1191
+ // Per MCP Apps spec, the host waits for this before sending tool-result.
1192
+ var targetOrigin = self.trustedOrigin || '*';
1193
+ window.parent.postMessage({
1194
+ jsonrpc: '2.0',
1195
+ method: 'ui/notifications/initialized',
1196
+ params: {}
1197
+ }, targetOrigin);
1198
+ resolve();
1199
+ },
1200
+ reject: function(err) { resolve(); }, // Non-fatal
1201
+ timeout: timeout
1202
+ };
1203
+
1204
+ // Use '*' for the initial handshake \u2014 we don't know the host origin yet.
1205
+ // The sandbox proxy will relay this to the host.
1206
+ window.parent.postMessage({
1207
+ jsonrpc: '2.0', id: id, method: 'ui/initialize', params: params
1208
+ }, '*');
1103
1209
  });
1104
1210
  },
1211
+ performHandshake: function(context) {
1212
+ return this.sendHandshake(context);
1213
+ },
1105
1214
  callTool: function(context, name, args) {
1106
- if (!this.hostCapabilities.serverToolProxy) {
1215
+ if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
1107
1216
  return Promise.reject(new Error('Server tool proxy not supported'));
1108
1217
  }
1109
- return this.sendRequest('ui/callServerTool', { name: name, arguments: args });
1218
+ // Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
1219
+ return this.sendRequest('tools/call', { name: name, arguments: args || {} });
1110
1220
  },
1111
1221
  sendMessage: function(context, content) {
1112
1222
  return this.sendRequest('ui/message', { content: content });
@@ -1 +1 @@
1
- {"version":3,"file":"transpiler.d.ts","sourceRoot":"","sources":["../../src/component/transpiler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAkB9E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,GACpB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAuDlB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUpE"}
1
+ {"version":3,"file":"transpiler.d.ts","sourceRoot":"","sources":["../../src/component/transpiler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAkB9E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,GACpB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CA4GlB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUpE"}
@@ -183,6 +183,7 @@ function safeJsonForScript(value) {
183
183
  function buildDataInjectionScript(options) {
184
184
  const { toolName, input, output, structuredContent } = options;
185
185
  const lines = [
186
+ `window.__mcpAppsEnabled = true;`,
186
187
  `window.__mcpToolName = ${safeJsonForScript(toolName)};`,
187
188
  `window.__mcpToolInput = ${safeJsonForScript(input ?? null)};`,
188
189
  `window.__mcpToolOutput = ${safeJsonForScript(output ?? null)};`,
@@ -273,6 +274,19 @@ function generateBridgeIIFE(options = {}) {
273
274
  parts.push("});");
274
275
  parts.push("");
275
276
  parts.push("window.FrontMcpBridge = bridge;");
277
+ parts.push("function __showLoading() {");
278
+ parts.push(' var root = document.getElementById("root");');
279
+ parts.push(" if (root && !root.hasChildNodes()) {");
280
+ parts.push(
281
+ ` root.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;padding:2rem;color:#6b7280;font-family:system-ui,sans-serif"><div style="text-align:center"><div style="width:24px;height:24px;border:2px solid #e5e7eb;border-top-color:#3b82f6;border-radius:50%;animation:__spin 0.6s linear infinite;margin:0 auto 12px"></div><div style="font-size:0.875rem">Loading widget...</div></div></div><style>@keyframes __spin{to{transform:rotate(360deg)}}</style>';`
282
+ );
283
+ parts.push(" }");
284
+ parts.push("}");
285
+ parts.push('if (document.readyState === "loading") {');
286
+ parts.push(' document.addEventListener("DOMContentLoaded", __showLoading);');
287
+ parts.push("} else {");
288
+ parts.push(" __showLoading();");
289
+ parts.push("}");
276
290
  parts.push("})();");
277
291
  const code = parts.join("\n");
278
292
  if (minify) {
@@ -449,7 +463,20 @@ var ExtAppsAdapter = {
449
463
  self.handleMessage(context, event);
450
464
  });
451
465
 
452
- return self.performHandshake(context);
466
+ // Defer handshake until after the document is fully loaded.
467
+ // During document.write() (used by the sandbox proxy), postMessage
468
+ // from the inner iframe may not reach the parent. Waiting for DOMContentLoaded
469
+ // or using setTimeout ensures the iframe is fully attached.
470
+ return new Promise(function(resolve) {
471
+ function doHandshake() {
472
+ self.sendHandshake(context).then(resolve, resolve);
473
+ }
474
+ if (document.readyState === 'loading') {
475
+ document.addEventListener('DOMContentLoaded', doHandshake);
476
+ } else {
477
+ setTimeout(doHandshake, 0);
478
+ }
479
+ });
453
480
  },
454
481
  handleMessage: function(context, event) {
455
482
  if (!this.isOriginTrusted(event.origin)) return;
@@ -543,32 +570,67 @@ var ExtAppsAdapter = {
543
570
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
544
571
  });
545
572
  },
546
- performHandshake: function(context) {
573
+ sendHandshake: function(context) {
574
+ // Send ui/initialize using '*' as target origin since TOFU hasn't
575
+ // been established yet. The response from the host will establish
576
+ // TOFU trust via handleMessage \u2192 isOriginTrusted.
547
577
  var self = this;
578
+ var id = ++this.requestId;
548
579
  var params = {
549
580
  appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
550
581
  appCapabilities: { tools: { listChanged: false } },
551
582
  protocolVersion: '2024-11-05'
552
583
  };
553
584
 
554
- return this.sendRequest('ui/initialize', params).then(function(result) {
555
- self.hostCapabilities = result.hostCapabilities || {};
556
- self.capabilities = Object.assign({}, self.capabilities, {
557
- canCallTools: Boolean(self.hostCapabilities.serverToolProxy),
558
- canSendMessages: true,
559
- canOpenLinks: Boolean(self.hostCapabilities.openLink),
560
- supportsDisplayModes: true
561
- });
562
- if (result.hostContext) {
563
- Object.assign(context.hostContext, result.hostContext);
564
- }
585
+ return new Promise(function(resolve, reject) {
586
+ var timeout = setTimeout(function() {
587
+ delete self.pendingRequests[id];
588
+ // Handshake timeout is non-fatal \u2014 notifications may still arrive
589
+ resolve();
590
+ }, 10000);
591
+
592
+ self.pendingRequests[id] = {
593
+ resolve: function(result) {
594
+ self.hostCapabilities = result.hostCapabilities || {};
595
+ self.capabilities = Object.assign({}, self.capabilities, {
596
+ canCallTools: Boolean(self.hostCapabilities.serverTools || self.hostCapabilities.serverToolProxy),
597
+ canSendMessages: true,
598
+ canOpenLinks: Boolean(self.hostCapabilities.openLinks || self.hostCapabilities.openLink),
599
+ supportsDisplayModes: true
600
+ });
601
+ if (result.hostContext) {
602
+ Object.assign(context.hostContext, result.hostContext);
603
+ }
604
+ // Send ui/notifications/initialized to tell the host the view is ready.
605
+ // Per MCP Apps spec, the host waits for this before sending tool-result.
606
+ var targetOrigin = self.trustedOrigin || '*';
607
+ window.parent.postMessage({
608
+ jsonrpc: '2.0',
609
+ method: 'ui/notifications/initialized',
610
+ params: {}
611
+ }, targetOrigin);
612
+ resolve();
613
+ },
614
+ reject: function(err) { resolve(); }, // Non-fatal
615
+ timeout: timeout
616
+ };
617
+
618
+ // Use '*' for the initial handshake \u2014 we don't know the host origin yet.
619
+ // The sandbox proxy will relay this to the host.
620
+ window.parent.postMessage({
621
+ jsonrpc: '2.0', id: id, method: 'ui/initialize', params: params
622
+ }, '*');
565
623
  });
566
624
  },
625
+ performHandshake: function(context) {
626
+ return this.sendHandshake(context);
627
+ },
567
628
  callTool: function(context, name, args) {
568
- if (!this.hostCapabilities.serverToolProxy) {
629
+ if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
569
630
  return Promise.reject(new Error('Server tool proxy not supported'));
570
631
  }
571
- return this.sendRequest('ui/callServerTool', { name: name, arguments: args });
632
+ // Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
633
+ return this.sendRequest('tools/call', { name: name, arguments: args || {} });
572
634
  },
573
635
  sendMessage: function(context, content) {
574
636
  return this.sendRequest('ui/message', { content: content });
@@ -1750,22 +1812,71 @@ function bundleFileSource(source, filename, resolveDir, componentName) {
1750
1812
  const esbuild = __require("esbuild");
1751
1813
  const mountCode = `
1752
1814
  // --- Auto-generated mount ---
1815
+ import { createElement as __h } from 'react';
1753
1816
  import { createRoot } from 'react-dom/client';
1754
1817
  import { McpBridgeProvider } from '@frontmcp/ui/react';
1755
- import React from 'react';
1756
- const __root = document.getElementById('root');
1818
+ var __root = document.getElementById('root');
1757
1819
  if (__root) {
1758
- createRoot(__root).render(
1759
- React.createElement(McpBridgeProvider, null,
1760
- React.createElement(${componentName})
1761
- )
1762
- );
1820
+ var __reactRoot = createRoot(__root);
1821
+ function __hasData(v) { return v !== undefined; }
1822
+ function __render(output) {
1823
+ __reactRoot.render(
1824
+ __h(McpBridgeProvider, null,
1825
+ __h(${componentName}, { output: output !== undefined ? output : null, input: window.__mcpToolInput, loading: !__hasData(output) })
1826
+ )
1827
+ );
1828
+ }
1829
+ // Render immediately (component shows loading state until data arrives)
1830
+ __render(undefined);
1831
+ // 1. Try OpenAI SDK (toolOutput set synchronously or after load)
1832
+ if (typeof window !== 'undefined') {
1833
+ if (!window.openai) window.openai = {};
1834
+ var __cur = window.openai.toolOutput;
1835
+ if (__hasData(__cur)) { __render(__cur); }
1836
+ Object.defineProperty(window.openai, 'toolOutput', {
1837
+ get: function() { return __cur; },
1838
+ set: function(v) { __cur = v; __render(v); },
1839
+ configurable: true, enumerable: true
1840
+ });
1841
+ }
1842
+ // 2. Try injected data globals
1843
+ if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
1844
+ // 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
1845
+ var __bridge = window.FrontMcpBridge;
1846
+ if (__bridge && typeof __bridge.onToolResult === 'function') {
1847
+ __bridge.onToolResult(function(data) { __render(data); });
1848
+ } else {
1849
+ // 4. Fallback: listen for tool:result CustomEvent (standalone / MCP Inspector)
1850
+ window.addEventListener('tool:result', function(e) {
1851
+ var d = e.detail;
1852
+ if (d) __render(d.structuredContent !== undefined ? d.structuredContent : d.content !== undefined ? d.content : d);
1853
+ });
1854
+ }
1763
1855
  }`;
1764
1856
  const loader = {
1765
1857
  ".tsx": "tsx",
1766
1858
  ".jsx": "jsx"
1767
1859
  };
1768
1860
  const stdinLoader = filename.endsWith(".tsx") ? "tsx" : "jsx";
1861
+ const alias = {};
1862
+ try {
1863
+ const nodePath = __require("path");
1864
+ const nodeFs = __require("fs");
1865
+ const candidates = [
1866
+ nodePath.join(process.cwd(), "node_modules", "@frontmcp", "ui", "dist", "esm"),
1867
+ nodePath.join(resolveDir, "node_modules", "@frontmcp", "ui", "dist", "esm")
1868
+ ];
1869
+ for (const uiEsmBase of candidates) {
1870
+ if (!nodeFs.existsSync(uiEsmBase)) continue;
1871
+ const subpaths = ["components", "react", "theme", "bridge", "runtime"];
1872
+ for (const sub of subpaths) {
1873
+ const mjs = nodePath.join(uiEsmBase, sub, "index.mjs");
1874
+ if (nodeFs.existsSync(mjs)) alias[`@frontmcp/ui/${sub}`] = mjs;
1875
+ }
1876
+ if (Object.keys(alias).length > 0) break;
1877
+ }
1878
+ } catch {
1879
+ }
1769
1880
  try {
1770
1881
  const result = esbuild.buildSync({
1771
1882
  stdin: {
@@ -1778,10 +1889,9 @@ if (__root) {
1778
1889
  write: false,
1779
1890
  format: "esm",
1780
1891
  target: "es2020",
1781
- jsx: "transform",
1782
- jsxFactory: "React.createElement",
1783
- jsxFragment: "React.Fragment",
1784
- external: ["react", "react-dom"],
1892
+ jsx: "automatic",
1893
+ external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
1894
+ alias,
1785
1895
  define: { "process.env.NODE_ENV": '"production"' },
1786
1896
  platform: "browser",
1787
1897
  treeShaking: true,
@@ -2259,8 +2369,26 @@ function wrapDetectedContent(value) {
2259
2369
  }
2260
2370
 
2261
2371
  // libs/uipack/src/adapters/template-renderer.ts
2372
+ function buildCspConfig(resolver) {
2373
+ const cspResourceDomains = ["https://esm.sh"];
2374
+ const cspConnectDomains = ["https://esm.sh"];
2375
+ if (resolver && "overrides" in resolver) {
2376
+ const overrides = resolver.overrides;
2377
+ if (overrides) {
2378
+ for (const url of Object.values(overrides)) {
2379
+ try {
2380
+ const origin = new URL(url).origin;
2381
+ if (!cspResourceDomains.includes(origin)) cspResourceDomains.push(origin);
2382
+ if (!cspConnectDomains.includes(origin)) cspConnectDomains.push(origin);
2383
+ } catch {
2384
+ }
2385
+ }
2386
+ }
2387
+ }
2388
+ return { resourceDomains: cspResourceDomains, connectDomains: cspConnectDomains };
2389
+ }
2262
2390
  function renderToolTemplate(options) {
2263
- const { toolName, input, output, template, platformType, resolver } = options;
2391
+ const { toolName, input, output, template, resolver } = options;
2264
2392
  const uiType = detectUIType(template);
2265
2393
  const shellConfig = {
2266
2394
  toolName,
@@ -2273,25 +2401,7 @@ function renderToolTemplate(options) {
2273
2401
  let hash = "";
2274
2402
  let size = 0;
2275
2403
  if (typeof template === "object" && template !== null && "file" in template) {
2276
- const cspResourceDomains = ["https://esm.sh"];
2277
- const cspConnectDomains = ["https://esm.sh"];
2278
- if (resolver && "overrides" in resolver) {
2279
- const overrides = resolver.overrides;
2280
- if (overrides) {
2281
- for (const url of Object.values(overrides)) {
2282
- try {
2283
- const origin = new URL(url).origin;
2284
- if (!cspResourceDomains.includes(origin)) cspResourceDomains.push(origin);
2285
- if (!cspConnectDomains.includes(origin)) cspConnectDomains.push(origin);
2286
- } catch {
2287
- }
2288
- }
2289
- }
2290
- }
2291
- const cspConfig = {
2292
- resourceDomains: cspResourceDomains,
2293
- connectDomains: cspConnectDomains
2294
- };
2404
+ const cspConfig = buildCspConfig(resolver);
2295
2405
  const result = renderComponent({ source: template }, { ...shellConfig, csp: cspConfig });
2296
2406
  html = result.html;
2297
2407
  hash = result.hash;
@@ -52,6 +52,19 @@ function generateBridgeIIFE(options = {}) {
52
52
  parts.push("});");
53
53
  parts.push("");
54
54
  parts.push("window.FrontMcpBridge = bridge;");
55
+ parts.push("function __showLoading() {");
56
+ parts.push(' var root = document.getElementById("root");');
57
+ parts.push(" if (root && !root.hasChildNodes()) {");
58
+ parts.push(
59
+ ` root.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;padding:2rem;color:#6b7280;font-family:system-ui,sans-serif"><div style="text-align:center"><div style="width:24px;height:24px;border:2px solid #e5e7eb;border-top-color:#3b82f6;border-radius:50%;animation:__spin 0.6s linear infinite;margin:0 auto 12px"></div><div style="font-size:0.875rem">Loading widget...</div></div></div><style>@keyframes __spin{to{transform:rotate(360deg)}}</style>';`
60
+ );
61
+ parts.push(" }");
62
+ parts.push("}");
63
+ parts.push('if (document.readyState === "loading") {');
64
+ parts.push(' document.addEventListener("DOMContentLoaded", __showLoading);');
65
+ parts.push("} else {");
66
+ parts.push(" __showLoading();");
67
+ parts.push("}");
55
68
  parts.push("})();");
56
69
  const code = parts.join("\n");
57
70
  if (minify) {
@@ -228,7 +241,20 @@ var ExtAppsAdapter = {
228
241
  self.handleMessage(context, event);
229
242
  });
230
243
 
231
- return self.performHandshake(context);
244
+ // Defer handshake until after the document is fully loaded.
245
+ // During document.write() (used by the sandbox proxy), postMessage
246
+ // from the inner iframe may not reach the parent. Waiting for DOMContentLoaded
247
+ // or using setTimeout ensures the iframe is fully attached.
248
+ return new Promise(function(resolve) {
249
+ function doHandshake() {
250
+ self.sendHandshake(context).then(resolve, resolve);
251
+ }
252
+ if (document.readyState === 'loading') {
253
+ document.addEventListener('DOMContentLoaded', doHandshake);
254
+ } else {
255
+ setTimeout(doHandshake, 0);
256
+ }
257
+ });
232
258
  },
233
259
  handleMessage: function(context, event) {
234
260
  if (!this.isOriginTrusted(event.origin)) return;
@@ -322,32 +348,67 @@ var ExtAppsAdapter = {
322
348
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
323
349
  });
324
350
  },
325
- performHandshake: function(context) {
351
+ sendHandshake: function(context) {
352
+ // Send ui/initialize using '*' as target origin since TOFU hasn't
353
+ // been established yet. The response from the host will establish
354
+ // TOFU trust via handleMessage \u2192 isOriginTrusted.
326
355
  var self = this;
356
+ var id = ++this.requestId;
327
357
  var params = {
328
358
  appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
329
359
  appCapabilities: { tools: { listChanged: false } },
330
360
  protocolVersion: '2024-11-05'
331
361
  };
332
362
 
333
- return this.sendRequest('ui/initialize', params).then(function(result) {
334
- self.hostCapabilities = result.hostCapabilities || {};
335
- self.capabilities = Object.assign({}, self.capabilities, {
336
- canCallTools: Boolean(self.hostCapabilities.serverToolProxy),
337
- canSendMessages: true,
338
- canOpenLinks: Boolean(self.hostCapabilities.openLink),
339
- supportsDisplayModes: true
340
- });
341
- if (result.hostContext) {
342
- Object.assign(context.hostContext, result.hostContext);
343
- }
363
+ return new Promise(function(resolve, reject) {
364
+ var timeout = setTimeout(function() {
365
+ delete self.pendingRequests[id];
366
+ // Handshake timeout is non-fatal \u2014 notifications may still arrive
367
+ resolve();
368
+ }, 10000);
369
+
370
+ self.pendingRequests[id] = {
371
+ resolve: function(result) {
372
+ self.hostCapabilities = result.hostCapabilities || {};
373
+ self.capabilities = Object.assign({}, self.capabilities, {
374
+ canCallTools: Boolean(self.hostCapabilities.serverTools || self.hostCapabilities.serverToolProxy),
375
+ canSendMessages: true,
376
+ canOpenLinks: Boolean(self.hostCapabilities.openLinks || self.hostCapabilities.openLink),
377
+ supportsDisplayModes: true
378
+ });
379
+ if (result.hostContext) {
380
+ Object.assign(context.hostContext, result.hostContext);
381
+ }
382
+ // Send ui/notifications/initialized to tell the host the view is ready.
383
+ // Per MCP Apps spec, the host waits for this before sending tool-result.
384
+ var targetOrigin = self.trustedOrigin || '*';
385
+ window.parent.postMessage({
386
+ jsonrpc: '2.0',
387
+ method: 'ui/notifications/initialized',
388
+ params: {}
389
+ }, targetOrigin);
390
+ resolve();
391
+ },
392
+ reject: function(err) { resolve(); }, // Non-fatal
393
+ timeout: timeout
394
+ };
395
+
396
+ // Use '*' for the initial handshake \u2014 we don't know the host origin yet.
397
+ // The sandbox proxy will relay this to the host.
398
+ window.parent.postMessage({
399
+ jsonrpc: '2.0', id: id, method: 'ui/initialize', params: params
400
+ }, '*');
344
401
  });
345
402
  },
403
+ performHandshake: function(context) {
404
+ return this.sendHandshake(context);
405
+ },
346
406
  callTool: function(context, name, args) {
347
- if (!this.hostCapabilities.serverToolProxy) {
407
+ if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
348
408
  return Promise.reject(new Error('Server tool proxy not supported'));
349
409
  }
350
- return this.sendRequest('ui/callServerTool', { name: name, arguments: args });
410
+ // Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
411
+ return this.sendRequest('tools/call', { name: name, arguments: args || {} });
351
412
  },
352
413
  sendMessage: function(context, content) {
353
414
  return this.sendRequest('ui/message', { content: content });