@kolbo/mcp 1.48.0 → 1.49.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/README.md +12 -10
- package/package.json +2 -2
- package/skill/SKILL.md +2 -1
- package/skill/references/models/music.md +1 -1
- package/src/apps/bridge.js +1 -1
- package/src/apps/index.js +14 -2
- package/src/apps/theme.js +18 -0
- package/src/apps/widgets/generation.js +109 -20
- package/src/apps/widgets/transcript.js +38 -9
- package/src/index.js +6 -6
- package/src/tools/_shared.js +19 -2
- package/src/tools/generate.js +43 -20
- package/src/tools/music_library.js +141 -174
- package/src/tools/projects.js +6 -3
- package/src/tools/stock_library.js +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Use [Kolbo AI](https://kolbo.ai) as native tools in Claude Code and Claude Deskt
|
|
|
4
4
|
|
|
5
5
|
Generate images, videos, music, speech, sound effects, multi-scene campaigns, and conversational chat — all from natural language in your coding environment. 100+ AI models behind Smart Select routing, with reusable Visual DNA profiles for character/style consistency.
|
|
6
6
|
|
|
7
|
-
**✨ Interactive widgets (v1.30+):** in claude.ai and
|
|
7
|
+
**✨ Interactive widgets (v1.30+):** in claude.ai, Claude Desktop, and Codex Desktop, generations render as live Kolbo cards — real-time progress with model + settings chips, an inline result gallery / video or audio player, and one-click **Animate · Edit · Recreate · Download** actions. Multi-audio generations render every track with its own player and Download button. Library and model searches render as browsable grids with audio preview. Text-only clients (Claude Code, Codex CLI, Cursor) keep the classic text responses.
|
|
8
8
|
|
|
9
9
|
## Set up — paste one prompt, or one config block (keyless, no API key)
|
|
10
10
|
|
|
@@ -113,7 +113,7 @@ Just ask your agent naturally:
|
|
|
113
113
|
|
|
114
114
|
Without the optional skill, the config block alone already exposes every tool — you just describe what you want. With the skill installed, each of these is also routed to the right MCP tool with the right defaults — UGC mode picks 9:16 + sound-off + no-captions, marketplace mode enforces compliance (pure white bg, no text, no props), product photoshoot mode uses the right aspect for the platform (2:3 Pinterest, 16:9 hero banner, 1:1 IG feed), etc. The routing logic is shared with [Kolbo Code](https://github.com/Zoharvan12/kolbo-code), so the behavior is identical however you connect.
|
|
115
115
|
|
|
116
|
-
## Available Tools (
|
|
116
|
+
## Available Tools (95)
|
|
117
117
|
|
|
118
118
|
**Generation**
|
|
119
119
|
| Tool | Description |
|
|
@@ -193,16 +193,18 @@ Every generation tool also accepts an optional `project_id` arg that routes the
|
|
|
193
193
|
|------|-------------|
|
|
194
194
|
| `publish_html_artifact` | Publish an HTML page, SVG, or Mermaid diagram and get a public shareable URL on `sites.kolbo.ai`. Pass `share_token` from a prior publish to update the same URL in place (old content kept in version history). |
|
|
195
195
|
|
|
196
|
-
**Music Library** (
|
|
196
|
+
**SYNCI Music Library** (licensed production music)
|
|
197
197
|
| Tool | Description |
|
|
198
198
|
|------|-------------|
|
|
199
|
-
| `search_music_library` | Search the licensed
|
|
200
|
-
| `analyze_script_for_music` |
|
|
201
|
-
| `browse_music_library` | Browse the catalog without a query
|
|
202
|
-
| `get_music_library_facets` | List
|
|
203
|
-
| `get_music_track_audio` | Get
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
199
|
+
| `search_music_library` | Search the licensed catalog; results contain watermarked previews only. |
|
|
200
|
+
| `analyze_script_for_music` | Turn a script or scene description into a music search. |
|
|
201
|
+
| `browse_music_library` | Browse the catalog without a query. |
|
|
202
|
+
| `get_music_library_facets` | List genres, moods, instruments, BPM, and duration filters. |
|
|
203
|
+
| `get_music_track_audio` | Get watermarked preview URLs for a track. |
|
|
204
|
+
| `acquire_clean_music_track` | Spend one SYNCI vendor credit and return clean MP3/WAV signed URLs. Idempotent with `request_id`. |
|
|
205
|
+
| `import_music_track_to_library` | Spend one vendor credit and copy a clean MP3/WAV into Kolbo's media library. |
|
|
206
|
+
| `get_music_track_related` | Get stems/alternate-version metadata (purchasing remains unsupported). |
|
|
207
|
+
| `get_music_track_lyrics` | Get lyrics metadata. |
|
|
206
208
|
|
|
207
209
|
**Stock Library** (multi-source stock media: Pexels, Pixabay, Sketchfab 3D, Music)
|
|
208
210
|
| Tool | Description |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolbo/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.49.0",
|
|
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": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@modelcontextprotocol/ext-apps": "^1.7.4",
|
|
45
45
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
46
|
-
"form-data": "^4.0.
|
|
46
|
+
"form-data": "^4.0.6",
|
|
47
47
|
"zod": "^3.25.0"
|
|
48
48
|
},
|
|
49
49
|
"overrides": {
|
package/skill/SKILL.md
CHANGED
|
@@ -115,7 +115,8 @@ Each `references/models/*.md` mirrors the matching skill prompt in `kolbo-api/sr
|
|
|
115
115
|
| `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` |
|
|
116
116
|
| `create_visual_dna` / `generate_character_sheet` / `list_visual_dnas` / `get_visual_dna` / `delete_visual_dna` / `*_visual_dna_folder` (5 folder tools) | Visual DNA (+ character sheet, character folders) — see `workflows/visual-dna.md` |
|
|
117
117
|
| `list_moodboards` / `get_moodboard` / `list_presets` | Style overlays |
|
|
118
|
-
| `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.
|
|
118
|
+
| `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. |
|
|
119
|
+
| `search_music_library` / `browse_music_library` / `get_music_track_audio` / `acquire_clean_music_track` / `import_music_track_to_library` | SYNCI licensed music. Search/playback is watermarked preview-only. `acquire_clean_music_track` and `import_music_track_to_library` immediately consume one SYNCI vendor credit and return/import an unwatermarked MP3/WAV; reuse `request_id` on retries. |
|
|
119
120
|
| `list_projects` / `move_session` | Projects: resolve a project NAME → the `project_id` you pass on generation/upload/doc calls; `move_session` relocates a whole session + its media when work landed in the wrong project. NOT the same as `app_builder_list_projects`. See "Projects — Where Work Lands" below. |
|
|
120
121
|
| `create_project` / `update_project` / `archive_project` / `unarchive_project` / `list_sessions` | Project lifecycle + session inventory (deletion stays in-app). Create a project when the user starts new work, then pass its id on EVERY call. |
|
|
121
122
|
| `add_project_context` / `list_project_context` / `delete_project_context` / `get_project_profile` / `regenerate_project_profile` | Project knowledge base (RAG): feed scripts/URLs/notes; `get_project_profile` = the living brief — read it to ground work in the project |
|
|
@@ -8,7 +8,7 @@ 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"`
|
|
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"` for free Kolbo/Coverr results. For SYNCI's licensed catalog use `search_music_library`; its playback URLs are watermarked previews. Final use must call `acquire_clean_music_track` or `import_music_track_to_library`, which immediately consumes one SYNCI vendor credit and returns/imports a clean MP3/WAV. Reuse `request_id` when retrying the same action.
|
|
12
12
|
|
|
13
13
|
## CRITICAL Kolbo Platform Rules
|
|
14
14
|
|
package/src/apps/bridge.js
CHANGED
|
@@ -69,7 +69,7 @@ const BRIDGE_JS = `
|
|
|
69
69
|
} else if (m.method === 'ui/notifications/tool-input' || m.method === 'ui/notifications/tool-input-partial') {
|
|
70
70
|
// Fires while the tool is still RUNNING — lets widgets show a real
|
|
71
71
|
// "preparing" state instead of a blank card until the result lands.
|
|
72
|
-
toolInputFns.forEach(function (f) { try { f((m.params && m.params.arguments) || {}); } catch (e) {} });
|
|
72
|
+
toolInputFns.forEach(function (f) { try { f((m.params && m.params.arguments) || {}, m.params || {}); } catch (e) {} });
|
|
73
73
|
} else if (m.method === 'ui/notifications/host-context-changed') {
|
|
74
74
|
hostContext = (m.params && m.params.hostContext) || m.params || hostContext;
|
|
75
75
|
themeFns.forEach(function (f) { try { f(hostContext); } catch (e) {} });
|
package/src/apps/index.js
CHANGED
|
@@ -124,7 +124,19 @@ function appsEnabled(server, opts = {}) {
|
|
|
124
124
|
if (opts.apps === true || process.env.KOLBO_MCP_APPS === '1') return true;
|
|
125
125
|
try {
|
|
126
126
|
const caps = server?.server?.getClientCapabilities?.();
|
|
127
|
-
|
|
127
|
+
if (getUiCapability(caps) !== undefined) return true;
|
|
128
|
+
|
|
129
|
+
// Codex Desktop currently mounts MCP App resources from tool `_meta`, but
|
|
130
|
+
// its initialize handshake identifies as `codex-mcp-client` with an empty
|
|
131
|
+
// capabilities object. Without this compatibility path the host mounts a
|
|
132
|
+
// "Preparing" card while the tool takes the blocking/text fallback, so the
|
|
133
|
+
// completed media never reaches the iframe as structuredContent.
|
|
134
|
+
//
|
|
135
|
+
// Keep the desktop-origin check: Codex CLI uses the same client name but is
|
|
136
|
+
// a text surface, where returning immediately would remove the final URLs.
|
|
137
|
+
const info = server?.server?.getClientVersion?.();
|
|
138
|
+
const origin = process.env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE || '';
|
|
139
|
+
return info?.name === 'codex-mcp-client' && /codex desktop/i.test(origin);
|
|
128
140
|
} catch (_) {
|
|
129
141
|
return false;
|
|
130
142
|
}
|
|
@@ -272,9 +284,9 @@ const TOOL_WIDGETS = {
|
|
|
272
284
|
// media grid
|
|
273
285
|
list_media: UI.mediaGrid,
|
|
274
286
|
search_stock_media: UI.mediaGrid,
|
|
275
|
-
get_stock_collections: UI.mediaGrid,
|
|
276
287
|
search_music_library: UI.mediaGrid,
|
|
277
288
|
browse_music_library: UI.mediaGrid,
|
|
289
|
+
get_stock_collections: UI.mediaGrid,
|
|
278
290
|
list_presets: UI.mediaGrid,
|
|
279
291
|
list_voices: UI.mediaGrid,
|
|
280
292
|
list_visual_dnas: UI.mediaGrid,
|
package/src/apps/theme.js
CHANGED
|
@@ -259,6 +259,24 @@ html.k-fullscreen .k-actions { flex: none; padding-top: 8px; }
|
|
|
259
259
|
.k-audio-meta { flex: 1; min-width: 0; }
|
|
260
260
|
.k-audio-title { font-size: 12.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
261
261
|
.k-audio-sub { font-size: 11px; color: var(--text-faint); }
|
|
262
|
+
.k-generated-audio {
|
|
263
|
+
display: grid; grid-template-columns: 40px minmax(0, 1fr) auto;
|
|
264
|
+
align-items: center; gap: 7px 10px;
|
|
265
|
+
}
|
|
266
|
+
.k-generated-audio .k-audio-placeholder {
|
|
267
|
+
display: flex; align-items: center; justify-content: center; color: var(--brand);
|
|
268
|
+
}
|
|
269
|
+
.k-generated-audio .k-audio-placeholder svg { width: 19px; height: 19px; }
|
|
270
|
+
.k-generated-audio .k-audio-download { white-space: nowrap; }
|
|
271
|
+
.k-generated-audio .k-audio-player {
|
|
272
|
+
grid-column: 2 / -1; display: block; width: 100%; min-width: 0; height: 34px;
|
|
273
|
+
}
|
|
274
|
+
@media (max-width: 520px) {
|
|
275
|
+
.k-generated-audio { grid-template-columns: 36px minmax(0, 1fr); }
|
|
276
|
+
.k-generated-audio .k-audio-art { width: 36px; height: 36px; }
|
|
277
|
+
.k-generated-audio .k-audio-download { grid-column: 2; justify-self: start; }
|
|
278
|
+
.k-generated-audio .k-audio-player { grid-column: 1 / -1; }
|
|
279
|
+
}
|
|
262
280
|
.k-play {
|
|
263
281
|
width: 32px; height: 32px; border-radius: 50%; flex: none; border: 1px solid rgba(255,255,255,0.18);
|
|
264
282
|
background: rgba(0,0,0,0.5); color: #fff; cursor: pointer;
|
|
@@ -17,6 +17,7 @@ const { widgetPage } = require('../html');
|
|
|
17
17
|
* settings: { duration, resolution, aspect_ratio, audio, voice, mode },
|
|
18
18
|
* reference_image, // thumbnail URL (optional)
|
|
19
19
|
* urls, thumbnail_url, title, duration, credits_used,
|
|
20
|
+
* tracks: [{ title, duration, thumbnail_url, model }], // optional audio metadata by URL index
|
|
20
21
|
* scenes: [{ scene_number, title, image_urls, video_urls }],
|
|
21
22
|
* error,
|
|
22
23
|
* open_url // "Open in Kolbo" target (optional)
|
|
@@ -53,6 +54,8 @@ const SCRIPT = `
|
|
|
53
54
|
var state = null; // current structuredContent
|
|
54
55
|
var selected = 0; // selected result index
|
|
55
56
|
var pollTimer = null;
|
|
57
|
+
var originTool = null;
|
|
58
|
+
var originArgs = {};
|
|
56
59
|
|
|
57
60
|
el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
|
|
58
61
|
el('action-cancel').innerHTML = ICONS.x;
|
|
@@ -122,23 +125,26 @@ function renderGenerating(sc) {
|
|
|
122
125
|
schedulePoll(sc);
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
// Poll ceilings so the card never spins
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
var MAX_POLL_MS =
|
|
129
|
-
var MAX_POLL_ERRORS =
|
|
128
|
+
// Poll ceilings so the card never spins forever. Losing widget-side tracking
|
|
129
|
+
// is not a generation failure: keep retries disabled because the paid server
|
|
130
|
+
// job may still complete.
|
|
131
|
+
var MAX_POLL_MS = 35 * 60 * 1000;
|
|
132
|
+
var MAX_POLL_ERRORS = 30;
|
|
130
133
|
var pollStart = 0, pollErrors = 0;
|
|
131
134
|
|
|
132
135
|
function schedulePoll(sc) {
|
|
133
136
|
if (!pollStart) pollStart = Date.now();
|
|
134
137
|
clearTimeout(pollTimer);
|
|
135
|
-
|
|
138
|
+
// The call itself long-waits server-side (normally up to three minutes).
|
|
139
|
+
// This short pause only separates successive wait windows.
|
|
140
|
+
var delay = 1500;
|
|
141
|
+
pollTimer = setTimeout(function () { poll(sc); }, delay);
|
|
136
142
|
}
|
|
137
143
|
function poll(sc) {
|
|
138
144
|
if (pollStart && (Date.now() - pollStart) > MAX_POLL_MS) {
|
|
139
|
-
return
|
|
145
|
+
return renderTrackingIssue('This is still running longer than the tracking window. Do not retry it — any completed result will appear in your Kolbo library.');
|
|
140
146
|
}
|
|
141
|
-
var args = sc.status_args || { generation_id: sc.generation_id };
|
|
147
|
+
var args = sc.status_args || { generation_id: sc.generation_id, wait: true };
|
|
142
148
|
window.kolbo.callTool(sc.poll_tool || 'get_generation_status', args).then(function (res) {
|
|
143
149
|
var st = structured(res) || {};
|
|
144
150
|
var stateName = st.state || st.phase || st.status;
|
|
@@ -146,7 +152,7 @@ function poll(sc) {
|
|
|
146
152
|
// generation state — count it toward the consecutive-error cap so a record
|
|
147
153
|
// that can't be resolved errors out fast instead of polling forever.
|
|
148
154
|
if ((res && res.isError) || st.success === false || (st.error && !stateName)) {
|
|
149
|
-
if (++pollErrors >= MAX_POLL_ERRORS) return
|
|
155
|
+
if (++pollErrors >= MAX_POLL_ERRORS) return renderTrackingIssue(st.error || 'Tracking paused. The generation may still be running.');
|
|
150
156
|
return schedulePoll(sc);
|
|
151
157
|
}
|
|
152
158
|
if (stateName === 'completed') {
|
|
@@ -163,9 +169,13 @@ function poll(sc) {
|
|
|
163
169
|
renderResult(done);
|
|
164
170
|
// Let the model know the outcome without it having to poll.
|
|
165
171
|
try {
|
|
172
|
+
var completedUrls = (done.urls || []).slice();
|
|
173
|
+
(done.scenes || []).forEach(function (scene) {
|
|
174
|
+
completedUrls = completedUrls.concat(scene.image_urls || [], scene.video_urls || []);
|
|
175
|
+
});
|
|
166
176
|
window.kolbo.updateModelContext(
|
|
167
177
|
'Generation ' + (sc.generation_id || '') + ' completed (' + (sc.tool || '') + ').' +
|
|
168
|
-
'\\nOutput URLs:\\n' +
|
|
178
|
+
'\\nOutput URLs:\\n' + completedUrls.join('\\n') +
|
|
169
179
|
(done.credits_used != null ? '\\nCredits used: ' + done.credits_used : ''));
|
|
170
180
|
} catch (e) {}
|
|
171
181
|
} else if (stateName === 'failed' || stateName === 'error' || stateName === 'cancelled') {
|
|
@@ -175,7 +185,7 @@ function poll(sc) {
|
|
|
175
185
|
schedulePoll(sc);
|
|
176
186
|
}
|
|
177
187
|
}).catch(function () {
|
|
178
|
-
if (++pollErrors >= MAX_POLL_ERRORS) return
|
|
188
|
+
if (++pollErrors >= MAX_POLL_ERRORS) return renderTrackingIssue('Tracking paused after repeated connection errors. The generation may still be running.');
|
|
179
189
|
schedulePoll(sc);
|
|
180
190
|
});
|
|
181
191
|
}
|
|
@@ -240,13 +250,35 @@ function renderVideo(sc, urls) {
|
|
|
240
250
|
|
|
241
251
|
function renderAudio(sc, urls) {
|
|
242
252
|
el('stage').innerHTML = urls.map(function (u, i) {
|
|
243
|
-
var
|
|
244
|
-
|
|
245
|
-
|
|
253
|
+
var track = (sc.tracks && sc.tracks[i]) || {};
|
|
254
|
+
var titleBase = track.title || sc.title || (TOOL_TITLES[sc.tool] || 'Audio');
|
|
255
|
+
var title = titleBase + (urls.length > 1 ? ' — Track ' + (i + 1) : '');
|
|
256
|
+
var duration = track.duration != null ? track.duration : sc.duration;
|
|
257
|
+
var artwork = track.thumbnail_url || sc.thumbnail_url;
|
|
258
|
+
return '<div class="k-audio-row k-generated-audio">' +
|
|
259
|
+
(artwork ? '<img class="k-audio-art" src="' + esc(artwork) + '" alt="">' :
|
|
260
|
+
'<div class="k-audio-art k-audio-placeholder">' + ICONS.audio + '</div>') +
|
|
246
261
|
'<div class="k-audio-meta"><div class="k-audio-title">' + esc(title) + '</div>' +
|
|
247
|
-
'<div class="k-audio-sub">' + esc(
|
|
248
|
-
'
|
|
262
|
+
'<div class="k-audio-sub">' + esc(track.model || sc.model || '') +
|
|
263
|
+
(duration ? ' · ' + fmtDur(duration) : '') + '</div></div>' +
|
|
264
|
+
'<button class="k-btn k-audio-download" data-audio-download="' + esc(u) +
|
|
265
|
+
'" aria-label="Download ' + esc(title) + '">' + ICONS.download + ' Download</button>' +
|
|
266
|
+
'<audio class="k-audio-player" src="' + esc(u) + '" controls preload="metadata" aria-label="Play ' +
|
|
267
|
+
esc(title) + '"></audio></div>';
|
|
249
268
|
}).join('');
|
|
269
|
+
Array.prototype.forEach.call(el('stage').querySelectorAll('[data-audio-download]'), function (b) {
|
|
270
|
+
b.onclick = function () {
|
|
271
|
+
window.kolbo.openLink(downloadUrl(b.getAttribute('data-audio-download')));
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
var players = el('stage').querySelectorAll('.k-audio-player');
|
|
275
|
+
Array.prototype.forEach.call(players, function (player) {
|
|
276
|
+
player.addEventListener('play', function () {
|
|
277
|
+
Array.prototype.forEach.call(players, function (other) {
|
|
278
|
+
if (other !== player) other.pause();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
250
282
|
}
|
|
251
283
|
|
|
252
284
|
function render3d(sc, urls) {
|
|
@@ -363,6 +395,22 @@ function renderError(msg) {
|
|
|
363
395
|
window.kolbo.notifySize();
|
|
364
396
|
}
|
|
365
397
|
|
|
398
|
+
function renderTrackingIssue(msg) {
|
|
399
|
+
clearTimeout(pollTimer);
|
|
400
|
+
setPhaseChip('Still working', false);
|
|
401
|
+
el('stage').innerHTML = '<div class="k-error">' + ICONS.clock + ' ' + esc(msg) + '</div>';
|
|
402
|
+
el('actions').innerHTML = '<button class="k-btn primary" id="status-btn">' + ICONS.clock + ' Check status</button>' +
|
|
403
|
+
'<button class="k-btn ghost" id="tracking-open">Open in Kolbo ' + ICONS.open + '</button>';
|
|
404
|
+
el('status-btn').onclick = function () {
|
|
405
|
+
window.kolbo.sendMessage('Check the existing Kolbo generation status without retrying it.' +
|
|
406
|
+
(state && state.generation_id ? '\\nGeneration ID: ' + state.generation_id : ''));
|
|
407
|
+
};
|
|
408
|
+
el('tracking-open').onclick = function () {
|
|
409
|
+
window.kolbo.openLink((state && state.open_url) || 'https://app.kolbo.ai');
|
|
410
|
+
};
|
|
411
|
+
window.kolbo.notifySize();
|
|
412
|
+
}
|
|
413
|
+
|
|
366
414
|
/* ---------- fullscreen viewer ---------- */
|
|
367
415
|
var isFullscreen = false;
|
|
368
416
|
function toggleFullscreen() {
|
|
@@ -417,9 +465,10 @@ function renderActions(sc) {
|
|
|
417
465
|
}
|
|
418
466
|
if (sc.kind === 'video') {
|
|
419
467
|
a.push('<button class="k-btn primary" id="btn-download">' + ICONS.download + ' Download</button>');
|
|
420
|
-
} else if (hasSingleUrl) {
|
|
468
|
+
} else if (hasSingleUrl && sc.kind !== 'audio') {
|
|
421
469
|
// Scenes (Creative Director) have no single "current" url — per-item hover
|
|
422
|
-
// download buttons cover them instead.
|
|
470
|
+
// download buttons cover them instead. Audio has a visible download button
|
|
471
|
+
// on every track so multi-output generations never download only track 1.
|
|
423
472
|
a.push('<button class="k-btn" id="btn-download">' + ICONS.download + ' Download</button>');
|
|
424
473
|
}
|
|
425
474
|
a.push('<button class="k-btn" id="btn-recreate">' + ICONS.retry + ' Recreate</button>');
|
|
@@ -471,6 +520,8 @@ function openPromptRow(placeholder, onSend) {
|
|
|
471
520
|
take many seconds (model resolution, file upload, submit). Show a live
|
|
472
521
|
shell immediately instead of a blank card. */
|
|
473
522
|
function bootPre(toolName, args) {
|
|
523
|
+
if (toolName) originTool = toolName;
|
|
524
|
+
if (args) originArgs = args;
|
|
474
525
|
if (state) return; // real data already arrived
|
|
475
526
|
el('tool-title').textContent = TOOL_TITLES[toolName] || 'Generation';
|
|
476
527
|
if (args && (args.prompt || args.text)) {
|
|
@@ -484,23 +535,61 @@ function bootPre(toolName, args) {
|
|
|
484
535
|
window.kolbo.notifySize();
|
|
485
536
|
}
|
|
486
537
|
|
|
538
|
+
function kindFromTool(tool, sc) {
|
|
539
|
+
if (sc && sc.scenes) return 'scenes';
|
|
540
|
+
if (/video|lipsync|elements|first_last/i.test(tool || '')) return 'video';
|
|
541
|
+
if (/music|speech|sound/i.test(tool || '')) return 'audio';
|
|
542
|
+
if (/3d/i.test(tool || '')) return '3d';
|
|
543
|
+
var firstUrl = sc && Array.isArray(sc.urls) ? String(sc.urls[0] || '').split('?')[0].toLowerCase() : '';
|
|
544
|
+
if (/\\.(mp4|mov|webm|mkv)$/.test(firstUrl)) return 'video';
|
|
545
|
+
if (/\\.(mp3|wav|m4a|aac|ogg|flac)$/.test(firstUrl)) return 'audio';
|
|
546
|
+
if (/\\.(glb|gltf|fbx|obj|usdz)$/.test(firstUrl)) return '3d';
|
|
547
|
+
return 'image';
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Recover a successful legacy/text result when a host mounted the iframe from
|
|
551
|
+
// declaration metadata but the server did not recognize its Apps capability.
|
|
552
|
+
function completedFromPlain(sc) {
|
|
553
|
+
if (!sc || (!Array.isArray(sc.urls) && !Array.isArray(sc.scenes))) return null;
|
|
554
|
+
return Object.assign({}, sc, {
|
|
555
|
+
widget: 'generation',
|
|
556
|
+
phase: 'completed',
|
|
557
|
+
tool: originTool,
|
|
558
|
+
kind: kindFromTool(originTool, sc),
|
|
559
|
+
prompt: originArgs.prompt || originArgs.text || sc.prompt_used || '',
|
|
560
|
+
model: sc.model || originArgs.model,
|
|
561
|
+
settings: {
|
|
562
|
+
duration: sc.duration || originArgs.duration,
|
|
563
|
+
resolution: originArgs.resolution,
|
|
564
|
+
aspect_ratio: originArgs.aspect_ratio
|
|
565
|
+
},
|
|
566
|
+
urls: sc.urls || []
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
487
570
|
/* ---------- wire host events ---------- */
|
|
488
571
|
window.kolbo.onToolResult(function (result) {
|
|
489
572
|
var sc = result.structuredContent || structured(result);
|
|
490
573
|
if (sc && (sc.phase || sc.widget)) return boot(sc);
|
|
574
|
+
var recovered = completedFromPlain(sc);
|
|
575
|
+
if (recovered) return boot(recovered);
|
|
491
576
|
// Tool errored (or returned plain text): show it instead of a dead blank card.
|
|
492
577
|
var txt = '';
|
|
493
578
|
try { txt = (result.content || []).filter(function (c) { return c.type === 'text'; }).map(function (c) { return c.text; }).join(' '); } catch (e) {}
|
|
579
|
+
if (/timed out|timeout/i.test(txt)) {
|
|
580
|
+
return renderTrackingIssue((txt || 'Tracking timed out. The generation may still be running.').slice(0, 300));
|
|
581
|
+
}
|
|
494
582
|
if (result.isError || /error|failed/i.test(txt)) {
|
|
495
583
|
renderError((txt || 'The request failed.').slice(0, 300));
|
|
496
584
|
}
|
|
497
585
|
});
|
|
498
|
-
window.kolbo.onToolInput(function (args) { bootPre(
|
|
586
|
+
window.kolbo.onToolInput(function (args, info) { bootPre(info && info.name, args); });
|
|
499
587
|
window.kolbo.ready(function (ctx) {
|
|
500
588
|
var info = ctx && ctx.toolInfo;
|
|
501
589
|
if (!state && info) {
|
|
590
|
+
originTool = (info.tool && info.tool.name) || originTool;
|
|
502
591
|
if (info.result && info.result.structuredContent) return boot(info.result.structuredContent);
|
|
503
|
-
bootPre(
|
|
592
|
+
bootPre(originTool, null);
|
|
504
593
|
}
|
|
505
594
|
});
|
|
506
595
|
`;
|
|
@@ -36,7 +36,7 @@ const BODY = `
|
|
|
36
36
|
const SCRIPT = `
|
|
37
37
|
el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
|
|
38
38
|
el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink((state && state.open_url) || 'https://app.kolbo.ai'); };
|
|
39
|
-
var state = null, pollTimer = null;
|
|
39
|
+
var state = null, pollTimer = null, originArgs = {};
|
|
40
40
|
|
|
41
41
|
function boot(sc) {
|
|
42
42
|
if (!sc) return;
|
|
@@ -83,40 +83,68 @@ function boot(sc) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
var pollStart = 0, pollErrors = 0;
|
|
86
|
-
var MAX_POLL_MS =
|
|
86
|
+
var MAX_POLL_MS = 35 * 60 * 1000, MAX_POLL_ERRORS = 30;
|
|
87
87
|
function poll() {
|
|
88
88
|
if (!pollStart) pollStart = Date.now();
|
|
89
89
|
if ((Date.now() - pollStart) > MAX_POLL_MS) {
|
|
90
|
-
return
|
|
90
|
+
return trackingIssue('This transcription is still running longer than the tracking window. Do not retry it — any completed result will appear in your Kolbo library.');
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
var args = state.status_args || { generation_id: state.generation_id, wait: true };
|
|
93
|
+
window.kolbo.callTool(state.poll_tool || 'get_generation_status', args)
|
|
93
94
|
.then(function (res) {
|
|
94
95
|
var st = structured(res) || {};
|
|
95
96
|
var s = st.state || st.phase;
|
|
96
97
|
if ((res && res.isError) || st.success === false || (st.error && !s)) {
|
|
97
|
-
if (++pollErrors >= MAX_POLL_ERRORS) return
|
|
98
|
-
pollTimer = setTimeout(poll,
|
|
98
|
+
if (++pollErrors >= MAX_POLL_ERRORS) return trackingIssue(st.error || 'Tracking paused. The transcription may still be running.');
|
|
99
|
+
pollTimer = setTimeout(poll, pollDelay()); return;
|
|
99
100
|
}
|
|
100
101
|
if (s === 'completed') {
|
|
101
102
|
var r = st.result || st;
|
|
102
103
|
boot(Object.assign({}, state, r, { phase: 'completed', credits_used: st.credits_used }));
|
|
103
104
|
} else if (s === 'failed' || s === 'cancelled') {
|
|
104
105
|
boot(Object.assign({}, state, { phase: 'failed', error: st.error }));
|
|
105
|
-
} else { pollErrors = 0; pollTimer = setTimeout(poll,
|
|
106
|
+
} else { pollErrors = 0; pollTimer = setTimeout(poll, pollDelay()); }
|
|
106
107
|
}).catch(function () {
|
|
107
|
-
if (++pollErrors >= MAX_POLL_ERRORS) return
|
|
108
|
-
pollTimer = setTimeout(poll,
|
|
108
|
+
if (++pollErrors >= MAX_POLL_ERRORS) return trackingIssue('Tracking paused after repeated connection errors. The transcription may still be running.');
|
|
109
|
+
pollTimer = setTimeout(poll, pollDelay());
|
|
109
110
|
});
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
function pollDelay() {
|
|
114
|
+
// The call itself long-waits server-side; only briefly back off between
|
|
115
|
+
// successive wait windows.
|
|
116
|
+
return 1500;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function trackingIssue(msg) {
|
|
120
|
+
clearTimeout(pollTimer);
|
|
121
|
+
el('phase-chip').style.display = '';
|
|
122
|
+
el('phase-chip').textContent = 'Still working';
|
|
123
|
+
el('stage').classList.remove('k-empty');
|
|
124
|
+
el('stage').innerHTML = '<div class="k-error">' + ICONS.clock + ' ' + esc(msg) + '</div>';
|
|
125
|
+
el('actions').innerHTML = '<button class="k-btn primary" id="transcript-status">' + ICONS.clock + ' Check status</button>';
|
|
126
|
+
el('transcript-status').onclick = function () {
|
|
127
|
+
window.kolbo.sendMessage('Check the existing Kolbo transcription status without retrying it.' +
|
|
128
|
+
(state && state.generation_id ? '\\nGeneration ID: ' + state.generation_id : ''));
|
|
129
|
+
};
|
|
130
|
+
window.kolbo.notifySize();
|
|
131
|
+
}
|
|
132
|
+
|
|
112
133
|
window.kolbo.onToolResult(function (result) {
|
|
113
134
|
var sc = result.structuredContent || structured(result);
|
|
114
135
|
// Only boot on a real widget contract (phase present). A text-JSON
|
|
115
136
|
// fallback like { status: 'submitted' } has no phase and would render a
|
|
116
137
|
// bogus "(empty transcript)" completed view.
|
|
117
138
|
if (sc && (sc.phase || sc.widget === 'transcript')) return boot(sc);
|
|
139
|
+
if (sc && (typeof sc.text === 'string' || sc.srt_url || sc.txt_url)) {
|
|
140
|
+
return boot(Object.assign({}, sc, {
|
|
141
|
+
widget: 'transcript', phase: 'completed',
|
|
142
|
+
audio_url: originArgs.source && /^https?:\\/\\//i.test(originArgs.source) ? originArgs.source : undefined
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
118
145
|
var txt = '';
|
|
119
146
|
try { txt = (result.content || []).filter(function (c) { return c.type === 'text'; }).map(function (c) { return c.text; }).join(' '); } catch (e) {}
|
|
147
|
+
if (/timed out|timeout/i.test(txt)) return trackingIssue((txt || 'Tracking timed out.').slice(0, 300));
|
|
120
148
|
if (result.isError || /error|failed/i.test(txt)) {
|
|
121
149
|
el('phase-chip').style.display = 'none';
|
|
122
150
|
el('stage').classList.remove('k-empty');
|
|
@@ -129,6 +157,7 @@ window.kolbo.onToolResult(function (result) {
|
|
|
129
157
|
if (card) card.style.display = 'none';
|
|
130
158
|
window.kolbo.notifySize();
|
|
131
159
|
});
|
|
160
|
+
window.kolbo.onToolInput(function (args) { originArgs = args || {}; });
|
|
132
161
|
`;
|
|
133
162
|
|
|
134
163
|
function transcriptWidgetHtml() {
|
package/src/index.js
CHANGED
|
@@ -70,9 +70,9 @@ const { registerArtifactTools } = require('./tools/artifacts');
|
|
|
70
70
|
const { registerProjectTools } = require('./tools/projects');
|
|
71
71
|
const { registerAgentTools } = require('./tools/agents');
|
|
72
72
|
const { registerDocTools } = require('./tools/docs');
|
|
73
|
-
const { registerVoiceTools } = require('./tools/voices');
|
|
74
|
-
const { registerMusicLibraryTools } = require('./tools/music_library');
|
|
75
|
-
const { registerStockLibraryTools } = require('./tools/stock_library');
|
|
73
|
+
const { registerVoiceTools } = require('./tools/voices');
|
|
74
|
+
const { registerMusicLibraryTools } = require('./tools/music_library');
|
|
75
|
+
const { registerStockLibraryTools } = require('./tools/stock_library');
|
|
76
76
|
const { registerApps, attachToolWidgetMeta } = require('./apps');
|
|
77
77
|
|
|
78
78
|
/**
|
|
@@ -139,9 +139,9 @@ function createServer(opts = {}) {
|
|
|
139
139
|
registerArtifactTools(server, client, toolOptions);
|
|
140
140
|
registerProjectTools(server, client, toolOptions);
|
|
141
141
|
registerAgentTools(server, client, toolOptions);
|
|
142
|
-
registerDocTools(server, client, toolOptions);
|
|
143
|
-
registerMusicLibraryTools(server, client, toolOptions);
|
|
144
|
-
registerStockLibraryTools(server, client, toolOptions);
|
|
142
|
+
registerDocTools(server, client, toolOptions);
|
|
143
|
+
registerMusicLibraryTools(server, client, toolOptions);
|
|
144
|
+
registerStockLibraryTools(server, client, toolOptions);
|
|
145
145
|
|
|
146
146
|
// MCP Apps widget resources (ui://kolbo/*). Registering resources is inert
|
|
147
147
|
// for text-only hosts — they never fetch them.
|
package/src/tools/_shared.js
CHANGED
|
@@ -363,6 +363,19 @@ function buildOpenUrl(tool, gen) {
|
|
|
363
363
|
return url;
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Build the "Open in Kolbo" deep link for a PROJECT. Lands on the Media hub
|
|
368
|
+
* (all-assets view) pre-filtered to this project, where the user sees every
|
|
369
|
+
* generation/media item in it and can switch projects via the in-page selector.
|
|
370
|
+
* The media page reads `?project=<id>` on load and selects it. Returns undefined
|
|
371
|
+
* for a missing id or the default "API Generations" bucket (no useful landing —
|
|
372
|
+
* it is the catch-all, not a real workspace the user navigates to).
|
|
373
|
+
*/
|
|
374
|
+
function buildProjectUrl(projectId, opts = {}) {
|
|
375
|
+
if (!projectId || opts.is_default) return undefined;
|
|
376
|
+
return `${APP_BASE_URL}/media?project=${encodeURIComponent(projectId)}`;
|
|
377
|
+
}
|
|
378
|
+
|
|
366
379
|
// ─── MCP Apps generation widget helpers ──────────────────────────────────────
|
|
367
380
|
// When the host renders MCP Apps (claude.ai via the remote connector, Claude
|
|
368
381
|
// Desktop over stdio), generation tools return IMMEDIATELY after submit and the
|
|
@@ -380,7 +393,7 @@ const { UI, uiResult, appsEnabled, modelIcon } = require('../apps');
|
|
|
380
393
|
* client KolboClient (for model icon lookup)
|
|
381
394
|
* model, prompt, count, settings, reference_image, estimated_seconds
|
|
382
395
|
* poll_tool widget-side status tool (default 'get_generation_status')
|
|
383
|
-
* status_args args for poll_tool (default { generation_id })
|
|
396
|
+
* status_args args for poll_tool (default { generation_id, wait: true })
|
|
384
397
|
*/
|
|
385
398
|
async function uiGenerating(p) {
|
|
386
399
|
// No ETAs anywhere — just a spinner until the poll flips to completed.
|
|
@@ -392,7 +405,10 @@ async function uiGenerating(p) {
|
|
|
392
405
|
tool: p.tool,
|
|
393
406
|
generation_id: p.gen.generation_id,
|
|
394
407
|
poll_tool: p.poll_tool || 'get_generation_status',
|
|
395
|
-
|
|
408
|
+
// Keep at most one long-wait status call in flight per widget. Without
|
|
409
|
+
// wait=true, every open card calls tools/call every few seconds, flooding
|
|
410
|
+
// the host's global progress/context stream and API rate limits.
|
|
411
|
+
status_args: p.status_args || { generation_id: p.gen.generation_id, wait: true },
|
|
396
412
|
model: p.model || 'Smart Select',
|
|
397
413
|
model_icon: icon,
|
|
398
414
|
prompt: p.prompt,
|
|
@@ -451,6 +467,7 @@ module.exports = {
|
|
|
451
467
|
projectScopeReadField,
|
|
452
468
|
inlineImageBlocks,
|
|
453
469
|
buildOpenUrl,
|
|
470
|
+
buildProjectUrl,
|
|
454
471
|
uiGenerating,
|
|
455
472
|
uiCompleted,
|
|
456
473
|
appsEnabled,
|
package/src/tools/generate.js
CHANGED
|
@@ -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, buildOpenUrl, uiGenerating,
|
|
9
|
+
const { resolveToBuffer, creditFields, projectIdField, inlineImageBlocks, buildOpenUrl, uiGenerating, appsEnabled } = require('./_shared');
|
|
10
10
|
const { UI, uiResult, canonicalModelId } = require('../apps');
|
|
11
11
|
|
|
12
12
|
// ─── Cinematic Dimensions schema (shared by generate_image + generate_image_edit) ───
|
|
@@ -43,8 +43,8 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
43
43
|
// tool output is unchanged: a text block with the image URL.
|
|
44
44
|
const inlineImages = !!options.inlineImages;
|
|
45
45
|
// MCP Apps hosts (claude.ai remote connector, Claude Desktop) get an instant
|
|
46
|
-
// "submitted" response + a live ui://kolbo/generation.html widget that
|
|
47
|
-
//
|
|
46
|
+
// "submitted" response + a live ui://kolbo/generation.html widget that keeps
|
|
47
|
+
// one wait=true status call in flight. Text-only hosts never take this branch.
|
|
48
48
|
const ui = () => appsEnabled(server, options);
|
|
49
49
|
// ─── generate_image ────────────────────────────────────────
|
|
50
50
|
server.tool(
|
|
@@ -183,6 +183,15 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
183
183
|
});
|
|
184
184
|
|
|
185
185
|
const cdStatusUrl = `/v1/generate/creative-director/${gen.generation_id}/status`;
|
|
186
|
+
if (ui()) return uiGenerating({
|
|
187
|
+
tool: 'generate_creative_director', kind: 'scenes', gen, client, model, prompt,
|
|
188
|
+
count: scene_count || 4,
|
|
189
|
+
settings: { duration, resolution, aspect_ratio, mode: workflow_type || 'image' },
|
|
190
|
+
reference_image: reference_images?.[0],
|
|
191
|
+
poll_tool: 'get_creative_director_status',
|
|
192
|
+
status_args: { generation_id: gen.generation_id, wait: true }
|
|
193
|
+
});
|
|
194
|
+
|
|
186
195
|
// Video mode runs N scenes as parallel video generations, each of which
|
|
187
196
|
// can take several minutes — a whole batch routinely exceeds the 10-min
|
|
188
197
|
// image window. Give video batches 30 min (the server watchdog finalizes
|
|
@@ -237,15 +246,6 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
237
246
|
_followup_hint: 'Each scene is a separate asset. If the user asks to edit one scene, find that scene by scene_number/title and pass its image_urls[0] (or video_urls[0]) to generate_image_edit / edit_image / edit_video / generate_video_from_video. Do NOT re-run generate_creative_director unless the user explicitly wants a brand-new set.'
|
|
238
247
|
}, null, 2);
|
|
239
248
|
|
|
240
|
-
// Creative Director polls a dedicated status route the widget can't reach
|
|
241
|
-
// through get_generation_status, so it stays blocking on UI hosts too and
|
|
242
|
-
// renders the completed scene gallery.
|
|
243
|
-
if (ui()) return uiCompleted({
|
|
244
|
-
tool: 'generate_creative_director', kind: 'scenes', gen, client, model, prompt,
|
|
245
|
-
settings: { duration, resolution, mode: workflow_type }, scenes,
|
|
246
|
-
credits_used: creditFields(result).credits_used
|
|
247
|
-
}, cdText);
|
|
248
|
-
|
|
249
249
|
return { content: [{ type: 'text', text: cdText }] };
|
|
250
250
|
}
|
|
251
251
|
);
|
|
@@ -258,12 +258,30 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
258
258
|
// blocking poll window) needs this tool to be re-checked until done.
|
|
259
259
|
server.tool(
|
|
260
260
|
'get_creative_director_status',
|
|
261
|
-
'Check the status of a Creative Director batch (from generate_creative_director) by its generation_id. Returns overall state ("processing" until EVERY scene is terminal, then "completed"/"failed") plus each scene\'s number, title, per-scene status, and image_urls/video_urls.
|
|
261
|
+
'Check the status of a Creative Director batch (from generate_creative_director) by its generation_id. Returns overall state ("processing" until EVERY scene is terminal, then "completed"/"failed") plus each scene\'s number, title, per-scene status, and image_urls/video_urls. Set wait=true to block for up to ~3 minutes instead of repeatedly calling this tool. Do NOT use the generic get_generation_status for Creative Director ids; it points at the wrong endpoint.',
|
|
262
262
|
{
|
|
263
|
-
generation_id: z.string().describe('The Creative Director generation_id returned by generate_creative_director.')
|
|
263
|
+
generation_id: z.string().describe('The Creative Director generation_id returned by generate_creative_director.'),
|
|
264
|
+
wait: z.boolean().optional().describe('If true, block until the batch is terminal, up to ~3 minutes. Use this instead of repeatedly checking in a loop.')
|
|
264
265
|
},
|
|
265
|
-
async ({ generation_id }) => {
|
|
266
|
-
const
|
|
266
|
+
async ({ generation_id, wait }) => {
|
|
267
|
+
const statusUrl = `/v1/generate/creative-director/${encodeURIComponent(generation_id)}/status`;
|
|
268
|
+
let status;
|
|
269
|
+
if (wait) {
|
|
270
|
+
try {
|
|
271
|
+
status = await pollUntilDone(client, generation_id, {
|
|
272
|
+
interval: 15000,
|
|
273
|
+
timeout: 180000,
|
|
274
|
+
statusUrl
|
|
275
|
+
});
|
|
276
|
+
} catch (err) {
|
|
277
|
+
// A tracking timeout is not a failed paid generation. Return the
|
|
278
|
+
// latest batch state so the widget can start one new long wait.
|
|
279
|
+
if (!err?.timedOut) throw err;
|
|
280
|
+
status = await client.get(statusUrl);
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
status = await client.get(statusUrl);
|
|
284
|
+
}
|
|
267
285
|
const scenes = (status.scenes || []).map(s => ({
|
|
268
286
|
scene_number: s.scene_number,
|
|
269
287
|
status: s.status,
|
|
@@ -273,6 +291,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
273
291
|
}));
|
|
274
292
|
const completed = scenes.filter(s => s.status === 'completed').length;
|
|
275
293
|
return { content: [{ type: 'text', text: JSON.stringify({
|
|
294
|
+
...creditFields(status),
|
|
276
295
|
state: status.state,
|
|
277
296
|
generation_id,
|
|
278
297
|
progress: status.progress,
|
|
@@ -281,7 +300,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
281
300
|
completed_scenes: completed,
|
|
282
301
|
_hint: status.state === 'completed'
|
|
283
302
|
? 'All scenes terminal. Every completed scene\'s image_urls/video_urls are final.'
|
|
284
|
-
: 'Still running
|
|
303
|
+
: 'Still running. Call get_creative_director_status once with wait=true; do not poll it in a loop.'
|
|
285
304
|
}, null, 2) }] };
|
|
286
305
|
}
|
|
287
306
|
);
|
|
@@ -596,7 +615,10 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
596
615
|
const checkOne = async (id) => {
|
|
597
616
|
try {
|
|
598
617
|
if (wait) {
|
|
599
|
-
|
|
618
|
+
// Widgets use this long-wait path. A 15s API check cadence keeps
|
|
619
|
+
// completion responsive without multiplying backend traffic for
|
|
620
|
+
// every card left open in a host conversation.
|
|
621
|
+
const result = await pollUntilDone(client, id, { interval: 15000, timeout: 180000 });
|
|
600
622
|
return { generation_id: id, ...result };
|
|
601
623
|
}
|
|
602
624
|
const result = await client.get(`/v1/generate/${encodeURIComponent(id)}/status`);
|
|
@@ -621,8 +643,8 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
621
643
|
: 'Some generations are still processing. Do NOT re-call this tool in a loop — call it ONCE with wait=true (and all pending generation_ids) to block until they finish.';
|
|
622
644
|
|
|
623
645
|
// Single-id calls keep the original flat shape — the generation widget
|
|
624
|
-
//
|
|
625
|
-
// top level.
|
|
646
|
+
// waits on this tool with { generation_id, wait:true } and reads
|
|
647
|
+
// state/result at the top level.
|
|
626
648
|
if (!generation_ids || generation_ids.length === 0) {
|
|
627
649
|
const single = results[0];
|
|
628
650
|
single._hint = pending.length === 0
|
|
@@ -1069,6 +1091,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1069
1091
|
widget: 'transcript', phase: 'generating',
|
|
1070
1092
|
generation_id: startResponse.generation_id,
|
|
1071
1093
|
poll_tool: 'get_generation_status',
|
|
1094
|
+
status_args: { generation_id: startResponse.generation_id, wait: true },
|
|
1072
1095
|
audio_url: isUrl ? source : undefined,
|
|
1073
1096
|
open_url: buildOpenUrl('transcribe_audio', startResponse),
|
|
1074
1097
|
});
|
|
@@ -1,236 +1,203 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* CONTRACT. Never rename, remove, or break an existing tool/arg. Full rules: ../index.js top-of-file.
|
|
3
|
-
*
|
|
4
|
-
* DEPRECATED FAMILY (2026-07-11): these tools originally fronted the dedicated
|
|
5
|
-
* Synci-only /v1/music-library/* routes. The Synci partner key expired and the
|
|
6
|
-
* unified STOCK LIBRARY covers music anyway (kolbo-ai catalog + Coverr + Synci
|
|
7
|
-
* when its key is live), so every tool here is now a thin adapter over
|
|
8
|
-
* /v1/stock/* — same tool names/args, better backend, and old cached installs
|
|
9
|
-
* keep working. New integrations should use search_stock_media /
|
|
10
|
-
* get_stock_asset with mediaType "music" directly.
|
|
11
|
-
*/
|
|
1
|
+
/* Public MCP contract: keep existing tool and argument names backward compatible. */
|
|
12
2
|
|
|
13
3
|
const { z } = require('zod');
|
|
14
4
|
const { UI, uiResult, appsEnabled } = require('../apps');
|
|
15
5
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// Format a stock music asset into a compact human-readable line.
|
|
19
|
-
function assetTrackLine(a) {
|
|
6
|
+
function trackLine(track) {
|
|
20
7
|
const meta = [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
8
|
+
track.durationSeconds != null ? `${Math.round(track.durationSeconds)}s` : null,
|
|
9
|
+
track.artist || null,
|
|
10
|
+
track.bpm ? `${track.bpm} BPM` : null,
|
|
11
|
+
track.hqAvailable ? 'WAV available' : 'MP3 only',
|
|
24
12
|
].filter(Boolean).join(' · ');
|
|
25
|
-
|
|
26
|
-
return `${a.source}:${a.sourceId} — ${a.title || '(untitled)'}\n ${meta}${preview ? `\n preview: ${preview}` : ''}`;
|
|
13
|
+
return `[${track.id}] ${track.title || '(untitled)'}${meta ? `\n ${meta}` : ''}`;
|
|
27
14
|
}
|
|
28
15
|
|
|
29
|
-
|
|
30
|
-
function assetTrackItem(a) {
|
|
16
|
+
function trackItem(track) {
|
|
31
17
|
return {
|
|
32
|
-
id:
|
|
33
|
-
title:
|
|
34
|
-
subtitle: [
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
a.source,
|
|
38
|
-
].filter(Boolean).join(' · '),
|
|
39
|
-
thumbnail: a.thumbnailUrl || null,
|
|
18
|
+
id: track.id,
|
|
19
|
+
title: track.title || '(untitled)',
|
|
20
|
+
subtitle: [track.artist, track.durationSeconds != null ? `${Math.round(track.durationSeconds)}s` : null]
|
|
21
|
+
.filter(Boolean).join(' · '),
|
|
22
|
+
thumbnail: track.artworkUrl || null,
|
|
40
23
|
media_type: 'audio',
|
|
41
|
-
preview_audio:
|
|
42
|
-
use_hint:
|
|
24
|
+
preview_audio: track.previewAudioUrl || track.audioUrl || track.audioUrl128 || null,
|
|
25
|
+
use_hint: `Acquire a clean track with acquire_clean_music_track track_id="${track.id}" format="mp3".`,
|
|
43
26
|
};
|
|
44
27
|
}
|
|
45
28
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (i > 0) return { source: trackId.slice(0, i), id: trackId.slice(i + 1) };
|
|
62
|
-
return { source: 'synci', id: trackId };
|
|
29
|
+
function tracksResult(ui, title, tracks, total) {
|
|
30
|
+
if (!tracks.length) return { content: [{ type: 'text', text: 'No SYNCI tracks found.' }] };
|
|
31
|
+
const text = [
|
|
32
|
+
`Found ${tracks.length} track${tracks.length === 1 ? '' : 's'}${total ? ` (of ${total})` : ''}.`,
|
|
33
|
+
'Playback URLs are watermarked previews. Use acquire_clean_music_track for final use; it consumes one SYNCI vendor credit.',
|
|
34
|
+
'',
|
|
35
|
+
tracks.map(trackLine).join('\n\n'),
|
|
36
|
+
].join('\n');
|
|
37
|
+
if (!ui()) return { content: [{ type: 'text', text }] };
|
|
38
|
+
return uiResult(UI.mediaGrid, text, {
|
|
39
|
+
widget: 'media-grid',
|
|
40
|
+
title,
|
|
41
|
+
items: tracks.slice(0, 20).map(trackItem),
|
|
42
|
+
total: total != null ? total : tracks.length,
|
|
43
|
+
});
|
|
63
44
|
}
|
|
64
45
|
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
46
|
+
function cleanResult(result, requestId) {
|
|
47
|
+
return {
|
|
48
|
+
content: [{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: JSON.stringify({
|
|
51
|
+
track_id: result.trackId,
|
|
52
|
+
format: result.format,
|
|
53
|
+
audio_url: result.audioUrl,
|
|
54
|
+
download_url: result.downloadUrl,
|
|
55
|
+
watermarked: false,
|
|
56
|
+
credits_remaining: result.creditsRemaining,
|
|
57
|
+
request_id: requestId,
|
|
58
|
+
reused: !!result.reused,
|
|
59
|
+
}, null, 2),
|
|
60
|
+
}],
|
|
61
|
+
};
|
|
80
62
|
}
|
|
81
63
|
|
|
82
64
|
function registerMusicLibraryTools(server, client, options = {}) {
|
|
83
65
|
const ui = () => appsEnabled(server, options);
|
|
84
66
|
|
|
85
|
-
// ─── search_music_library (adapter → stock search) ─────────────
|
|
86
67
|
server.tool(
|
|
87
68
|
'search_music_library',
|
|
88
|
-
|
|
69
|
+
'Search the licensed SYNCI catalog. Results contain watermarked preview audio only. For any download or timeline use, call acquire_clean_music_track, which consumes one SYNCI vendor credit.',
|
|
89
70
|
{
|
|
90
|
-
query: z.string().max(200).optional()
|
|
91
|
-
mood: z.string().optional()
|
|
92
|
-
genre: z.string().optional()
|
|
93
|
-
bpmMin: z.number().optional()
|
|
94
|
-
bpmMax: z.number().optional()
|
|
95
|
-
durationMin: z.number().optional()
|
|
96
|
-
durationMax: z.number().optional()
|
|
97
|
-
hasStems: z.boolean().optional()
|
|
98
|
-
hasLyrics: z.boolean().optional()
|
|
99
|
-
sort: z.enum(['duration-asc', 'duration-desc', 'bpm-asc', 'bpm-desc', 'title']).optional()
|
|
100
|
-
limit: z.number().int().min(1).max(
|
|
101
|
-
offset: z.number().int().min(0).optional()
|
|
71
|
+
query: z.string().max(200).optional(),
|
|
72
|
+
mood: z.string().optional(),
|
|
73
|
+
genre: z.string().optional(),
|
|
74
|
+
bpmMin: z.number().optional(),
|
|
75
|
+
bpmMax: z.number().optional(),
|
|
76
|
+
durationMin: z.number().optional(),
|
|
77
|
+
durationMax: z.number().optional(),
|
|
78
|
+
hasStems: z.boolean().optional(),
|
|
79
|
+
hasLyrics: z.boolean().optional(),
|
|
80
|
+
sort: z.enum(['duration-asc', 'duration-desc', 'bpm-asc', 'bpm-desc', 'title']).optional(),
|
|
81
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
82
|
+
offset: z.number().int().min(0).optional(),
|
|
102
83
|
},
|
|
103
84
|
async (args) => {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
'No tracks found matching that query. Try a broader vibe description, or use search_stock_media with mediaType="music".');
|
|
108
|
-
}
|
|
85
|
+
const result = await client.post('/v1/music-library/search', args);
|
|
86
|
+
return tracksResult(ui, `SYNCI — ${args.query || 'Search'}`, result.tracks || [], result.total);
|
|
87
|
+
},
|
|
109
88
|
);
|
|
110
89
|
|
|
111
|
-
// ─── analyze_script_for_music (unchanged — doesn't touch Synci) ─
|
|
112
90
|
server.tool(
|
|
113
91
|
'analyze_script_for_music',
|
|
114
|
-
'
|
|
115
|
-
{
|
|
116
|
-
script: z.string().min(1).describe('The video or voiceover script / scene description to analyze (up to ~8000 chars).')
|
|
117
|
-
},
|
|
92
|
+
'Turn a script or scene description into a SYNCI music search.',
|
|
93
|
+
{ script: z.string().min(1).max(8000) },
|
|
118
94
|
async ({ script }) => {
|
|
119
95
|
const result = await client.post('/v1/music-library/analyze-script', { script });
|
|
120
|
-
return {
|
|
121
|
-
|
|
122
|
-
type: 'text',
|
|
123
|
-
text: JSON.stringify({
|
|
124
|
-
query: result.query,
|
|
125
|
-
mood: result.mood,
|
|
126
|
-
genre: result.genre,
|
|
127
|
-
keywords: result.keywords,
|
|
128
|
-
_followup_hint: 'Pass query + mood + genre into search_music_library (or search_stock_media mediaType="music").'
|
|
129
|
-
}, null, 2)
|
|
130
|
-
}]
|
|
131
|
-
};
|
|
132
|
-
}
|
|
96
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
97
|
+
},
|
|
133
98
|
);
|
|
134
99
|
|
|
135
|
-
// ─── browse_music_library (adapter → stock browse feed) ────────
|
|
136
100
|
server.tool(
|
|
137
101
|
'browse_music_library',
|
|
138
|
-
|
|
102
|
+
'Browse the licensed SYNCI catalog. Playback remains watermarked; final use requires acquire_clean_music_track.',
|
|
139
103
|
{
|
|
140
|
-
sort: z.enum(['duration-asc', 'duration-desc', 'bpm-asc', 'bpm-desc', 'title']).optional()
|
|
141
|
-
limit: z.number().int().min(1).max(50).optional()
|
|
142
|
-
offset: z.number().int().min(0).optional()
|
|
104
|
+
sort: z.enum(['duration-asc', 'duration-desc', 'bpm-asc', 'bpm-desc', 'title']).optional(),
|
|
105
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
106
|
+
offset: z.number().int().min(0).optional(),
|
|
107
|
+
},
|
|
108
|
+
async ({ sort, limit, offset }) => {
|
|
109
|
+
const params = new URLSearchParams();
|
|
110
|
+
if (sort) params.set('sort', sort);
|
|
111
|
+
if (limit != null) params.set('limit', String(limit));
|
|
112
|
+
if (offset != null) params.set('offset', String(offset));
|
|
113
|
+
const result = await client.get(`/v1/music-library/catalog?${params.toString()}`);
|
|
114
|
+
return tracksResult(ui, 'SYNCI Music Library', result.tracks || [], result.total);
|
|
143
115
|
},
|
|
144
|
-
async ({ limit, offset }) => {
|
|
145
|
-
const { assets, total } = await stockMusicSearch(client, '', limit, offset);
|
|
146
|
-
return musicResult(ui, null, assets, total, 'No tracks returned.');
|
|
147
|
-
}
|
|
148
116
|
);
|
|
149
117
|
|
|
150
|
-
// ─── get_music_library_facets (adapter → stock categories) ─────
|
|
151
118
|
server.tool(
|
|
152
119
|
'get_music_library_facets',
|
|
153
|
-
|
|
120
|
+
'List SYNCI genres, moods, instruments, BPM, and duration filters.',
|
|
154
121
|
{},
|
|
155
122
|
async () => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
categories = (result.categories || []).map(c => c.name || c.providerParam).filter(Boolean);
|
|
160
|
-
} catch (_) { /* categories are best-effort */ }
|
|
161
|
-
return {
|
|
162
|
-
content: [{
|
|
163
|
-
type: 'text',
|
|
164
|
-
text: JSON.stringify({
|
|
165
|
-
genres: categories,
|
|
166
|
-
moods: [],
|
|
167
|
-
instruments: [],
|
|
168
|
-
bpmRange: null,
|
|
169
|
-
durationRange: null,
|
|
170
|
-
_note: 'Semantic search: any natural-language mood/vibe works as a query — exact facet values are no longer required.'
|
|
171
|
-
}, null, 2)
|
|
172
|
-
}]
|
|
173
|
-
};
|
|
174
|
-
}
|
|
123
|
+
const result = await client.get('/v1/music-library/facets');
|
|
124
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
125
|
+
},
|
|
175
126
|
);
|
|
176
127
|
|
|
177
|
-
// ─── get_music_track_audio (adapter → stock asset) ──────────────
|
|
178
128
|
server.tool(
|
|
179
129
|
'get_music_track_audio',
|
|
180
|
-
|
|
130
|
+
'Get watermarked preview URLs for a SYNCI track. These URLs are never licensed masters; call acquire_clean_music_track for final use.',
|
|
131
|
+
{ track_id: z.string().min(1).max(64) },
|
|
132
|
+
async ({ track_id }) => {
|
|
133
|
+
const result = await client.get(`/v1/music-library/track/${encodeURIComponent(track_id)}/audio`);
|
|
134
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...result, preview_only: true }, null, 2) }] };
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
server.tool(
|
|
139
|
+
'acquire_clean_music_track',
|
|
140
|
+
'Acquire a clean, unwatermarked SYNCI MP3 or WAV for download or Adobe timeline use. This immediately consumes one SYNCI vendor credit with no confirmation dialog. Reuse request_id when retrying the same intended action.',
|
|
181
141
|
{
|
|
182
|
-
track_id: z.string().
|
|
142
|
+
track_id: z.string().min(1).max(64),
|
|
143
|
+
format: z.enum(['mp3', 'wav']).optional().describe('Default mp3. Use wav only when the search result reports hqAvailable=true.'),
|
|
144
|
+
purpose: z.enum(['download', 'timeline']).optional(),
|
|
145
|
+
request_id: z.string().regex(/^[A-Za-z0-9_-]{8,80}$/).describe('Required idempotency key. Reuse it for retries of the same action.'),
|
|
146
|
+
project_id: z.string().optional(),
|
|
147
|
+
},
|
|
148
|
+
async ({ track_id, format = 'mp3', purpose = 'download', request_id, project_id }) => {
|
|
149
|
+
const requestId = request_id;
|
|
150
|
+
const result = await client.post(`/v1/music-library/clean/${encodeURIComponent(track_id)}`, {
|
|
151
|
+
format,
|
|
152
|
+
purpose,
|
|
153
|
+
requestId,
|
|
154
|
+
projectId: project_id,
|
|
155
|
+
});
|
|
156
|
+
return cleanResult(result, requestId);
|
|
183
157
|
},
|
|
184
|
-
async ({ track_id }) => {
|
|
185
|
-
const { source, id } = parseTrackId(track_id);
|
|
186
|
-
// mediaType hint required for sources that share ids across types (kolbo-ai).
|
|
187
|
-
const result = await client.get(`/v1/stock/asset/${encodeURIComponent(source)}/${encodeURIComponent(id)}?mediaType=music`);
|
|
188
|
-
const a = result.asset || result;
|
|
189
|
-
const urls = {};
|
|
190
|
-
(a.downloadVariants || []).forEach(v => { if (v.url) urls[v.label || 'audio'] = v.url; });
|
|
191
|
-
return {
|
|
192
|
-
content: [{
|
|
193
|
-
type: 'text',
|
|
194
|
-
text: JSON.stringify({ id: track_id, title: a.title, urls }, null, 2)
|
|
195
|
-
}]
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
158
|
);
|
|
199
159
|
|
|
200
|
-
// ─── get_music_track_related (graceful stub — no stock equivalent) ─
|
|
201
160
|
server.tool(
|
|
202
|
-
'
|
|
203
|
-
|
|
161
|
+
'import_music_track_to_library',
|
|
162
|
+
'Acquire one clean SYNCI file and copy it into the Kolbo media library. This immediately consumes one SYNCI vendor credit unless the track is already in the library. Defaults to clean MP3.',
|
|
204
163
|
{
|
|
205
|
-
track_id: z.string().
|
|
164
|
+
track_id: z.string().min(1).max(64),
|
|
165
|
+
format: z.enum(['mp3', 'wav']).optional(),
|
|
166
|
+
request_id: z.string().regex(/^[A-Za-z0-9_-]{8,80}$/).describe('Required idempotency key; reuse it for retries.'),
|
|
167
|
+
project_id: z.string().optional(),
|
|
168
|
+
track: z.record(z.string(), z.unknown()).optional().describe('Optional track snapshot from search_music_library.'),
|
|
169
|
+
},
|
|
170
|
+
async ({ track_id, format = 'mp3', request_id, project_id, track }) => {
|
|
171
|
+
const requestId = request_id;
|
|
172
|
+
const result = await client.post('/v1/music-library/import', {
|
|
173
|
+
trackId: track_id,
|
|
174
|
+
format,
|
|
175
|
+
requestId,
|
|
176
|
+
projectId: project_id,
|
|
177
|
+
track,
|
|
178
|
+
});
|
|
179
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...result, requestId }, null, 2) }] };
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
server.tool(
|
|
184
|
+
'get_music_track_related',
|
|
185
|
+
'Get SYNCI stems and alternate versions metadata. Purchasing stems or alternate versions is not supported.',
|
|
186
|
+
{ track_id: z.string().min(1).max(64) },
|
|
187
|
+
async ({ track_id }) => {
|
|
188
|
+
const result = await client.get(`/v1/music-library/track/${encodeURIComponent(track_id)}/related`);
|
|
189
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
206
190
|
},
|
|
207
|
-
async ({ track_id }) => ({
|
|
208
|
-
content: [{
|
|
209
|
-
type: 'text',
|
|
210
|
-
text: JSON.stringify({
|
|
211
|
-
stems: [], versions: [],
|
|
212
|
-
_note: `Stems/alternate versions are not available via the stock library. Use get_music_track_audio ("${track_id}") for the downloadable variants, or generate_music to compose a custom track.`
|
|
213
|
-
}, null, 2)
|
|
214
|
-
}]
|
|
215
|
-
})
|
|
216
191
|
);
|
|
217
192
|
|
|
218
|
-
// ─── get_music_track_lyrics (graceful stub — no stock equivalent) ─
|
|
219
193
|
server.tool(
|
|
220
194
|
'get_music_track_lyrics',
|
|
221
|
-
|
|
222
|
-
{
|
|
223
|
-
|
|
195
|
+
'Get SYNCI lyrics metadata for a track.',
|
|
196
|
+
{ track_id: z.string().min(1).max(64) },
|
|
197
|
+
async ({ track_id }) => {
|
|
198
|
+
const result = await client.get(`/v1/music-library/track/${encodeURIComponent(track_id)}/lyrics`);
|
|
199
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
224
200
|
},
|
|
225
|
-
async () => ({
|
|
226
|
-
content: [{
|
|
227
|
-
type: 'text',
|
|
228
|
-
text: JSON.stringify({
|
|
229
|
-
hasLyrics: false, lyrics: null, lyricalTheme: null, explicit: null,
|
|
230
|
-
_note: 'Lyrics metadata is not available via the stock library. For a song with specific lyrics, use generate_music with the lyrics field.'
|
|
231
|
-
}, null, 2)
|
|
232
|
-
}]
|
|
233
|
-
})
|
|
234
201
|
);
|
|
235
202
|
}
|
|
236
203
|
|
package/src/tools/projects.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
|
|
5
5
|
|
|
6
6
|
const { z } = require('zod');
|
|
7
|
+
const { buildProjectUrl } = require('./_shared');
|
|
7
8
|
|
|
8
9
|
function registerProjectTools(server, client) {
|
|
9
10
|
// ─── list_projects ─────────────────────────────────────────
|
|
@@ -17,7 +18,8 @@ function registerProjectTools(server, client) {
|
|
|
17
18
|
id: p.id,
|
|
18
19
|
name: p.name,
|
|
19
20
|
role: p.role,
|
|
20
|
-
is_default: !!p.is_default
|
|
21
|
+
is_default: !!p.is_default,
|
|
22
|
+
open_url: buildProjectUrl(p.id, { is_default: !!p.is_default })
|
|
21
23
|
}));
|
|
22
24
|
return {
|
|
23
25
|
content: [{
|
|
@@ -25,7 +27,7 @@ function registerProjectTools(server, client) {
|
|
|
25
27
|
text: JSON.stringify({
|
|
26
28
|
projects,
|
|
27
29
|
count: projects.length,
|
|
28
|
-
_hint: 'Pass the chosen `id` as `project_id` on any generate_* tool to drop the generation into that project. Omit project_id to use the project flagged is_default:true.'
|
|
30
|
+
_hint: 'Pass the chosen `id` as `project_id` on any generate_* tool to drop the generation into that project. Omit project_id to use the project flagged is_default:true. `open_url` opens that project\'s media in the web app (share it with the user).'
|
|
29
31
|
}, null, 2)
|
|
30
32
|
}]
|
|
31
33
|
};
|
|
@@ -68,7 +70,8 @@ function registerProjectTools(server, client) {
|
|
|
68
70
|
const body = { name };
|
|
69
71
|
if (description) body.description = description;
|
|
70
72
|
const result = await client.post('/v1/projects', body);
|
|
71
|
-
|
|
73
|
+
const open_url = buildProjectUrl(result.project && result.project.id, { is_default: !!(result.project && result.project.is_default) });
|
|
74
|
+
return { content: [{ type: 'text', text: JSON.stringify({ project: result.project, open_url, _hint: 'Pass this id as project_id on every subsequent call for this work. `open_url` opens the project in the web app — share it with the user.' }, null, 2) }] };
|
|
72
75
|
}
|
|
73
76
|
);
|
|
74
77
|
|
|
@@ -181,7 +181,7 @@ function registerStockLibraryTools(server, client, options = {}) {
|
|
|
181
181
|
// ─── import_stock_asset ───────────────────────────────────────
|
|
182
182
|
server.tool(
|
|
183
183
|
'import_stock_asset',
|
|
184
|
-
"Copy a stock asset into the account's Kolbo media library (downloaded to Kolbo's CDN with a stable URL) so it can be used in projects/generations. Free. Returns the created media library item. Works for Kolbo SFX (source='kolbo-ai', mediaType='sfx') and external visual/audio sources.
|
|
184
|
+
"Copy a stock asset into the account's Kolbo media library (downloaded to Kolbo's CDN with a stable URL) so it can be used in projects/generations. Free. Returns the created media library item. Works for Kolbo SFX (source='kolbo-ai', mediaType='sfx') and supported external visual/audio sources. For SYNCI music use import_music_track_to_library; that paid action acquires a clean licensed file.",
|
|
185
185
|
{
|
|
186
186
|
source: z.enum(['kolbo-ai', 'pexels', 'pixabay', 'sketchfab', 'freesound']).describe('The asset source.'),
|
|
187
187
|
id: z.string().describe('The provider asset id (sourceId).'),
|