@kolbo/mcp 1.5.5 → 1.6.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/README.md CHANGED
@@ -65,6 +65,8 @@ Just ask Claude naturally:
65
65
 
66
66
  Every image/video/creative-director tool accepts `visual_dna_ids` and `moodboard_id` for character/style consistency across outputs — you can compose `create_visual_dna` → `generate_image` (with the DNA applied server-side) in a single agent turn. `generate_creative_director` also accepts `moodboard_ids` plural for blending.
67
67
 
68
+ Every generation tool also accepts an optional `resolution` arg. Images use `"1K"` (~1024px) / `"2K"` (Full HD) / `"3K"` (QHD) / `"4K"` (UHD); videos use vertical-pixel tiers like `"720p"` / `"1080p"` / `"1440p"` / `"2160p"`. Values are model-dependent — call `list_models` and read the chosen model's `supported_resolutions` and `resolutionMultipliers`. Omit to use the model default.
69
+
68
70
  **Chat & Vision**
69
71
  | Tool | Description |
70
72
  |------|-------------|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.5.5",
3
+ "version": "1.6.5",
4
4
  "description": "Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -66,6 +66,7 @@ const { registerVisualDnaTools } = require('./tools/visual_dna');
66
66
  const { registerMoodboardTools } = require('./tools/moodboards');
67
67
  const { registerMediaTools } = require('./tools/media');
68
68
  const { registerPresetTools } = require('./tools/presets');
69
+ const { registerAppBuilderTools } = require('./tools/app_builder');
69
70
 
