@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.87.8",
3
+ "version": "1.87.10",
4
4
  "description": "Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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
- registerAppResource(
149
- server, name, uri,
150
- { mimeType: RESOURCE_MIME_TYPE, _meta: { csp: WIDGET_CSP, ui: { csp: WIDGET_CSP } } },
151
- async () => ({
152
- contents: [{
153
- uri, mimeType: RESOURCE_MIME_TYPE, text: widgetHtml(uri),
154
- _meta: { csp: WIDGET_CSP, ui: { csp: WIDGET_CSP } },
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
- return { [RESOURCE_URI_META_KEY]: uri, ui: { resourceUri: uri } };
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
- _meta: uiMeta(uri),
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,