@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/shell/index.js
CHANGED
|
@@ -317,12 +317,38 @@ function __applyHostTheme(theme) {
|
|
|
317
317
|
} catch (e) {}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
function extractToolOutput(params) {
|
|
321
|
+
// A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
|
|
322
|
+
if (!params) return undefined;
|
|
323
|
+
if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
|
|
324
|
+
var content = params.content;
|
|
325
|
+
if (Array.isArray(content)) {
|
|
326
|
+
for (var i = 0; i < content.length; i++) {
|
|
327
|
+
var item = content[i];
|
|
328
|
+
if (item && item.type === 'text' && typeof item.text === 'string') {
|
|
329
|
+
try { return JSON.parse(item.text); } catch (e) { return item.text; }
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return content;
|
|
334
|
+
}
|
|
335
|
+
|
|
320
336
|
function readInjectedData() {
|
|
321
337
|
var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
|
|
322
338
|
if (typeof window !== 'undefined') {
|
|
323
339
|
if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
|
|
324
340
|
if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
|
|
325
341
|
if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
|
|
342
|
+
// The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
|
|
343
|
+
// (toolOutput is the tool's structuredContent).
|
|
344
|
+
var oa = window.openai;
|
|
345
|
+
if (oa && typeof oa === 'object') {
|
|
346
|
+
if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
|
|
347
|
+
if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
|
|
348
|
+
if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
|
|
349
|
+
if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
326
352
|
}
|
|
327
353
|
return data;
|
|
328
354
|
}
|
|
@@ -371,7 +397,24 @@ var OpenAIAdapter = {
|
|
|
371
397
|
if (sdk.displayMode) {
|
|
372
398
|
context.hostContext.displayMode = sdk.displayMode;
|
|
373
399
|
}
|
|
374
|
-
//
|
|
400
|
+
// The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
|
|
401
|
+
window.addEventListener('openai:set_globals', function(event) {
|
|
402
|
+
var globals = (event && event.detail && event.detail.globals) || window.openai || {};
|
|
403
|
+
if (globals.toolInput !== undefined && globals.toolInput !== null) {
|
|
404
|
+
context.toolInput = globals.toolInput;
|
|
405
|
+
window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
|
|
406
|
+
}
|
|
407
|
+
if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
|
|
408
|
+
context.structuredContent = globals.toolOutput;
|
|
409
|
+
context.notifyToolResult(globals.toolOutput);
|
|
410
|
+
}
|
|
411
|
+
if (globals.theme && globals.theme !== context.hostContext.theme) {
|
|
412
|
+
context.notifyContextChange({ theme: globals.theme });
|
|
413
|
+
}
|
|
414
|
+
if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
|
|
415
|
+
context.notifyContextChange({ displayMode: globals.displayMode });
|
|
416
|
+
}
|
|
417
|
+
});
|
|
375
418
|
return Promise.resolve();
|
|
376
419
|
},
|
|
377
420
|
callTool: function(context, name, args) {
|
|
@@ -518,9 +561,10 @@ var ExtAppsAdapter = {
|
|
|
518
561
|
window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
|
|
519
562
|
break;
|
|
520
563
|
case 'ui/notifications/tool-result':
|
|
521
|
-
|
|
564
|
+
var resultOutput = extractToolOutput(params);
|
|
565
|
+
context.toolOutput = resultOutput;
|
|
522
566
|
context.structuredContent = params.structuredContent;
|
|
523
|
-
context.notifyToolResult(
|
|
567
|
+
context.notifyToolResult(resultOutput);
|
|
524
568
|
window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
|
|
525
569
|
break;
|
|
526
570
|
case 'ui/notifications/host-context-changed':
|
|
@@ -574,6 +618,15 @@ var ExtAppsAdapter = {
|
|
|
574
618
|
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
|
|
575
619
|
});
|
|
576
620
|
},
|
|
621
|
+
sendNotification: function(method, params) {
|
|
622
|
+
// JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
|
|
623
|
+
if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
624
|
+
return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
|
|
625
|
+
}
|
|
626
|
+
var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
|
|
627
|
+
window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
|
|
628
|
+
return Promise.resolve();
|
|
629
|
+
},
|
|
577
630
|
sendHandshake: function(context) {
|
|
578
631
|
// Send ui/initialize using '*' as target origin since TOFU hasn't
|
|
579
632
|
// been established yet. The response from the host will establish
|
|
@@ -639,7 +692,7 @@ var ExtAppsAdapter = {
|
|
|
639
692
|
var pending = this.pendingSize;
|
|
640
693
|
if (!pending) return;
|
|
641
694
|
this.pendingSize = null;
|
|
642
|
-
this.
|
|
695
|
+
this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
|
|
643
696
|
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
|
|
644
697
|
}, function(err) {
|
|
645
698
|
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
|
|
@@ -666,13 +719,11 @@ var ExtAppsAdapter = {
|
|
|
666
719
|
return this.sendRequest('ui/setDisplayMode', { mode: mode });
|
|
667
720
|
},
|
|
668
721
|
setSize: function(context, size) {
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
var params = {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
aspectRatio: size && size.aspectRatio
|
|
675
|
-
};
|
|
722
|
+
// Standard ext-apps sizing: the view tells the host its dimensions with the
|
|
723
|
+
// ui/notifications/size-changed notification ({ width?, height? } in px).
|
|
724
|
+
var params = {};
|
|
725
|
+
if (size && typeof size.width === 'number') params.width = size.width;
|
|
726
|
+
if (size && typeof size.height === 'number') params.height = size.height;
|
|
676
727
|
if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
677
728
|
// No host origin yet: keep only the latest size and send it when the
|
|
678
729
|
// handshake settles; every caller's promise follows that one request.
|
|
@@ -683,7 +734,7 @@ var ExtAppsAdapter = {
|
|
|
683
734
|
self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
|
|
684
735
|
});
|
|
685
736
|
}
|
|
686
|
-
return this.
|
|
737
|
+
return this.sendNotification('ui/notifications/size-changed', params);
|
|
687
738
|
},
|
|
688
739
|
requestClose: function(context) {
|
|
689
740
|
return this.sendRequest('ui/close', {});
|
|
@@ -1106,7 +1157,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
|
|
|
1106
1157
|
|
|
1107
1158
|
// Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
|
|
1108
1159
|
// Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
|
|
1109
|
-
// ext-apps sends ui/
|
|
1160
|
+
// ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
|
|
1110
1161
|
FrontMcpBridge.prototype.setSize = function(size) {
|
|
1111
1162
|
if (!this._adapter) return Promise.reject(new Error('Not initialized'));
|
|
1112
1163
|
if (!this._adapter.setSize) return Promise.resolve();
|
|
@@ -1208,7 +1259,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
|
|
|
1208
1259
|
target.classList.add('opacity-50', 'cursor-not-allowed');
|
|
1209
1260
|
|
|
1210
1261
|
// Add spinner for buttons
|
|
1211
|
-
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>';
|
|
1262
|
+
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>';
|
|
1212
1263
|
if (target.tagName === 'BUTTON') {
|
|
1213
1264
|
target.innerHTML = spinner + 'Loading...';
|
|
1214
1265
|
}
|
|
@@ -1289,7 +1340,22 @@ function minifyJS(code) {
|
|
|
1289
1340
|
if (code.length > MAX_MINIFY_CODE_LENGTH) {
|
|
1290
1341
|
return code;
|
|
1291
1342
|
}
|
|
1292
|
-
|
|
1343
|
+
const out = [];
|
|
1344
|
+
let inBlockComment = false;
|
|
1345
|
+
for (const raw of code.split("\n")) {
|
|
1346
|
+
const line = raw.trim();
|
|
1347
|
+
if (inBlockComment) {
|
|
1348
|
+
if (line.includes("*/")) inBlockComment = false;
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
1352
|
+
if (line.startsWith("/*")) {
|
|
1353
|
+
if (!line.includes("*/")) inBlockComment = true;
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
out.push(line);
|
|
1357
|
+
}
|
|
1358
|
+
return out.join("\n");
|
|
1293
1359
|
}
|
|
1294
1360
|
function generatePlatformBundle(platform, options = {}) {
|
|
1295
1361
|
const platformAdapters = {
|
|
@@ -1353,11 +1419,8 @@ function buildCSPDirectives(csp) {
|
|
|
1353
1419
|
directives.push(`img-src ${imgSources.join(" ")}`);
|
|
1354
1420
|
const fontSources = ["'self'", "data:", ...allResourceDomains];
|
|
1355
1421
|
directives.push(`font-src ${fontSources.join(" ")}`);
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
} else {
|
|
1359
|
-
directives.push(`connect-src ${allResourceDomains.join(" ")}`);
|
|
1360
|
-
}
|
|
1422
|
+
const connectSources = [.../* @__PURE__ */ new Set([...allResourceDomains, ...validConnectDomains])];
|
|
1423
|
+
directives.push(`connect-src ${connectSources.join(" ")}`);
|
|
1361
1424
|
directives.push("object-src 'self' data:");
|
|
1362
1425
|
return directives;
|
|
1363
1426
|
}
|
|
@@ -1366,26 +1429,34 @@ function buildCSPMetaTag(csp) {
|
|
|
1366
1429
|
const content = directives.join("; ");
|
|
1367
1430
|
return `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(content)}">`;
|
|
1368
1431
|
}
|
|
1432
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
1369
1433
|
function validateCSPDomain(domain) {
|
|
1370
|
-
if (domain
|
|
1371
|
-
|
|
1372
|
-
|
|
1434
|
+
if (typeof domain !== "string" || /[\s;,'"]/.test(domain)) return false;
|
|
1435
|
+
if (/[?#]/.test(domain)) return false;
|
|
1436
|
+
if (domain.includes("*")) {
|
|
1437
|
+
const wildcard = /^(?:https|wss):\/\/\*\.(.*)$/.exec(domain);
|
|
1438
|
+
return wildcard !== null && /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(wildcard[1]);
|
|
1373
1439
|
}
|
|
1374
1440
|
try {
|
|
1375
1441
|
const url = new URL(domain);
|
|
1376
|
-
|
|
1442
|
+
if (url.protocol === "https:" || url.protocol === "wss:") return true;
|
|
1443
|
+
return (url.protocol === "http:" || url.protocol === "ws:") && LOOPBACK_HOSTS.has(url.hostname);
|
|
1377
1444
|
} catch {
|
|
1378
1445
|
return false;
|
|
1379
1446
|
}
|
|
1380
1447
|
}
|
|
1448
|
+
var reportedInvalidDomains = /* @__PURE__ */ new Set();
|
|
1381
1449
|
function sanitizeCSPDomains(domains) {
|
|
1382
1450
|
if (!domains) return [];
|
|
1383
1451
|
const valid = [];
|
|
1384
1452
|
for (const domain of domains) {
|
|
1385
1453
|
if (validateCSPDomain(domain)) {
|
|
1386
1454
|
valid.push(domain);
|
|
1387
|
-
} else {
|
|
1388
|
-
|
|
1455
|
+
} else if (!reportedInvalidDomains.has(domain)) {
|
|
1456
|
+
reportedInvalidDomains.add(domain);
|
|
1457
|
+
console.warn(
|
|
1458
|
+
`Invalid CSP domain ignored: ${domain} (expected an https:// or wss:// origin, or http:// / ws:// on localhost)`
|
|
1459
|
+
);
|
|
1389
1460
|
}
|
|
1390
1461
|
}
|
|
1391
1462
|
return valid;
|
package/types/ui-config.d.ts
CHANGED
|
@@ -418,7 +418,7 @@ export interface UITemplateConfig<In = unknown, Out = unknown> {
|
|
|
418
418
|
* - Claude / static widgets: the host measures DOM height itself, so this is
|
|
419
419
|
* a CSS-only no-op.
|
|
420
420
|
* - OpenAI: forwarded through the Apps SDK sizing API.
|
|
421
|
-
* - ext-apps hosts: reported
|
|
421
|
+
* - ext-apps hosts: reported with the standard `ui/notifications/size-changed` notification.
|
|
422
422
|
*
|
|
423
423
|
* @default true (when the widget configures sizing)
|
|
424
424
|
*/
|