@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.
@@ -463,22 +463,71 @@ function bundleFileSource(source, filename, resolveDir, componentName) {
463
463
  const esbuild = __require("esbuild");
464
464
  const mountCode = `
465
465
  // --- Auto-generated mount ---
466
+ import { createElement as __h } from 'react';
466
467
  import { createRoot } from 'react-dom/client';
467
468
  import { McpBridgeProvider } from '@frontmcp/ui/react';
468
- import React from 'react';
469
- const __root = document.getElementById('root');
469
+ var __root = document.getElementById('root');
470
470
  if (__root) {
471
- createRoot(__root).render(
472
- React.createElement(McpBridgeProvider, null,
473
- React.createElement(${componentName})
474
- )
475
- );
471
+ var __reactRoot = createRoot(__root);
472
+ function __hasData(v) { return v !== undefined; }
473
+ function __render(output) {
474
+ __reactRoot.render(
475
+ __h(McpBridgeProvider, null,
476
+ __h(${componentName}, { output: output !== undefined ? output : null, input: window.__mcpToolInput, loading: !__hasData(output) })
477
+ )
478
+ );
479
+ }
480
+ // Render immediately (component shows loading state until data arrives)
481
+ __render(undefined);
482
+ // 1. Try OpenAI SDK (toolOutput set synchronously or after load)
483
+ if (typeof window !== 'undefined') {
484
+ if (!window.openai) window.openai = {};
485
+ var __cur = window.openai.toolOutput;
486
+ if (__hasData(__cur)) { __render(__cur); }
487
+ Object.defineProperty(window.openai, 'toolOutput', {
488
+ get: function() { return __cur; },
489
+ set: function(v) { __cur = v; __render(v); },
490
+ configurable: true, enumerable: true
491
+ });
492
+ }
493
+ // 2. Try injected data globals
494
+ if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
495
+ // 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
496
+ var __bridge = window.FrontMcpBridge;
497
+ if (__bridge && typeof __bridge.onToolResult === 'function') {
498
+ __bridge.onToolResult(function(data) { __render(data); });
499
+ } else {
500
+ // 4. Fallback: listen for tool:result CustomEvent (standalone / MCP Inspector)
501
+ window.addEventListener('tool:result', function(e) {
502
+ var d = e.detail;
503
+ if (d) __render(d.structuredContent !== undefined ? d.structuredContent : d.content !== undefined ? d.content : d);
504
+ });
505
+ }
476
506
  }`;
477
507
  const loader = {
478
508
  ".tsx": "tsx",
479
509
  ".jsx": "jsx"
480
510
  };
481
511
  const stdinLoader = filename.endsWith(".tsx") ? "tsx" : "jsx";
512
+ const alias = {};
513
+ try {
514
+ const nodePath = __require("path");
515
+ const nodeFs = __require("fs");
516
+ const candidates = [
517
+ nodePath.join(process.cwd(), "node_modules", "@frontmcp", "ui", "dist", "esm"),
518
+ nodePath.join(resolveDir, "node_modules", "@frontmcp", "ui", "dist", "esm")
519
+ ];
520
+ for (const uiEsmBase of candidates) {
521
+ if (!nodeFs.existsSync(uiEsmBase)) continue;
522
+ const subpaths = ["components", "react", "theme", "bridge", "runtime"];
523
+ for (const sub of subpaths) {
524
+ const mjs = nodePath.join(uiEsmBase, sub, "index.mjs");
525
+ if (nodeFs.existsSync(mjs)) alias[`@frontmcp/ui/${sub}`] = mjs;
526
+ }
527
+ if (Object.keys(alias).length > 0) break;
528
+ }
529
+ } catch {
530
+ }
482
531
  try {
483
532
  const result = esbuild.buildSync({
484
533
  stdin: {
@@ -491,10 +540,9 @@ if (__root) {
491
540
  write: false,
492
541
  format: "esm",
493
542
  target: "es2020",
494
- jsx: "transform",
495
- jsxFactory: "React.createElement",
496
- jsxFragment: "React.Fragment",
497
- external: ["react", "react-dom"],
543
+ jsx: "automatic",
544
+ external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
545
+ alias,
498
546
  define: { "process.env.NODE_ENV": '"production"' },
499
547
  platform: "browser",
500
548
  treeShaking: true,
@@ -718,6 +766,7 @@ function escapeAttribute(str) {
718
766
  function buildDataInjectionScript(options) {
719
767
  const { toolName, input, output, structuredContent } = options;
720
768
  const lines = [
769
+ `window.__mcpAppsEnabled = true;`,
721
770
  `window.__mcpToolName = ${safeJsonForScript(toolName)};`,
722
771
  `window.__mcpToolInput = ${safeJsonForScript(input ?? null)};`,
723
772
  `window.__mcpToolOutput = ${safeJsonForScript(output ?? null)};`,
@@ -782,6 +831,19 @@ function generateBridgeIIFE(options = {}) {
782
831
  parts.push("});");
783
832
  parts.push("");
784
833
  parts.push("window.FrontMcpBridge = bridge;");
834
+ parts.push("function __showLoading() {");
835
+ parts.push(' var root = document.getElementById("root");');
836
+ parts.push(" if (root && !root.hasChildNodes()) {");
837
+ parts.push(
838
+ ` 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>';`
839
+ );
840
+ parts.push(" }");
841
+ parts.push("}");
842
+ parts.push('if (document.readyState === "loading") {');
843
+ parts.push(' document.addEventListener("DOMContentLoaded", __showLoading);');
844
+ parts.push("} else {");
845
+ parts.push(" __showLoading();");
846
+ parts.push("}");
785
847
  parts.push("})();");
786
848
  const code = parts.join("\n");
787
849
  if (minify) {
@@ -958,7 +1020,20 @@ var ExtAppsAdapter = {
958
1020
  self.handleMessage(context, event);
959
1021
  });
960
1022
 
961
- return self.performHandshake(context);
1023
+ // Defer handshake until after the document is fully loaded.
1024
+ // During document.write() (used by the sandbox proxy), postMessage
1025
+ // from the inner iframe may not reach the parent. Waiting for DOMContentLoaded
1026
+ // or using setTimeout ensures the iframe is fully attached.
1027
+ return new Promise(function(resolve) {
1028
+ function doHandshake() {
1029
+ self.sendHandshake(context).then(resolve, resolve);
1030
+ }
1031
+ if (document.readyState === 'loading') {
1032
+ document.addEventListener('DOMContentLoaded', doHandshake);
1033
+ } else {
1034
+ setTimeout(doHandshake, 0);
1035
+ }
1036
+ });
962
1037
  },
963
1038
  handleMessage: function(context, event) {
964
1039
  if (!this.isOriginTrusted(event.origin)) return;
@@ -1052,32 +1127,67 @@ var ExtAppsAdapter = {
1052
1127
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
1053
1128
  });
1054
1129
  },
1055
- performHandshake: function(context) {
1130
+ sendHandshake: function(context) {
1131
+ // Send ui/initialize using '*' as target origin since TOFU hasn't
1132
+ // been established yet. The response from the host will establish
1133
+ // TOFU trust via handleMessage \u2192 isOriginTrusted.
1056
1134
  var self = this;
1135
+ var id = ++this.requestId;
1057
1136
  var params = {
1058
1137
  appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
1059
1138
  appCapabilities: { tools: { listChanged: false } },
1060
1139
  protocolVersion: '2024-11-05'
1061
1140
  };
1062
1141
 
1063
- return this.sendRequest('ui/initialize', params).then(function(result) {
1064
- self.hostCapabilities = result.hostCapabilities || {};
1065
- self.capabilities = Object.assign({}, self.capabilities, {
1066
- canCallTools: Boolean(self.hostCapabilities.serverToolProxy),
1067
- canSendMessages: true,
1068
- canOpenLinks: Boolean(self.hostCapabilities.openLink),
1069
- supportsDisplayModes: true
1070
- });
1071
- if (result.hostContext) {
1072
- Object.assign(context.hostContext, result.hostContext);
1073
- }
1142
+ return new Promise(function(resolve, reject) {
1143
+ var timeout = setTimeout(function() {
1144
+ delete self.pendingRequests[id];
1145
+ // Handshake timeout is non-fatal \u2014 notifications may still arrive
1146
+ resolve();
1147
+ }, 10000);
1148
+
1149
+ self.pendingRequests[id] = {
1150
+ resolve: function(result) {
1151
+ self.hostCapabilities = result.hostCapabilities || {};
1152
+ self.capabilities = Object.assign({}, self.capabilities, {
1153
+ canCallTools: Boolean(self.hostCapabilities.serverTools || self.hostCapabilities.serverToolProxy),
1154
+ canSendMessages: true,
1155
+ canOpenLinks: Boolean(self.hostCapabilities.openLinks || self.hostCapabilities.openLink),
1156
+ supportsDisplayModes: true
1157
+ });
1158
+ if (result.hostContext) {
1159
+ Object.assign(context.hostContext, result.hostContext);
1160
+ }
1161
+ // Send ui/notifications/initialized to tell the host the view is ready.
1162
+ // Per MCP Apps spec, the host waits for this before sending tool-result.
1163
+ var targetOrigin = self.trustedOrigin || '*';
1164
+ window.parent.postMessage({
1165
+ jsonrpc: '2.0',
1166
+ method: 'ui/notifications/initialized',
1167
+ params: {}
1168
+ }, targetOrigin);
1169
+ resolve();
1170
+ },
1171
+ reject: function(err) { resolve(); }, // Non-fatal
1172
+ timeout: timeout
1173
+ };
1174
+
1175
+ // Use '*' for the initial handshake \u2014 we don't know the host origin yet.
1176
+ // The sandbox proxy will relay this to the host.
1177
+ window.parent.postMessage({
1178
+ jsonrpc: '2.0', id: id, method: 'ui/initialize', params: params
1179
+ }, '*');
1074
1180
  });
1075
1181
  },
1182
+ performHandshake: function(context) {
1183
+ return this.sendHandshake(context);
1184
+ },
1076
1185
  callTool: function(context, name, args) {
1077
- if (!this.hostCapabilities.serverToolProxy) {
1186
+ if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
1078
1187
  return Promise.reject(new Error('Server tool proxy not supported'));
1079
1188
  }
1080
- return this.sendRequest('ui/callServerTool', { name: name, arguments: args });
1189
+ // Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
1190
+ return this.sendRequest('tools/call', { name: name, arguments: args || {} });
1081
1191
  },
1082
1192
  sendMessage: function(context, content) {
1083
1193
  return this.sendRequest('ui/message', { content: content });
package/esm/index.mjs CHANGED
@@ -181,6 +181,19 @@ function generateBridgeIIFE(options = {}) {
181
181
  parts.push("});");
182
182
  parts.push("");
183
183
  parts.push("window.FrontMcpBridge = bridge;");
184
+ parts.push("function __showLoading() {");
185
+ parts.push(' var root = document.getElementById("root");');
186
+ parts.push(" if (root && !root.hasChildNodes()) {");
187
+ parts.push(
188
+ ` 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>';`
189
+ );
190
+ parts.push(" }");
191
+ parts.push("}");
192
+ parts.push('if (document.readyState === "loading") {');
193
+ parts.push(' document.addEventListener("DOMContentLoaded", __showLoading);');
194
+ parts.push("} else {");
195
+ parts.push(" __showLoading();");
196
+ parts.push("}");
184
197
  parts.push("})();");
185
198
  const code = parts.join("\n");
186
199
  if (minify) {
@@ -357,7 +370,20 @@ var ExtAppsAdapter = {
357
370
  self.handleMessage(context, event);
358
371
  });
359
372
 
360
- return self.performHandshake(context);
373
+ // Defer handshake until after the document is fully loaded.
374
+ // During document.write() (used by the sandbox proxy), postMessage
375
+ // from the inner iframe may not reach the parent. Waiting for DOMContentLoaded
376
+ // or using setTimeout ensures the iframe is fully attached.
377
+ return new Promise(function(resolve) {
378
+ function doHandshake() {
379
+ self.sendHandshake(context).then(resolve, resolve);
380
+ }
381
+ if (document.readyState === 'loading') {
382
+ document.addEventListener('DOMContentLoaded', doHandshake);
383
+ } else {
384
+ setTimeout(doHandshake, 0);
385
+ }
386
+ });
361
387
  },
362
388
  handleMessage: function(context, event) {
363
389
  if (!this.isOriginTrusted(event.origin)) return;
@@ -451,32 +477,67 @@ var ExtAppsAdapter = {
451
477
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
452
478
  });
453
479
  },
454
- performHandshake: function(context) {
480
+ sendHandshake: function(context) {
481
+ // Send ui/initialize using '*' as target origin since TOFU hasn't
482
+ // been established yet. The response from the host will establish
483
+ // TOFU trust via handleMessage \u2192 isOriginTrusted.
455
484
  var self = this;
485
+ var id = ++this.requestId;
456
486
  var params = {
457
487
  appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
458
488
  appCapabilities: { tools: { listChanged: false } },
459
489
  protocolVersion: '2024-11-05'
460
490
  };
461
491
 
462
- return this.sendRequest('ui/initialize', params).then(function(result) {
463
- self.hostCapabilities = result.hostCapabilities || {};
464
- self.capabilities = Object.assign({}, self.capabilities, {
465
- canCallTools: Boolean(self.hostCapabilities.serverToolProxy),
466
- canSendMessages: true,
467
- canOpenLinks: Boolean(self.hostCapabilities.openLink),
468
- supportsDisplayModes: true
469
- });
470
- if (result.hostContext) {
471
- Object.assign(context.hostContext, result.hostContext);
472
- }
492
+ return new Promise(function(resolve, reject) {
493
+ var timeout = setTimeout(function() {
494
+ delete self.pendingRequests[id];
495
+ // Handshake timeout is non-fatal \u2014 notifications may still arrive
496
+ resolve();
497
+ }, 10000);
498
+
499
+ self.pendingRequests[id] = {
500
+ resolve: function(result) {
501
+ self.hostCapabilities = result.hostCapabilities || {};
502
+ self.capabilities = Object.assign({}, self.capabilities, {
503
+ canCallTools: Boolean(self.hostCapabilities.serverTools || self.hostCapabilities.serverToolProxy),
504
+ canSendMessages: true,
505
+ canOpenLinks: Boolean(self.hostCapabilities.openLinks || self.hostCapabilities.openLink),
506
+ supportsDisplayModes: true
507
+ });
508
+ if (result.hostContext) {
509
+ Object.assign(context.hostContext, result.hostContext);
510
+ }
511
+ // Send ui/notifications/initialized to tell the host the view is ready.
512
+ // Per MCP Apps spec, the host waits for this before sending tool-result.
513
+ var targetOrigin = self.trustedOrigin || '*';
514
+ window.parent.postMessage({
515
+ jsonrpc: '2.0',
516
+ method: 'ui/notifications/initialized',
517
+ params: {}
518
+ }, targetOrigin);
519
+ resolve();
520
+ },
521
+ reject: function(err) { resolve(); }, // Non-fatal
522
+ timeout: timeout
523
+ };
524
+
525
+ // Use '*' for the initial handshake \u2014 we don't know the host origin yet.
526
+ // The sandbox proxy will relay this to the host.
527
+ window.parent.postMessage({
528
+ jsonrpc: '2.0', id: id, method: 'ui/initialize', params: params
529
+ }, '*');
473
530
  });
474
531
  },
532
+ performHandshake: function(context) {
533
+ return this.sendHandshake(context);
534
+ },
475
535
  callTool: function(context, name, args) {
476
- if (!this.hostCapabilities.serverToolProxy) {
536
+ if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
477
537
  return Promise.reject(new Error('Server tool proxy not supported'));
478
538
  }
479
- return this.sendRequest('ui/callServerTool', { name: name, arguments: args });
539
+ // Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
540
+ return this.sendRequest('tools/call', { name: name, arguments: args || {} });
480
541
  },
481
542
  sendMessage: function(context, content) {
482
543
  return this.sendRequest('ui/message', { content: content });
@@ -2657,6 +2718,7 @@ function escapeAttribute(str) {
2657
2718
  function buildDataInjectionScript(options) {
2658
2719
  const { toolName, input, output, structuredContent } = options;
2659
2720
  const lines = [
2721
+ `window.__mcpAppsEnabled = true;`,
2660
2722
  `window.__mcpToolName = ${safeJsonForScript(toolName)};`,
2661
2723
  `window.__mcpToolInput = ${safeJsonForScript(input ?? null)};`,
2662
2724
  `window.__mcpToolOutput = ${safeJsonForScript(output ?? null)};`,
@@ -2987,22 +3049,71 @@ function bundleFileSource(source, filename, resolveDir, componentName) {
2987
3049
  const esbuild = __require("esbuild");
2988
3050
  const mountCode = `
2989
3051
  // --- Auto-generated mount ---
3052
+ import { createElement as __h } from 'react';
2990
3053
  import { createRoot } from 'react-dom/client';
2991
3054
  import { McpBridgeProvider } from '@frontmcp/ui/react';
2992
- import React from 'react';
2993
- const __root = document.getElementById('root');
3055
+ var __root = document.getElementById('root');
2994
3056
  if (__root) {
2995
- createRoot(__root).render(
2996
- React.createElement(McpBridgeProvider, null,
2997
- React.createElement(${componentName})
2998
- )
2999
- );
3057
+ var __reactRoot = createRoot(__root);
3058
+ function __hasData(v) { return v !== undefined; }
3059
+ function __render(output) {
3060
+ __reactRoot.render(
3061
+ __h(McpBridgeProvider, null,
3062
+ __h(${componentName}, { output: output !== undefined ? output : null, input: window.__mcpToolInput, loading: !__hasData(output) })
3063
+ )
3064
+ );
3065
+ }
3066
+ // Render immediately (component shows loading state until data arrives)
3067
+ __render(undefined);
3068
+ // 1. Try OpenAI SDK (toolOutput set synchronously or after load)
3069
+ if (typeof window !== 'undefined') {
3070
+ if (!window.openai) window.openai = {};
3071
+ var __cur = window.openai.toolOutput;
3072
+ if (__hasData(__cur)) { __render(__cur); }
3073
+ Object.defineProperty(window.openai, 'toolOutput', {
3074
+ get: function() { return __cur; },
3075
+ set: function(v) { __cur = v; __render(v); },
3076
+ configurable: true, enumerable: true
3077
+ });
3078
+ }
3079
+ // 2. Try injected data globals
3080
+ if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
3081
+ // 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
3082
+ var __bridge = window.FrontMcpBridge;
3083
+ if (__bridge && typeof __bridge.onToolResult === 'function') {
3084
+ __bridge.onToolResult(function(data) { __render(data); });
3085
+ } else {
3086
+ // 4. Fallback: listen for tool:result CustomEvent (standalone / MCP Inspector)
3087
+ window.addEventListener('tool:result', function(e) {
3088
+ var d = e.detail;
3089
+ if (d) __render(d.structuredContent !== undefined ? d.structuredContent : d.content !== undefined ? d.content : d);
3090
+ });
3091
+ }
3000
3092
  }`;
3001
3093
  const loader = {
3002
3094
  ".tsx": "tsx",
3003
3095
  ".jsx": "jsx"
3004
3096
  };
3005
3097
  const stdinLoader = filename.endsWith(".tsx") ? "tsx" : "jsx";
3098
+ const alias = {};
3099
+ try {
3100
+ const nodePath = __require("path");
3101
+ const nodeFs = __require("fs");
3102
+ const candidates = [
3103
+ nodePath.join(process.cwd(), "node_modules", "@frontmcp", "ui", "dist", "esm"),
3104
+ nodePath.join(resolveDir, "node_modules", "@frontmcp", "ui", "dist", "esm")
3105
+ ];
3106
+ for (const uiEsmBase of candidates) {
3107
+ if (!nodeFs.existsSync(uiEsmBase)) continue;
3108
+ const subpaths = ["components", "react", "theme", "bridge", "runtime"];
3109
+ for (const sub of subpaths) {
3110
+ const mjs = nodePath.join(uiEsmBase, sub, "index.mjs");
3111
+ if (nodeFs.existsSync(mjs)) alias[`@frontmcp/ui/${sub}`] = mjs;
3112
+ }
3113
+ if (Object.keys(alias).length > 0) break;
3114
+ }
3115
+ } catch {
3116
+ }
3006
3117
  try {
3007
3118
  const result = esbuild.buildSync({
3008
3119
  stdin: {
@@ -3015,10 +3126,9 @@ if (__root) {
3015
3126
  write: false,
3016
3127
  format: "esm",
3017
3128
  target: "es2020",
3018
- jsx: "transform",
3019
- jsxFactory: "React.createElement",
3020
- jsxFragment: "React.Fragment",
3021
- external: ["react", "react-dom"],
3129
+ jsx: "automatic",
3130
+ external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
3131
+ alias,
3022
3132
  define: { "process.env.NODE_ENV": '"production"' },
3023
3133
  platform: "browser",
3024
3134
  treeShaking: true,
@@ -3541,8 +3651,26 @@ function wrapDetectedContent(value) {
3541
3651
  }
3542
3652
 
3543
3653
  // libs/uipack/src/adapters/template-renderer.ts
3654
+ function buildCspConfig(resolver) {
3655
+ const cspResourceDomains = ["https://esm.sh"];
3656
+ const cspConnectDomains = ["https://esm.sh"];
3657
+ if (resolver && "overrides" in resolver) {
3658
+ const overrides = resolver.overrides;
3659
+ if (overrides) {
3660
+ for (const url of Object.values(overrides)) {
3661
+ try {
3662
+ const origin = new URL(url).origin;
3663
+ if (!cspResourceDomains.includes(origin)) cspResourceDomains.push(origin);
3664
+ if (!cspConnectDomains.includes(origin)) cspConnectDomains.push(origin);
3665
+ } catch {
3666
+ }
3667
+ }
3668
+ }
3669
+ }
3670
+ return { resourceDomains: cspResourceDomains, connectDomains: cspConnectDomains };
3671
+ }
3544
3672
  function renderToolTemplate(options) {
3545
- const { toolName, input, output, template, platformType, resolver } = options;
3673
+ const { toolName, input, output, template, resolver } = options;
3546
3674
  const uiType = detectUIType(template);
3547
3675
  const shellConfig = {
3548
3676
  toolName,
@@ -3555,25 +3683,7 @@ function renderToolTemplate(options) {
3555
3683
  let hash = "";
3556
3684
  let size = 0;
3557
3685
  if (typeof template === "object" && template !== null && "file" in template) {
3558
- const cspResourceDomains = ["https://esm.sh"];
3559
- const cspConnectDomains = ["https://esm.sh"];
3560
- if (resolver && "overrides" in resolver) {
3561
- const overrides = resolver.overrides;
3562
- if (overrides) {
3563
- for (const url of Object.values(overrides)) {
3564
- try {
3565
- const origin = new URL(url).origin;
3566
- if (!cspResourceDomains.includes(origin)) cspResourceDomains.push(origin);
3567
- if (!cspConnectDomains.includes(origin)) cspConnectDomains.push(origin);
3568
- } catch {
3569
- }
3570
- }
3571
- }
3572
- }
3573
- const cspConfig = {
3574
- resourceDomains: cspResourceDomains,
3575
- connectDomains: cspConnectDomains
3576
- };
3686
+ const cspConfig = buildCspConfig(resolver);
3577
3687
  const result = renderComponent({ source: template }, { ...shellConfig, csp: cspConfig });
3578
3688
  html = result.html;
3579
3689
  hash = result.hash;
package/esm/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/uipack",
3
- "version": "1.0.0-beta.9",
3
+ "version": "1.0.0",
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",
@@ -52,7 +52,7 @@
52
52
  "./esm": null
53
53
  },
54
54
  "engines": {
55
- "node": ">=22.0.0"
55
+ "node": ">=24.0.0"
56
56
  },
57
57
  "dependencies": {
58
58
  "zod": "^4.0.0"
@@ -116,6 +116,7 @@ function safeJsonForScript(value) {
116
116
  function buildDataInjectionScript(options) {
117
117
  const { toolName, input, output, structuredContent } = options;
118
118
  const lines = [
119
+ `window.__mcpAppsEnabled = true;`,
119
120
  `window.__mcpToolName = ${safeJsonForScript(toolName)};`,
120
121
  `window.__mcpToolInput = ${safeJsonForScript(input ?? null)};`,
121
122
  `window.__mcpToolOutput = ${safeJsonForScript(output ?? null)};`,
@@ -206,6 +207,19 @@ function generateBridgeIIFE(options = {}) {
206
207
  parts.push("});");
207
208
  parts.push("");
208
209
  parts.push("window.FrontMcpBridge = bridge;");
210
+ parts.push("function __showLoading() {");
211
+ parts.push(' var root = document.getElementById("root");');
212
+ parts.push(" if (root && !root.hasChildNodes()) {");
213
+ parts.push(
214
+ ` 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>';`
215
+ );
216
+ parts.push(" }");
217
+ parts.push("}");
218
+ parts.push('if (document.readyState === "loading") {');
219
+ parts.push(' document.addEventListener("DOMContentLoaded", __showLoading);');
220
+ parts.push("} else {");
221
+ parts.push(" __showLoading();");
222
+ parts.push("}");
209
223
  parts.push("})();");
210
224
  const code = parts.join("\n");
211
225
  if (minify) {
@@ -382,7 +396,20 @@ var ExtAppsAdapter = {
382
396
  self.handleMessage(context, event);
383
397
  });
384
398
 
385
- return self.performHandshake(context);
399
+ // Defer handshake until after the document is fully loaded.
400
+ // During document.write() (used by the sandbox proxy), postMessage
401
+ // from the inner iframe may not reach the parent. Waiting for DOMContentLoaded
402
+ // or using setTimeout ensures the iframe is fully attached.
403
+ return new Promise(function(resolve) {
404
+ function doHandshake() {
405
+ self.sendHandshake(context).then(resolve, resolve);
406
+ }
407
+ if (document.readyState === 'loading') {
408
+ document.addEventListener('DOMContentLoaded', doHandshake);
409
+ } else {
410
+ setTimeout(doHandshake, 0);
411
+ }
412
+ });
386
413
  },
387
414
  handleMessage: function(context, event) {
388
415
  if (!this.isOriginTrusted(event.origin)) return;
@@ -476,32 +503,67 @@ var ExtAppsAdapter = {
476
503
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
477
504
  });
478
505
  },
479
- performHandshake: function(context) {
506
+ sendHandshake: function(context) {
507
+ // Send ui/initialize using '*' as target origin since TOFU hasn't
508
+ // been established yet. The response from the host will establish
509
+ // TOFU trust via handleMessage \u2192 isOriginTrusted.
480
510
  var self = this;
511
+ var id = ++this.requestId;
481
512
  var params = {
482
513
  appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
483
514
  appCapabilities: { tools: { listChanged: false } },
484
515
  protocolVersion: '2024-11-05'
485
516
  };
486
517
 
487
- return this.sendRequest('ui/initialize', params).then(function(result) {
488
- self.hostCapabilities = result.hostCapabilities || {};
489
- self.capabilities = Object.assign({}, self.capabilities, {
490
- canCallTools: Boolean(self.hostCapabilities.serverToolProxy),
491
- canSendMessages: true,
492
- canOpenLinks: Boolean(self.hostCapabilities.openLink),
493
- supportsDisplayModes: true
494
- });
495
- if (result.hostContext) {
496
- Object.assign(context.hostContext, result.hostContext);
497
- }
518
+ return new Promise(function(resolve, reject) {
519
+ var timeout = setTimeout(function() {
520
+ delete self.pendingRequests[id];
521
+ // Handshake timeout is non-fatal \u2014 notifications may still arrive
522
+ resolve();
523
+ }, 10000);
524
+
525
+ self.pendingRequests[id] = {
526
+ resolve: function(result) {
527
+ self.hostCapabilities = result.hostCapabilities || {};
528
+ self.capabilities = Object.assign({}, self.capabilities, {
529
+ canCallTools: Boolean(self.hostCapabilities.serverTools || self.hostCapabilities.serverToolProxy),
530
+ canSendMessages: true,
531
+ canOpenLinks: Boolean(self.hostCapabilities.openLinks || self.hostCapabilities.openLink),
532
+ supportsDisplayModes: true
533
+ });
534
+ if (result.hostContext) {
535
+ Object.assign(context.hostContext, result.hostContext);
536
+ }
537
+ // Send ui/notifications/initialized to tell the host the view is ready.
538
+ // Per MCP Apps spec, the host waits for this before sending tool-result.
539
+ var targetOrigin = self.trustedOrigin || '*';
540
+ window.parent.postMessage({
541
+ jsonrpc: '2.0',
542
+ method: 'ui/notifications/initialized',
543
+ params: {}
544
+ }, targetOrigin);
545
+ resolve();
546
+ },
547
+ reject: function(err) { resolve(); }, // Non-fatal
548
+ timeout: timeout
549
+ };
550
+
551
+ // Use '*' for the initial handshake \u2014 we don't know the host origin yet.
552
+ // The sandbox proxy will relay this to the host.
553
+ window.parent.postMessage({
554
+ jsonrpc: '2.0', id: id, method: 'ui/initialize', params: params
555
+ }, '*');
498
556
  });
499
557
  },
558
+ performHandshake: function(context) {
559
+ return this.sendHandshake(context);
560
+ },
500
561
  callTool: function(context, name, args) {
501
- if (!this.hostCapabilities.serverToolProxy) {
562
+ if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
502
563
  return Promise.reject(new Error('Server tool proxy not supported'));
503
564
  }
504
- return this.sendRequest('ui/callServerTool', { name: name, arguments: args });
565
+ // Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
566
+ return this.sendRequest('tools/call', { name: name, arguments: args || {} });
505
567
  },
506
568
  sendMessage: function(context, content) {
507
569
  return this.sendRequest('ui/message', { content: content });