@kolbo/mcp 1.0.0 → 1.2.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.
@@ -1,224 +1,723 @@
1
- const { pollUntilDone } = require('../polling');
2
-
3
- function registerGenerateTools(server, client) {
4
- // ─── generate_image ────────────────────────────────────────
5
- server.tool(
6
- 'generate_image',
7
- 'Generate image(s) from a text prompt using Kolbo AI. Returns the final image URL(s) when complete.',
8
- {
9
- prompt: { type: 'string', description: 'Text description of the image to generate' },
10
- model: { type: 'string', description: 'Model identifier (e.g., "fal-ai/flux/schnell"). Use list_models to see available models. Omit for auto-selection.' },
11
- aspect_ratio: { type: 'string', description: 'Aspect ratio (e.g., "1:1", "16:9", "9:16"). Default: "1:1"' },
12
- enhance_prompt: { type: 'boolean', description: 'Enhance the prompt for better results. Default: true' }
13
- },
14
- async ({ prompt, model, aspect_ratio, enhance_prompt }) => {
15
- const gen = await client.post('/v1/generate/image', {
16
- prompt, model, aspect_ratio, enhance_prompt
17
- });
18
-
19
- const result = await pollUntilDone(client, gen.generation_id, {
20
- interval: (gen.poll_interval_hint || 3) * 1000,
21
- timeout: 120000
22
- });
23
-
24
- return {
25
- content: [{
26
- type: 'text',
27
- text: JSON.stringify({
28
- urls: result.result.urls,
29
- model: result.result.model,
30
- prompt_used: result.result.prompt_used
31
- }, null, 2)
32
- }]
33
- };
34
- }
35
- );
36
-
37
- // ─── generate_video ────────────────────────────────────────
38
- server.tool(
39
- 'generate_video',
40
- 'Generate a video from a text prompt using Kolbo AI. Returns the final video URL when complete.',
41
- {
42
- prompt: { type: 'string', description: 'Text description of the video to generate' },
43
- model: { type: 'string', description: 'Model identifier. Use list_models with type "video" to see options.' },
44
- aspect_ratio: { type: 'string', description: 'Aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"' },
45
- duration: { type: 'number', description: 'Duration in seconds (e.g., 5, 10). Default: 5' },
46
- enhance_prompt: { type: 'boolean', description: 'Enhance the prompt for better results. Default: true' }
47
- },
48
- async ({ prompt, model, aspect_ratio, duration, enhance_prompt }) => {
49
- const gen = await client.post('/v1/generate/video', {
50
- prompt, model, aspect_ratio, duration, enhance_prompt
51
- });
52
-
53
- const result = await pollUntilDone(client, gen.generation_id, {
54
- interval: (gen.poll_interval_hint || 8) * 1000,
55
- timeout: 300000
56
- });
57
-
58
- return {
59
- content: [{
60
- type: 'text',
61
- text: JSON.stringify({
62
- urls: result.result.urls,
63
- model: result.result.model,
64
- duration: result.result.duration,
65
- thumbnail_url: result.result.thumbnail_url,
66
- prompt_used: result.result.prompt_used
67
- }, null, 2)
68
- }]
69
- };
70
- }
71
- );
72
-
73
- // ─── generate_video_from_image ─────────────────────────────
74
- server.tool(
75
- 'generate_video_from_image',
76
- 'Animate an image into a video using Kolbo AI. Returns the final video URL when complete.',
77
- {
78
- image_url: { type: 'string', description: 'URL of the source image to animate' },
79
- prompt: { type: 'string', description: 'Text description of the desired motion/animation' },
80
- model: { type: 'string', description: 'Model identifier. Use list_models with type "video_from_image" to see options.' },
81
- duration: { type: 'number', description: 'Duration in seconds (e.g., 5, 10). Default: 5' },
82
- enhance_prompt: { type: 'boolean', description: 'Enhance the prompt. Default: true' }
83
- },
84
- async ({ image_url, prompt, model, duration, enhance_prompt }) => {
85
- const gen = await client.post('/v1/generate/video/from-image', {
86
- image_url, prompt, model, duration, enhance_prompt
87
- });
88
-
89
- const result = await pollUntilDone(client, gen.generation_id, {
90
- interval: (gen.poll_interval_hint || 8) * 1000,
91
- timeout: 300000
92
- });
93
-
94
- return {
95
- content: [{
96
- type: 'text',
97
- text: JSON.stringify({
98
- urls: result.result.urls,
99
- model: result.result.model,
100
- duration: result.result.duration,
101
- thumbnail_url: result.result.thumbnail_url
102
- }, null, 2)
103
- }]
104
- };
105
- }
106
- );
107
-
108
- // ─── generate_music ────────────────────────────────────────
109
- server.tool(
110
- 'generate_music',
111
- 'Generate music from a text description using Kolbo AI. Returns the final audio URL when complete.',
112
- {
113
- prompt: { type: 'string', description: 'Text description of the music to generate (e.g., "upbeat electronic dance track with synthesizers")' },
114
- style: { type: 'string', description: 'Music style (e.g., "pop", "rock", "electronic", "jazz")' },
115
- instrumental: { type: 'boolean', description: 'Generate instrumental only (no vocals). Default: false' },
116
- lyrics: { type: 'string', description: 'Custom lyrics for the song' }
117
- },
118
- async ({ prompt, style, instrumental, lyrics }) => {
119
- const gen = await client.post('/v1/generate/music', {
120
- prompt, style, instrumental, lyrics
121
- });
122
-
123
- const result = await pollUntilDone(client, gen.generation_id, {
124
- interval: (gen.poll_interval_hint || 8) * 1000,
125
- timeout: 300000
126
- });
127
-
128
- return {
129
- content: [{
130
- type: 'text',
131
- text: JSON.stringify({
132
- urls: result.result.urls,
133
- title: result.result.title,
134
- duration: result.result.duration,
135
- lyrics: result.result.lyrics
136
- }, null, 2)
137
- }]
138
- };
139
- }
140
- );
141
-
142
- // ─── generate_speech ───────────────────────────────────────
143
- server.tool(
144
- 'generate_speech',
145
- 'Convert text to speech using Kolbo AI. Returns the final audio URL when complete.',
146
- {
147
- text: { type: 'string', description: 'The text to convert to speech' },
148
- voice: { type: 'string', description: 'Voice ID or name (e.g., "Rachel", "Adam"). Default: "Rachel"' },
149
- language: { type: 'string', description: 'Language code (e.g., "en-US", "he-IL"). Default: "en-US"' }
150
- },
151
- async ({ text, voice, language }) => {
152
- const gen = await client.post('/v1/generate/speech', {
153
- text, voice, language
154
- });
155
-
156
- const result = await pollUntilDone(client, gen.generation_id, {
157
- interval: (gen.poll_interval_hint || 5) * 1000,
158
- timeout: 120000
159
- });
160
-
161
- return {
162
- content: [{
163
- type: 'text',
164
- text: JSON.stringify({
165
- urls: result.result.urls,
166
- voice: result.result.voice,
167
- duration: result.result.duration
168
- }, null, 2)
169
- }]
170
- };
171
- }
172
- );
173
-
174
- // ─── generate_sound ────────────────────────────────────────
175
- server.tool(
176
- 'generate_sound',
177
- 'Generate sound effects from a text description using Kolbo AI. Returns the final audio URL when complete.',
178
- {
179
- prompt: { type: 'string', description: 'Text description of the sound effect (e.g., "thunder clap with rain", "door creaking open")' },
180
- duration: { type: 'number', description: 'Duration in seconds. Omit for auto duration.' }
181
- },
182
- async ({ prompt, duration }) => {
183
- const gen = await client.post('/v1/generate/sound', {
184
- prompt, duration
185
- });
186
-
187
- const result = await pollUntilDone(client, gen.generation_id, {
188
- interval: (gen.poll_interval_hint || 5) * 1000,
189
- timeout: 120000
190
- });
191
-
192
- return {
193
- content: [{
194
- type: 'text',
195
- text: JSON.stringify({
196
- urls: result.result.urls,
197
- duration: result.result.duration
198
- }, null, 2)
199
- }]
200
- };
201
- }
202
- );
203
-
204
- // ─── get_generation_status ─────────────────────────────────
205
- server.tool(
206
- 'get_generation_status',
207
- 'Check the status of a generation. Use this if a generation tool timed out or you need to check progress.',
208
- {
209
- generation_id: { type: 'string', description: 'The generation ID to check' }
210
- },
211
- async ({ generation_id }) => {
212
- const result = await client.get(`/v1/generate/${generation_id}/status`);
213
-
214
- return {
215
- content: [{
216
- type: 'text',
217
- text: JSON.stringify(result, null, 2)
218
- }]
219
- };
220
- }
221
- );
222
- }
223
-
224
- module.exports = { registerGenerateTools };
1
+ /* BACKWARD COMPATIBILITY: Tool names and arg names below are a PUBLIC
2
+ * CONTRACT. Never rename, remove, or break an existing tool/arg — old cached
3
+ * `npx @kolbo/mcp` installs in the wild will break silently. Add new tools or
4
+ * new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
5
+
6
+ const FormData = require('form-data');
7
+ const { pollUntilDone } = require('../polling');
8
+ const { resolveToBuffer } = require('./_shared');
9
+
10
+ function registerGenerateTools(server, client) {
11
+ // ─── generate_image ────────────────────────────────────────
12
+ server.tool(
13
+ 'generate_image',
14
+ 'Generate image(s) from a text prompt using Kolbo AI. Supports Visual DNA profiles (for character/style/product consistency), moodboards (for style direction), reference images (for composition guidance), batch generation (num_images), and web-search grounding. For EDITING an existing image, use generate_image_edit instead. For a coordinated multi-scene set (storyboard, ad campaign), use generate_creative_director. Returns the final image URL(s) when complete.',
15
+ {
16
+ prompt: { type: 'string', description: 'Text description of the image to generate' },
17
+ model: { type: 'string', description: 'Model identifier. Use list_models type="image" to see options. Omit for Smart Select.' },
18
+ aspect_ratio: { type: 'string', description: 'Aspect ratio (e.g., "1:1", "16:9", "9:16"). Default: "1:1"' },
19
+ enhance_prompt: { type: 'boolean', description: 'Enhance the prompt for better results. Default: true' },
20
+ num_images: { type: 'number', description: 'Number of images to generate in one call. Default: 1' },
21
+ reference_images: { type: 'array', description: 'Array of image URLs used as composition/style references (NOT as source images for editing — use generate_image_edit for that).' },
22
+ visual_dna_ids: { type: 'array', description: 'Array of Visual DNA profile IDs (from create_visual_dna / list_visual_dnas) to apply for character / style / product / scene consistency. Pass the `id` field of each profile. Use this when the user wants to keep the same character or style across multiple images.' },
23
+ moodboard_id: { type: 'string', description: 'Moodboard ID (from list_moodboards / get_moodboard) whose master_prompt and style_guide should be applied to this generation.' },
24
+ enable_web_search: { type: 'boolean', description: 'Enable web-search grounding for the prompt (useful for current events, brand references, real-world accuracy). Default: false' }
25
+ },
26
+ async ({ prompt, model, aspect_ratio, enhance_prompt, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search }) => {
27
+ const gen = await client.post('/v1/generate/image', {
28
+ prompt, model, aspect_ratio, enhance_prompt, num_images,
29
+ reference_images, visual_dna_ids, moodboard_id, enable_web_search
30
+ });
31
+
32
+ const result = await pollUntilDone(client, gen.generation_id, {
33
+ interval: (gen.poll_interval_hint || 3) * 1000,
34
+ timeout: 120000
35
+ });
36
+
37
+ return {
38
+ content: [{
39
+ type: 'text',
40
+ text: JSON.stringify({
41
+ urls: result.result.urls,
42
+ model: result.result.model,
43
+ prompt_used: result.result.prompt_used
44
+ }, null, 2)
45
+ }]
46
+ };
47
+ }
48
+ );
49
+
50
+ // ─── generate_image_edit ──────────────────────────────────
51
+ server.tool(
52
+ 'generate_image_edit',
53
+ 'Edit or transform an existing image using AI. Provide the source image URL(s) in `source_images` and describe the edit in `prompt` (e.g., "remove the background", "change the car color to red", "add sunglasses to the person"). Supports Visual DNA profiles and moodboards for style-consistent edits. For creating a brand new image from scratch, use generate_image. Returns the edited image URL(s) when complete.',
54
+ {
55
+ prompt: { type: 'string', description: 'Description of the edit to apply (e.g., "remove the background", "change the sky to sunset")' },
56
+ model: { type: 'string', description: 'Model identifier. Use list_models type="image_edit" to see options. Omit for Smart Select.' },
57
+ source_images: { type: 'array', description: 'Array of source image URLs to edit. Typically one, but some models accept multiple for compositing.' },
58
+ aspect_ratio: { type: 'string', description: 'Output aspect ratio (e.g., "1:1", "16:9", "9:16"). Default: "1:1"' },
59
+ enhance_prompt: { type: 'boolean', description: 'Enhance the prompt for better results. Default: true' },
60
+ num_images: { type: 'number', description: 'Number of output images. Default: 1' },
61
+ visual_dna_ids: { type: 'array', description: 'Array of Visual DNA profile IDs to apply for consistency with an existing character / style / product.' },
62
+ moodboard_id: { type: 'string', description: 'Moodboard ID whose master_prompt and style_guide should be applied.' },
63
+ enable_web_search: { type: 'boolean', description: 'Enable web-search grounding. Default: false' }
64
+ },
65
+ async ({ prompt, model, source_images, aspect_ratio, enhance_prompt, num_images, visual_dna_ids, moodboard_id, enable_web_search }) => {
66
+ const gen = await client.post('/v1/generate/image-edit', {
67
+ prompt, model, source_images, aspect_ratio, enhance_prompt, num_images,
68
+ visual_dna_ids, moodboard_id, enable_web_search
69
+ });
70
+
71
+ const result = await pollUntilDone(client, gen.generation_id, {
72
+ interval: (gen.poll_interval_hint || 3) * 1000,
73
+ timeout: 120000
74
+ });
75
+
76
+ return {
77
+ content: [{
78
+ type: 'text',
79
+ text: JSON.stringify({
80
+ urls: result.result.urls,
81
+ model: result.result.model,
82
+ prompt_used: result.result.prompt_used
83
+ }, null, 2)
84
+ }]
85
+ };
86
+ }
87
+ );
88
+
89
+ // ─── generate_creative_director ─────────────────────────────
90
+ server.tool(
91
+ 'generate_creative_director',
92
+ 'Generate a multi-scene coordinated set from ONE creative brief. Use this INSTEAD of calling generate_image/generate_video multiple times when the user wants a storyboard, multi-scene ad, product showcase, or any set of related outputs that should share visual language. Produces 1–8 scenes in a single request with consistent style. Supports image mode and video mode (`workflow_type`). Visual DNA and moodboard references keep character/style consistent across every scene.',
93
+ {
94
+ prompt: { type: 'string', description: 'Creative brief or concept describing the full set of scenes to generate' },
95
+ scene_count: { type: 'number', description: 'Number of scenes to generate, 1–8. Default: 4' },
96
+ model: { type: 'string', description: 'Model identifier applied to every scene. Omit for Smart Select.' },
97
+ aspect_ratio: { type: 'string', description: 'Aspect ratio applied to every scene (e.g., "1:1", "16:9", "9:16"). Default: "1:1"' },
98
+ workflow_type: { type: 'string', description: '"image" (default) or "video"' },
99
+ duration: { type: 'number', description: 'Duration in seconds per scene (video mode only). E.g., 5 or 10.' },
100
+ enhance_prompt: { type: 'boolean', description: 'Enhance prompts per scene. Default: true' },
101
+ reference_images: { type: 'array', description: 'Array of reference image URLs to guide style/composition of every scene.' },
102
+ visual_dna_ids: { type: 'array', description: 'Array of Visual DNA profile IDs to apply consistently across every scene. This is the ideal way to keep a character or product looking the same in all scenes of a campaign.' },
103
+ moodboard_id: { type: 'string', description: 'A single moodboard ID whose master_prompt and style_guide should shape every scene.' },
104
+ moodboard_ids: { type: 'array', description: 'Multiple moodboard IDs when blending styles. Prefer `moodboard_id` for single moodboards.' }
105
+ },
106
+ async ({ prompt, scene_count, model, aspect_ratio, workflow_type, duration, enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids }) => {
107
+ const gen = await client.post('/v1/generate/creative-director', {
108
+ prompt, scene_count, model, aspect_ratio, workflow_type, duration,
109
+ enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids
110
+ });
111
+
112
+ const result = await pollUntilDone(client, gen.generation_id, {
113
+ interval: (gen.poll_interval_hint || 5) * 1000,
114
+ timeout: 600000,
115
+ statusUrl: `/v1/generate/creative-director/${gen.generation_id}/status`
116
+ });
117
+
118
+ const scenes = (result.scenes || [])
119
+ .filter(s => s.status === 'completed')
120
+ .map(s => ({
121
+ scene_number: s.scene_number,
122
+ title: s.title,
123
+ image_urls: s.image_urls,
124
+ video_urls: s.video_urls
125
+ }));
126
+
127
+ return {
128
+ content: [{
129
+ type: 'text',
130
+ text: JSON.stringify({
131
+ scenes,
132
+ total_scenes: result.scenes?.length || 0,
133
+ completed_scenes: scenes.length
134
+ }, null, 2)
135
+ }]
136
+ };
137
+ }
138
+ );
139
+
140
+ // ─── generate_video ────────────────────────────────────────
141
+ server.tool(
142
+ 'generate_video',
143
+ 'Generate a video from a text prompt using Kolbo AI. For animating an existing still image into motion, use generate_video_from_image instead. For a coordinated multi-scene video campaign, use generate_creative_director with workflow_type="video". Supports Visual DNA profiles (for character consistency) and reference images (for style guidance). Returns the final video URL when complete.',
144
+ {
145
+ prompt: { type: 'string', description: 'Text description of the video to generate' },
146
+ model: { type: 'string', description: 'Model identifier. Use list_models type="video" to see options. Check supported_durations and supported_aspect_ratios.' },
147
+ aspect_ratio: { type: 'string', description: 'Aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"' },
148
+ duration: { type: 'number', description: 'Duration in seconds. Must be a value the chosen model supports — check supported_durations from list_models. Default: 5' },
149
+ enhance_prompt: { type: 'boolean', description: 'Enhance the prompt. Default: true' },
150
+ reference_images: { type: 'array', description: 'Array of image URLs used as visual references (style / composition / subject).' },
151
+ visual_dna_ids: { type: 'array', description: 'Array of Visual DNA profile IDs to keep a character / style consistent with prior generations.' }
152
+ },
153
+ async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, visual_dna_ids }) => {
154
+ const gen = await client.post('/v1/generate/video', {
155
+ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, visual_dna_ids
156
+ });
157
+
158
+ const result = await pollUntilDone(client, gen.generation_id, {
159
+ interval: (gen.poll_interval_hint || 8) * 1000,
160
+ timeout: 300000
161
+ });
162
+
163
+ return {
164
+ content: [{
165
+ type: 'text',
166
+ text: JSON.stringify({
167
+ urls: result.result.urls,
168
+ model: result.result.model,
169
+ duration: result.result.duration,
170
+ thumbnail_url: result.result.thumbnail_url,
171
+ prompt_used: result.result.prompt_used
172
+ }, null, 2)
173
+ }]
174
+ };
175
+ }
176
+ );
177
+
178
+ // ─── generate_video_from_image ─────────────────────────────
179
+ server.tool(
180
+ 'generate_video_from_image',
181
+ 'Animate an existing still image into a video using Kolbo AI. The image comes from `image_url`; `prompt` describes the motion (not the subject — the subject is already in the image). For generating a video from scratch, use generate_video. Returns the final video URL when complete.',
182
+ {
183
+ image_url: { type: 'string', description: 'URL of the source image to animate' },
184
+ prompt: { type: 'string', description: 'Text description of the desired MOTION (e.g., "camera slowly pans right while the character walks forward")' },
185
+ model: { type: 'string', description: 'Model identifier. Use list_models type="video_from_image" to see options.' },
186
+ aspect_ratio: { type: 'string', description: 'Output aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"' },
187
+ duration: { type: 'number', description: 'Duration in seconds. Must be a value the chosen model supports. Default: 5' },
188
+ enhance_prompt: { type: 'boolean', description: 'Enhance the motion prompt. Default: true' },
189
+ visual_dna_ids: { type: 'array', description: 'Array of Visual DNA profile IDs to maintain consistency with prior characters / styles.' }
190
+ },
191
+ async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids }) => {
192
+ const gen = await client.post('/v1/generate/video/from-image', {
193
+ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids
194
+ });
195
+
196
+ const result = await pollUntilDone(client, gen.generation_id, {
197
+ interval: (gen.poll_interval_hint || 8) * 1000,
198
+ timeout: 300000
199
+ });
200
+
201
+ return {
202
+ content: [{
203
+ type: 'text',
204
+ text: JSON.stringify({
205
+ urls: result.result.urls,
206
+ model: result.result.model,
207
+ duration: result.result.duration,
208
+ thumbnail_url: result.result.thumbnail_url
209
+ }, null, 2)
210
+ }]
211
+ };
212
+ }
213
+ );
214
+
215
+ // ─── generate_music ────────────────────────────────────────
216
+ server.tool(
217
+ 'generate_music',
218
+ 'Generate music from a text description using Kolbo AI. Supports instrumental mode, custom lyrics, style direction, and vocal gender. Default model is Suno. Returns the final audio URL when complete.',
219
+ {
220
+ prompt: { type: 'string', description: 'Text description of the music to generate (e.g., "upbeat electronic dance track with synthesizers")' },
221
+ model: { type: 'string', description: 'Model identifier. Use list_models type="music" to see options. Omit for Suno (default).' },
222
+ style: { type: 'string', description: 'Music style / genre (e.g., "pop", "rock", "lo-fi", "electronic", "jazz")' },
223
+ instrumental: { type: 'boolean', description: 'Generate instrumental only, no vocals. Default: false' },
224
+ lyrics: { type: 'string', description: 'Custom lyrics for the song. If omitted, lyrics are generated automatically from the prompt unless instrumental is true.' },
225
+ vocal_gender: { type: 'string', description: 'Preferred vocal gender: "male" or "female". Only applies when instrumental is false.' },
226
+ enhance_prompt: { type: 'boolean', description: 'Enhance the prompt. Default: true' }
227
+ },
228
+ async ({ prompt, model, style, instrumental, lyrics, vocal_gender, enhance_prompt }) => {
229
+ const gen = await client.post('/v1/generate/music', {
230
+ prompt, model, style, instrumental, lyrics, vocal_gender, enhance_prompt
231
+ });
232
+
233
+ const result = await pollUntilDone(client, gen.generation_id, {
234
+ interval: (gen.poll_interval_hint || 8) * 1000,
235
+ timeout: 300000
236
+ });
237
+
238
+ return {
239
+ content: [{
240
+ type: 'text',
241
+ text: JSON.stringify({
242
+ urls: result.result.urls,
243
+ title: result.result.title,
244
+ duration: result.result.duration,
245
+ lyrics: result.result.lyrics
246
+ }, null, 2)
247
+ }]
248
+ };
249
+ }
250
+ );
251
+
252
+ // ─── generate_speech ───────────────────────────────────────
253
+ server.tool(
254
+ 'generate_speech',
255
+ 'Convert text to speech using Kolbo AI. Default provider is ElevenLabs. To pick a specific voice by language/gender, call list_voices first and pass the returned voice_id (or a voice display name — both work). Returns the final audio URL when complete.',
256
+ {
257
+ text: { type: 'string', description: 'The text to convert to speech' },
258
+ voice: { type: 'string', description: 'Voice ID (from list_voices) or voice display name (e.g., "Rachel", "Adam"). Default: "Rachel"' },
259
+ model: { type: 'string', description: 'Model identifier. Use list_models type="speech" to see options. Default: eleven_v3' },
260
+ language: { type: 'string', description: 'Language code (e.g., "en-US", "he-IL", "es-ES"). Default: "en-US"' }
261
+ },
262
+ async ({ text, voice, model, language }) => {
263
+ const gen = await client.post('/v1/generate/speech', {
264
+ text, voice, model, language
265
+ });
266
+
267
+ const result = await pollUntilDone(client, gen.generation_id, {
268
+ interval: (gen.poll_interval_hint || 5) * 1000,
269
+ timeout: 120000
270
+ });
271
+
272
+ return {
273
+ content: [{
274
+ type: 'text',
275
+ text: JSON.stringify({
276
+ urls: result.result.urls,
277
+ voice: result.result.voice,
278
+ duration: result.result.duration
279
+ }, null, 2)
280
+ }]
281
+ };
282
+ }
283
+ );
284
+
285
+ // ─── generate_sound ────────────────────────────────────────
286
+ server.tool(
287
+ 'generate_sound',
288
+ 'Generate sound effects (not music, not speech) from a text description using Kolbo AI. Use this for ambient sounds, foley, impacts, atmospheres, UI sounds, etc. For music use generate_music; for voice use generate_speech. Returns the final audio URL when complete.',
289
+ {
290
+ prompt: { type: 'string', description: 'Text description of the sound effect (e.g., "thunder clap with rain", "door creaking open", "futuristic UI beep")' },
291
+ model: { type: 'string', description: 'Model identifier. Use list_models type="sound" to see options. Default: elevenlabs-sound-effects-v1' },
292
+ duration: { type: 'number', description: 'Duration in seconds. Omit for automatic duration.' }
293
+ },
294
+ async ({ prompt, model, duration }) => {
295
+ const gen = await client.post('/v1/generate/sound', {
296
+ prompt, model, duration
297
+ });
298
+
299
+ const result = await pollUntilDone(client, gen.generation_id, {
300
+ interval: (gen.poll_interval_hint || 5) * 1000,
301
+ timeout: 120000
302
+ });
303
+
304
+ return {
305
+ content: [{
306
+ type: 'text',
307
+ text: JSON.stringify({
308
+ urls: result.result.urls,
309
+ duration: result.result.duration
310
+ }, null, 2)
311
+ }]
312
+ };
313
+ }
314
+ );
315
+
316
+ // ─── list_voices ─────────────────────────────────────────────
317
+ server.tool(
318
+ 'list_voices',
319
+ 'List available TTS voices for generate_speech. Returns preset voices and the user\'s own cloned/designed voices. Filter by provider, language, or gender to find the right voice. Use the returned `voice_id` as the `voice` parameter in generate_speech.',
320
+ {
321
+ provider: { type: 'string', description: 'Filter by provider (e.g., "elevenLabs", "google")' },
322
+ language: { type: 'string', description: 'Filter by language name or code (e.g., "English", "en-US")' },
323
+ gender: { type: 'string', description: 'Filter by gender (e.g., "Female", "Male")' }
324
+ },
325
+ async ({ provider, language, gender }) => {
326
+ const params = new URLSearchParams();
327
+ if (provider) params.set('provider', provider);
328
+ if (language) params.set('language', language);
329
+ if (gender) params.set('gender', gender);
330
+
331
+ const qs = params.toString();
332
+ const result = await client.get(`/v1/voices${qs ? '?' + qs : ''}`);
333
+
334
+ // Summarize for context window efficiency
335
+ const voices = (result.voices || []).map(v => ({
336
+ voice_id: v.voice_id,
337
+ name: v.name,
338
+ provider: v.provider,
339
+ language: v.language,
340
+ gender: v.gender,
341
+ custom: v.custom
342
+ }));
343
+
344
+ return {
345
+ content: [{
346
+ type: 'text',
347
+ text: JSON.stringify({ voices, count: result.count }, null, 2)
348
+ }]
349
+ };
350
+ }
351
+ );
352
+
353
+ // ─── get_generation_status ─────────────────────────────────
354
+ server.tool(
355
+ 'get_generation_status',
356
+ 'Check the status of a generation. Use this as a FALLBACK when a generation tool returned a timeout error — the generation is probably still running on the server. Pass the generation_id from the timeout error (or from any prior generation response).',
357
+ {
358
+ generation_id: { type: 'string', description: 'The generation ID to check' }
359
+ },
360
+ async ({ generation_id }) => {
361
+ const result = await client.get(`/v1/generate/${encodeURIComponent(generation_id)}/status`);
362
+
363
+ return {
364
+ content: [{
365
+ type: 'text',
366
+ text: JSON.stringify(result, null, 2)
367
+ }]
368
+ };
369
+ }
370
+ );
371
+
372
+ // ═════════════════════════════════════════════════════════════
373
+ // ─── 2026-04 SDK Expansion Batch ─────────────────────────────
374
+ // ═════════════════════════════════════════════════════════════
375
+
376
+ // ─── generate_elements ─────────────────────────────────────
377
+ server.tool(
378
+ 'generate_elements',
379
+ 'Generate a video from reference elements (images and/or videos) + a text prompt. Use when the user wants to animate specific uploaded/referenced assets — e.g. "animate this product", "put these 3 characters into a scene". Supports Visual DNA for character consistency. For text-only → video use generate_video instead. For animating a single still image use generate_video_from_image. Returns the final video URL when complete.',
380
+ {
381
+ prompt: { type: 'string', description: 'Text description of the desired video / animation' },
382
+ model: { type: 'string', description: 'Model identifier. Use list_models type="video" to see options. Omit for Smart Select.' },
383
+ reference_images: { type: 'array', description: 'Array of public image URLs used as reference elements (product shots, character references, etc.). URL mode.' },
384
+ files: { type: 'array', description: 'Array of URLs or absolute local paths — alternative to reference_images. Use this when you have local files to upload. Each item can be a URL OR a local path.' },
385
+ duration: { type: 'number', description: 'Duration in seconds. Default: 5' },
386
+ aspect_ratio: { type: 'string', description: 'Aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"' },
387
+ motion: { type: 'string', description: 'Motion style / intensity hint (optional)' },
388
+ preset_id: { type: 'string', description: 'Preset ID from list_presets type="video" (optional)' },
389
+ enhance_prompt: { type: 'boolean', description: 'Enhance the prompt. Default: true' },
390
+ visual_dna_ids: { type: 'array', description: 'Array of Visual DNA profile IDs to apply for character/style consistency across outputs.' }
391
+ },
392
+ async ({ prompt, model, reference_images, files, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids }) => {
393
+ if (!prompt) throw new Error('prompt is required');
394
+
395
+ let startResponse;
396
+ if (files && files.length > 0) {
397
+ // Multipart mode: resolve each file source to a buffer and upload.
398
+ const resolved = await Promise.all(files.map(src => resolveToBuffer(src, 'image')));
399
+ const form = new FormData();
400
+ form.append('prompt', prompt);
401
+ if (model) form.append('model', model);
402
+ if (duration !== undefined) form.append('duration', String(duration));
403
+ if (aspect_ratio) form.append('aspect_ratio', aspect_ratio);
404
+ if (motion) form.append('motion', motion);
405
+ if (preset_id) form.append('preset_id', preset_id);
406
+ if (enhance_prompt !== undefined) form.append('enhance_prompt', String(enhance_prompt));
407
+ if (visual_dna_ids) form.append('visual_dna_ids', JSON.stringify(visual_dna_ids));
408
+ if (reference_images) form.append('reference_images', JSON.stringify(reference_images));
409
+ for (const f of resolved) {
410
+ form.append('files', f.buffer, { filename: f.filename, contentType: f.contentType });
411
+ }
412
+ startResponse = await client.postMultipart('/v1/generate/elements', form);
413
+ } else {
414
+ // URL-only mode: plain JSON.
415
+ startResponse = await client.post('/v1/generate/elements', {
416
+ prompt, model, reference_images, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids
417
+ });
418
+ }
419
+
420
+ const result = await pollUntilDone(client, startResponse.generation_id, {
421
+ interval: (startResponse.poll_interval_hint || 8) * 1000,
422
+ timeout: 600000
423
+ });
424
+
425
+ return {
426
+ content: [{
427
+ type: 'text',
428
+ text: JSON.stringify({
429
+ urls: result.result?.urls || [],
430
+ thumbnail_url: result.result?.thumbnail_url || null,
431
+ duration: result.result?.duration || null,
432
+ model: result.result?.model || null
433
+ }, null, 2)
434
+ }]
435
+ };
436
+ }
437
+ );
438
+
439
+ // ─── generate_first_last_frame ─────────────────────────────
440
+ server.tool(
441
+ 'generate_first_last_frame',
442
+ 'Generate a video that morphs / interpolates from a FIRST frame to a LAST frame. Provide the two frames as URLs (first_frame_url + last_frame_url) OR as local file paths (first_frame + last_frame). Optional prompt describes the desired motion/transition. Do NOT mix URL and file inputs. Returns the final video URL when complete.',
443
+ {
444
+ first_frame_url: { type: 'string', description: 'Public URL of the first frame image (URL mode)' },
445
+ last_frame_url: { type: 'string', description: 'Public URL of the last frame image (URL mode)' },
446
+ first_frame: { type: 'string', description: 'URL or absolute local path to the first frame (file mode — alternative to first_frame_url)' },
447
+ last_frame: { type: 'string', description: 'URL or absolute local path to the last frame (file mode — alternative to last_frame_url)' },
448
+ prompt: { type: 'string', description: 'Optional description of the desired motion between the two frames (e.g. "smooth camera dolly in")' },
449
+ model: { type: 'string', description: 'Model identifier. Use list_models type="video_from_image" to see options. Omit for Smart Select.' },
450
+ duration: { type: 'number', description: 'Duration in seconds. Default: 5' },
451
+ aspect_ratio: { type: 'string', description: 'Aspect ratio (auto-detected from first frame if not provided). Default: "16:9"' },
452
+ enhance_prompt: { type: 'boolean', description: 'Enhance the prompt. Default: true' },
453
+ visual_dna_ids: { type: 'array', description: 'Array of Visual DNA profile IDs to apply.' }
454
+ },
455
+ async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids }) => {
456
+ const urlMode = first_frame_url && last_frame_url;
457
+ const fileMode = first_frame && last_frame;
458
+ if (!urlMode && !fileMode) {
459
+ throw new Error('Provide either both first_frame_url + last_frame_url OR both first_frame + last_frame (URL/local path).');
460
+ }
461
+ if (urlMode && fileMode) {
462
+ throw new Error('Do not mix URL and file inputs. Provide either URLs OR file sources, not both.');
463
+ }
464
+
465
+ let startResponse;
466
+ if (fileMode) {
467
+ const [firstResolved, lastResolved] = await Promise.all([
468
+ resolveToBuffer(first_frame, 'image'),
469
+ resolveToBuffer(last_frame, 'image')
470
+ ]);
471
+ const form = new FormData();
472
+ form.append('files', firstResolved.buffer, { filename: firstResolved.filename, contentType: firstResolved.contentType });
473
+ form.append('files', lastResolved.buffer, { filename: lastResolved.filename, contentType: lastResolved.contentType });
474
+ if (prompt) form.append('prompt', prompt);
475
+ if (model) form.append('model', model);
476
+ if (duration !== undefined) form.append('duration', String(duration));
477
+ if (aspect_ratio) form.append('aspect_ratio', aspect_ratio);
478
+ if (enhance_prompt !== undefined) form.append('enhance_prompt', String(enhance_prompt));
479
+ if (visual_dna_ids) form.append('visual_dna_ids', JSON.stringify(visual_dna_ids));
480
+ startResponse = await client.postMultipart('/v1/generate/first-last-frame', form);
481
+ } else {
482
+ startResponse = await client.post('/v1/generate/first-last-frame', {
483
+ first_frame_url, last_frame_url, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids
484
+ });
485
+ }
486
+
487
+ const result = await pollUntilDone(client, startResponse.generation_id, {
488
+ interval: (startResponse.poll_interval_hint || 8) * 1000,
489
+ timeout: 300000
490
+ });
491
+
492
+ return {
493
+ content: [{
494
+ type: 'text',
495
+ text: JSON.stringify({
496
+ urls: result.result?.urls || [],
497
+ thumbnail_url: result.result?.thumbnail_url || null,
498
+ duration: result.result?.duration || null,
499
+ model: result.result?.model || null
500
+ }, null, 2)
501
+ }]
502
+ };
503
+ }
504
+ );
505
+
506
+ // ─── generate_lipsync ──────────────────────────────────────
507
+ server.tool(
508
+ 'generate_lipsync',
509
+ 'Lipsync an audio track to a source image or video. Both `source` (image or video) and `audio` can be provided as URLs or as absolute local file paths. Pass a text_prompt only if the model supports it (some lipsync models do character performance from a prompt). Returns a lipsynced video URL.',
510
+ {
511
+ source: { type: 'string', description: 'URL or absolute local path to the source image or video (the face to animate)' },
512
+ audio: { type: 'string', description: 'URL or absolute local path to the audio track (the voice to sync to)' },
513
+ text_prompt: { type: 'string', description: 'Optional text prompt (for performance-capable models)' },
514
+ model: { type: 'string', description: 'Model identifier. Use list_models type="lipsync" to see options. Omit for Smart Select.' },
515
+ bounding_box_target: { type: 'array', description: 'Optional bounding box [x, y, w, h] for multi-face inputs (Hedra Character3 style). Leave empty for single-face.' }
516
+ },
517
+ async ({ source, audio, text_prompt, model, bounding_box_target }) => {
518
+ if (!source) throw new Error('source is required (URL or absolute local path to image/video)');
519
+ if (!audio) throw new Error('audio is required (URL or absolute local path to audio file)');
520
+
521
+ const sourceIsUrl = typeof source === 'string' && /^https?:\/\//i.test(source);
522
+ const audioIsUrl = typeof audio === 'string' && /^https?:\/\//i.test(audio);
523
+
524
+ let startResponse;
525
+ if (sourceIsUrl && audioIsUrl) {
526
+ // URL mode
527
+ startResponse = await client.post('/v1/generate/lipsync', {
528
+ source_url: source,
529
+ audio_url: audio,
530
+ prompt: text_prompt,
531
+ model,
532
+ bounding_box_target
533
+ });
534
+ } else {
535
+ // File mode (or mixed — resolve any local paths, pass URLs through as body fields)
536
+ const form = new FormData();
537
+ if (!sourceIsUrl) {
538
+ const resolved = await resolveToBuffer(source, /\.(mp4|mov|webm|mkv)$/i.test(source) ? 'video' : 'image');
539
+ // Decide field name by kind — lipsync controller uses .fields() with image/video/audio.
540
+ const isVideo = /\.(mp4|mov|webm|mkv|avi|m4v)$/i.test(resolved.filename);
541
+ form.append(isVideo ? 'video' : 'image', resolved.buffer, { filename: resolved.filename, contentType: resolved.contentType });
542
+ } else {
543
+ form.append('source_url', source);
544
+ }
545
+ if (!audioIsUrl) {
546
+ const resolved = await resolveToBuffer(audio, 'audio');
547
+ form.append('audio', resolved.buffer, { filename: resolved.filename, contentType: resolved.contentType });
548
+ } else {
549
+ form.append('audio_url', audio);
550
+ }
551
+ if (text_prompt) form.append('prompt', text_prompt);
552
+ if (model) form.append('model', model);
553
+ if (bounding_box_target) form.append('bounding_box_target', JSON.stringify(bounding_box_target));
554
+ startResponse = await client.postMultipart('/v1/generate/lipsync', form);
555
+ }
556
+
557
+ const result = await pollUntilDone(client, startResponse.generation_id, {
558
+ interval: (startResponse.poll_interval_hint || 8) * 1000,
559
+ timeout: 600000
560
+ });
561
+
562
+ return {
563
+ content: [{
564
+ type: 'text',
565
+ text: JSON.stringify({
566
+ urls: result.result?.urls || [],
567
+ thumbnail_url: result.result?.thumbnail_url || null,
568
+ duration: result.result?.duration || null,
569
+ model: result.result?.model || null
570
+ }, null, 2)
571
+ }]
572
+ };
573
+ }
574
+ );
575
+
576
+ // ─── generate_video_from_video ─────────────────────────────
577
+ server.tool(
578
+ 'generate_video_from_video',
579
+ 'Restyle / transform an existing video using a text prompt (video-to-video). Use for style transfer, scene restyling, subject swap — anything where you want to keep the motion from the input video but change the look. Source video can be a URL or absolute local path. For animating a still image use generate_video_from_image instead. For text-only → video use generate_video.',
580
+ {
581
+ source_video: { type: 'string', description: 'URL or absolute local path to the source video to restyle' },
582
+ prompt: { type: 'string', description: 'Text description of the desired restyle / transformation' },
583
+ model: { type: 'string', description: 'Model identifier. Omit for Smart Select.' },
584
+ aspect_ratio: { type: 'string', description: 'Output aspect ratio. Default: matches source' },
585
+ duration: { type: 'number', description: 'Duration in seconds (default: matches source)' },
586
+ enhance_prompt: { type: 'boolean', description: 'Enhance the prompt. Default: true' },
587
+ visual_dna_ids: { type: 'array', description: 'Array of Visual DNA profile IDs to apply for character/style consistency.' }
588
+ },
589
+ async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids }) => {
590
+ if (!source_video) throw new Error('source_video is required');
591
+ if (!prompt) throw new Error('prompt is required');
592
+
593
+ const isUrl = /^https?:\/\//i.test(source_video);
594
+ let startResponse;
595
+ if (isUrl) {
596
+ startResponse = await client.post('/v1/generate/video-from-video', {
597
+ video_url: source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids
598
+ });
599
+ } else {
600
+ const resolved = await resolveToBuffer(source_video, 'video');
601
+ const form = new FormData();
602
+ form.append('files', resolved.buffer, { filename: resolved.filename, contentType: resolved.contentType });
603
+ form.append('prompt', prompt);
604
+ if (model) form.append('model', model);
605
+ if (aspect_ratio) form.append('aspect_ratio', aspect_ratio);
606
+ if (duration !== undefined) form.append('duration', String(duration));
607
+ if (enhance_prompt !== undefined) form.append('enhance_prompt', String(enhance_prompt));
608
+ if (visual_dna_ids) form.append('visual_dna_ids', JSON.stringify(visual_dna_ids));
609
+ startResponse = await client.postMultipart('/v1/generate/video-from-video', form);
610
+ }
611
+
612
+ const result = await pollUntilDone(client, startResponse.generation_id, {
613
+ interval: (startResponse.poll_interval_hint || 8) * 1000,
614
+ timeout: 600000
615
+ });
616
+
617
+ return {
618
+ content: [{
619
+ type: 'text',
620
+ text: JSON.stringify({
621
+ urls: result.result?.urls || [],
622
+ thumbnail_url: result.result?.thumbnail_url || null,
623
+ duration: result.result?.duration || null,
624
+ model: result.result?.model || null
625
+ }, null, 2)
626
+ }]
627
+ };
628
+ }
629
+ );
630
+
631
+ // ─── transcribe_audio ──────────────────────────────────────
632
+ server.tool(
633
+ 'transcribe_audio',
634
+ 'Transcribe audio or video into text + SRT subtitles. Source can be a URL or an absolute local file path. Returns the full text, SRT content, duration, and download URLs for .srt/.txt files. Works on both audio-only files (mp3, wav, m4a) and videos with audio tracks (mp4, mov, webm).',
635
+ {
636
+ source: { type: 'string', description: 'URL or absolute local path to the audio / video file to transcribe' }
637
+ },
638
+ async ({ source }) => {
639
+ if (!source) throw new Error('source is required (URL or absolute local path)');
640
+
641
+ const isUrl = /^https?:\/\//i.test(source);
642
+ let startResponse;
643
+ if (isUrl) {
644
+ startResponse = await client.post('/v1/transcribe', { audio_url: source });
645
+ } else {
646
+ const resolved = await resolveToBuffer(source, 'audio');
647
+ const form = new FormData();
648
+ form.append('file', resolved.buffer, { filename: resolved.filename, contentType: resolved.contentType });
649
+ startResponse = await client.postMultipart('/v1/transcribe', form);
650
+ }
651
+
652
+ const result = await pollUntilDone(client, startResponse.generation_id, {
653
+ interval: (startResponse.poll_interval_hint || 5) * 1000,
654
+ timeout: 1800000 // 30 minutes — long podcasts are a thing
655
+ });
656
+
657
+ return {
658
+ content: [{
659
+ type: 'text',
660
+ text: JSON.stringify({
661
+ text: result.result?.text || '',
662
+ srt_url: result.result?.srt_url || null,
663
+ txt_url: result.result?.txt_url || null,
664
+ duration: result.result?.duration || null
665
+ }, null, 2)
666
+ }]
667
+ };
668
+ }
669
+ );
670
+
671
+ // ─── generate_3d ───────────────────────────────────────────
672
+ server.tool(
673
+ 'generate_3d',
674
+ 'Generate a 3D model from a text prompt, a single reference image, or multiple reference images (for multi-view reconstruction). Returns model URLs in multiple formats (GLB, FBX, OBJ, USDZ). Modes: "text" (prompt-only), "single" (one image), "multi" (multiple images for better quality). The mode is auto-detected from the inputs if not specified.',
675
+ {
676
+ prompt: { type: 'string', description: 'Text description of the 3D object to generate (used in text mode and also as a hint in image modes)' },
677
+ reference_images: { type: 'array', description: 'Array of public image URLs. 1 image → single mode, 2+ → multi mode.' },
678
+ mode: { type: 'string', description: 'Explicitly set mode: "text" | "single" | "multi". Auto-detected from reference_images if omitted.' },
679
+ texture_prompt: { type: 'string', description: 'Optional prompt to guide texture generation' },
680
+ model: { type: 'string', description: 'Model identifier. Use list_models type="three_d" to see options.' },
681
+ topology: { type: 'string', description: 'Topology preset (optional, model-specific)' },
682
+ target_polycount: { type: 'number', description: 'Target polygon count (optional, model-specific)' },
683
+ enable_tpose: { type: 'boolean', description: 'Force T-pose for character models (optional)' },
684
+ enable_pbr: { type: 'boolean', description: 'Enable PBR textures (optional)' }
685
+ },
686
+ async ({ prompt, reference_images, mode, texture_prompt, model, topology, target_polycount, enable_tpose, enable_pbr }) => {
687
+ if (!prompt && !(reference_images && reference_images.length > 0)) {
688
+ throw new Error('Provide prompt (text mode) or reference_images (single/multi mode)');
689
+ }
690
+
691
+ const startResponse = await client.post('/v1/generate/3d', {
692
+ mode,
693
+ prompt,
694
+ reference_images,
695
+ texture_prompt,
696
+ model,
697
+ topology,
698
+ target_polycount,
699
+ enable_tpose,
700
+ enable_pbr
701
+ });
702
+
703
+ const result = await pollUntilDone(client, startResponse.generation_id, {
704
+ interval: (startResponse.poll_interval_hint || 8) * 1000,
705
+ timeout: 900000 // 15 minutes — 3D generation is slow
706
+ });
707
+
708
+ return {
709
+ content: [{
710
+ type: 'text',
711
+ text: JSON.stringify({
712
+ urls: result.result?.urls || [],
713
+ thumbnail_url: result.result?.thumbnail_url || null,
714
+ mode: result.result?.mode || null,
715
+ prompt_used: result.result?.prompt_used || null
716
+ }, null, 2)
717
+ }]
718
+ };
719
+ }
720
+ );
721
+ }
722
+
723
+ module.exports = { registerGenerateTools };