@kolbo/mcp 1.87.8 → 1.87.10
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/package.json +1 -1
- package/src/apps/bridge.js +69 -0
- package/src/apps/index.js +43 -18
package/package.json
CHANGED
package/src/apps/bridge.js
CHANGED
|
@@ -210,6 +210,75 @@ const BRIDGE_JS = `
|
|
|
210
210
|
},
|
|
211
211
|
hostContext: function () { return hostContext; }
|
|
212
212
|
};
|
|
213
|
+
|
|
214
|
+
// ChatGPT (OpenAI Apps SDK) host. It mounts the same iframe but speaks no
|
|
215
|
+
// JSON-RPC: tool input / output / theme arrive as window.openai globals plus
|
|
216
|
+
// an 'openai:set_globals' DOM event, and actions are methods on that object.
|
|
217
|
+
// Without this adapter the card rendered, waited for a tool-input that never
|
|
218
|
+
// came, and sat on "Preparing" with no prompt, no settings and no result —
|
|
219
|
+
// even after the tool had returned. Everything here maps onto the same
|
|
220
|
+
// callbacks the JSON-RPC path drives, so widgets need no host-specific code.
|
|
221
|
+
var oa = window.openai && typeof window.openai === 'object' ? window.openai : null;
|
|
222
|
+
if (oa) {
|
|
223
|
+
var oaInputSeen = false, oaOutputSeen = false;
|
|
224
|
+
var oaName = function () {
|
|
225
|
+
var meta = oa.toolResponseMetadata || {};
|
|
226
|
+
return oa.toolName || meta['kolbo/tool'] || (oa.toolOutput && oa.toolOutput.tool) || undefined;
|
|
227
|
+
};
|
|
228
|
+
var oaSync = function () {
|
|
229
|
+
var name = oaName();
|
|
230
|
+
// Same shape claude.ai hands ready(): toolInfo.tool.name is what widgets read.
|
|
231
|
+
hostContext = { theme: oa.theme, displayMode: oa.displayMode, toolInfo: { name: name, tool: { name: name }, arguments: oa.toolInput || {} } };
|
|
232
|
+
if (!initialized) {
|
|
233
|
+
initialized = true;
|
|
234
|
+
queue = [];
|
|
235
|
+
readyFns.forEach(function (f) { try { f(hostContext); } catch (e) {} });
|
|
236
|
+
} else {
|
|
237
|
+
themeFns.forEach(function (f) { try { f(hostContext); } catch (e) {} });
|
|
238
|
+
}
|
|
239
|
+
if (oa.toolInput && !oaInputSeen) {
|
|
240
|
+
oaInputSeen = true;
|
|
241
|
+
toolInputFns.forEach(function (f) { try { f(oa.toolInput, { name: oaName(), arguments: oa.toolInput }); } catch (e) {} });
|
|
242
|
+
}
|
|
243
|
+
if (oa.toolOutput && !oaOutputSeen) {
|
|
244
|
+
oaOutputSeen = true;
|
|
245
|
+
var res = { structuredContent: oa.toolOutput, _meta: oa.toolResponseMetadata || {} };
|
|
246
|
+
toolResultFns.forEach(function (f) { try { f(res); } catch (e) {} });
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
var oaCall = function (method) { return typeof oa[method] === 'function'; };
|
|
250
|
+
window.kolbo.callTool = function (name, args) {
|
|
251
|
+
if (!oaCall('callTool')) return Promise.reject(new Error('host cannot call tools'));
|
|
252
|
+
return Promise.resolve(oa.callTool(name, args || {})).then(function (r) {
|
|
253
|
+
// The SDK resolves with the CallToolResult (content + structuredContent);
|
|
254
|
+
// some builds unwrap to the structured payload — normalise either.
|
|
255
|
+
if (r && (r.structuredContent || r.content)) return r;
|
|
256
|
+
return { structuredContent: r, content: [] };
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
window.kolbo.sendMessage = function (text) {
|
|
260
|
+
return oaCall('sendFollowUpMessage') ? Promise.resolve(oa.sendFollowUpMessage({ prompt: text })) : Promise.reject(new Error('unsupported'));
|
|
261
|
+
};
|
|
262
|
+
window.kolbo.insertText = function () { return Promise.reject(new Error('unsupported')); };
|
|
263
|
+
window.kolbo.openLink = function (url) {
|
|
264
|
+
return oaCall('openExternal') ? Promise.resolve(oa.openExternal({ href: url })) : Promise.resolve(window.open(url, '_blank'));
|
|
265
|
+
};
|
|
266
|
+
window.kolbo.copyText = function (t) {
|
|
267
|
+
return navigator.clipboard && navigator.clipboard.writeText ? navigator.clipboard.writeText(t) : Promise.reject(new Error('unsupported'));
|
|
268
|
+
};
|
|
269
|
+
window.kolbo.attachMedia = function () { return Promise.reject(new Error('unsupported')); };
|
|
270
|
+
window.kolbo.updateModelContext = function () { return Promise.resolve(); };
|
|
271
|
+
window.kolbo.requestDisplayMode = function (mode) {
|
|
272
|
+
if (!oaCall('requestDisplayMode')) return Promise.resolve({ mode: 'inline' });
|
|
273
|
+
return Promise.resolve(oa.requestDisplayMode({ mode: mode })).then(function (r) { return { mode: (r && r.mode) || mode }; });
|
|
274
|
+
};
|
|
275
|
+
window.addEventListener('openai:set_globals', oaSync);
|
|
276
|
+
// Deferred: the widget's own script (which registers ready/onToolInput/
|
|
277
|
+
// onToolResult) runs AFTER this bridge block in the same document, and a
|
|
278
|
+
// synchronous sync here fired into empty listener lists and marked the
|
|
279
|
+
// input/output as seen.
|
|
280
|
+
setTimeout(oaSync, 0);
|
|
281
|
+
}
|
|
213
282
|
})();
|
|
214
283
|
`;
|
|
215
284
|
|
package/src/apps/index.js
CHANGED
|
@@ -48,10 +48,29 @@ const WIDGET_BUILDERS = {
|
|
|
48
48
|
// Widgets are pure functions of source — build once per process.
|
|
49
49
|
const htmlCache = new Map();
|
|
50
50
|
function widgetHtml(uri) {
|
|
51
|
+
uri = String(uri).split('?')[0]; // versioned and bare URIs serve the same page
|
|
51
52
|
if (!htmlCache.has(uri)) htmlCache.set(uri, WIDGET_BUILDERS[uri]());
|
|
52
53
|
return htmlCache.get(uri);
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
// ChatGPT treats the widget URI as a permanent cache key: it fetched
|
|
57
|
+
// generation.html once per app link and kept serving that snapshot across every
|
|
58
|
+
// later release (1.87.9's ChatGPT fix never reached a card until the URI
|
|
59
|
+
// changed — verified 2026-09-06 via its widget endpoint, where only
|
|
60
|
+
// force_local=true returned the new HTML). OpenAI's own guidance is "treat the
|
|
61
|
+
// resource URI as a cache key; publish a new URI on every change", so the URI
|
|
62
|
+
// every tool declares and returns carries a content hash of the HTML it points
|
|
63
|
+
// at. The bare URI stays registered for hosts holding an older tools/list.
|
|
64
|
+
const crypto = require('crypto');
|
|
65
|
+
const versionCache = new Map();
|
|
66
|
+
function versionedUri(uri) {
|
|
67
|
+
if (!versionCache.has(uri)) {
|
|
68
|
+
const v = crypto.createHash('sha1').update(widgetHtml(uri)).digest('hex').slice(0, 10);
|
|
69
|
+
versionCache.set(uri, uri + '?v=' + v);
|
|
70
|
+
}
|
|
71
|
+
return versionCache.get(uri);
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
// Hosts apply a deny-by-default CSP to widget iframes — without this
|
|
56
75
|
// declaration EVERY external asset (generated images/videos on the CDN, model
|
|
57
76
|
// icons, Google Fonts) is silently blocked. resourceDomains maps to
|
|
@@ -71,12 +90,12 @@ const WIDGET_CSP = {
|
|
|
71
90
|
// Public hosts owned by Kolbo.
|
|
72
91
|
'https://api.kolbo.ai',
|
|
73
92
|
'https://app.kolbo.ai',
|
|
74
|
-
'https://cdn.kolbo.ai',
|
|
75
|
-
// Preset thumbnails and library media live on the media-* hosts (prod
|
|
76
|
-
// presets still reference media-dev); without these every preset tile
|
|
77
|
-
// rendered as a black box. All three are on kolbo-api's download allowlist.
|
|
78
|
-
'https://media.kolbo.ai',
|
|
79
|
-
'https://media-staging.kolbo.ai',
|
|
93
|
+
'https://cdn.kolbo.ai',
|
|
94
|
+
// Preset thumbnails and library media live on the media-* hosts (prod
|
|
95
|
+
// presets still reference media-dev); without these every preset tile
|
|
96
|
+
// rendered as a black box. All three are on kolbo-api's download allowlist.
|
|
97
|
+
'https://media.kolbo.ai',
|
|
98
|
+
'https://media-staging.kolbo.ai',
|
|
80
99
|
'https://media-dev.kolbo.ai',
|
|
81
100
|
...KOLBO_MEDIA_DOMAINS,
|
|
82
101
|
'https://kolbo-general-media.fra1.digitaloceanspaces.com',
|
|
@@ -145,22 +164,25 @@ function registerApps(server) {
|
|
|
145
164
|
[UI.list, 'Kolbo List Widget'],
|
|
146
165
|
[UI.plans, 'Kolbo Plans Widget'],
|
|
147
166
|
]) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
167
|
+
for (const u of [versionedUri(uri), uri]) {
|
|
168
|
+
registerAppResource(
|
|
169
|
+
server, name + (u === uri ? ' (unversioned)' : ''), u,
|
|
170
|
+
{ mimeType: RESOURCE_MIME_TYPE, _meta: { csp: WIDGET_CSP, ui: { csp: WIDGET_CSP } } },
|
|
171
|
+
async () => ({
|
|
172
|
+
contents: [{
|
|
173
|
+
uri: u, mimeType: RESOURCE_MIME_TYPE, text: widgetHtml(uri),
|
|
174
|
+
_meta: { csp: WIDGET_CSP, ui: { csp: WIDGET_CSP } },
|
|
175
|
+
}],
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
}
|
|
158
179
|
}
|
|
159
180
|
}
|
|
160
181
|
|
|
161
182
|
/** `_meta` for a tool RESULT (and optionally for tool registration). */
|
|
162
183
|
function uiMeta(uri) {
|
|
163
|
-
|
|
184
|
+
const u = versionedUri(uri);
|
|
185
|
+
return { [RESOURCE_URI_META_KEY]: u, ui: { resourceUri: u } };
|
|
164
186
|
}
|
|
165
187
|
|
|
166
188
|
/**
|
|
@@ -201,7 +223,9 @@ function uiResult(uri, text, structured) {
|
|
|
201
223
|
return {
|
|
202
224
|
content: [{ type: 'text', text }],
|
|
203
225
|
structuredContent: structured,
|
|
204
|
-
|
|
226
|
+
// 'kolbo/tool' reaches ChatGPT widgets as toolResponseMetadata — that host
|
|
227
|
+
// never tells the iframe which tool ran, and the card title depends on it.
|
|
228
|
+
_meta: { ...uiMeta(uri), ...(structured && structured.tool ? { 'kolbo/tool': structured.tool } : {}) },
|
|
205
229
|
};
|
|
206
230
|
}
|
|
207
231
|
|
|
@@ -738,6 +762,7 @@ module.exports = {
|
|
|
738
762
|
TOOL_WIDGETS,
|
|
739
763
|
registerApps,
|
|
740
764
|
attachToolWidgetMeta,
|
|
765
|
+
versionedUri,
|
|
741
766
|
uiMeta,
|
|
742
767
|
uiResult,
|
|
743
768
|
listResult,
|