@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.
- package/adapters/index.js +156 -46
- package/adapters/template-renderer.d.ts +8 -5
- package/adapters/template-renderer.d.ts.map +1 -1
- package/bridge-runtime/iife-generator.d.ts.map +1 -1
- package/bridge-runtime/index.js +76 -15
- package/component/index.js +136 -26
- package/component/transpiler.d.ts.map +1 -1
- package/esm/adapters/index.mjs +156 -46
- package/esm/bridge-runtime/index.mjs +76 -15
- package/esm/component/index.mjs +136 -26
- package/esm/index.mjs +156 -46
- package/esm/package.json +2 -2
- package/esm/shell/index.mjs +77 -15
- package/index.js +156 -46
- package/package.json +2 -2
- package/shell/data-injector.d.ts.map +1 -1
- package/shell/index.js +77 -15
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
|
-
|
|
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
|
-
|
|
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
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1788
|
-
const __root = document.getElementById('root');
|
|
1850
|
+
var __root = document.getElementById('root');
|
|
1789
1851
|
if (__root) {
|
|
1790
|
-
createRoot(__root)
|
|
1791
|
-
|
|
1792
|
-
|
|
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: "
|
|
1814
|
-
|
|
1815
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
*
|
|
45
|
-
* -
|
|
46
|
-
* -
|
|
47
|
-
* -
|
|
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,
|
|
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,
|
|
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"}
|
package/bridge-runtime/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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 });
|