@kolbo/mcp 1.31.4 → 1.31.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/apps/bridge.js +12 -0
- package/src/apps/theme.js +6 -2
- package/src/apps/widgets/generation.js +36 -15
- package/src/tools/music_library.js +136 -119
package/package.json
CHANGED
package/src/apps/bridge.js
CHANGED
|
@@ -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,7 +175,8 @@ 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%;
|
|
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
|
|
|
@@ -185,7 +186,10 @@ html.k-fullscreen .k-card { height: 100%; display: flex; flex-direction: column;
|
|
|
185
186
|
html.k-fullscreen .k-body { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
186
187
|
html.k-fullscreen .k-viewer { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; }
|
|
187
188
|
html.k-fullscreen .k-viewer img, html.k-fullscreen .k-viewer video {
|
|
188
|
-
|
|
189
|
+
/* hard viewport cap — the image must NEVER exceed the screen or cover the
|
|
190
|
+
host's chrome, whatever the host's iframe sizing does */
|
|
191
|
+
max-height: min(100%, calc(100dvh - 130px)); max-width: 100%;
|
|
192
|
+
width: auto; margin: 0 auto; cursor: zoom-out; object-fit: contain; }
|
|
189
193
|
html.k-fullscreen .k-thumbs .k-thumb { width: 64px; height: 64px; }
|
|
190
194
|
.k-expand-hint { display: none; }
|
|
191
195
|
.k-thumbs { display: flex; gap: 6px; margin: 10px 0 2px; }
|
|
@@ -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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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 = '';
|
|
@@ -1,85 +1,117 @@
|
|
|
1
1
|
/* ⛔ BACKWARD COMPATIBILITY: Tool names and arg names below are a PUBLIC
|
|
2
|
-
* CONTRACT. Never rename, remove, or break an existing tool/arg. Full rules: ../index.js top-of-file.
|
|
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
|
+
*/
|
|
3
12
|
|
|
4
13
|
const { z } = require('zod');
|
|
5
14
|
const { UI, uiResult, appsEnabled } = require('../apps');
|
|
6
15
|
|
|
7
|
-
|
|
8
|
-
|
|
16
|
+
const DEPRECATION_NOTE = '[Deprecated — served by the unified Stock Library now; prefer search_stock_media / get_stock_asset with mediaType "music".] ';
|
|
17
|
+
|
|
18
|
+
// Format a stock music asset into a compact human-readable line.
|
|
19
|
+
function assetTrackLine(a) {
|
|
9
20
|
const meta = [
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Array.isArray(t.genres) && t.genres.length ? t.genres.join('/') : t.genre,
|
|
14
|
-
Array.isArray(t.moodTags) && t.moodTags.length ? t.moodTags.slice(0, 3).join(', ') : null,
|
|
21
|
+
a.durationSeconds != null ? `${Math.round(a.durationSeconds)}s` : null,
|
|
22
|
+
a.author?.name || null,
|
|
23
|
+
a.source,
|
|
15
24
|
].filter(Boolean).join(' · ');
|
|
16
|
-
const
|
|
17
|
-
return `${
|
|
25
|
+
const preview = a.previewUrl || a.downloadVariants?.[0]?.url || null;
|
|
26
|
+
return `${a.source}:${a.sourceId} — ${a.title || '(untitled)'}\n ${meta}${preview ? `\n preview: ${preview}` : ''}`;
|
|
18
27
|
}
|
|
19
28
|
|
|
20
|
-
// Map a
|
|
21
|
-
function
|
|
29
|
+
// Map a stock music asset onto the media-grid widget item contract.
|
|
30
|
+
function assetTrackItem(a) {
|
|
22
31
|
return {
|
|
23
|
-
id:
|
|
24
|
-
title:
|
|
32
|
+
id: `${a.source}:${a.sourceId}`,
|
|
33
|
+
title: a.title || '(untitled)',
|
|
25
34
|
subtitle: [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
a.author?.name || null,
|
|
36
|
+
a.durationSeconds != null ? Math.round(a.durationSeconds) + 's' : null,
|
|
37
|
+
a.source,
|
|
29
38
|
].filter(Boolean).join(' · '),
|
|
30
|
-
thumbnail:
|
|
39
|
+
thumbnail: a.thumbnailUrl || null,
|
|
31
40
|
media_type: 'audio',
|
|
32
|
-
preview_audio:
|
|
41
|
+
preview_audio: a.previewUrl || a.downloadVariants?.[0]?.url || null,
|
|
33
42
|
use_hint: 'Get download links for track "{TITLE}" (id: {ID}) via get_music_track_audio.'
|
|
34
43
|
};
|
|
35
44
|
}
|
|
36
45
|
|
|
46
|
+
// Search the unified stock library for music. `query` may be empty (browse).
|
|
47
|
+
async function stockMusicSearch(client, query, limit, offset) {
|
|
48
|
+
const params = new URLSearchParams();
|
|
49
|
+
if (query) params.set('query', query);
|
|
50
|
+
params.set('mediaType', 'music');
|
|
51
|
+
params.set('source', 'all');
|
|
52
|
+
params.set('perPage', String(Math.min(Math.max(limit || 20, 1), 40)));
|
|
53
|
+
if (offset) params.set('page', String(Math.floor(offset / (limit || 20)) + 1));
|
|
54
|
+
const result = await client.get(`/v1/stock/search?${params.toString()}`);
|
|
55
|
+
return { assets: result.assets || [], total: result.total };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// "source:sourceId" (new) or a bare legacy id (assume synci — the old backend).
|
|
59
|
+
function parseTrackId(trackId) {
|
|
60
|
+
const i = String(trackId).indexOf(':');
|
|
61
|
+
if (i > 0) return { source: trackId.slice(0, i), id: trackId.slice(i + 1) };
|
|
62
|
+
return { source: 'synci', id: trackId };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function musicResult(ui, args, assets, total, emptyText) {
|
|
66
|
+
if (assets.length === 0) {
|
|
67
|
+
return { content: [{ type: 'text', text: emptyText }] };
|
|
68
|
+
}
|
|
69
|
+
const head = `Found ${assets.length} track${assets.length === 1 ? '' : 's'}${total ? ` (of ${total})` : ''}:`;
|
|
70
|
+
const text = `${head}\n\n${assets.map(assetTrackLine).join('\n\n')}\n\nUse the track id with get_music_track_audio to get downloadable URLs (or get_stock_asset directly).`;
|
|
71
|
+
if (ui()) {
|
|
72
|
+
return uiResult(UI.mediaGrid, text, {
|
|
73
|
+
widget: 'media-grid',
|
|
74
|
+
title: 'Music Library' + (args && args.query ? ' — "' + args.query + '"' : ''),
|
|
75
|
+
items: assets.slice(0, 20).map(assetTrackItem),
|
|
76
|
+
total: total != null ? total : assets.length
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return { content: [{ type: 'text', text }] };
|
|
80
|
+
}
|
|
81
|
+
|
|
37
82
|
function registerMusicLibraryTools(server, client, options = {}) {
|
|
38
83
|
const ui = () => appsEnabled(server, options);
|
|
39
|
-
|
|
84
|
+
|
|
85
|
+
// ─── search_music_library (adapter → stock search) ─────────────
|
|
40
86
|
server.tool(
|
|
41
87
|
'search_music_library',
|
|
42
|
-
'Search
|
|
88
|
+
DEPRECATION_NOTE + 'Search stock/production music by keyword. Mood/genre are folded into the semantic query (the Kolbo catalog matches by vibe, e.g. "uplifting corporate", "tense cinematic"). Returns tracks with id, title, duration, and preview/download URLs. For scripts, call analyze_script_for_music first.',
|
|
43
89
|
{
|
|
44
|
-
query: z.string().max(200).optional().describe('Keyword search, e.g. "uplifting corporate", "tense cinematic", "lofi hip hop". If omitted, falls back to the mood/genre filter as the search term.'),
|
|
45
|
-
mood: z.string().optional().describe('Mood
|
|
46
|
-
genre: z.string().optional().describe('Genre
|
|
47
|
-
bpmMin: z.number().optional().describe('
|
|
48
|
-
bpmMax: z.number().optional().describe('
|
|
49
|
-
durationMin: z.number().optional().describe('
|
|
50
|
-
durationMax: z.number().optional().describe('
|
|
51
|
-
hasStems: z.boolean().optional().describe('
|
|
52
|
-
hasLyrics: z.boolean().optional().describe('
|
|
53
|
-
sort: z.enum(['duration-asc', 'duration-desc', 'bpm-asc', 'bpm-desc', 'title']).optional().describe('
|
|
90
|
+
query: z.string().max(200).optional().describe('Keyword/vibe search, e.g. "uplifting corporate", "tense cinematic", "lofi hip hop". If omitted, falls back to the mood/genre filter as the search term.'),
|
|
91
|
+
mood: z.string().optional().describe('Mood keyword, folded into the search query (e.g. "Emotional", "Energetic").'),
|
|
92
|
+
genre: z.string().optional().describe('Genre keyword, folded into the search query (e.g. "Corporate", "Hip Hop").'),
|
|
93
|
+
bpmMin: z.number().optional().describe('Legacy filter — accepted but no longer applied.'),
|
|
94
|
+
bpmMax: z.number().optional().describe('Legacy filter — accepted but no longer applied.'),
|
|
95
|
+
durationMin: z.number().optional().describe('Legacy filter — accepted but no longer applied.'),
|
|
96
|
+
durationMax: z.number().optional().describe('Legacy filter — accepted but no longer applied.'),
|
|
97
|
+
hasStems: z.boolean().optional().describe('Legacy filter — accepted but no longer applied.'),
|
|
98
|
+
hasLyrics: z.boolean().optional().describe('Legacy filter — accepted but no longer applied.'),
|
|
99
|
+
sort: z.enum(['duration-asc', 'duration-desc', 'bpm-asc', 'bpm-desc', 'title']).optional().describe('Legacy sort — accepted but no longer applied (relevance order).'),
|
|
54
100
|
limit: z.number().int().min(1).max(40).optional().describe('Results per page (max 40, default 20).'),
|
|
55
101
|
offset: z.number().int().min(0).optional().describe('Pagination offset for loading more results.')
|
|
56
102
|
},
|
|
57
103
|
async (args) => {
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
const head = `Found ${tracks.length} track${tracks.length === 1 ? '' : 's'}${result.total ? ` (of ${result.total} sorted)` : ''}:`;
|
|
64
|
-
const text = `${head}\n\n${tracks.map(trackLine).join('\n\n')}\n\nUse the track id with get_music_track_audio to get the downloadable 128/320/wav URLs.`;
|
|
65
|
-
|
|
66
|
-
if (ui()) {
|
|
67
|
-
return uiResult(UI.mediaGrid, text, {
|
|
68
|
-
widget: 'media-grid',
|
|
69
|
-
title: 'Music Library' + (args.query ? ' — "' + args.query + '"' : ''),
|
|
70
|
-
items: tracks.slice(0, 20).map(trackItem),
|
|
71
|
-
total: result.total != null ? result.total : tracks.length
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return { content: [{ type: 'text', text }] };
|
|
104
|
+
const query = [args.query, args.mood, args.genre].filter(Boolean).join(' ').trim();
|
|
105
|
+
const { assets, total } = await stockMusicSearch(client, query, args.limit, args.offset);
|
|
106
|
+
return musicResult(ui, args, assets, total,
|
|
107
|
+
'No tracks found matching that query. Try a broader vibe description, or use search_stock_media with mediaType="music".');
|
|
76
108
|
}
|
|
77
109
|
);
|
|
78
110
|
|
|
79
|
-
// ─── analyze_script_for_music
|
|
111
|
+
// ─── analyze_script_for_music (unchanged — doesn't touch Synci) ─
|
|
80
112
|
server.tool(
|
|
81
113
|
'analyze_script_for_music',
|
|
82
|
-
'AI helper that turns a video or voiceover script into a music search. Returns { query, mood, genre, keywords } you can pass straight into search_music_library to find a fitting background track. Use this first when the user gives you a script/scene description rather than explicit music keywords.',
|
|
114
|
+
'AI helper that turns a video or voiceover script into a music search. Returns { query, mood, genre, keywords } you can pass straight into search_music_library (or search_stock_media with mediaType="music") to find a fitting background track. Use this first when the user gives you a script/scene description rather than explicit music keywords.',
|
|
83
115
|
{
|
|
84
116
|
script: z.string().min(1).describe('The video or voiceover script / scene description to analyze (up to ~8000 chars).')
|
|
85
117
|
},
|
|
@@ -93,127 +125,112 @@ function registerMusicLibraryTools(server, client, options = {}) {
|
|
|
93
125
|
mood: result.mood,
|
|
94
126
|
genre: result.genre,
|
|
95
127
|
keywords: result.keywords,
|
|
96
|
-
_followup_hint: 'Pass query + mood + genre into search_music_library.'
|
|
128
|
+
_followup_hint: 'Pass query + mood + genre into search_music_library (or search_stock_media mediaType="music").'
|
|
97
129
|
}, null, 2)
|
|
98
130
|
}]
|
|
99
131
|
};
|
|
100
132
|
}
|
|
101
133
|
);
|
|
102
134
|
|
|
103
|
-
// ─── browse_music_library
|
|
135
|
+
// ─── browse_music_library (adapter → stock browse feed) ────────
|
|
104
136
|
server.tool(
|
|
105
137
|
'browse_music_library',
|
|
106
|
-
'Browse the music
|
|
138
|
+
DEPRECATION_NOTE + 'Browse the music catalog without a search query (paginated feed). For targeted search use search_music_library or search_stock_media.',
|
|
107
139
|
{
|
|
108
|
-
sort: z.enum(['duration-asc', 'duration-desc', 'bpm-asc', 'bpm-desc', 'title']).optional().describe('
|
|
109
|
-
limit: z.number().int().min(1).max(50).optional().describe('Results per page (max
|
|
140
|
+
sort: z.enum(['duration-asc', 'duration-desc', 'bpm-asc', 'bpm-desc', 'title']).optional().describe('Legacy sort — accepted but no longer applied (feed order).'),
|
|
141
|
+
limit: z.number().int().min(1).max(50).optional().describe('Results per page (max 40 now, default 20).'),
|
|
110
142
|
offset: z.number().int().min(0).optional().describe('Pagination offset for loading more results.')
|
|
111
143
|
},
|
|
112
|
-
async ({
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
if (limit != null) params.set('limit', String(limit));
|
|
116
|
-
if (offset != null) params.set('offset', String(offset));
|
|
117
|
-
const path = `/v1/music-library/catalog${params.toString() ? '?' + params.toString() : ''}`;
|
|
118
|
-
const result = await client.get(path);
|
|
119
|
-
const tracks = result.tracks || [];
|
|
120
|
-
if (tracks.length === 0) {
|
|
121
|
-
return { content: [{ type: 'text', text: 'No tracks returned.' }] };
|
|
122
|
-
}
|
|
123
|
-
const text = `Catalog (${tracks.length} track${tracks.length === 1 ? '' : 's'}):\n\n${tracks.map(trackLine).join('\n\n')}`;
|
|
124
|
-
|
|
125
|
-
if (ui()) {
|
|
126
|
-
return uiResult(UI.mediaGrid, text, {
|
|
127
|
-
widget: 'media-grid',
|
|
128
|
-
title: 'Music Library',
|
|
129
|
-
items: tracks.slice(0, 20).map(trackItem),
|
|
130
|
-
total: result.total != null ? result.total : tracks.length
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return { content: [{ type: 'text', text }] };
|
|
144
|
+
async ({ limit, offset }) => {
|
|
145
|
+
const { assets, total } = await stockMusicSearch(client, '', limit, offset);
|
|
146
|
+
return musicResult(ui, null, assets, total, 'No tracks returned.');
|
|
135
147
|
}
|
|
136
148
|
);
|
|
137
149
|
|
|
138
|
-
// ─── get_music_library_facets
|
|
150
|
+
// ─── get_music_library_facets (adapter → stock categories) ─────
|
|
139
151
|
server.tool(
|
|
140
152
|
'get_music_library_facets',
|
|
141
|
-
'List the
|
|
153
|
+
DEPRECATION_NOTE + 'List the music category/genre chips available in the stock library. The catalog matches semantically, so any natural-language vibe also works as a query.',
|
|
142
154
|
{},
|
|
143
155
|
async () => {
|
|
144
|
-
|
|
156
|
+
let categories = [];
|
|
157
|
+
try {
|
|
158
|
+
const result = await client.get('/v1/stock/categories?mediaType=music');
|
|
159
|
+
categories = (result.categories || []).map(c => c.name || c.providerParam).filter(Boolean);
|
|
160
|
+
} catch (_) { /* categories are best-effort */ }
|
|
145
161
|
return {
|
|
146
162
|
content: [{
|
|
147
163
|
type: 'text',
|
|
148
164
|
text: JSON.stringify({
|
|
149
|
-
genres:
|
|
150
|
-
moods:
|
|
151
|
-
instruments:
|
|
152
|
-
bpmRange:
|
|
153
|
-
durationRange:
|
|
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.'
|
|
154
171
|
}, null, 2)
|
|
155
172
|
}]
|
|
156
173
|
};
|
|
157
174
|
}
|
|
158
175
|
);
|
|
159
176
|
|
|
160
|
-
// ─── get_music_track_audio
|
|
177
|
+
// ─── get_music_track_audio (adapter → stock asset) ──────────────
|
|
161
178
|
server.tool(
|
|
162
179
|
'get_music_track_audio',
|
|
163
|
-
'Get the downloadable audio URLs
|
|
180
|
+
DEPRECATION_NOTE + 'Get the downloadable audio URLs for a music track by id ("source:sourceId" from search results).',
|
|
164
181
|
{
|
|
165
|
-
track_id: z.string().describe('
|
|
182
|
+
track_id: z.string().describe('Track id from search_music_library / browse_music_library (format "source:sourceId").')
|
|
166
183
|
},
|
|
167
184
|
async ({ track_id }) => {
|
|
168
|
-
const
|
|
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; });
|
|
169
191
|
return {
|
|
170
192
|
content: [{
|
|
171
193
|
type: 'text',
|
|
172
|
-
text: JSON.stringify({ id:
|
|
194
|
+
text: JSON.stringify({ id: track_id, title: a.title, urls }, null, 2)
|
|
173
195
|
}]
|
|
174
196
|
};
|
|
175
197
|
}
|
|
176
198
|
);
|
|
177
199
|
|
|
178
|
-
// ─── get_music_track_related
|
|
200
|
+
// ─── get_music_track_related (graceful stub — no stock equivalent) ─
|
|
179
201
|
server.tool(
|
|
180
202
|
'get_music_track_related',
|
|
181
|
-
|
|
203
|
+
DEPRECATION_NOTE + 'Stems/alternate versions are not exposed by the unified stock library. Returns an empty set with guidance.',
|
|
182
204
|
{
|
|
183
205
|
track_id: z.string().describe('The master track id.')
|
|
184
206
|
},
|
|
185
|
-
async ({ track_id }) => {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
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
|
+
})
|
|
194
216
|
);
|
|
195
217
|
|
|
196
|
-
// ─── get_music_track_lyrics
|
|
218
|
+
// ─── get_music_track_lyrics (graceful stub — no stock equivalent) ─
|
|
197
219
|
server.tool(
|
|
198
220
|
'get_music_track_lyrics',
|
|
199
|
-
'
|
|
221
|
+
DEPRECATION_NOTE + 'Lyrics metadata is not exposed by the unified stock library. Returns hasLyrics: false with guidance.',
|
|
200
222
|
{
|
|
201
223
|
track_id: z.string().describe('The track id.')
|
|
202
224
|
},
|
|
203
|
-
async (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
explicit: result.explicit
|
|
213
|
-
}, null, 2)
|
|
214
|
-
}]
|
|
215
|
-
};
|
|
216
|
-
}
|
|
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
|
+
})
|
|
217
234
|
);
|
|
218
235
|
}
|
|
219
236
|
|