70
71
  async function main() {
71
72
  const client = new KolboClient();
@@ -83,6 +84,7 @@ async function main() {
83
84
  registerMoodboardTools(server, client);
84
85
  registerMediaTools(server, client);
85
86
  registerPresetTools(server, client);
87
+ registerAppBuilderTools(server, client);
86
88
 
87
89
  // Start the server with stdio transport
88
90
  const transport = new StdioServerTransport();
@@ -0,0 +1,254 @@
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 { z } = require('zod');
7
+ const { PollingTimeoutError } = require('../polling');
8
+
9
+ // ─── Build-status polling (App Builder uses a different endpoint than /v1/generate) ──
10
+ async function pollBuildStatus(client, sessionId, options = {}) {
11
+ const {
12
+ interval = 5000,
13
+ timeout = 300000 // 5 minutes
14
+ } = options;
15
+
16
+ const startTime = Date.now();
17
+ const url = `/app-builder/${encodeURIComponent(sessionId)}/build-status`;
18
+
19
+ while (true) {
20
+ if (Date.now() - startTime > timeout) {
21
+ throw new PollingTimeoutError(sessionId, timeout);
22
+ }
23
+
24
+ const result = await client.get(url);
25
+
26
+ if (result.buildStatus === 'deployed') {
27
+ return result;
28
+ }
29
+
30
+ if (result.buildStatus === 'failed') {
31
+ throw new Error(
32
+ `App build failed for session_id="${sessionId}". ` +
33
+ `Call app_builder_get_build_status to check the current state.`
34
+ );
35
+ }
36
+
37
+ await new Promise(resolve => setTimeout(resolve, interval));
38
+ }
39
+ }
40
+
41
+ function registerAppBuilderTools(server, client) {
42
+ // ─── app_builder_list_projects ─────────────────────────────────────────────
43
+ server.tool(
44
+ 'app_builder_list_projects',
45
+ 'List all Kolbo projects for the authenticated user. Use this to find the project_id required by app_builder_create_session and app_builder_list_sessions. Projects are the top-level containers — each project can hold multiple App Builder sessions.',
46
+ {},
47
+ async () => {
48
+ const res = await client.get('/project/lightweight');
49
+ const projects = (Array.isArray(res) ? res : (res.data || [])).map(p => ({
50
+ project_id: p._id,
51
+ name: p.name,
52
+ description: p.description || '',
53
+ created_at: p.createdAt
54
+ }));
55
+ return {
56
+ content: [{ type: 'text', text: JSON.stringify(projects, null, 2) }]
57
+ };
58
+ }
59
+ );
60
+
61
+ // ─── app_builder_create_session ────────────────────────────────────────────
62
+ server.tool(
63
+ 'app_builder_create_session',
64
+ 'Create a new App Builder session inside a Kolbo project. Returns a session_id to pass to app_builder_generate_app. Sessions hold the full app state across multiple generations and edits.',
65
+ {
66
+ project_id: z.string().describe('Kolbo project ID to scope this session. Use app_builder_list_projects to find your project_id.'),
67
+ name: z.string().optional().describe('Optional initial session name. The backend will auto-generate a name on first generation if omitted.')
68
+ },
69
+ async ({ project_id, name }) => {
70
+ const body = name ? { name } : {};
71
+ const res = await client.post(`/app-builder/session/${encodeURIComponent(project_id)}`, body);
72
+ const session = res.data || res;
73
+ return {
74
+ content: [{
75
+ type: 'text',
76
+ text: JSON.stringify({
77
+ session_id: session._id,
78
+ name: session.name,
79
+ build_status: session.buildStatus,
80
+ deployment_url: session.deploymentUrl || null
81
+ }, null, 2)
82
+ }]
83
+ };
84
+ }
85
+ );
86
+
87
+ // ─── app_builder_generate_app ──────────────────────────────────────────────
88
+ server.tool(
89
+ 'app_builder_generate_app',
90
+ 'Generate a React app from a text prompt inside an App Builder session. On the FIRST call the backend auto-generates a punchy app name, URL slug, GitHub repo, and (if needed) a Supabase database. The build runs in the background — this tool polls until the app is deployed (up to 5 minutes) then returns the live deployment_url. Always show the user the deployment_url when done.',
91
+ {
92
+ session_id: z.string().describe('Session ID from app_builder_create_session.'),
93
+ prompt: z.string().describe('Natural language description of the app to build (e.g. "a todo app with drag-and-drop and Supabase persistence").')
94
+ },
95
+ async ({ session_id, prompt }) => {
96
+ await client.post(`/app-builder/generation/${encodeURIComponent(session_id)}`, { userPrompt: prompt });
97
+ const status = await pollBuildStatus(client, session_id);
98
+ return {
99
+ content: [{
100
+ type: 'text',
101
+ text: JSON.stringify({
102
+ session_id,
103
+ build_status: status.buildStatus,
104
+ deployment_url: status.deploymentUrl || null,
105
+ app_name: status.appName || null
106
+ }, null, 2)
107
+ }]
108
+ };
109
+ }
110
+ );
111
+
112
+ // ─── app_builder_edit_app ──────────────────────────────────────────────────
113
+ server.tool(
114
+ 'app_builder_edit_app',
115
+ 'Edit an existing generated app with a natural language instruction — "add a dark mode toggle", "change the color scheme to blue", "add a contact form". Like app_builder_generate_app but for modifications. Use app_builder_list_generations to get the current generation_id before calling this.',
116
+ {
117
+ session_id: z.string().describe('Session ID of the app to edit.'),
118
+ generation_id: z.string().describe('The generation to edit. Use app_builder_list_generations to find the latest generation_id.'),
119
+ edit_prompt: z.string().describe('Natural language instruction describing the change to make.')
120
+ },
121
+ async ({ session_id, generation_id, edit_prompt }) => {
122
+ await client.request(
123
+ 'PUT',
124
+ `/app-builder/generation/${encodeURIComponent(session_id)}/${encodeURIComponent(generation_id)}`,
125
+ { editPrompt: edit_prompt }
126
+ );
127
+ const status = await pollBuildStatus(client, session_id);
128
+ return {
129
+ content: [{
130
+ type: 'text',
131
+ text: JSON.stringify({
132
+ session_id,
133
+ build_status: status.buildStatus,
134
+ deployment_url: status.deploymentUrl || null,
135
+ app_name: status.appName || null
136
+ }, null, 2)
137
+ }]
138
+ };
139
+ }
140
+ );
141
+
142
+ // ─── app_builder_get_build_status ──────────────────────────────────────────
143
+ server.tool(
144
+ 'app_builder_get_build_status',
145
+ 'Check the current build status of an App Builder session. Use this to manually poll after app_builder_generate_app or app_builder_edit_app, or to check on an app at any time. Returns "deployed" when the live URL is ready.',
146
+ {
147
+ session_id: z.string().describe('Session ID to check.')
148
+ },
149
+ async ({ session_id }) => {
150
+ const result = await client.get(`/app-builder/${encodeURIComponent(session_id)}/build-status`);
151
+ return {
152
+ content: [{
153
+ type: 'text',
154
+ text: JSON.stringify({
155
+ build_status: result.buildStatus,
156
+ deployment_url: result.deploymentUrl || null,
157
+ deployed_at: result.deployedAt || null
158
+ }, null, 2)
159
+ }]
160
+ };
161
+ }
162
+ );
163
+
164
+ // ─── app_builder_get_session ───────────────────────────────────────────────
165
+ server.tool(
166
+ 'app_builder_get_session',
167
+ 'Get full details of an App Builder session including metadata, build status, deployment URL, and GitHub/Supabase integration info. Use this when the user wants to clone the app locally — it returns the GitHub repo URL and Supabase connection details needed for local development.',
168
+ {
169
+ session_id: z.string().describe('Session ID to retrieve.')
170
+ },
171
+ async ({ session_id }) => {
172
+ const res = await client.get(`/app-builder/session/${encodeURIComponent(session_id)}`);
173
+ const session = res.data || res;
174
+ return {
175
+ content: [{
176
+ type: 'text',
177
+ text: JSON.stringify({
178
+ session_id: session._id,
179
+ name: session.name,
180
+ build_status: session.buildStatus,
181
+ deployment_url: session.deploymentUrl || null,
182
+ github_repo_url: session.githubRepoUrl || null,
183
+ supabase_url: session.supabaseUrl || null,
184
+ supabase_anon_key: session.supabaseAnonKey || null,
185
+ created_at: session.createdAt
186
+ }, null, 2)
187
+ }]
188
+ };
189
+ }
190
+ );
191
+
192
+ // ─── app_builder_list_sessions ─────────────────────────────────────────────
193
+ server.tool(
194
+ 'app_builder_list_sessions',
195
+ 'List all App Builder sessions in a project. Use this to find existing sessions before creating a new one, or to pick a session_id to continue working on.',
196
+ {
197
+ project_id: z.string().describe('Kolbo project ID. Use app_builder_list_projects to find it.')
198
+ },
199
+ async ({ project_id }) => {
200
+ const res = await client.get(`/app-builder/sessions/${encodeURIComponent(project_id)}`);
201
+ const sessions = (Array.isArray(res) ? res : (res.data || [])).map(s => ({
202
+ session_id: s._id,
203
+ name: s.name,
204
+ build_status: s.buildStatus,
205
+ deployment_url: s.deploymentUrl || null,
206
+ created_at: s.createdAt
207
+ }));
208
+ return {
209
+ content: [{ type: 'text', text: JSON.stringify(sessions, null, 2) }]
210
+ };
211
+ }
212
+ );
213
+
214
+ // ─── app_builder_list_generations ──────────────────────────────────────────
215
+ server.tool(
216
+ 'app_builder_list_generations',
217
+ 'List all generations for an App Builder session, newest first. Use this to find the current generation_id before calling app_builder_edit_app.',
218
+ {
219
+ session_id: z.string().describe('Session ID to list generations for.')
220
+ },
221
+ async ({ session_id }) => {
222
+ const res = await client.get(`/app-builder/generations/${encodeURIComponent(session_id)}`);
223
+ const generations = (Array.isArray(res) ? res : (res.data || [])).map(g => ({
224
+ generation_id: g._id,
225
+ user_prompt: g.userPrompt || g.editPrompt || '',
226
+ build_status: g.buildStatus,
227
+ created_at: g.createdAt
228
+ }));
229
+ return {
230
+ content: [{ type: 'text', text: JSON.stringify(generations, null, 2) }]
231
+ };
232
+ }
233
+ );
234
+
235
+ // ─── app_builder_delete_session ────────────────────────────────────────────
236
+ server.tool(
237
+ 'app_builder_delete_session',
238
+ 'Permanently delete an App Builder session and ALL associated resources: GitHub repo, Supabase database (unless user-connected), deployed files, generation history, messages, and form submissions. THIS IS IRREVERSIBLE — always confirm with the user before calling.',
239
+ {
240
+ session_id: z.string().describe('Session ID to permanently delete. This cannot be undone.')
241
+ },
242
+ async ({ session_id }) => {
243
+ await client.delete(`/app-builder/session/${encodeURIComponent(session_id)}`);
244
+ return {
245
+ content: [{
246
+ type: 'text',
247
+ text: JSON.stringify({ success: true }, null, 2)
248
+ }]
249
+ };
250
+ }
251
+ );
252
+ }
253
+
254
+ module.exports = { registerAppBuilderTools };
@@ -15,19 +15,21 @@ function registerGenerateTools(server, client) {
15
15
  '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.',
16
16
  {
17
17
  prompt: z.string().describe('Text description of the image to generate'),
18
- model: z.string().optional().describe('Model identifier. Use list_models type="image" to see options. Omit for Smart Select.'),
18
+ model: z.string().optional().describe('Model identifier. Use list_models type="text_to_img" to see options. Omit for Smart Select.'),
19
19
  aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "1:1", "16:9", "9:16"). Default: "1:1"'),
20
20
  enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: true'),
21
21
  num_images: z.number().optional().describe('Number of images to generate in one call. Default: 1'),
22
22
  reference_images: z.array(z.string()).optional().describe('Array of image URLs used as composition/style references (NOT as source images for editing — use generate_image_edit for that).'),
23
23
  visual_dna_ids: z.array(z.string()).optional().describe('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.'),
24
24
  moodboard_id: z.string().optional().describe('Moodboard ID (from list_moodboards / get_moodboard) whose master_prompt and style_guide should be applied to this generation.'),
25
- enable_web_search: z.boolean().optional().describe('Enable web-search grounding for the prompt (useful for current events, brand references, real-world accuracy). Default: false')
25
+ enable_web_search: z.boolean().optional().describe('Enable web-search grounding for the prompt (useful for current events, brand references, real-world accuracy). Default: false'),
26
+ resolution: z.string().optional().describe('Image resolution tier: "1K" (~1024px), "2K" (Full HD), "3K" (QHD), or "4K" (UHD). Model-dependent — call list_models and read supported_resolutions on the chosen model. Read resolutionMultipliers on the same model to predict credit cost. Omit to use the model default.'),
27
+ preset_id: z.string().optional().describe('Preset ID from list_presets type="image" to apply a saved style preset to this generation.')
26
28
  },
27
- async ({ prompt, model, aspect_ratio, enhance_prompt, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search }) => {
29
+ async ({ prompt, model, aspect_ratio, enhance_prompt, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, preset_id }) => {
28
30
  const gen = await client.post('/v1/generate/image', {
29
31
  prompt, model, aspect_ratio, enhance_prompt, num_images,
30
- reference_images, visual_dna_ids, moodboard_id, enable_web_search
32
+ reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, preset_id
31
33
  });
32
34
 
33
35
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -54,19 +56,20 @@ function registerGenerateTools(server, client) {
54
56
  '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.',
55
57
  {
56
58
  prompt: z.string().describe('Description of the edit to apply (e.g., "remove the background", "change the sky to sunset")'),
57
- model: z.string().optional().describe('Model identifier. Use list_models type="image_edit" to see options. Omit for Smart Select.'),
59
+ model: z.string().optional().describe('Model identifier. Use list_models type="image_editing" to see options. Omit for Smart Select.'),
58
60
  source_images: z.array(z.string()).describe('Array of source image URLs to edit. Typically one, but some models accept multiple for compositing.'),
59
61
  aspect_ratio: z.string().optional().describe('Output aspect ratio (e.g., "1:1", "16:9", "9:16"). Default: "1:1"'),
60
62
  enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: true'),
61
63
  num_images: z.number().optional().describe('Number of output images. Default: 1'),
62
64
  visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for consistency with an existing character / style / product.'),
63
65
  moodboard_id: z.string().optional().describe('Moodboard ID whose master_prompt and style_guide should be applied.'),
64
- enable_web_search: z.boolean().optional().describe('Enable web-search grounding. Default: false')
66
+ enable_web_search: z.boolean().optional().describe('Enable web-search grounding. Default: false'),
67
+ resolution: z.string().optional().describe('Image resolution tier: "1K" / "2K" / "3K" / "4K". Model-dependent — call list_models and read supported_resolutions. Default: "1K" for most edit models.')
65
68
  },
66
- async ({ prompt, model, source_images, aspect_ratio, enhance_prompt, num_images, visual_dna_ids, moodboard_id, enable_web_search }) => {
69
+ async ({ prompt, model, source_images, aspect_ratio, enhance_prompt, num_images, visual_dna_ids, moodboard_id, enable_web_search, resolution }) => {
67
70
  const gen = await client.post('/v1/generate/image-edit', {
68
71
  prompt, model, source_images, aspect_ratio, enhance_prompt, num_images,
69
- visual_dna_ids, moodboard_id, enable_web_search
72
+ visual_dna_ids, moodboard_id, enable_web_search, resolution
70
73
  });
71
74
 
72
75
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -102,12 +105,13 @@ function registerGenerateTools(server, client) {
102
105
  reference_images: z.array(z.string()).optional().describe('Array of reference image URLs to guide style/composition of every scene.'),
103
106
  visual_dna_ids: z.array(z.string()).optional().describe('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.'),
104
107
  moodboard_id: z.string().optional().describe('A single moodboard ID whose master_prompt and style_guide should shape every scene.'),
105
- moodboard_ids: z.array(z.string()).optional().describe('Multiple moodboard IDs when blending styles. Prefer `moodboard_id` for single moodboards.')
108
+ moodboard_ids: z.array(z.string()).optional().describe('Multiple moodboard IDs when blending styles. Prefer `moodboard_id` for single moodboards.'),
109
+ resolution: z.string().optional().describe('Resolution tier applied to every scene. Images: "1K" / "2K" / "3K" / "4K". Videos: "720p" / "1080p" / "1440p" / "2160p". Values are model-dependent — call list_models and read supported_resolutions on the target model. Multiplied across every scene.')
106
110
  },
107
- async ({ prompt, scene_count, model, aspect_ratio, workflow_type, duration, enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids }) => {
111
+ async ({ prompt, scene_count, model, aspect_ratio, workflow_type, duration, enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution }) => {
108
112
  const gen = await client.post('/v1/generate/creative-director', {
109
113
  prompt, scene_count, model, aspect_ratio, workflow_type, duration,
110
- enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids
114
+ enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution
111
115
  });
112
116
 
113
117
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -144,16 +148,18 @@ function registerGenerateTools(server, client) {
144
148
  '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.',
145
149
  {
146
150
  prompt: z.string().describe('Text description of the video to generate'),
147
- model: z.string().optional().describe('Model identifier. Use list_models type="video" to see options. Check supported_durations and supported_aspect_ratios.'),
151
+ model: z.string().optional().describe('Model identifier. Use list_models type="text_to_video" to see options. Check supported_durations and supported_aspect_ratios.'),
148
152
  aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"'),
149
153
  duration: z.number().optional().describe('Duration in seconds. Must be a value the chosen model supports — check supported_durations from list_models. Default: 5'),
150
154
  enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
151
155
  reference_images: z.array(z.string()).optional().describe('Array of image URLs used as visual references (style / composition / subject).'),
152
- visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to keep a character / style consistent with prior generations.')
156
+ visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to keep a character / style consistent with prior generations.'),
157
+ resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Some models use labels like "512P"/"1024P"/"768P"/"1080P". Model-dependent — call list_models and read supported_resolutions. Read resolutionMultipliers to predict cost.'),
158
+ preset_id: z.string().optional().describe('Preset ID from list_presets type="video" to apply a saved motion/style preset to this generation.')
153
159
  },
154
- async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, visual_dna_ids }) => {
160
+ async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, visual_dna_ids, resolution, preset_id }) => {
155
161
  const gen = await client.post('/v1/generate/video', {
156
- prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, visual_dna_ids
162
+ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, visual_dna_ids, resolution, preset_id
157
163
  });
158
164
 
159
165
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -183,15 +189,16 @@ function registerGenerateTools(server, client) {
183
189
  {
184
190
  image_url: z.string().describe('URL of the source image to animate'),
185
191
  prompt: z.string().describe('Text description of the desired MOTION (e.g., "camera slowly pans right while the character walks forward")'),
186
- model: z.string().optional().describe('Model identifier. Use list_models type="video_from_image" to see options.'),
192
+ model: z.string().optional().describe('Model identifier. Use list_models type="img_to_video" to see options.'),
187
193
  aspect_ratio: z.string().optional().describe('Output aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"'),
188
194
  duration: z.number().optional().describe('Duration in seconds. Must be a value the chosen model supports. Default: 5'),
189
195
  enhance_prompt: z.boolean().optional().describe('Enhance the motion prompt. Default: true'),
190
- visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to maintain consistency with prior characters / styles.')
196
+ visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to maintain consistency with prior characters / styles.'),
197
+ resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Some models use labels like "512P"/"1024P"/"768P"/"1080P". Model-dependent — call list_models and read supported_resolutions. Read resolutionMultipliers to predict cost.')
191
198
  },
192
- async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids }) => {
199
+ async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution }) => {
193
200
  const gen = await client.post('/v1/generate/video/from-image', {
194
- image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids
201
+ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution
195
202
  });
196
203
 
197
204
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -219,16 +226,17 @@ function registerGenerateTools(server, client) {
219
226
  '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.',
220
227
  {
221
228
  prompt: z.string().describe('Text description of the music to generate (e.g., "upbeat electronic dance track with synthesizers")'),
222
- model: z.string().optional().describe('Model identifier. Use list_models type="music" to see options. Omit for Suno (default).'),
229
+ model: z.string().optional().describe('Model identifier. Use list_models type="music_gen" to see options. Omit for Suno (default).'),
223
230
  style: z.string().optional().describe('Music style / genre (e.g., "pop", "rock", "lo-fi", "electronic", "jazz")'),
224
231
  instrumental: z.boolean().optional().describe('Generate instrumental only, no vocals. Default: false'),
225
232
  lyrics: z.string().optional().describe('Custom lyrics for the song. If omitted, lyrics are generated automatically from the prompt unless instrumental is true.'),
226
233
  vocal_gender: z.string().optional().describe('Preferred vocal gender: "male" or "female". Only applies when instrumental is false.'),
227
- enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true')
234
+ enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
235
+ preset_id: z.string().optional().describe('Preset ID from list_presets type="music" to apply a saved music style preset.')
228
236
  },
229
- async ({ prompt, model, style, instrumental, lyrics, vocal_gender, enhance_prompt }) => {
237
+ async ({ prompt, model, style, instrumental, lyrics, vocal_gender, enhance_prompt, preset_id }) => {
230
238
  const gen = await client.post('/v1/generate/music', {
231
- prompt, model, style, instrumental, lyrics, vocal_gender, enhance_prompt
239
+ prompt, model, style, instrumental, lyrics, vocal_gender, enhance_prompt, preset_id
232
240
  });
233
241
 
234
242
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -257,7 +265,7 @@ function registerGenerateTools(server, client) {
257
265
  {
258
266
  text: z.string().describe('The text to convert to speech'),
259
267
  voice: z.string().optional().describe('Voice ID (from list_voices) or voice display name (e.g., "Rachel", "Adam"). Default: "Rachel"'),
260
- model: z.string().optional().describe('Model identifier. Use list_models type="speech" to see options. Default: eleven_v3'),
268
+ model: z.string().optional().describe('Model identifier. Use list_models type="text_to_speech" to see options. Default: eleven_v3'),
261
269
  language: z.string().optional().describe('Language code (e.g., "en-US", "he-IL", "es-ES"). Default: "en-US"')
262
270
  },
263
271
  async ({ text, voice, model, language }) => {
@@ -289,12 +297,13 @@ function registerGenerateTools(server, client) {
289
297
  '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.',
290
298
  {
291
299
  prompt: z.string().describe('Text description of the sound effect (e.g., "thunder clap with rain", "door creaking open", "futuristic UI beep")'),
292
- model: z.string().optional().describe('Model identifier. Use list_models type="sound" to see options. Default: elevenlabs-sound-effects-v1'),
293
- duration: z.number().optional().describe('Duration in seconds. Omit for automatic duration.')
300
+ model: z.string().optional().describe('Model identifier. Use list_models type="text_to_sound" to see options. Default: elevenlabs-sound-effects-v1'),
301
+ duration: z.number().optional().describe('Duration in seconds. Omit for automatic duration.'),
302
+ prompt_influence: z.number().optional().describe('How strongly the prompt guides the generation (0–1). Default: 0.5. Lower values give the model more creative freedom; higher values follow the prompt more literally.')
294
303
  },
295
- async ({ prompt, model, duration }) => {
304
+ async ({ prompt, model, duration, prompt_influence }) => {
296
305
  const gen = await client.post('/v1/generate/sound', {
297
- prompt, model, duration
306
+ prompt, model, duration, prompt_influence
298
307
  });
299
308
 
300
309
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -377,20 +386,23 @@ function registerGenerateTools(server, client) {
377
386
  // ─── generate_elements ─────────────────────────────────────
378
387
  server.tool(
379
388
  'generate_elements',
380
- '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.',
389
+ 'Generate a video from reference elements (images, videos, and/or audio) + 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". IMPORTANT: different models accept different numbers of inputs — call list_models type="elements" and read elementsMaxImages / elementsMaxVideos / elementsMaxAudio on the chosen model before generating. 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.',
381
390
  {
382
391
  prompt: z.string().describe('Text description of the desired video / animation'),
383
- model: z.string().optional().describe('Model identifier. Use list_models type="video" to see options. Omit for Smart Select.'),
384
- reference_images: z.array(z.string()).optional().describe('Array of public image URLs used as reference elements (product shots, character references, etc.). URL mode.'),
392
+ model: z.string().optional().describe('Model identifier. Use list_models type="elements" to see options (Seedance 2, Kling O3 Reference, Grok Imagine, Veo 3.1, etc.). Check elementsMaxImages / elementsMaxVideos / elementsMaxAudio on the model. Omit for Smart Select.'),
393
+ reference_images: z.array(z.string()).optional().describe('Array of public image URLs used as reference elements (product shots, character references, etc.). Check elementsMaxImages on the chosen model — pass at most that many URLs.'),
394
+ reference_videos: z.array(z.string()).optional().describe('Array of reference video URLs for models that accept video inputs (elementsMaxVideos > 0). Check elementsMaxVideos on the chosen model from list_models before passing.'),
395
+ audio_url: z.string().optional().describe('URL of a reference audio track for models that accept audio inputs (elementsMaxAudio > 0). Check elementsMaxAudio on the chosen model from list_models before passing.'),
385
396
  files: z.array(z.string()).optional().describe('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.'),
386
397
  duration: z.number().optional().describe('Duration in seconds. Default: 5'),
387
398
  aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"'),
388
399
  motion: z.string().optional().describe('Motion style / intensity hint (optional)'),
389
400
  preset_id: z.string().optional().describe('Preset ID from list_presets type="video" (optional)'),
390
401
  enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
391
- visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for character/style consistency across outputs.')
402
+ visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for character/style consistency across outputs.'),
403
+ resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.')
392
404
  },
393
- async ({ prompt, model, reference_images, files, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids }) => {
405
+ async ({ prompt, model, reference_images, reference_videos, audio_url, files, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution }) => {
394
406
  if (!prompt) throw new Error('prompt is required');
395
407
 
396
408
  let startResponse;
@@ -407,6 +419,9 @@ function registerGenerateTools(server, client) {
407
419
  if (enhance_prompt !== undefined) form.append('enhance_prompt', String(enhance_prompt));
408
420
  if (visual_dna_ids) form.append('visual_dna_ids', JSON.stringify(visual_dna_ids));
409
421
  if (reference_images) form.append('reference_images', JSON.stringify(reference_images));
422
+ if (reference_videos) form.append('reference_videos', JSON.stringify(reference_videos));
423
+ if (audio_url) form.append('audio_url', audio_url);
424
+ if (resolution) form.append('resolution', resolution);
410
425
  for (const f of resolved) {
411
426
  form.append('files', f.buffer, { filename: f.filename, contentType: f.contentType });
412
427
  }
@@ -414,7 +429,7 @@ function registerGenerateTools(server, client) {
414
429
  } else {
415
430
  // URL-only mode: plain JSON.
416
431
  startResponse = await client.post('/v1/generate/elements', {
417
- prompt, model, reference_images, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids
432
+ prompt, model, reference_images, reference_videos, audio_url, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution
418
433
  });
419
434
  }
420
435
 
@@ -447,13 +462,14 @@ function registerGenerateTools(server, client) {
447
462
  first_frame: z.string().optional().describe('URL or absolute local path to the first frame (file mode — alternative to first_frame_url)'),
448
463
  last_frame: z.string().optional().describe('URL or absolute local path to the last frame (file mode — alternative to last_frame_url)'),
449
464
  prompt: z.string().optional().describe('Optional description of the desired motion between the two frames (e.g. "smooth camera dolly in")'),
450
- model: z.string().optional().describe('Model identifier. Use list_models type="video_from_image" to see options. Omit for Smart Select.'),
465
+ model: z.string().optional().describe('Model identifier. Use list_models type="firstlastgenerations" to see options. Omit for Smart Select.'),
451
466
  duration: z.number().optional().describe('Duration in seconds. Default: 5'),
452
467
  aspect_ratio: z.string().optional().describe('Aspect ratio (auto-detected from first frame if not provided). Default: "16:9"'),
453
468
  enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
454
- visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply.')
469
+ visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply.'),
470
+ resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.')
455
471
  },
456
- async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids }) => {
472
+ async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids, resolution }) => {
457
473
  const urlMode = first_frame_url && last_frame_url;
458
474
  const fileMode = first_frame && last_frame;
459
475
  if (!urlMode && !fileMode) {
@@ -478,10 +494,11 @@ function registerGenerateTools(server, client) {
478
494
  if (aspect_ratio) form.append('aspect_ratio', aspect_ratio);
479
495
  if (enhance_prompt !== undefined) form.append('enhance_prompt', String(enhance_prompt));
480
496
  if (visual_dna_ids) form.append('visual_dna_ids', JSON.stringify(visual_dna_ids));
497
+ if (resolution) form.append('resolution', resolution);
481
498
  startResponse = await client.postMultipart('/v1/generate/first-last-frame', form);
482
499
  } else {
483
500
  startResponse = await client.post('/v1/generate/first-last-frame', {
484
- first_frame_url, last_frame_url, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids
501
+ first_frame_url, last_frame_url, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids, resolution
485
502
  });
486
503
  }
487
504
 
@@ -512,7 +529,7 @@ function registerGenerateTools(server, client) {
512
529
  source: z.string().describe('URL or absolute local path to the source image or video (the face to animate)'),
513
530
  audio: z.string().describe('URL or absolute local path to the audio track (the voice to sync to)'),
514
531
  text_prompt: z.string().optional().describe('Optional text prompt (for performance-capable models)'),
515
- model: z.string().optional().describe('Model identifier. Use list_models type="lipsync" to see options. Omit for Smart Select.'),
532
+ model: z.string().optional().describe('Model identifier. Use list_models type="lipsync-image" or type="lipsync-video" to see options. Omit for Smart Select.'),
516
533
  bounding_box_target: z.array(z.number()).optional().describe('Optional bounding box [x, y, w, h] for multi-face inputs (Hedra Character3 style). Leave empty for single-face.')
517
534
  },
518
535
  async ({ source, audio, text_prompt, model, bounding_box_target }) => {
@@ -577,17 +594,21 @@ function registerGenerateTools(server, client) {
577
594
  // ─── generate_video_from_video ─────────────────────────────
578
595
  server.tool(
579
596
  'generate_video_from_video',
580
- '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.',
597
+ 'Restyle / transform an existing video using a text prompt (video-to-video). Use for style transfer, scene restyling, subject swap, motion transfer, or character replacement. Source video can be a URL or absolute local path. IMPORTANT: different models support different extra inputs call list_models type="video_to_video" and read maxImages / maxVideos / maxElements on the chosen model before generating. Pass reference_images for models with maxImages > 0 (e.g. Kling O1/O3, Aleph, WAN VACE), reference_videos for models with maxVideos > 1 (e.g. WAN 2.6 reference-to-video accepts up to 3), and elements for models with maxElements > 0. For animating a still image use generate_video_from_image instead. For text-only → video use generate_video.',
581
598
  {
582
- source_video: z.string().describe('URL or absolute local path to the source video to restyle'),
599
+ source_video: z.string().describe('URL or absolute local path to the primary source video to restyle. For models that use reference_videos as their primary input (e.g. WAN 2.6 reference-to-video), pass the first reference video here and also include it in reference_videos.'),
583
600
  prompt: z.string().describe('Text description of the desired restyle / transformation'),
584
- model: z.string().optional().describe('Model identifier. Omit for Smart Select.'),
601
+ model: z.string().optional().describe('Model identifier. Use list_models type="video_to_video" to see options and check maxImages / maxVideos / maxElements per model. Omit for Smart Select.'),
585
602
  aspect_ratio: z.string().optional().describe('Output aspect ratio. Default: matches source'),
586
603
  duration: z.number().optional().describe('Duration in seconds (default: matches source)'),
587
604
  enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
588
- visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for character/style consistency.')
605
+ visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for character/style consistency.'),
606
+ resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.'),
607
+ reference_images: z.array(z.string()).optional().describe('Array of reference image URLs for models that support additional image inputs (maxImages > 0). Examples: character reference images for Kling O1/O3, style reference for Aleph/gen4_aleph, character image for WAN VACE video-edit. Check maxImages on the model from list_models before passing.'),
608
+ reference_videos: z.array(z.string()).optional().describe('Array of additional reference video URLs for models that support multiple video inputs (maxVideos > 1). Example: WAN 2.6 reference-to-video accepts 1–3 reference videos. Check maxVideos on the model from list_models before passing.'),
609
+ elements: z.array(z.string()).optional().describe('Array of element image URLs for models with maxElements > 0. Elements are used as style or character reference assets alongside the main video. Check maxElements on the model from list_models before passing.')
589
610
  },
590
- async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids }) => {
611
+ async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, reference_images, reference_videos, elements }) => {
591
612
  if (!source_video) throw new Error('source_video is required');
592
613
  if (!prompt) throw new Error('prompt is required');
593
614
 
@@ -595,7 +616,8 @@ function registerGenerateTools(server, client) {
595
616
  let startResponse;
596
617
  if (isUrl) {
597
618
  startResponse = await client.post('/v1/generate/video-from-video', {
598
- video_url: source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids
619
+ video_url: source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution,
620
+ reference_images, reference_videos, elements
599
621
  });
600
622
  } else {
601
623
  const resolved = await resolveToBuffer(source_video, 'video');
@@ -607,6 +629,10 @@ function registerGenerateTools(server, client) {
607
629
  if (duration !== undefined) form.append('duration', String(duration));
608
630
  if (enhance_prompt !== undefined) form.append('enhance_prompt', String(enhance_prompt));
609
631
  if (visual_dna_ids) form.append('visual_dna_ids', JSON.stringify(visual_dna_ids));
632
+ if (resolution) form.append('resolution', resolution);
633
+ if (reference_images) form.append('reference_images', JSON.stringify(reference_images));
634
+ if (reference_videos) form.append('reference_videos', JSON.stringify(reference_videos));
635
+ if (elements) form.append('elements', JSON.stringify(elements));
610
636
  startResponse = await client.postMultipart('/v1/generate/video-from-video', form);
611
637
  }
612
638
 
@@ -679,7 +705,7 @@ function registerGenerateTools(server, client) {
679
705
  reference_images: z.array(z.string()).optional().describe('Array of public image URLs. 1 image → single mode, 2+ → multi mode.'),
680
706
  mode: z.string().optional().describe('Explicitly set mode: "text" | "single" | "multi". Auto-detected from reference_images if omitted.'),
681
707
  texture_prompt: z.string().optional().describe('Optional prompt to guide texture generation'),
682
- model: z.string().optional().describe('Model identifier. Use list_models type="three_d" to see options.'),
708
+ model: z.string().optional().describe('Model identifier. Use list_models type="three_d" to see all 3D options, or filter by sub-type: "3d_text_to_model", "3d_image_to_model", "3d_multi_image_to_model", "3d_world".'),
683
709
  topology: z.string().optional().describe('Topology preset (optional, model-specific)'),
684
710
  target_polycount: z.number().optional().describe('Target polygon count (optional, model-specific)'),
685
711
  enable_tpose: z.boolean().optional().describe('Force T-pose for character models (optional)'),
@@ -720,6 +746,96 @@ function registerGenerateTools(server, client) {
720
746
  };
721
747
  }
722
748
  );
749
+ // ─── edit_image ────────────────────────────────────────────
750
+ server.tool(
751
+ 'edit_image',
752
+ 'Apply a targeted AI edit to an existing image. Use for upscaling resolution, changing aspect ratio (reframe), removing the background, portrait skin enhancement, or a text-guided edit (magic_edit). Faster and cheaper than generate_image_edit for these specific operations because it routes to specialized models. Returns the edited image URL when complete.',
753
+ {
754
+ image_url: z.string().describe('URL of the source image to edit'),
755
+ operation: z.enum(['upscale', 'reframe', 'removebg', 'enhance_skin', 'magic_edit'])
756
+ .describe('Edit operation to apply: "upscale" (increase resolution 2×–4×), "reframe" (change aspect ratio), "removebg" (remove background), "enhance_skin" (portrait retouching), "magic_edit" (text-guided edit — requires prompt)'),
757
+ model: z.string().optional().describe('Model identifier override. Omit to use the default model for the operation.'),
758
+ scale: z.number().optional().describe('Upscale factor: 2, 3, or 4. Only used when operation="upscale". Default: 2.'),
759
+ aspect_ratio: z.string().optional().describe('Target aspect ratio (e.g., "16:9", "9:16", "1:1"). Required for operation="reframe".'),
760
+ skin_strength: z.enum(['subtle', 'realistic', 'pimple', 'freckle']).optional()
761
+ .describe('Skin enhancement style. Only used when operation="enhance_skin". Default: "realistic".'),
762
+ prompt: z.string().optional().describe('Text instruction for the edit. Required for operation="magic_edit" (e.g., "add sunglasses", "change the sky to sunset").')
763
+ },
764
+ async ({ image_url, operation, model, scale, aspect_ratio, skin_strength, prompt }) => {
765
+ if (operation === 'magic_edit' && !prompt) throw new Error('prompt is required for magic_edit operation');
766
+ if (operation === 'reframe' && !aspect_ratio) throw new Error('aspect_ratio is required for reframe operation');
767
+
768
+ const gen = await client.post('/v1/edit/image', {
769
+ image_url, operation, model, scale, aspect_ratio, skin_strength, prompt
770
+ });
771
+
772
+ const result = await pollUntilDone(client, gen.generation_id, {
773
+ interval: (gen.poll_interval_hint || 5) * 1000,
774
+ timeout: 180000
775
+ });
776
+
777
+ return {
778
+ content: [{
779
+ type: 'text',
780
+ text: JSON.stringify({
781
+ urls: result.result?.urls || [],
782
+ edit_type: result.result?.edit_type || null,
783
+ model: result.result?.model || null
784
+ }, null, 2)
785
+ }]
786
+ };
787
+ }
788
+ );
789
+
790
+ // ─── edit_video ────────────────────────────────────────────
791
+ server.tool(
792
+ 'edit_video',
793
+ 'Apply a targeted AI edit to an existing video. Operations: upscale (4K resolution boost), reframe (change aspect ratio), generate_audio (add AI-generated sound/music from a prompt), remove_watermark, face_swap (replace faces using a reference image URL), extend (lengthen at start or end), magic_edit (restyle/transform with a prompt), lipsync (sync an audio track to a face in the video). Returns the edited video URL when complete.',
794
+ {
795
+ video_url: z.string().describe('URL of the source video to edit'),
796
+ operation: z.enum(['upscale', 'reframe', 'generate_audio', 'remove_watermark', 'face_swap', 'extend', 'magic_edit', 'lipsync'])
797
+ .describe('Edit operation: "upscale", "reframe" (requires aspect_ratio), "generate_audio" (requires prompt), "remove_watermark", "face_swap" (requires image_url), "extend" (requires duration), "magic_edit" (requires prompt), "lipsync" (requires audio_url)'),
798
+ model: z.string().optional().describe('Model identifier override. Omit to use the default model for the operation.'),
799
+ aspect_ratio: z.string().optional().describe('Target aspect ratio (e.g., "16:9", "9:16"). Required for operation="reframe".'),
800
+ scale: z.number().optional().describe('Upscale factor. Only used when operation="upscale".'),
801
+ prompt: z.string().optional().describe('Text prompt. Required for operation="magic_edit" and "generate_audio". Optional hint for "extend".'),
802
+ image_url: z.string().optional().describe('URL of the reference face image. Required for operation="face_swap".'),
803
+ audio_url: z.string().optional().describe('URL of the audio track to sync. Required for operation="lipsync".'),
804
+ duration: z.number().optional().describe('Seconds of video to generate. Required for operation="extend". Typical range: 1–20.'),
805
+ mode: z.string().optional().describe('Where to extend: "start" or "end". Only used when operation="extend". Default: "end".')
806
+ },
807
+ async ({ video_url, operation, model, aspect_ratio, scale, prompt, image_url, audio_url, duration, mode }) => {
808
+ if (operation === 'magic_edit' && !prompt) throw new Error('prompt is required for magic_edit');
809
+ if (operation === 'generate_audio' && !prompt) throw new Error('prompt is required for generate_audio');
810
+ if (operation === 'reframe' && !aspect_ratio) throw new Error('aspect_ratio is required for reframe');
811
+ if (operation === 'face_swap' && !image_url) throw new Error('image_url (reference face) is required for face_swap');
812
+ if (operation === 'lipsync' && !audio_url) throw new Error('audio_url is required for lipsync');
813
+ if (operation === 'extend' && !duration) throw new Error('duration is required for extend');
814
+
815
+ const gen = await client.post('/v1/edit/video', {
816
+ video_url, operation, model, aspect_ratio, scale, prompt,
817
+ image_url, audio_url, duration, mode
818
+ });
819
+
820
+ const result = await pollUntilDone(client, gen.generation_id, {
821
+ interval: (gen.poll_interval_hint || 8) * 1000,
822
+ timeout: 600000
823
+ });
824
+
825
+ return {
826
+ content: [{
827
+ type: 'text',
828
+ text: JSON.stringify({
829
+ urls: result.result?.urls || [],
830
+ download_url: result.result?.download_url || null,
831
+ edit_type: result.result?.edit_type || null,
832
+ duration: result.result?.duration || null,
833
+ model: result.result?.model || null
834
+ }, null, 2)
835
+ }]
836
+ };
837
+ }
838
+ );
723
839
  }
724
840
 
725
841
  module.exports = { registerGenerateTools };
@@ -11,7 +11,7 @@ function registerModelTools(server, client) {
11
11
  'list_models',
12
12
  'List available AI models on Kolbo. Filter by type to find models for a specific generation type.',
13
13
  {
14
- type: z.string().optional().describe('Filter by type: "image", "image_edit", "video", "video_from_image", "video_from_video", "music", "speech", "sound", "chat", "lipsync", "three_d", "elements", "first_last_frame", "transcription". Omit for all models.')
14
+ type: z.string().optional().describe('Filter by DB type name: "text_to_img", "image_editing", "text_to_video", "img_to_video", "draw_to_video", "video_to_video", "elements", "firstlastgenerations", "lipsync-image", "lipsync-video", "music_gen", "text_to_speech", "text_to_sound", "stt", "text". Legacy aliases also accepted: "image", "image_edit", "video", "video_from_image", "video_from_video", "music", "speech", "sound", "chat", "lipsync" (both lipsync types), "three_d" (all 3D types), "first_last_frame", "transcription". Omit for all models.')
15
15
  },
16
16
  async ({ type }) => {
17
17
  const path = type ? `/v1/models?type=${encodeURIComponent(type)}` : '/v1/models';