@kolbo/mcp 1.31.5 → 1.31.7

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.31.5",
3
+ "version": "1.31.7",
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": {
@@ -1,7 +1,7 @@
1
1
  # AUTO-GENERATED — do not edit
2
2
 
3
3
  This skill/ tree is mirrored from kolbo-code (the single source of truth)
4
- by .github/workflows/sync-skill-to-plugin.yml — synced from kolbo-code@9cf6c35.
4
+ by .github/workflows/sync-skill-to-plugin.yml — synced from kolbo-code@53720a8.
5
5
 
6
6
  It is the skill that 'npx @kolbo/mcp install' deploys into the user's agent.
7
7
  To change it, edit packages/opencode/skills/kolbo/ in kolbo-code and push;
package/skill/SKILL.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- version: 0.5.0
2
+ version: 0.5.1
3
3
  name: kolbo
4
4
  description: |
5
5
  Generate, edit, or analyze creative media via the Kolbo AI MCP server:
@@ -113,6 +113,7 @@ Each `references/models/*.md` mirrors the matching skill prompt in `kolbo-api/sr
113
113
  | `upload_media` / `list_media` / `get_media` / `get_media_stats` / `favorite_media` / `unfavorite_media` / `delete_media` / `restore_media` / `permanently_delete_media` / `move_media` / `bulk_*_media` / `*_media_folder` | Media library — see `workflows/media-library.md` |
114
114
  | `create_visual_dna` / `list_visual_dnas` / `get_visual_dna` / `delete_visual_dna` | Visual DNA — see `workflows/visual-dna.md` |
115
115
  | `list_moodboards` / `get_moodboard` / `list_presets` | Style overlays |
116
+ | `search_stock_media` / `get_stock_sources` / `get_stock_categories` / `get_stock_collections` / `get_stock_asset` / `analyze_script_for_stock` / `import_stock_asset` | Stock library (free, no credits) — EXISTING photos / videos / 3D / SFX / music. For stock **music** use `search_stock_media` with `mediaType: "music"` (semantic vibe query, e.g. "uplifting corporate background") → `get_stock_asset` for downloads. The older `*_music_library` tools are deprecated adapters over this — prefer the stock tools. |
116
117
  | `chat_send_message` / `chat_list_conversations` / `chat_get_messages` | Kolbo chat with optional `media_urls` (up to 10 per call) |
117
118
  | `app_builder_*` (9 tools) | Full React app generation — see `workflows/app-builder.md` |
118
119
  | `publish_html_artifact` | Publish HTML / SVG / Mermaid to `sites.kolbo.ai`. Server dedupes by content hash. Strict CSP. |
@@ -149,7 +150,7 @@ A user-named tool — in any language — overrides every other rule. Recognized
149
150
  1. **Check credits** ONCE per conversation (Step 0). Skip if already checked.
150
151
  2. **Discover models** with `list_models` using a `type` filter — but **skip when the user names a specific model**.
151
152
  3. **Pick the model**:
152
- - User named one → use it.
153
+ - User named one → use it. Model identifiers resolve leniently — shorthand like `"z-image"` or `"nano banana 2"` auto-resolves to the exact identifier, so don't over-engineer exact-id lookups (`list_models` is still authoritative for constraints, caps, and pricing).
153
154
  - Auto-select → only from "Auto-selectable" section (models with a `summary`). Cheapest fit. Prefer `[RECOMMENDED]` when cost is similar.
154
155
  - Never auto-select from "Named-only" section.
155
156
  4. **Validate inputs** against model caps — see `references/workflows/cost-and-validation.md`.
@@ -203,6 +204,8 @@ Existing video → modify → **single `generate_video_from_video` call** with s
203
204
 
204
205
  **Use a TRUE video-to-video model.** Image-to-video models reject with `WRONG_MODEL_TYPE`. Valid: `wan/2-7-videoedit`, `happyhorse/video-edit`, `kling-video/o3-video-to-video`, or any model whose DB `type` includes `video_to_video` (use `list_models({ type: "video_to_video" })`).
205
206
 
207
+ **Motion-control / animate-move models invert the inputs**: `reference_images[0]` = the CHARACTER IMAGE to animate, `source_video` = the driving/reference video whose motion is transferred. Omitting the character image returns a `MOTION_CONTROL_INPUTS` error.
208
+
206
209
  **Do NOT** decompose into frames. **Do NOT** re-fire if the first call returned URLs.
207
210
 
208
211
  ## ⚠️ Character-Driven Video — Frames First, Then Animate (CRITICAL)
package/skill/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.0
1
+ 0.5.1
@@ -8,9 +8,12 @@ Load this file when the user wants AI-generated **music** — full songs, lyrics
8
8
 
9
9
  **Kolbo MCP routing:** call `generate_music`. Suno is a model option — use `list_models({ type: "music_gen" })` to see versions. Pass `instrumental` and `duration` as separate params; pass the Style/Description text as `style` and the Lyrics as `lyrics`.
10
10
 
11
+ **Wants an EXISTING track, not a new song?** ("background music", "stock music", "royalty-free track") → don't generate. Use `search_stock_media` with `mediaType: "music"` (semantic vibe query — "tense cinematic pulse", "uplifting corporate background") → `get_stock_asset` for download URLs. Free, no credits. The older `*_music_library` tools are deprecated adapters over the stock library — prefer the stock tools.
12
+
11
13
  ## CRITICAL Kolbo Platform Rules
12
14
 
13
15
  - **Model version, duration, and instrumental toggle are MCP-tool params.** Don't write `v4.5`, `30 seconds`, or `instrumental: true` inside the prompt fields themselves.
16
+ - **Exact track length = `duration_seconds`** (clamped 5–300s). Only length-controllable models honor it (e.g. ElevenLabs Music, `music-v1`) — without it those models default to a ~10s track, so ALWAYS pass it for jingles/beds on those models. Suno ignores it and picks its own length.
14
17
  - Suno generations have **two separate input fields**: a **Style / Description** field (`style` param) and a **Lyrics** field (`lyrics` param). Output your prompt as **TWO separate fenced code blocks** so the user (and the tool call) know exactly what goes where.
15
18
  - Tell the user to run the prompt multiple times — Suno output varies significantly between generations, that's a feature. Use `num_generations` if the tool supports it, or fire 2–4 parallel `generate_music` calls.
16
19
 
@@ -85,7 +88,7 @@ Use Suno's section tags to control structure. Each tag goes on its own line, con
85
88
  ### Jingle / ad music (15–30s)
86
89
  - `style`: short, punchy descriptor (`upbeat retail pop jingle, female vocal, claps, glossy production, summer energy`)
87
90
  - `lyrics`: 2–4 short lines max, often just chorus
88
- - Pass the shortest `duration` the tool supports.
91
+ - Pass the shortest `duration` the tool supports — or, on a length-controllable model (ElevenLabs Music), pass the exact `duration_seconds` (e.g. `15` or `30`).
89
92
 
90
93
  ### Cinematic trailer / score
91
94
  - `style`: `cinematic orchestral trailer, swelling strings, taiko drums, hybrid choir, dramatic build, modern hybrid score`
@@ -94,12 +94,18 @@ const BRIDGE_JS = `
94
94
  readyFns.forEach(function (f) { try { f(hostContext); } catch (e) {} });
