@frontmcp/uipack 1.8.6 → 1.9.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 +224 -38
- package/adapters/markdown.d.ts +9 -0
- package/adapters/markdown.d.ts.map +1 -0
- package/adapters/template-renderer.d.ts +8 -0
- package/adapters/template-renderer.d.ts.map +1 -1
- package/bridge-runtime/iife-generator.d.ts.map +1 -1
- package/bridge-runtime/index.js +81 -15
- package/component/index.js +100 -29
- package/esm/adapters/index.mjs +227 -38
- package/esm/bridge-runtime/index.mjs +84 -15
- package/esm/component/index.mjs +103 -29
- package/esm/index.mjs +227 -38
- package/esm/package.json +2 -2
- package/esm/resolver/index.mjs +3 -0
- package/esm/shell/index.mjs +100 -26
- package/esm/types/index.mjs +3 -0
- package/esm/typings/index.mjs +3 -0
- package/esm/utils/index.mjs +3 -0
- package/index.js +224 -38
- package/package.json +2 -2
- package/shell/csp.d.ts +6 -1
- package/shell/csp.d.ts.map +1 -1
- package/shell/index.js +97 -26
- package/types/ui-config.d.ts +1 -1
package/esm/adapters/index.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
|
|
2
|
+
const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
|
|
3
|
+
// __frontmcpCreateRequire
|
|
1
4
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
5
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
6
|
}) : x)(function(x) {
|
|
@@ -797,12 +800,38 @@ function __applyHostTheme(theme) {
|
|
|
797
800
|
} catch (e) {}
|
|
798
801
|
}
|
|
799
802
|
|
|
803
|
+
function extractToolOutput(params) {
|
|
804
|
+
// A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
|
|
805
|
+
if (!params) return undefined;
|
|
806
|
+
if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
|
|
807
|
+
var content = params.content;
|
|
808
|
+
if (Array.isArray(content)) {
|
|
809
|
+
for (var i = 0; i < content.length; i++) {
|
|
810
|
+
var item = content[i];
|
|
811
|
+
if (item && item.type === 'text' && typeof item.text === 'string') {
|
|
812
|
+
try { return JSON.parse(item.text); } catch (e) { return item.text; }
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return content;
|
|
817
|
+
}
|
|
818
|
+
|
|
800
819
|
function readInjectedData() {
|
|
801
820
|
var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
|
|
802
821
|
if (typeof window !== 'undefined') {
|
|
803
822
|
if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
|
|
804
823
|
if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
|
|
805
824
|
if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
|
|
825
|
+
// The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
|
|
826
|
+
// (toolOutput is the tool's structuredContent).
|
|
827
|
+
var oa = window.openai;
|
|
828
|
+
if (oa && typeof oa === 'object') {
|
|
829
|
+
if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
|
|
830
|
+
if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
|
|
831
|
+
if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
|
|
832
|
+
if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
806
835
|
}
|
|
807
836
|
return data;
|
|
808
837
|
}
|
|
@@ -851,7 +880,24 @@ var OpenAIAdapter = {
|
|
|
851
880
|
if (sdk.displayMode) {
|
|
852
881
|
context.hostContext.displayMode = sdk.displayMode;
|
|
853
882
|
}
|
|
854
|
-
//
|
|
883
|
+
// The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
|
|
884
|
+
window.addEventListener('openai:set_globals', function(event) {
|
|
885
|
+
var globals = (event && event.detail && event.detail.globals) || window.openai || {};
|
|
886
|
+
if (globals.toolInput !== undefined && globals.toolInput !== null) {
|
|
887
|
+
context.toolInput = globals.toolInput;
|
|
888
|
+
window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
|
|
889
|
+
}
|
|
890
|
+
if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
|
|
891
|
+
context.structuredContent = globals.toolOutput;
|
|
892
|
+
context.notifyToolResult(globals.toolOutput);
|
|
893
|
+
}
|
|
894
|
+
if (globals.theme && globals.theme !== context.hostContext.theme) {
|
|
895
|
+
context.notifyContextChange({ theme: globals.theme });
|
|
896
|
+
}
|
|
897
|
+
if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
|
|
898
|
+
context.notifyContextChange({ displayMode: globals.displayMode });
|
|
899
|
+
}
|
|
900
|
+
});
|
|
855
901
|
return Promise.resolve();
|
|
856
902
|
},
|
|
857
903
|
callTool: function(context, name, args) {
|
|
@@ -998,9 +1044,10 @@ var ExtAppsAdapter = {
|
|
|
998
1044
|
window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
|
|
999
1045
|
break;
|
|
1000
1046
|
case 'ui/notifications/tool-result':
|
|
1001
|
-
|
|
1047
|
+
var resultOutput = extractToolOutput(params);
|
|
1048
|
+
context.toolOutput = resultOutput;
|
|
1002
1049
|
context.structuredContent = params.structuredContent;
|
|
1003
|
-
context.notifyToolResult(
|
|
1050
|
+
context.notifyToolResult(resultOutput);
|
|
1004
1051
|
window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
|
|
1005
1052
|
break;
|
|
1006
1053
|
case 'ui/notifications/host-context-changed':
|
|
@@ -1054,6 +1101,15 @@ var ExtAppsAdapter = {
|
|
|
1054
1101
|
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
|
|
1055
1102
|
});
|
|
1056
1103
|
},
|
|
1104
|
+
sendNotification: function(method, params) {
|
|
1105
|
+
// JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
|
|
1106
|
+
if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
1107
|
+
return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
|
|
1108
|
+
}
|
|
1109
|
+
var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
|
|
1110
|
+
window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
|
|
1111
|
+
return Promise.resolve();
|
|
1112
|
+
},
|
|
1057
1113
|
sendHandshake: function(context) {
|
|
1058
1114
|
// Send ui/initialize using '*' as target origin since TOFU hasn't
|
|
1059
1115
|
// been established yet. The response from the host will establish
|
|
@@ -1119,7 +1175,7 @@ var ExtAppsAdapter = {
|
|
|
1119
1175
|
var pending = this.pendingSize;
|
|
1120
1176
|
if (!pending) return;
|
|
1121
1177
|
this.pendingSize = null;
|
|
1122
|
-
this.
|
|
1178
|
+
this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
|
|
1123
1179
|
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
|
|
1124
1180
|
}, function(err) {
|
|
1125
1181
|
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
|
|
@@ -1146,13 +1202,11 @@ var ExtAppsAdapter = {
|
|
|
1146
1202
|
return this.sendRequest('ui/setDisplayMode', { mode: mode });
|
|
1147
1203
|
},
|
|
1148
1204
|
setSize: function(context, size) {
|
|
1149
|
-
//
|
|
1150
|
-
//
|
|
1151
|
-
var params = {
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
aspectRatio: size && size.aspectRatio
|
|
1155
|
-
};
|
|
1205
|
+
// Standard ext-apps sizing: the view tells the host its dimensions with the
|
|
1206
|
+
// ui/notifications/size-changed notification ({ width?, height? } in px).
|
|
1207
|
+
var params = {};
|
|
1208
|
+
if (size && typeof size.width === 'number') params.width = size.width;
|
|
1209
|
+
if (size && typeof size.height === 'number') params.height = size.height;
|
|
1156
1210
|
if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
1157
1211
|
// No host origin yet: keep only the latest size and send it when the
|
|
1158
1212
|
// handshake settles; every caller's promise follows that one request.
|
|
@@ -1163,7 +1217,7 @@ var ExtAppsAdapter = {
|
|
|
1163
1217
|
self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
|
|
1164
1218
|
});
|
|
1165
1219
|
}
|
|
1166
|
-
return this.
|
|
1220
|
+
return this.sendNotification('ui/notifications/size-changed', params);
|
|
1167
1221
|
},
|
|
1168
1222
|
requestClose: function(context) {
|
|
1169
1223
|
return this.sendRequest('ui/close', {});
|
|
@@ -1586,7 +1640,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
|
|
|
1586
1640
|
|
|
1587
1641
|
// Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
|
|
1588
1642
|
// Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
|
|
1589
|
-
// ext-apps sends ui/
|
|
1643
|
+
// ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
|
|
1590
1644
|
FrontMcpBridge.prototype.setSize = function(size) {
|
|
1591
1645
|
if (!this._adapter) return Promise.reject(new Error('Not initialized'));
|
|
1592
1646
|
if (!this._adapter.setSize) return Promise.resolve();
|
|
@@ -1688,7 +1742,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
|
|
|
1688
1742
|
target.classList.add('opacity-50', 'cursor-not-allowed');
|
|
1689
1743
|
|
|
1690
1744
|
// Add spinner for buttons
|
|
1691
|
-
var spinner = '<svg class="animate-spin -ml-1 mr-2 h-4 w-4 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
|
|
1745
|
+
var spinner = '<svg class="animate-spin -ml-1 mr-2 h-4 w-4 inline" style="width:1em;height:1em;vertical-align:-0.125em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
|
|
1692
1746
|
if (target.tagName === 'BUTTON') {
|
|
1693
1747
|
target.innerHTML = spinner + 'Loading...';
|
|
1694
1748
|
}
|
|
@@ -1769,7 +1823,22 @@ function minifyJS(code) {
|
|
|
1769
1823
|
if (code.length > MAX_MINIFY_CODE_LENGTH) {
|
|
1770
1824
|
return code;
|
|
1771
1825
|
}
|
|
1772
|
-
|
|
1826
|
+
const out = [];
|
|
1827
|
+
let inBlockComment = false;
|
|
1828
|
+
for (const raw of code.split("\n")) {
|
|
1829
|
+
const line = raw.trim();
|
|
1830
|
+
if (inBlockComment) {
|
|
1831
|
+
if (line.includes("*/")) inBlockComment = false;
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
1835
|
+
if (line.startsWith("/*")) {
|
|
1836
|
+
if (!line.includes("*/")) inBlockComment = true;
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1839
|
+
out.push(line);
|
|
1840
|
+
}
|
|
1841
|
+
return out.join("\n");
|
|
1773
1842
|
}
|
|
1774
1843
|
function generatePlatformBundle(platform, options = {}) {
|
|
1775
1844
|
const platformAdapters = {
|
|
@@ -1824,11 +1893,8 @@ function buildCSPDirectives(csp) {
|
|
|
1824
1893
|
directives.push(`img-src ${imgSources.join(" ")}`);
|
|
1825
1894
|
const fontSources = ["'self'", "data:", ...allResourceDomains];
|
|
1826
1895
|
directives.push(`font-src ${fontSources.join(" ")}`);
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
} else {
|
|
1830
|
-
directives.push(`connect-src ${allResourceDomains.join(" ")}`);
|
|
1831
|
-
}
|
|
1896
|
+
const connectSources = [.../* @__PURE__ */ new Set([...allResourceDomains, ...validConnectDomains])];
|
|
1897
|
+
directives.push(`connect-src ${connectSources.join(" ")}`);
|
|
1832
1898
|
directives.push("object-src 'self' data:");
|
|
1833
1899
|
return directives;
|
|
1834
1900
|
}
|
|
@@ -1837,26 +1903,34 @@ function buildCSPMetaTag(csp) {
|
|
|
1837
1903
|
const content = directives.join("; ");
|
|
1838
1904
|
return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
|
|
1839
1905
|
}
|
|
1906
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
1840
1907
|
function validateCSPDomain(domain) {
|
|
1841
|
-
if (domain
|
|
1842
|
-
|
|
1843
|
-
|
|
1908
|
+
if (typeof domain !== "string" || /[\s;,'"]/.test(domain)) return false;
|
|
1909
|
+
if (/[?#]/.test(domain)) return false;
|
|
1910
|
+
if (domain.includes("*")) {
|
|
1911
|
+
const wildcard = /^(?:https|wss):\/\/\*\.(.*)$/.exec(domain);
|
|
1912
|
+
return wildcard !== null && /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(wildcard[1]);
|
|
1844
1913
|
}
|
|
1845
1914
|
try {
|
|
1846
1915
|
const url = new URL(domain);
|
|
1847
|
-
|
|
1916
|
+
if (url.protocol === "https:" || url.protocol === "wss:") return true;
|
|
1917
|
+
return (url.protocol === "http:" || url.protocol === "ws:") && LOOPBACK_HOSTS.has(url.hostname);
|
|
1848
1918
|
} catch {
|
|
1849
1919
|
return false;
|
|
1850
1920
|
}
|
|
1851
1921
|
}
|
|
1922
|
+
var reportedInvalidDomains = /* @__PURE__ */ new Set();
|
|
1852
1923
|
function sanitizeCSPDomains(domains) {
|
|
1853
1924
|
if (!domains) return [];
|
|
1854
1925
|
const valid = [];
|
|
1855
1926
|
for (const domain of domains) {
|
|
1856
1927
|
if (validateCSPDomain(domain)) {
|
|
1857
1928
|
valid.push(domain);
|
|
1858
|
-
} else {
|
|
1859
|
-
|
|
1929
|
+
} else if (!reportedInvalidDomains.has(domain)) {
|
|
1930
|
+
reportedInvalidDomains.add(domain);
|
|
1931
|
+
console.warn(
|
|
1932
|
+
`Invalid CSP domain ignored: ${domain} (expected an https:// or wss:// origin, or http:// / ws:// on localhost)`
|
|
1933
|
+
);
|
|
1860
1934
|
}
|
|
1861
1935
|
}
|
|
1862
1936
|
return valid;
|
|
@@ -2260,8 +2334,8 @@ if (__root) {
|
|
|
2260
2334
|
configurable: true, enumerable: true
|
|
2261
2335
|
});
|
|
2262
2336
|
}
|
|
2263
|
-
// 2. Try injected data globals
|
|
2264
|
-
if (__hasData(window.__mcpToolOutput)) { __render(window.__mcpToolOutput); }
|
|
2337
|
+
// 2. Try injected data globals (null: the page carries no call's data, e.g. compiled at startup)
|
|
2338
|
+
if (__hasData(window.__mcpToolOutput) && window.__mcpToolOutput !== null) { __render(window.__mcpToolOutput); }
|
|
2265
2339
|
// 3. Listen for bridge tool-result (ext-apps / MCP Inspector)
|
|
2266
2340
|
var __bridge = window.FrontMcpBridge;
|
|
2267
2341
|
if (__bridge && typeof __bridge.onToolResult === 'function') {
|
|
@@ -2527,7 +2601,7 @@ createRoot(document.getElementById('root'))
|
|
|
2527
2601
|
return `${componentImport}
|
|
2528
2602
|
import React from 'react';
|
|
2529
2603
|
import { createRoot } from 'react-dom/client';
|
|
2530
|
-
const output = window.__mcpToolOutput;
|
|
2604
|
+
const output = window.__mcpToolOutput || {};
|
|
2531
2605
|
const props = ${propsCode};
|
|
2532
2606
|
createRoot(document.getElementById('root'))
|
|
2533
2607
|
.render(React.createElement(Component, props));`;
|
|
@@ -2831,6 +2905,86 @@ function wrapDetectedContent(value) {
|
|
|
2831
2905
|
}
|
|
2832
2906
|
}
|
|
2833
2907
|
|
|
2908
|
+
// libs/uipack/src/adapters/markdown.ts
|
|
2909
|
+
var SAFE_HREF = /^(https?:|mailto:|\/|#)/i;
|
|
2910
|
+
function emphasis(text) {
|
|
2911
|
+
return text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
2912
|
+
}
|
|
2913
|
+
function inline(text) {
|
|
2914
|
+
const stash = [];
|
|
2915
|
+
const hold = (html2) => `\uE000${stash.push(html2) - 1}\uE000`;
|
|
2916
|
+
let out = escapeHtml(text.replace(/\uE000/g, "")).replace(
|
|
2917
|
+
/`([^`]+)`/g,
|
|
2918
|
+
(_m, code) => hold(`<code>${code}</code>`)
|
|
2919
|
+
);
|
|
2920
|
+
out = out.replace(
|
|
2921
|
+
/\[([^\]]+)\]\(([^)\s]+)\)/g,
|
|
2922
|
+
(_m, label, href) => SAFE_HREF.test(href) ? hold(`<a href="${href}" rel="noopener noreferrer">${emphasis(label)}</a>`) : label
|
|
2923
|
+
);
|
|
2924
|
+
out = emphasis(out);
|
|
2925
|
+
for (let i = 0; i < 2; i++) out = out.replace(/\uE000(\d+)\uE000/g, (_m, n) => stash[Number(n)]);
|
|
2926
|
+
return out;
|
|
2927
|
+
}
|
|
2928
|
+
function markdownToHtml(source) {
|
|
2929
|
+
const lines = source.replace(/\r\n?/g, "\n").split("\n");
|
|
2930
|
+
const blocks = [];
|
|
2931
|
+
let paragraph = [];
|
|
2932
|
+
let list;
|
|
2933
|
+
const flushParagraph = () => {
|
|
2934
|
+
if (paragraph.length > 0) blocks.push(`<p>${inline(paragraph.join(" "))}</p>`);
|
|
2935
|
+
paragraph = [];
|
|
2936
|
+
};
|
|
2937
|
+
const flushList = () => {
|
|
2938
|
+
if (list)
|
|
2939
|
+
blocks.push(`<${list.tag}>${list.items.map((item) => `<li>${inline(item)}</li>`).join("")}</${list.tag}>`);
|
|
2940
|
+
list = void 0;
|
|
2941
|
+
};
|
|
2942
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2943
|
+
const line = lines[i];
|
|
2944
|
+
const fence = /^\s*(`{3,})/.exec(line);
|
|
2945
|
+
if (fence) {
|
|
2946
|
+
flushParagraph();
|
|
2947
|
+
flushList();
|
|
2948
|
+
const fenceLength = fence[1].length;
|
|
2949
|
+
const isClosing = (l) => {
|
|
2950
|
+
const m = /^\s*(`{3,})\s*$/.exec(l);
|
|
2951
|
+
return m !== null && m[1].length >= fenceLength;
|
|
2952
|
+
};
|
|
2953
|
+
const code = [];
|
|
2954
|
+
for (i++; i < lines.length && !isClosing(lines[i]); i++) code.push(lines[i]);
|
|
2955
|
+
blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}
|
|
2956
|
+
</code></pre>`);
|
|
2957
|
+
continue;
|
|
2958
|
+
}
|
|
2959
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
2960
|
+
if (heading) {
|
|
2961
|
+
flushParagraph();
|
|
2962
|
+
flushList();
|
|
2963
|
+
blocks.push(`<h${heading[1].length}>${inline(heading[2].trim())}</h${heading[1].length}>`);
|
|
2964
|
+
continue;
|
|
2965
|
+
}
|
|
2966
|
+
const item = /^\s*([-*+]|\d+\.)\s+(.*)$/.exec(line);
|
|
2967
|
+
if (item) {
|
|
2968
|
+
flushParagraph();
|
|
2969
|
+
const tag = /\d/.test(item[1]) ? "ol" : "ul";
|
|
2970
|
+
if (list && list.tag !== tag) flushList();
|
|
2971
|
+
list ??= { tag, items: [] };
|
|
2972
|
+
list.items.push(item[2]);
|
|
2973
|
+
continue;
|
|
2974
|
+
}
|
|
2975
|
+
if (line.trim() === "") {
|
|
2976
|
+
flushParagraph();
|
|
2977
|
+
flushList();
|
|
2978
|
+
continue;
|
|
2979
|
+
}
|
|
2980
|
+
flushList();
|
|
2981
|
+
paragraph.push(line.trim());
|
|
2982
|
+
}
|
|
2983
|
+
flushParagraph();
|
|
2984
|
+
flushList();
|
|
2985
|
+
return blocks.join("\n");
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2834
2988
|
// libs/uipack/src/adapters/type-detector.ts
|
|
2835
2989
|
function detectUIType(template) {
|
|
2836
2990
|
if (template === null || template === void 0) {
|
|
@@ -2864,9 +3018,15 @@ function detectUIType(template) {
|
|
|
2864
3018
|
}
|
|
2865
3019
|
|
|
2866
3020
|
// libs/uipack/src/adapters/template-renderer.ts
|
|
2867
|
-
function buildCspConfig(resolver) {
|
|
3021
|
+
function buildCspConfig(resolver, extra) {
|
|
2868
3022
|
const cspResourceDomains = ["https://esm.sh"];
|
|
2869
3023
|
const cspConnectDomains = ["https://esm.sh"];
|
|
3024
|
+
for (const domain of extra?.resourceDomains ?? []) {
|
|
3025
|
+
if (!cspResourceDomains.includes(domain)) cspResourceDomains.push(domain);
|
|
3026
|
+
}
|
|
3027
|
+
for (const domain of extra?.connectDomains ?? []) {
|
|
3028
|
+
if (!cspConnectDomains.includes(domain)) cspConnectDomains.push(domain);
|
|
3029
|
+
}
|
|
2870
3030
|
if (resolver && "overrides" in resolver) {
|
|
2871
3031
|
const overrides = resolver.overrides;
|
|
2872
3032
|
if (overrides) {
|
|
@@ -2906,6 +3066,24 @@ function renderUnwrappedResult(rawResult, options) {
|
|
|
2906
3066
|
}
|
|
2907
3067
|
return rawResult;
|
|
2908
3068
|
}
|
|
3069
|
+
var warnedComponentReferences = /* @__PURE__ */ new WeakMap();
|
|
3070
|
+
function warnComponentReference(template, toolName, logger) {
|
|
3071
|
+
const warned = warnedComponentReferences.get(template) ?? /* @__PURE__ */ new Set();
|
|
3072
|
+
if (warned.has(toolName)) return;
|
|
3073
|
+
warned.add(toolName);
|
|
3074
|
+
warnedComponentReferences.set(template, warned);
|
|
3075
|
+
logger.warn(
|
|
3076
|
+
`[FrontMCP] Tool "${toolName}": \`ui.template\` is a React component reference, which cannot be bundled (there is no source file to compile), so the widget would render an empty page. Use \`template: { file: './widget.tsx' }\` instead.`
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
function isReactElement(value) {
|
|
3080
|
+
return typeof value === "object" && value !== null && "$$typeof" in value;
|
|
3081
|
+
}
|
|
3082
|
+
function isDefinitelyReactComponent(template) {
|
|
3083
|
+
if (typeof template !== "function") return false;
|
|
3084
|
+
const proto = template.prototype;
|
|
3085
|
+
return typeof proto?.render === "function" || template["$$typeof"] !== void 0;
|
|
3086
|
+
}
|
|
2909
3087
|
function renderToolTemplate(options) {
|
|
2910
3088
|
const { toolName, input, output, template, resolver, platformType, sizing } = options;
|
|
2911
3089
|
const uiType = detectUIType(template);
|
|
@@ -2916,30 +3094,41 @@ function renderToolTemplate(options) {
|
|
|
2916
3094
|
output,
|
|
2917
3095
|
includeBridge: true,
|
|
2918
3096
|
resolver,
|
|
2919
|
-
sizing
|
|
3097
|
+
sizing,
|
|
3098
|
+
csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
|
|
2920
3099
|
};
|
|
2921
3100
|
let html2;
|
|
2922
3101
|
let hash = "";
|
|
2923
3102
|
let size = 0;
|
|
2924
3103
|
if (typeof template === "object" && template !== null && "file" in template) {
|
|
2925
|
-
const cspConfig = buildCspConfig(resolver);
|
|
2926
3104
|
const result = renderComponent(
|
|
2927
3105
|
{ source: template, inlineReact: resourceMode === "inline" },
|
|
2928
|
-
{ ...shellConfig, csp:
|
|
3106
|
+
{ ...shellConfig, csp: buildCspConfig(resolver, options.csp) }
|
|
2929
3107
|
);
|
|
2930
3108
|
html2 = result.html;
|
|
2931
3109
|
hash = result.hash;
|
|
2932
3110
|
size = result.size;
|
|
2933
3111
|
} else if (typeof template === "function") {
|
|
2934
|
-
|
|
3112
|
+
const helpers = createTemplateHelpers();
|
|
3113
|
+
const ctx = { input: input ?? {}, output: output ?? {}, helpers };
|
|
3114
|
+
let rawResult;
|
|
3115
|
+
let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
|
|
3116
|
+
if (isHtmlBuilder) {
|
|
3117
|
+
try {
|
|
3118
|
+
rawResult = template(ctx);
|
|
3119
|
+
isHtmlBuilder = !isReactElement(rawResult);
|
|
3120
|
+
} catch (error) {
|
|
3121
|
+
if (uiType !== "react") throw error;
|
|
3122
|
+
isHtmlBuilder = false;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
if (!isHtmlBuilder) {
|
|
3126
|
+
warnComponentReference(template, toolName, options.logger ?? console);
|
|
2935
3127
|
const shellResult = buildShell('<div id="root"></div>', shellConfig);
|
|
2936
3128
|
html2 = shellResult.html;
|
|
2937
3129
|
hash = shellResult.hash;
|
|
2938
3130
|
size = shellResult.size;
|
|
2939
3131
|
} else {
|
|
2940
|
-
const helpers = createTemplateHelpers();
|
|
2941
|
-
const ctx = { input, output, helpers };
|
|
2942
|
-
const rawResult = template(ctx);
|
|
2943
3132
|
const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
|
|
2944
3133
|
if (wrapped) {
|
|
2945
3134
|
html2 = wrapped;
|
|
@@ -2951,7 +3140,7 @@ function renderToolTemplate(options) {
|
|
|
2951
3140
|
}
|
|
2952
3141
|
}
|
|
2953
3142
|
} else if (typeof template === "string") {
|
|
2954
|
-
const shellResult = buildShell(template, shellConfig);
|
|
3143
|
+
const shellResult = buildShell(uiType === "markdown" ? markdownToHtml(template) : template, shellConfig);
|
|
2955
3144
|
html2 = shellResult.html;
|
|
2956
3145
|
hash = shellResult.hash;
|
|
2957
3146
|
size = shellResult.size;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
|
|
2
|
+
const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
|
|
3
|
+
// __frontmcpCreateRequire
|
|
1
4
|
// libs/uipack/src/bridge-runtime/iife-generator.ts
|
|
2
5
|
function generateBridgeIIFE(options = {}) {
|
|
3
6
|
const { debug = false, trustedOrigins = [], minify = false } = options;
|
|
@@ -265,12 +268,38 @@ function __applyHostTheme(theme) {
|
|
|
265
268
|
} catch (e) {}
|
|
266
269
|
}
|
|
267
270
|
|
|
271
|
+
function extractToolOutput(params) {
|
|
272
|
+
// A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
|
|
273
|
+
if (!params) return undefined;
|
|
274
|
+
if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
|
|
275
|
+
var content = params.content;
|
|
276
|
+
if (Array.isArray(content)) {
|
|
277
|
+
for (var i = 0; i < content.length; i++) {
|
|
278
|
+
var item = content[i];
|
|
279
|
+
if (item && item.type === 'text' && typeof item.text === 'string') {
|
|
280
|
+
try { return JSON.parse(item.text); } catch (e) { return item.text; }
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return content;
|
|
285
|
+
}
|
|
286
|
+
|
|
268
287
|
function readInjectedData() {
|
|
269
288
|
var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
|
|
270
289
|
if (typeof window !== 'undefined') {
|
|
271
290
|
if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
|
|
272
291
|
if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
|
|
273
292
|
if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
|
|
293
|
+
// The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
|
|
294
|
+
// (toolOutput is the tool's structuredContent).
|
|
295
|
+
var oa = window.openai;
|
|
296
|
+
if (oa && typeof oa === 'object') {
|
|
297
|
+
if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
|
|
298
|
+
if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
|
|
299
|
+
if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
|
|
300
|
+
if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
274
303
|
}
|
|
275
304
|
return data;
|
|
276
305
|
}
|
|
@@ -319,7 +348,24 @@ var OpenAIAdapter = {
|
|
|
319
348
|
if (sdk.displayMode) {
|
|
320
349
|
context.hostContext.displayMode = sdk.displayMode;
|
|
321
350
|
}
|
|
322
|
-
//
|
|
351
|
+
// The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
|
|
352
|
+
window.addEventListener('openai:set_globals', function(event) {
|
|
353
|
+
var globals = (event && event.detail && event.detail.globals) || window.openai || {};
|
|
354
|
+
if (globals.toolInput !== undefined && globals.toolInput !== null) {
|
|
355
|
+
context.toolInput = globals.toolInput;
|
|
356
|
+
window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
|
|
357
|
+
}
|
|
358
|
+
if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
|
|
359
|
+
context.structuredContent = globals.toolOutput;
|
|
360
|
+
context.notifyToolResult(globals.toolOutput);
|
|
361
|
+
}
|
|
362
|
+
if (globals.theme && globals.theme !== context.hostContext.theme) {
|
|
363
|
+
context.notifyContextChange({ theme: globals.theme });
|
|
364
|
+
}
|
|
365
|
+
if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
|
|
366
|
+
context.notifyContextChange({ displayMode: globals.displayMode });
|
|
367
|
+
}
|
|
368
|
+
});
|
|
323
369
|
return Promise.resolve();
|
|
324
370
|
},
|
|
325
371
|
callTool: function(context, name, args) {
|
|
@@ -466,9 +512,10 @@ var ExtAppsAdapter = {
|
|
|
466
512
|
window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
|
|
467
513
|
break;
|
|
468
514
|
case 'ui/notifications/tool-result':
|
|
469
|
-
|
|
515
|
+
var resultOutput = extractToolOutput(params);
|
|
516
|
+
context.toolOutput = resultOutput;
|
|
470
517
|
context.structuredContent = params.structuredContent;
|
|
471
|
-
context.notifyToolResult(
|
|
518
|
+
context.notifyToolResult(resultOutput);
|
|
472
519
|
window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
|
|
473
520
|
break;
|
|
474
521
|
case 'ui/notifications/host-context-changed':
|
|
@@ -522,6 +569,15 @@ var ExtAppsAdapter = {
|
|
|
522
569
|
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
|
|
523
570
|
});
|
|
524
571
|
},
|
|
572
|
+
sendNotification: function(method, params) {
|
|
573
|
+
// JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
|
|
574
|
+
if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
575
|
+
return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
|
|
576
|
+
}
|
|
577
|
+
var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
|
|
578
|
+
window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
|
|
579
|
+
return Promise.resolve();
|
|
580
|
+
},
|
|
525
581
|
sendHandshake: function(context) {
|
|
526
582
|
// Send ui/initialize using '*' as target origin since TOFU hasn't
|
|
527
583
|
// been established yet. The response from the host will establish
|
|
@@ -587,7 +643,7 @@ var ExtAppsAdapter = {
|
|
|
587
643
|
var pending = this.pendingSize;
|
|
588
644
|
if (!pending) return;
|
|
589
645
|
this.pendingSize = null;
|
|
590
|
-
this.
|
|
646
|
+
this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
|
|
591
647
|
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
|
|
592
648
|
}, function(err) {
|
|
593
649
|
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
|
|
@@ -614,13 +670,11 @@ var ExtAppsAdapter = {
|
|
|
614
670
|
return this.sendRequest('ui/setDisplayMode', { mode: mode });
|
|
615
671
|
},
|
|
616
672
|
setSize: function(context, size) {
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
var params = {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
aspectRatio: size && size.aspectRatio
|
|
623
|
-
};
|
|
673
|
+
// Standard ext-apps sizing: the view tells the host its dimensions with the
|
|
674
|
+
// ui/notifications/size-changed notification ({ width?, height? } in px).
|
|
675
|
+
var params = {};
|
|
676
|
+
if (size && typeof size.width === 'number') params.width = size.width;
|
|
677
|
+
if (size && typeof size.height === 'number') params.height = size.height;
|
|
624
678
|
if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
625
679
|
// No host origin yet: keep only the latest size and send it when the
|
|
626
680
|
// handshake settles; every caller's promise follows that one request.
|
|
@@ -631,7 +685,7 @@ var ExtAppsAdapter = {
|
|
|
631
685
|
self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
|
|
632
686
|
});
|
|
633
687
|
}
|
|
634
|
-
return this.
|
|
688
|
+
return this.sendNotification('ui/notifications/size-changed', params);
|
|
635
689
|
},
|
|
636
690
|
requestClose: function(context) {
|
|
637
691
|
return this.sendRequest('ui/close', {});
|
|
@@ -1054,7 +1108,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
|
|
|
1054
1108
|
|
|
1055
1109
|
// Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
|
|
1056
1110
|
// Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
|
|
1057
|
-
// ext-apps sends ui/
|
|
1111
|
+
// ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
|
|
1058
1112
|
FrontMcpBridge.prototype.setSize = function(size) {
|
|
1059
1113
|
if (!this._adapter) return Promise.reject(new Error('Not initialized'));
|
|
1060
1114
|
if (!this._adapter.setSize) return Promise.resolve();
|
|
@@ -1156,7 +1210,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
|
|
|
1156
1210
|
target.classList.add('opacity-50', 'cursor-not-allowed');
|
|
1157
1211
|
|
|
1158
1212
|
// Add spinner for buttons
|
|
1159
|
-
var spinner = '<svg class="animate-spin -ml-1 mr-2 h-4 w-4 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
|
|
1213
|
+
var spinner = '<svg class="animate-spin -ml-1 mr-2 h-4 w-4 inline" style="width:1em;height:1em;vertical-align:-0.125em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
|
|
1160
1214
|
if (target.tagName === 'BUTTON') {
|
|
1161
1215
|
target.innerHTML = spinner + 'Loading...';
|
|
1162
1216
|
}
|
|
@@ -1237,7 +1291,22 @@ function minifyJS(code) {
|
|
|
1237
1291
|
if (code.length > MAX_MINIFY_CODE_LENGTH) {
|
|
1238
1292
|
return code;
|
|
1239
1293
|
}
|
|
1240
|
-
|
|
1294
|
+
const out = [];
|
|
1295
|
+
let inBlockComment = false;
|
|
1296
|
+
for (const raw of code.split("\n")) {
|
|
1297
|
+
const line = raw.trim();
|
|
1298
|
+
if (inBlockComment) {
|
|
1299
|
+
if (line.includes("*/")) inBlockComment = false;
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
1303
|
+
if (line.startsWith("/*")) {
|
|
1304
|
+
if (!line.includes("*/")) inBlockComment = true;
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
out.push(line);
|
|
1308
|
+
}
|
|
1309
|
+
return out.join("\n");
|
|
1241
1310
|
}
|
|
1242
1311
|
function generatePlatformBundle(platform, options = {}) {
|
|
1243
1312
|
const platformAdapters = {
|