@frontmcp/uipack 1.0.0-beta.13 → 1.0.0-beta.14

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/adapters/index.js CHANGED
@@ -215,6 +215,7 @@ function safeJsonForScript(value) {
215
215
  function buildDataInjectionScript(options) {
216
216
  const { toolName, input, output, structuredContent } = options;
217
217
  const lines = [
218
+ `window.__mcpAppsEnabled = true;`,
218
219
  `window.__mcpToolName = ${safeJsonForScript(toolName)};`,
219
220
  `window.__mcpToolInput = ${safeJsonForScript(input ?? null)};`,
220
221
  `window.__mcpToolOutput = ${safeJsonForScript(output ?? null)};`,
@@ -305,6 +306,19 @@ function generateBridgeIIFE(options = {}) {
305
306
  parts.push("});");
306
307
  parts.push("");
307
308
  parts.push("window.FrontMcpBridge = bridge;");
309
+ parts.push("function __showLoading() {");
310
+ parts.push(' var root = document.getElementById("root");');
311
+ parts.push(" if (root && !root.hasChildNodes()) {");
312
+ parts.push(
313
+ ` 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>';`
314
+ );
315
+ parts.push(" }");
316
+ parts.push("}");
317
+ parts.push('if (document.readyState === "loading") {');
318
+ parts.push(' document.addEventListener("DOMContentLoaded", __showLoading);');
319
+ parts.push("} else {");
320
+ parts.push(" __showLoading();");
321
+ parts.push("}");
308
322
  parts.push("})();");
309
323
  const code = parts.join("\n");
310
324
  if (minify) {
@@ -481,7 +495,20 @@ var ExtAppsAdapter = {
481
495
  self.handleMessage(context, event);
482
496
  });
483
497
 
484
- return self.performHandshake(context);
498
+ // Defer handshake until after the document is fully loaded.
499
+ // During document.write() (used by the sandbox proxy), postMessage
500
+ // from the inner iframe may not reach the parent. Waiting for DOMContentLoaded
501
+ // or using setTimeout ensures the iframe is fully attached.
502
+ return new Promise(function(resolve) {
503
+ function doHandshake() {
504
+ self.sendHandshake(context).then(resolve, resolve);
505
+ }
506
+ if (document.readyState === 'loading') {
507
+ document.addEventListener('DOMContentLoaded', doHandshake);
508
+ } else {
509
+ setTimeout(doHandshake, 0);
510
+ }
511
+ });
485
512
  },
486
513
  handleMessage: function(context, event) {
487
514
  if (!this.isOriginTrusted(event.origin)) return;
@@ -575,32 +602,67 @@ var ExtAppsAdapter = {
575
602
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
576
603
  });
577
604
  },
578
- performHandshake: function(context) {
605
+ sendHandshake: function(context) {
606
+ // Send ui/initialize using '*' as target origin since TOFU hasn't
607
+ // been established yet. The response from the host will establish
608
+ // TOFU trust via handleMessage \u2192 isOriginTrusted.
579
609
  var self = this;
610
+ var id = ++this.requestId;
580
611
  var params = {
581
612
  appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
582
613
  appCapabilities: { tools: { listChanged: false } },
583
614
  protocolVersion: '2024-11-05'
584
615
  };
585
616
 
586
- return this.sendRequest('ui/initialize', params).then(function(result) {
587
- self.hostCapabilities = result.hostCapabilities || {};
588
- self.capabilities = Object.assign({}, self.capabilities, {
589
- canCallTools: Boolean(self.hostCapabilities.serverToolProxy),
590
- canSendMessages: true,
591
- canOpenLinks: Boolean(self.hostCapabilities.openLink),
592
- supportsDisplayModes: true
593
- });
594
- if (result.hostContext) {
595
- Object.assign(context.hostContext, result.hostContext);
596
- }
617
+ return new Promise(function(resolve, reject) {
618
+ var timeout = setTimeout(function() {
619
+ delete self.pendingRequests[id];
620
+ // Handshake timeout is non-fatal \u2014 notifications may still arrive
621
+ resolve();
622
+ }, 10000);
623
+
624
+ self.pendingRequests[id] = {
625
+ resolve: function(result) {
626
+ self.hostCapabilities = result.hostCapabilities || {};
627
+ self.capabilities = Object.assign({}, self.capabilities, {
628
+ canCallTools: Boolean(self.hostCapabilities.serverTools || self.hostCapabilities.serverToolProxy),
629
+ canSendMessages: true,
630
+ canOpenLinks: Boolean(self.hostCapabilities.openLinks || self.hostCapabilities.openLink),
631
+ supportsDisplayModes: true
632
+ });
633
+ if (result.hostContext) {
634
+ Object.assign(context.hostContext, result.hostContext);
635
+ }
636
+ // Send ui/notifications/initialized to tell the host the view is ready.
637
+ // Per MCP Apps spec, the host waits for this before sending tool-result.
638
+ var targetOrigin = self.trustedOrigin || '*';
639
+ window.parent.postMessage({
640
+ jsonrpc: '2.0',
641
+ method: 'ui/notifications/initialized',
642
+ params: {}
643
+ }, targetOrigin);
644
+ resolve();
645
+ },
646
+ reject: function(err) { resolve(); }, // Non-fatal
647
+ timeout: timeout
648
+ };
649
+
650
+ // Use '*' for the initial handshake \u2014 we don't know the host origin yet.
651
+ // The sandbox proxy will relay this to the host.
652
+ window.parent.postMessage({
653
+ jsonrpc: '2.0', id: id, method: 'ui/initialize', params: params
654
+ }, '*');
597
655
  });
598
656
  },
657
+ performHandshake: function(context) {
658
+ return this.sendHandshake(context);
659
+ },
599
660
  callTool: function(context, name, args) {
600
- if (!this.hostCapabilities.serverToolProxy) {
661
+ if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
601
662
  return Promise.reject(new Error('Server tool proxy not supported'));
602
663
  }
603
- return this.sendRequest('ui/callServerTool', { name: name, arguments: args });
664
+ // Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
665
+ return this.sendRequest('tools/call', { name: name, arguments: args || {} });
604
666
  },
605
667
  sendMessage: function(context, content) {
606
668
  return this.sendRequest('ui/message', { content: content });
@@ -1782,22 +1844,71 @@ function bundleFileSource(source, filename, resolveDir, componentName) {
1782
1844
  const esbuild = require("esbuild");
1783
1845
  const mountCode = `
1784
1846
  // --- Auto-generated mount ---
1847
+ import { createElement as __h } from 'react';
1785
1848
  import { createRoot } from 'react-dom/client';
1786
1849
  import { McpBridgeProvider } from '@frontmcp/ui/react';
1787
- import React from 'react';
1788
- const __root = document.getElementById('root');
1850
+ var __root = document.getElementById('root');
1789
1851
  if (__root) {
1790
- createRoot(__root).render(
1791
- React.createElement(McpBridgeProvider, null,
1792
- React.createElement(${componentName})
1793
- )
1794
- );
1852
+ var __reactRoot = createRoot(__root);
1853
+ function __hasData(v) { return v !== undefined; }
1854
+ function __render(output) {
1855
+ __reactRoot.render(
1856
+ __h(McpBridgeProvider, null,
1857
+ __h(${componentName}, { output: output !== undefined ? output : null, input: window.__mcpToolInput, loading: !__hasData(output) })
1858
+ )
1859
+ );
1860
+ }
1861
+ // Render immediately (component shows loading state until data arrives)
1862
+ __render(undefined);
1863
+ // 1. Try OpenAI SDK (toolOutput set synchronously or after load)
1864
+ if (typeof window !== 'undefined') {
1865
+ if (!window.openai) window.openai = {};
1866
+ var __cur = window.openai.toolOutput;
1867
+ if (__hasData(__cur)) { __render(__cur); }
1868
+ Object.defineProperty(window.openai, 'toolOutput', {
1869
+ get: function() { return __cur; },
1870
+ set: function(v) { __cur = v; __render(v); },
1871
+ configurable: true, enumerable: true
1872
+ });
1873
+ }
1874
+ // 2. Try injected data globals
1875
+ if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
1876
+ // 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
1877
+ var __bridge = window.FrontMcpBridge;
1878
+ if (__bridge && typeof __bridge.onToolResult === 'function') {
1879
+ __bridge.onToolResult(function(data) { __render(data); });
1880
+ } else {
1881
+ // 4. Fallback: listen for tool:result CustomEvent (standalone / MCP Inspector)
1882
+ window.addEventListener('tool:result', function(e) {
1883
+ var d = e.detail;
1884
+ if (d) __render(d.structuredContent !== undefined ? d.structuredContent : d.content !== undefined ? d.content : d);
1885
+ });
1886
+ }
1795
1887
  }`;
1796
1888
  const loader = {
1797
1889
  ".tsx": "tsx",
1798
1890
  ".jsx": "jsx"
1799
1891
  };
1800
1892
  const stdinLoader = filename.endsWith(".tsx") ? "tsx" : "jsx";
1893
+ const alias = {};
1894
+ try {
1895
+ const nodePath = require("path");
1896
+ const nodeFs = require("fs");
1897
+ const candidates = [
1898
+ nodePath.join(process.cwd(), "node_modules", "@frontmcp", "ui", "dist", "esm"),
1899
+ nodePath.join(resolveDir, "node_modules", "@frontmcp", "ui", "dist", "esm")
1900
+ ];
1901
+ for (const uiEsmBase of candidates) {
1902
+ if (!nodeFs.existsSync(uiEsmBase)) continue;
1903
+ const subpaths = ["components", "react", "theme", "bridge", "runtime"];
1904
+ for (const sub of subpaths) {
1905
+ const mjs = nodePath.join(uiEsmBase, sub, "index.mjs");
1906
+ if (nodeFs.existsSync(mjs)) alias[`@frontmcp/ui/${sub}`] = mjs;
1907
+ }
1908
+ if (Object.keys(alias).length > 0) break;
1909
+ }
1910
+ } catch {
1911
+ }
1801
1912
  try {
1802
1913
  const result = esbuild.buildSync({
1803
1914
  stdin: {
@@ -1810,10 +1921,9 @@ if (__root) {
1810
1921
  write: false,
1811
1922
  format: "esm",
1812
1923
  target: "es2020",
1813
- jsx: "transform",
1814
- jsxFactory: "React.createElement",
1815
- jsxFragment: "React.Fragment",
1816
- external: ["react", "react-dom"],
1924
+ jsx: "automatic",
1925
+ external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
1926
+ alias,
1817
1927
  define: { "process.env.NODE_ENV": '"production"' },
1818
1928
  platform: "browser",
1819
1929
  treeShaking: true,
@@ -2291,8 +2401,26 @@ function wrapDetectedContent(value) {
2291
2401
  }
2292
2402
 
2293
2403
  // libs/uipack/src/adapters/template-renderer.ts
2404
+ function buildCspConfig(resolver) {
2405
+ const cspResourceDomains = ["https://esm.sh"];
2406
+ const cspConnectDomains = ["https://esm.sh"];
2407
+ if (resolver && "overrides" in resolver) {
2408
+ const overrides = resolver.overrides;
2409
+ if (overrides) {
2410
+ for (const url of Object.values(overrides)) {
2411
+ try {
2412
+ const origin = new URL(url).origin;
2413
+ if (!cspResourceDomains.includes(origin)) cspResourceDomains.push(origin);
2414
+ if (!cspConnectDomains.includes(origin)) cspConnectDomains.push(origin);
2415
+ } catch {
2416
+ }
2417
+ }
2418
+ }
2419
+ }
2420
+ return { resourceDomains: cspResourceDomains, connectDomains: cspConnectDomains };
2421
+ }
2294
2422
  function renderToolTemplate(options) {
2295
- const { toolName, input, output, template, platformType, resolver } = options;
2423
+ const { toolName, input, output, template, resolver } = options;
2296
2424
  const uiType = detectUIType(template);
2297
2425
  const shellConfig = {
2298
2426
  toolName,
@@ -2305,25 +2433,7 @@ function renderToolTemplate(options) {
2305
2433
  let hash = "";
2306
2434
  let size = 0;
2307
2435
  if (typeof template === "object" && template !== null && "file" in template) {
2308
- const cspResourceDomains = ["https://esm.sh"];
2309
- const cspConnectDomains = ["https://esm.sh"];
2310
- if (resolver && "overrides" in resolver) {
2311
- const overrides = resolver.overrides;
2312
- if (overrides) {
2313
- for (const url of Object.values(overrides)) {
2314
- try {
2315
- const origin = new URL(url).origin;
2316
- if (!cspResourceDomains.includes(origin)) cspResourceDomains.push(origin);
2317
- if (!cspConnectDomains.includes(origin)) cspConnectDomains.push(origin);
2318
- } catch {
2319
- }
2320
- }
2321
- }
2322
- }
2323
- const cspConfig = {
2324
- resourceDomains: cspResourceDomains,
2325
- connectDomains: cspConnectDomains
2326
- };
2436
+ const cspConfig = buildCspConfig(resolver);
2327
2437
  const result = renderComponent({ source: template }, { ...shellConfig, csp: cspConfig });
2328
2438
  html = result.html;
2329
2439
  hash = result.hash;
@@ -16,7 +16,7 @@ export interface RenderToolTemplateOptions {
16
16
  input: unknown;
17
17
  /** Tool output */
18
18
  output: unknown;
19
- /** The template (function, string, or React component) */
19
+ /** The template — FileSource, function, or string */
20
20
  template: unknown;
21
21
  /** Platform type for rendering decisions */
22
22
  platformType?: string;
@@ -41,10 +41,13 @@ export interface RenderToolTemplateResult {
41
41
  /**
42
42
  * Render a tool template into HTML.
43
43
  *
44
- * - Function templates: Calls `template(ctx)` with createTemplateHelpers()
45
- * - React components: Delegates to renderComponent() from uipack/component
46
- * - String templates: Wraps in buildShell()
47
- * - Auto-detect: wraps detected content (chart, mermaid, PDF, HTML)
44
+ * Supported template types:
45
+ * - FileSource object `{ file: './widget.tsx' }` bundled with esbuild, React loaded from esm.sh
46
+ * - HTML template builder function `(ctx) => string`
47
+ * - Static HTML/MDX string
48
+ *
49
+ * React function references (`template: MyComponent`) are NOT supported for bundling.
50
+ * Use `template: { file: './my-component.tsx' }` instead.
48
51
  */
49
52
  export declare function renderToolTemplate(options: RenderToolTemplateOptions): RenderToolTemplateResult;
50
53
  //# sourceMappingURL=template-renderer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"template-renderer.d.ts","sourceRoot":"","sources":["../../src/adapters/template-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,kBAAkB;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,0DAA0D;IAC1D,QAAQ,EAAE,OAAO,CAAC;IAClB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,CAwG/F"}
1
+ {"version":3,"file":"template-renderer.d.ts","sourceRoot":"","sources":["../../src/adapters/template-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,kBAAkB;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,qDAAqD;IACrD,QAAQ,EAAE,OAAO,CAAC;IAClB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAsBD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,CAgF/F"}
@@ -1 +1 @@
1
- {"version":3,"file":"iife-generator.d.ts","sourceRoot":"","sources":["../../src/bridge-runtime/iife-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC,EAAE,CAAC;IACvE,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,wBAAwB;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAyF7E;AAm7BD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,EACvD,OAAO,GAAE,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAM,GACnD,MAAM,CAYR;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,QAAuB,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;CAK9B,CAAC"}
1
+ {"version":3,"file":"iife-generator.d.ts","sourceRoot":"","sources":["../../src/bridge-runtime/iife-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC,EAAE,CAAC;IACvE,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,wBAAwB;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,CA0G7E;AAm+BD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,EACvD,OAAO,GAAE,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAM,GACnD,MAAM,CAYR;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,QAAuB,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;CAK9B,CAAC"}
@@ -81,6 +81,19 @@ function generateBridgeIIFE(options = {}) {
81
81
  parts.push("});");
82
82
  parts.push("");
83
83
  parts.push("window.FrontMcpBridge = bridge;");
84
+ parts.push("function __showLoading() {");
85
+ parts.push(' var root = document.getElementById("root");');
86
+ parts.push(" if (root && !root.hasChildNodes()) {");
87
+ parts.push(
88
+ ` 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>';`
89
+ );
90
+ parts.push(" }");
91
+ parts.push("}");
92
+ parts.push('if (document.readyState === "loading") {');
93
+ parts.push(' document.addEventListener("DOMContentLoaded", __showLoading);');
94
+ parts.push("} else {");
95
+ parts.push(" __showLoading();");
96
+ parts.push("}");
84
97
  parts.push("})();");
85
98
  const code = parts.join("\n");
86
99
  if (minify) {
@@ -257,7 +270,20 @@ var ExtAppsAdapter = {
257
270
  self.handleMessage(context, event);
258
271
  });
259
272
 
260
- return self.performHandshake(context);
273
+ // Defer handshake until after the document is fully loaded.
274
+ // During document.write() (used by the sandbox proxy), postMessage
275
+ // from the inner iframe may not reach the parent. Waiting for DOMContentLoaded
276
+ // or using setTimeout ensures the iframe is fully attached.
277
+ return new Promise(function(resolve) {
278
+ function doHandshake() {
279
+ self.sendHandshake(context).then(resolve, resolve);
280
+ }
281
+ if (document.readyState === 'loading') {
282
+ document.addEventListener('DOMContentLoaded', doHandshake);
283
+ } else {
284
+ setTimeout(doHandshake, 0);
285
+ }
286
+ });
261
287
  },
262
288
  handleMessage: function(context, event) {
263
289
  if (!this.isOriginTrusted(event.origin)) return;
@@ -351,32 +377,67 @@ var ExtAppsAdapter = {
351
377
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
352
378
  });
353
379
  },
354
- performHandshake: function(context) {
380
+ sendHandshake: function(context) {
381
+ // Send ui/initialize using '*' as target origin since TOFU hasn't
382
+ // been established yet. The response from the host will establish
383
+ // TOFU trust via handleMessage \u2192 isOriginTrusted.
355
384
  var self = this;
385
+ var id = ++this.requestId;
356
386
  var params = {
357
387
  appInfo: { name: 'FrontMCP Widget', version: '1.0.0' },
358
388
  appCapabilities: { tools: { listChanged: false } },
359
389
  protocolVersion: '2024-11-05'
360
390
  };
361
391
 
362
- return this.sendRequest('ui/initialize', params).then(function(result) {
363
- self.hostCapabilities = result.hostCapabilities || {};
364
- self.capabilities = Object.assign({}, self.capabilities, {
365
- canCallTools: Boolean(self.hostCapabilities.serverToolProxy),
366
- canSendMessages: true,
367
- canOpenLinks: Boolean(self.hostCapabilities.openLink),
368
- supportsDisplayModes: true
369
- });
370
- if (result.hostContext) {
371
- Object.assign(context.hostContext, result.hostContext);
372
- }
392
+ return new Promise(function(resolve, reject) {
393
+ var timeout = setTimeout(function() {
394
+ delete self.pendingRequests[id];
395
+ // Handshake timeout is non-fatal \u2014 notifications may still arrive
396
+ resolve();
397
+ }, 10000);
398
+
399
+ self.pendingRequests[id] = {
400
+ resolve: function(result) {
401
+ self.hostCapabilities = result.hostCapabilities || {};
402
+ self.capabilities = Object.assign({}, self.capabilities, {
403
+ canCallTools: Boolean(self.hostCapabilities.serverTools || self.hostCapabilities.serverToolProxy),
404
+ canSendMessages: true,
405
+ canOpenLinks: Boolean(self.hostCapabilities.openLinks || self.hostCapabilities.openLink),
406
+ supportsDisplayModes: true
407
+ });
408
+ if (result.hostContext) {
409
+ Object.assign(context.hostContext, result.hostContext);
410
+ }
411
+ // Send ui/notifications/initialized to tell the host the view is ready.
412
+ // Per MCP Apps spec, the host waits for this before sending tool-result.
413
+ var targetOrigin = self.trustedOrigin || '*';
414
+ window.parent.postMessage({
415
+ jsonrpc: '2.0',
416
+ method: 'ui/notifications/initialized',
417
+ params: {}
418
+ }, targetOrigin);
419
+ resolve();
420
+ },
421
+ reject: function(err) { resolve(); }, // Non-fatal
422
+ timeout: timeout
423
+ };
424
+
425
+ // Use '*' for the initial handshake \u2014 we don't know the host origin yet.
426
+ // The sandbox proxy will relay this to the host.
427
+ window.parent.postMessage({
428
+ jsonrpc: '2.0', id: id, method: 'ui/initialize', params: params
429
+ }, '*');
373
430
  });
374
431
  },
432
+ performHandshake: function(context) {
433
+ return this.sendHandshake(context);
434
+ },
375
435
  callTool: function(context, name, args) {
376
- if (!this.hostCapabilities.serverToolProxy) {
436
+ if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
377
437
  return Promise.reject(new Error('Server tool proxy not supported'));
378
438
  }
379
- return this.sendRequest('ui/callServerTool', { name: name, arguments: args });
439
+ // Per ext-apps spec: use standard MCP method 'tools/call' (not 'ui/callServerTool')
440
+ return this.sendRequest('tools/call', { name: name, arguments: args || {} });
380
441
  },
381
442
  sendMessage: function(context, content) {
382
443
  return this.sendRequest('ui/message', { content: content });