95
95
  }).catch(function () { /* host without apps support — widget stays static */ });
96
96
 
97
+ var fsMode = false; // fullscreen: the HOST owns layout — size reports there
98
+ // made the inline iframe balloon over the chat composer.
97
99
  function notifySize() {
100
+ if (fsMode) return;
98
101
  // Measure the widget card itself — documentElement.scrollHeight over-reports
99
102
  // in some hosts and leaves a huge empty iframe below the card.
100
103
  var card = document.querySelector('.k-card');
101
104
  var rect = card ? card.getBoundingClientRect() : null;
102
105
  var height = rect ? Math.ceil(rect.bottom + 8) : document.documentElement.scrollHeight;
106
+ // Never ask the host for more than a viewport of inline height — a card
107
+ // taller than the screen overlaps Claude's prompt area.
108
+ height = Math.min(height, Math.max(window.innerHeight || 900, 500));
103
109
  notify('ui/notifications/size-changed', {
104
110
  width: document.documentElement.scrollWidth, height: height
105
111
  });
@@ -151,6 +157,12 @@ const BRIDGE_JS = `
151
157
  return request('ui/request-display-mode', { mode: mode });
152
158
  },
153
159
  notifySize: notifySize,
160
+ // Toggle fullscreen mode: suppresses size reports while the host owns the
161
+ // layout, and re-syncs the inline size on exit.
162
+ setFullscreen: function (on) {
163
+ fsMode = !!on;
164
+ if (!on) queueSize(60);
165
+ },
154
166
  hostContext: function () { return hostContext; }
155
167
  };
156
168
  })();
package/src/apps/theme.js CHANGED
@@ -175,18 +175,26 @@ body {
175
175
  header + prompt + chips + viewer + thumbs + actions + footer must all fit,
176
176
  or claude.ai adds an inner scrollbar. Click the image to expand in-Claude. */
177
177
  .k-viewer { margin-bottom: 10px; }
178
- .k-viewer img, .k-viewer video { display: block; width: 100%; max-height: 340px; object-fit: contain;
178
+ .k-viewer img, .k-viewer video { display: block; width: 100%;
179
+ max-height: min(340px, 55vh); object-fit: contain;
179
180
  border-radius: 12px; background: #000; border: 1px solid var(--border); cursor: zoom-in; }
180
181
  .k-viewer video { cursor: default; }
181
182
 
182
- /* ---- Fullscreen (ui/request-display-mode granted) ---- */
183
- html.k-fullscreen, html.k-fullscreen body { height: 100%; }
184
- html.k-fullscreen .k-card { height: 100%; display: flex; flex-direction: column; border-radius: 0; }
185
- html.k-fullscreen .k-body { flex: 1; min-height: 0; display: flex; flex-direction: column; }
186
- html.k-fullscreen .k-viewer { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; }
183
+ /* ---- Fullscreen (ui/request-display-mode granted) ----
184
+ position:fixed pins the card to the iframe viewport regardless of document
185
+ flow/host sizing quirks the media physically cannot exceed the screen. */
186
+ html.k-fullscreen, html.k-fullscreen body { height: 100%; overflow: hidden; }
187
+ html.k-fullscreen .k-card { position: fixed; inset: 0; display: flex; flex-direction: column; border-radius: 0; }
188
+ html.k-fullscreen .k-body { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
189
+ html.k-fullscreen .k-prompt, html.k-fullscreen .k-chips { flex: none; }
190
+ html.k-fullscreen #stage { flex: 1; min-height: 0; display: flex; flex-direction: column; }
191
+ html.k-fullscreen .k-viewer { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; margin-bottom: 6px; }
187
192
  html.k-fullscreen .k-viewer img, html.k-fullscreen .k-viewer video {
188
- max-height: 100%; max-width: 100%; width: auto; margin: 0 auto; cursor: zoom-out; }
189
- html.k-fullscreen .k-thumbs .k-thumb { width: 64px; height: 64px; }
193
+ max-height: 100%; max-width: 100%; height: auto;
194
+ width: auto; margin: 0 auto; cursor: zoom-out; object-fit: contain; }
195
+ html.k-fullscreen .k-thumbs { flex: none; }
196
+ html.k-fullscreen .k-thumbs .k-thumb { width: 56px; height: 56px; }
197
+ html.k-fullscreen .k-actions { flex: none; padding-top: 8px; }
190
198
  .k-expand-hint { display: none; }
191
199
  .k-thumbs { display: flex; gap: 6px; margin: 10px 0 2px; }
192
200
  .k-thumbs .k-thumb { width: 48px; height: 48px; border-radius: 8px; overflow: hidden; cursor: pointer;
@@ -258,23 +258,43 @@ function wireDlButtons(root) {
258
258
  });
259
259
  }
260
260
 
261
+ // CD scenes render as the SAME viewer+thumbnail carousel as image batches —
262
+ // a stacked column of full-size scenes buried the card (and the chat).
263
+ function sceneItems(sc) {
264
+ var items = [];
265
+ (sc.scenes || []).forEach(function (scene) {
266
+ var label = 'Scene ' + scene.scene_number + (scene.title ? ' — ' + scene.title : '');
267
+ (scene.image_urls || []).forEach(function (u) { items.push({ url: u, type: 'image', label: label }); });
268
+ (scene.video_urls || []).forEach(function (u) { items.push({ url: u, type: 'video', label: label }); });
269
+ });
270
+ return items;
271
+ }
272
+
261
273
  function renderScenes(sc) {
262
- el('stage').innerHTML = sc.scenes.map(function (scene) {
263
- var media = (scene.video_urls || []).map(function (u) {
264
- return '<div class="k-media"><video src="' + esc(u) + '" controls playsinline></video>' + dlBtnHTML(u) + '</div>';
265
- }).join('') + (scene.image_urls || []).map(function (u) {
266
- return '<div class="k-media" data-focus="' + esc(u) + '"><img src="' + esc(u) + '" alt="">' + dlBtnHTML(u) + '</div>';
267
- }).join('');
268
- return '<div style="margin-bottom:14px"><div style="font-size:12px;font-weight:600;color:var(--text-muted);margin-bottom:8px">Scene ' +
269
- esc(scene.scene_number) + (scene.title ? ' ' + esc(scene.title) : '') + '</div>' +
270
- '<div class="k-gen-grid n2">' + media + '</div></div>';
271
- }).join('');
274
+ var items = sceneItems(sc);
275
+ if (!items.length) return renderError('No completed scenes received');
276
+ selected = Math.min(selected, items.length - 1);
277
+ var it = items[selected];
278
+ var mediaHtml = it.type === 'video'
279
+ ? '<video id="scene-main" src="' + esc(it.url) + '" controls playsinline></video>'
280
+ : '<img id="scene-main" src="' + esc(it.url) + '" alt="" style="cursor:zoom-in">';
281
+ var thumbs = '<div class="k-thumbs">' + items.map(function (t, i) {
282
+ var inner = t.type === 'video'
283
+ ? '<span style="display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:rgba(255,255,255,0.06);font-size:14px">▶</span>'
284
+ : '<img src="' + esc(t.url) + '" alt="" loading="lazy">';
285
+ return '<div class="k-thumb' + (i === selected ? ' active' : '') + '" data-i="' + i + '" title="' + esc(t.label) + '">' + inner + '</div>';
286
+ }).join('') + '</div>';
287
+ el('stage').innerHTML =
288
+ '<div class="k-viewer">' + mediaHtml + dlBtnHTML(it.url) + '</div>' +
289
+ '<div style="font-size:11px;color:var(--text-faint);margin:2px 2px 0">' + esc(it.label) + '</div>' +
290
+ thumbs;
272
291
  wireDlButtons(el('stage'));
273
- // Click a scene image → fullscreen focus on THAT image (videos keep their
274
- // native controls; clicking them shouldn't hijack playback).
275
- Array.prototype.forEach.call(el('stage').querySelectorAll('.k-media[data-focus]'), function (m) {
276
- m.style.cursor = 'zoom-in';
277
- m.onclick = function () { focusMedia(m.getAttribute('data-focus')); };
292
+ if (it.type === 'image') {
293
+ var main = el('scene-main');
294
+ if (main) main.onclick = function () { focusMedia(it.url); };
295
+ }
296
+ Array.prototype.forEach.call(el('stage').querySelectorAll('.k-thumb'), function (t) {
297
+ t.onclick = function () { selected = +t.getAttribute('data-i'); renderScenes(sc); window.kolbo.notifySize(); };
278
298
  });
279
299
  renderActions(sc);
280
300
  }
@@ -331,6 +351,7 @@ function toggleFullscreen() {
331
351
  }
332
352
  function applyFullscreen(on, exitHandler) {
333
353
  document.documentElement.classList.toggle('k-fullscreen', on);
354
+ if (window.kolbo.setFullscreen) window.kolbo.setFullscreen(on);
334
355
  var c = el('phase-chip');
335
356
  if (on) {
336
357
  c.style.display = '';
@@ -366,7 +387,6 @@ function renderActions(sc) {
366
387
  }
367
388
  if (sc.kind === 'video') {
368
389
  a.push('<button class="k-btn primary" id="btn-download">⬇ Download</button>');
369
- a.push('<button class="k-btn" id="btn-analyze">📊 Analyze</button>');
370
390
  } else if (hasSingleUrl) {
371
391
  // Scenes (Creative Director) have no single "current" url — per-item hover
372
392
  // download buttons cover them instead.
@@ -400,9 +420,6 @@ function renderActions(sc) {
400
420
  '\\nEdit instruction: ' + text);
401
421
  });
402
422
  });
403
- bind('btn-analyze', function () {
404
- window.kolbo.sendMessage('Analyze this video and give me an engagement/virality read — hook strength, pacing, retention risks, and concrete improvement tips:\\n' + currentUrl());
405
- });
406
423
  }
407
424
  function bind(id, fn) { var b = el(id); if (b) b.onclick = fn; }
408
425
 
@@ -8,7 +8,8 @@ const { widgetPage } = require('../html');
8
8
  * structuredContent: {
9
9
  * widget: 'transcript', phase: 'completed'|'generating'|'failed',
10
10
  * generation_id, poll_tool, text, duration, audio_url, srt_url,
11
- * word_by_word_srt_url, txt_url, credits_used, error
11
+ * word_by_word_srt_url, txt_url, credits_used, error,
12
+ * open_url // "Open in Kolbo" session deep link (optional)
12
13
  * }
13
14
  */
14
15
 
@@ -34,7 +35,7 @@ const BODY = `
34
35
 
35
36
  const SCRIPT = `
36
37
  el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
37
- el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink('https://app.kolbo.ai'); };
38
+ el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink((state && state.open_url) || 'https://app.kolbo.ai'); };
38
39
  var state = null, pollTimer = null;
39
40
 
40
41
  function boot(sc) {
@@ -305,6 +305,56 @@ async function inlineImageBlocks(urls, opts = {}) {
305
305
  return blocks.filter(Boolean);
306
306
  }
307
307
 
308
+ // ─── "Open in Kolbo" deep links ───────────────────────────────────────────────
309
+ // kolbo-api submit responses include `session_id` + `project_id`. Map each MCP
310
+ // tool to the frontend page + tool slug whose session view can RESUME that
311
+ // session (mirrors kolbo-map src/constants/sessionTypes.js resumeUrl map — the
312
+ // route must match the SESSION MODEL the SDK created, per sdkSessionManager):
313
+ // ImageSession → /image-tools?tool=text-to-image
314
+ // imgEditSession (image_edit AND edit_image/global_image_edit) → /image-tools?tool=image-editing
315
+ // textToVideoSession → /video-tools?tool=text-to-video
316
+ // imgToVideoSession (video_from_image, elements, first_last_frame) → /video-tools?tool=image-to-video
317
+ // videoToVideoSession → /video-tools?tool=video-to-video
318
+ // lipsyncSession → /video-tools?tool=lipsync
319
+ // MusicGeneratorSession / TextToSpeechSession / textToSoundSession /
320
+ // speechToTextSession → /audio-tools with the matching slug
321
+ // CreativeDirectorSession → /creative-director?session=... (no tool param)
322
+ // Intentionally ABSENT (no deep-linkable session page — widget falls back to
323
+ // plain https://app.kolbo.ai): edit_video (GlobalVideoEditSession has no
324
+ // session deep-link), generate_3d (project-scoped, no session), shorts render.
325
+ const APP_BASE_URL = 'https://app.kolbo.ai';
326
+ const OPEN_URL_ROUTES = {
327
+ generate_image: { path: '/image-tools', tool: 'text-to-image' },
328
+ generate_image_edit: { path: '/image-tools', tool: 'image-editing' },
329
+ edit_image: { path: '/image-tools', tool: 'image-editing' },
330
+ generate_video: { path: '/video-tools', tool: 'text-to-video' },
331
+ generate_video_from_image: { path: '/video-tools', tool: 'image-to-video' },
332
+ generate_elements: { path: '/video-tools', tool: 'image-to-video' },
333
+ generate_first_last_frame: { path: '/video-tools', tool: 'image-to-video' },
334
+ generate_video_from_video: { path: '/video-tools', tool: 'video-to-video' },
335
+ generate_lipsync: { path: '/video-tools', tool: 'lipsync' },
336
+ generate_music: { path: '/audio-tools', tool: 'music-generator' },
337
+ generate_speech: { path: '/audio-tools', tool: 'text-to-speech' },
338
+ generate_sound: { path: '/audio-tools', tool: 'text-to-sound' },
339
+ transcribe_audio: { path: '/audio-tools', tool: 'speech-to-text' },
340
+ generate_creative_director: { path: '/creative-director' },
341
+ };
342
+
343
+ /**
344
+ * Build the "Open in Kolbo" deep link for a generation's actual session.
345
+ * Returns undefined (widget falls back to app.kolbo.ai) when the tool has no
346
+ * deep-linkable page or the submit response carried no session_id (older
347
+ * kolbo-api, shorts render, 3D).
348
+ */
349
+ function buildOpenUrl(tool, gen) {
350
+ const route = OPEN_URL_ROUTES[tool];
351
+ if (!route || !gen || !gen.session_id) return undefined;
352
+ let url = `${APP_BASE_URL}${route.path}?session=${encodeURIComponent(gen.session_id)}`;
353
+ if (route.tool) url += `&tool=${route.tool}`;
354
+ if (gen.project_id) url += `&project=${encodeURIComponent(gen.project_id)}`;
355
+ return url;
356
+ }
357
+
308
358
  // ─── MCP Apps generation widget helpers ──────────────────────────────────────
309
359
  // When the host renders MCP Apps (claude.ai via the remote connector, Claude
310
360
  // Desktop over stdio), generation tools return IMMEDIATELY after submit and the
@@ -341,6 +391,7 @@ async function uiGenerating(p) {
341
391
  count: p.count || 1,
342
392
  settings: p.settings || {},
343
393
  reference_image: p.reference_image,
394
+ open_url: buildOpenUrl(p.tool, p.gen),
344
395
  };
345
396
  const text = JSON.stringify({
346
397
  status: 'submitted',
@@ -373,6 +424,7 @@ async function uiCompleted(p, textPayload) {
373
424
  duration: p.duration,
374
425
  scenes: p.scenes,
375
426
  credits_used: p.credits_used,
427
+ open_url: buildOpenUrl(p.tool, p.gen),
376
428
  };
377
429
  return uiResult(UI.generation, textPayload, structured);
378
430
  }
@@ -389,6 +441,7 @@ module.exports = {
389
441
  creditFields,
390
442
  projectIdField,
391
443
  inlineImageBlocks,
444
+ buildOpenUrl,
392
445
  uiGenerating,
393
446
  uiCompleted,
394
447
  appsEnabled,
@@ -6,7 +6,7 @@
6
6
  const { z } = require('zod');
7
7
  const FormData = require('form-data');
8
8
  const { pollUntilDone } = require('../polling');
9
- const { resolveToBuffer, creditFields, projectIdField, inlineImageBlocks, uiGenerating, uiCompleted, appsEnabled } = require('./_shared');
9
+ const { resolveToBuffer, creditFields, projectIdField, inlineImageBlocks, buildOpenUrl, uiGenerating, uiCompleted, appsEnabled } = require('./_shared');
10
10
  const { UI, uiResult, canonicalModelId } = require('../apps');
11
11
 
12
12
  function registerGenerateTools(server, client, options = {}) {
@@ -178,7 +178,7 @@ function registerGenerateTools(server, client, options = {}) {
178
178
  // through get_generation_status, so it stays blocking on UI hosts too and
179
179
  // renders the completed scene gallery.
180
180
  if (ui()) return uiCompleted({
181
- tool: 'generate_creative_director', kind: 'scenes', client, model, prompt,
181
+ tool: 'generate_creative_director', kind: 'scenes', gen, client, model, prompt,
182
182
  settings: { duration, resolution, mode: workflow_type }, scenes,
183
183
  credits_used: creditFields(result).credits_used
184
184
  }, cdText);
@@ -839,6 +839,7 @@ function registerGenerateTools(server, client, options = {}) {
839
839
  generation_id: startResponse.generation_id,
840
840
  poll_tool: 'get_generation_status',
841
841
  audio_url: isUrl ? source : undefined,
842
+ open_url: buildOpenUrl('transcribe_audio', startResponse),
842
843
  });
843
844
  }
844
845