@kolbo/mcp 1.31.3 → 1.31.5
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/widgets/generation.js +30 -3
- package/src/tools/music_library.js +136 -119
package/package.json
CHANGED
|
@@ -263,16 +263,43 @@ function renderScenes(sc) {
|
|
|
263
263
|
var media = (scene.video_urls || []).map(function (u) {
|
|
264
264
|
return '<div class="k-media"><video src="' + esc(u) + '" controls playsinline></video>' + dlBtnHTML(u) + '</div>';
|
|
265
265
|
}).join('') + (scene.image_urls || []).map(function (u) {
|
|
266
|
-
return '<div class="k-media"><img src="' + esc(u) + '" alt="">' + dlBtnHTML(u) + '</div>';
|
|
266
|
+
return '<div class="k-media" data-focus="' + esc(u) + '"><img src="' + esc(u) + '" alt="">' + dlBtnHTML(u) + '</div>';
|
|
267
267
|
}).join('');
|
|
268
268
|
return '<div style="margin-bottom:14px"><div style="font-size:12px;font-weight:600;color:var(--text-muted);margin-bottom:8px">Scene ' +
|
|
269
269
|
esc(scene.scene_number) + (scene.title ? ' — ' + esc(scene.title) : '') + '</div>' +
|
|
270
270
|
'<div class="k-gen-grid n2">' + media + '</div></div>';
|
|
271
271
|
}).join('');
|
|
272
272
|
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')); };
|
|
278
|
+
});
|
|
273
279
|
renderActions(sc);
|
|
274
280
|
}
|
|
275
281
|
|
|
282
|
+
// Fullscreen a single item out of a multi-item grid (Creative Director
|
|
283
|
+
// scenes). Exit restores the grid.
|
|
284
|
+
function focusMedia(url) {
|
|
285
|
+
window.kolbo.requestDisplayMode('fullscreen').then(function (res) {
|
|
286
|
+
if (!(res && res.mode === 'fullscreen')) return window.kolbo.openLink(url);
|
|
287
|
+
isFullscreen = true;
|
|
288
|
+
el('stage').innerHTML = '<div class="k-viewer"><img id="focus-img" src="' + esc(url) + '" alt="" style="cursor:zoom-out">' + dlBtnHTML(url) + '</div>';
|
|
289
|
+
wireDlButtons(el('stage'));
|
|
290
|
+
el('focus-img').onclick = exitFocus;
|
|
291
|
+
applyFullscreen(true, exitFocus);
|
|
292
|
+
window.kolbo.notifySize();
|
|
293
|
+
}).catch(function () { window.kolbo.openLink(url); });
|
|
294
|
+
}
|
|
295
|
+
function exitFocus() {
|
|
296
|
+
window.kolbo.requestDisplayMode('inline').catch(function () {});
|
|
297
|
+
isFullscreen = false;
|
|
298
|
+
applyFullscreen(false);
|
|
299
|
+
renderScenes(state); // restore the grid
|
|
300
|
+
window.kolbo.notifySize();
|
|
301
|
+
}
|
|
302
|
+
|
|
276
303
|
function renderError(msg) {
|
|
277
304
|
clearTimeout(pollTimer);
|
|
278
305
|
|
|
@@ -302,14 +329,14 @@ function toggleFullscreen() {
|
|
|
302
329
|
if (!isFullscreen) window.kolbo.openLink(state.urls && state.urls[selected]);
|
|
303
330
|
});
|
|
304
331
|
}
|
|
305
|
-
function applyFullscreen(on) {
|
|
332
|
+
function applyFullscreen(on, exitHandler) {
|
|
306
333
|
document.documentElement.classList.toggle('k-fullscreen', on);
|
|
307
334
|
var c = el('phase-chip');
|
|
308
335
|
if (on) {
|
|
309
336
|
c.style.display = '';
|
|
310
337
|
c.innerHTML = '✕ ' + esc('Exit');
|
|
311
338
|
c.style.cursor = 'pointer';
|
|
312
|
-
c.onclick = toggleFullscreen;
|
|
339
|
+
c.onclick = exitHandler || toggleFullscreen;
|
|
313
340
|
} else {
|
|
314
341
|
c.style.display = 'none';
|
|
315
342
|
c.onclick = null;
|
|
@@ -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
|
|