@kolbo/mcp 1.5.6 → 1.6.8
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 +2 -0
- package/package.json +1 -1
- package/src/client.js +4 -0
- package/src/index.js +2 -0
- package/src/tools/app_builder.js +253 -0
- package/src/tools/generate.js +175 -47
- package/src/tools/models.js +57 -1
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
package/src/client.js
CHANGED
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,253 @@
|
|
|
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.put(
|
|
123
|
+
`/app-builder/generation/${encodeURIComponent(session_id)}/${encodeURIComponent(generation_id)}`,
|
|
124
|
+
{ editPrompt: edit_prompt }
|
|
125
|
+
);
|
|
126
|
+
const status = await pollBuildStatus(client, session_id);
|
|
127
|
+
return {
|
|
128
|
+
content: [{
|
|
129
|
+
type: 'text',
|
|
130
|
+
text: JSON.stringify({
|
|
131
|
+
session_id,
|
|
132
|
+
build_status: status.buildStatus,
|
|
133
|
+
deployment_url: status.deploymentUrl || null,
|
|
134
|
+
app_name: status.appName || null
|
|
135
|
+
}, null, 2)
|
|
136
|
+
}]
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// ─── app_builder_get_build_status ──────────────────────────────────────────
|
|
142
|
+
server.tool(
|
|
143
|
+
'app_builder_get_build_status',
|
|
144
|
+
'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.',
|
|
145
|
+
{
|
|
146
|
+
session_id: z.string().describe('Session ID to check.')
|
|
147
|
+
},
|
|
148
|
+
async ({ session_id }) => {
|
|
149
|
+
const result = await client.get(`/app-builder/${encodeURIComponent(session_id)}/build-status`);
|
|
150
|
+
return {
|
|
151
|
+
content: [{
|
|
152
|
+
type: 'text',
|
|
153
|
+
text: JSON.stringify({
|
|
154
|
+
build_status: result.buildStatus,
|
|
155
|
+
deployment_url: result.deploymentUrl || null,
|
|
156
|
+
deployed_at: result.deployedAt || null
|
|
157
|
+
}, null, 2)
|
|
158
|
+
}]
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// ─── app_builder_get_session ───────────────────────────────────────────────
|
|
164
|
+
server.tool(
|
|
165
|
+
'app_builder_get_session',
|
|
166
|
+
'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.',
|
|
167
|
+
{
|
|
168
|
+
session_id: z.string().describe('Session ID to retrieve.')
|
|
169
|
+
},
|
|
170
|
+
async ({ session_id }) => {
|
|
171
|
+
const res = await client.get(`/app-builder/session/${encodeURIComponent(session_id)}`);
|
|
172
|
+
const session = res.data || res;
|
|
173
|
+
return {
|
|
174
|
+
content: [{
|
|
175
|
+
type: 'text',
|
|
176
|
+
text: JSON.stringify({
|
|
177
|
+
session_id: session._id,
|
|
178
|
+
name: session.name,
|
|
179
|
+
build_status: session.buildStatus,
|
|
180
|
+
deployment_url: session.deploymentUrl || null,
|
|
181
|
+
github_repo_url: session.githubRepoUrl || null,
|
|
182
|
+
supabase_url: session.supabaseUrl || null,
|
|
183
|
+
supabase_anon_key: session.supabaseAnonKey || null,
|
|
184
|
+
created_at: session.createdAt
|
|
185
|
+
}, null, 2)
|
|
186
|
+
}]
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// ─── app_builder_list_sessions ─────────────────────────────────────────────
|
|
192
|
+
server.tool(
|
|
193
|
+
'app_builder_list_sessions',
|
|
194
|
+
'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.',
|
|
195
|
+
{
|
|
196
|
+
project_id: z.string().describe('Kolbo project ID. Use app_builder_list_projects to find it.')
|
|
197
|
+
},
|
|
198
|
+
async ({ project_id }) => {
|
|
199
|
+
const res = await client.get(`/app-builder/sessions/${encodeURIComponent(project_id)}`);
|
|
200
|
+
const sessions = (Array.isArray(res) ? res : (res.data || [])).map(s => ({
|
|
201
|
+
session_id: s._id,
|
|
202
|
+
name: s.name,
|
|
203
|
+
build_status: s.buildStatus,
|
|
204
|
+
deployment_url: s.deploymentUrl || null,
|
|
205
|
+
created_at: s.createdAt
|
|
206
|
+
}));
|
|
207
|
+
return {
|
|
208
|
+
content: [{ type: 'text', text: JSON.stringify(sessions, null, 2) }]
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// ─── app_builder_list_generations ──────────────────────────────────────────
|
|
214
|
+
server.tool(
|
|
215
|
+
'app_builder_list_generations',
|
|
216
|
+
'List all generations for an App Builder session, newest first. Use this to find the current generation_id before calling app_builder_edit_app.',
|
|
217
|
+
{
|
|
218
|
+
session_id: z.string().describe('Session ID to list generations for.')
|
|
219
|
+
},
|
|
220
|
+
async ({ session_id }) => {
|
|
221
|
+
const res = await client.get(`/app-builder/generations/${encodeURIComponent(session_id)}`);
|
|
222
|
+
const generations = (Array.isArray(res) ? res : (res.data || [])).map(g => ({
|
|
223
|
+
generation_id: g._id,
|
|
224
|
+
user_prompt: g.userPrompt || g.editPrompt || '',
|
|
225
|
+
build_status: g.buildStatus,
|
|
226
|
+
created_at: g.createdAt
|
|
227
|
+
}));
|
|
228
|
+
return {
|
|
229
|
+
content: [{ type: 'text', text: JSON.stringify(generations, null, 2) }]
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// ─── app_builder_delete_session ────────────────────────────────────────────
|
|
235
|
+
server.tool(
|
|
236
|
+
'app_builder_delete_session',
|
|
237
|
+
'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.',
|
|
238
|
+
{
|
|
239
|
+
session_id: z.string().describe('Session ID to permanently delete. This cannot be undone.')
|
|
240
|
+
},
|
|
241
|
+
async ({ session_id }) => {
|
|
242
|
+
await client.delete(`/app-builder/session/${encodeURIComponent(session_id)}`);
|
|
243
|
+
return {
|
|
244
|
+
content: [{
|
|
245
|
+
type: 'text',
|
|
246
|
+
text: JSON.stringify({ success: true }, null, 2)
|
|
247
|
+
}]
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = { registerAppBuilderTools };
|
package/src/tools/generate.js
CHANGED
|
@@ -19,15 +19,17 @@ function registerGenerateTools(server, client) {
|
|
|
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
|
-
reference_images: z.array(z.string()).optional().describe('Array of image URLs used
|
|
23
|
-
visual_dna_ids: z.array(z.string()).optional().describe('
|
|
22
|
+
reference_images: z.array(z.string()).optional().describe('STYLE/COMPOSITION inspiration only — does NOT embed reference pixels. Array of image URLs used to guide the look-and-feel of a brand-new generation. The model interprets the references and regenerates approximations conditioned on them. It will NOT copy pixels from these images into the output. To embed a specific logo, icon, watermark, or asset pixel-accurately, use generate_image_edit with the asset in source_images. To EDIT an existing image, also use generate_image_edit.'),
|
|
23
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Visual DNA profile IDs (from create_visual_dna / list_visual_dnas) for character / style / product / scene consistency. How DNA works: the server fetches the DNA\'s reference images AND always injects its `description` field into the prompt as plaintext (this is by design — the description carries the identity signal, independent of enhance_prompt). Practical implication: do NOT also write physical descriptors of the same subject in your own prompt — they will compete with the DNA description text. For pixel-accurate face anchoring of a specific person, prefer passing the DNA\'s reference image directly via source_images on generate_image_edit and OMIT visual_dna_ids. visual_dna_ids is best for style / scene / product DNAs and for soft consistency across a set.'),
|
|
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 resolution_multipliers 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, {
|
|
@@ -41,7 +43,8 @@ function registerGenerateTools(server, client) {
|
|
|
41
43
|
text: JSON.stringify({
|
|
42
44
|
urls: result.result.urls,
|
|
43
45
|
model: result.result.model,
|
|
44
|
-
prompt_used: result.result.prompt_used
|
|
46
|
+
prompt_used: result.result.prompt_used,
|
|
47
|
+
_followup_hint: 'If the user asks to edit/change/modify this image next, pass urls[0] to generate_image_edit (free-form edits) or edit_image (upscale/reframe/removebg/enhance_skin/magic_edit). Do NOT call generate_image again.'
|
|
45
48
|
}, null, 2)
|
|
46
49
|
}]
|
|
47
50
|
};
|
|
@@ -55,23 +58,28 @@ function registerGenerateTools(server, client) {
|
|
|
55
58
|
{
|
|
56
59
|
prompt: z.string().describe('Description of the edit to apply (e.g., "remove the background", "change the sky to sunset")'),
|
|
57
60
|
model: z.string().optional().describe('Model identifier. Use list_models type="image_editing" to see options. Omit for Smart Select.'),
|
|
58
|
-
source_images: z.array(z.string()).describe('Array of source image URLs
|
|
61
|
+
source_images: z.array(z.string()).describe('PIXEL-ACCURATE compositing. Array of source image URLs whose pixel content is composited into the output. Three modes the model auto-detects from input shape: (1) Single image → edit/transform that image. (2) Multiple images, one base + others → composite the others into the base. (3) Multiple images with no clear base → generate a new scene that pixel-accurately embeds the supplied images at positions described in the prompt. Mode 3 is the canonical pattern for thumbnails / branded compositions where exact-pixel logo + face fidelity matter. Refer to source images in the prompt by ordinal position ("FIRST source image", "SECOND source image"). Add "composite AS-IS, do not redraw or restyle" to lock pixels.'),
|
|
59
62
|
aspect_ratio: z.string().optional().describe('Output aspect ratio (e.g., "1:1", "16:9", "9:16"). Default: "1:1"'),
|
|
60
63
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: true'),
|
|
61
64
|
num_images: z.number().optional().describe('Number of output images. Default: 1'),
|
|
62
|
-
visual_dna_ids: z.array(z.string()).optional().describe('
|
|
65
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Visual DNA profile IDs for character / style / product consistency. How DNA works: the server fetches the DNA\'s reference images AND always injects its `description` field into the prompt as plaintext (by design — independent of enhance_prompt). For pixel-accurate face anchoring of a specific person on this tool, the PREFERRED pattern is to pass the face photo directly via source_images and OMIT visual_dna_ids — that way the face pixels anchor the output and no description text competes. Do NOT pass visual_dna_ids if source_images already contains the same person\'s face (face averaging). visual_dna_ids is best here for style / product DNAs.'),
|
|
63
66
|
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')
|
|
67
|
+
enable_web_search: z.boolean().optional().describe('Enable web-search grounding. Default: false'),
|
|
68
|
+
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
69
|
},
|
|
66
|
-
async ({ prompt, model, source_images, aspect_ratio, enhance_prompt, num_images, visual_dna_ids, moodboard_id, enable_web_search }) => {
|
|
70
|
+
async ({ prompt, model, source_images, aspect_ratio, enhance_prompt, num_images, visual_dna_ids, moodboard_id, enable_web_search, resolution }) => {
|
|
67
71
|
const gen = await client.post('/v1/generate/image-edit', {
|
|
68
72
|
prompt, model, source_images, aspect_ratio, enhance_prompt, num_images,
|
|
69
|
-
visual_dna_ids, moodboard_id, enable_web_search
|
|
73
|
+
visual_dna_ids, moodboard_id, enable_web_search, resolution
|
|
70
74
|
});
|
|
71
75
|
|
|
76
|
+
// Multi-source compositing or DNA-anchored edits routinely exceed 120s
|
|
77
|
+
// server-side. Extend the polling window in those cases to avoid forcing
|
|
78
|
+
// every call into the timeout-and-recover path via get_generation_status.
|
|
79
|
+
const heavy = (source_images && source_images.length > 1) || (visual_dna_ids && visual_dna_ids.length > 0);
|
|
72
80
|
const result = await pollUntilDone(client, gen.generation_id, {
|
|
73
81
|
interval: (gen.poll_interval_hint || 3) * 1000,
|
|
74
|
-
timeout: 120000
|
|
82
|
+
timeout: heavy ? 240000 : 120000
|
|
75
83
|
});
|
|
76
84
|
|
|
77
85
|
return {
|
|
@@ -80,7 +88,8 @@ function registerGenerateTools(server, client) {
|
|
|
80
88
|
text: JSON.stringify({
|
|
81
89
|
urls: result.result.urls,
|
|
82
90
|
model: result.result.model,
|
|
83
|
-
prompt_used: result.result.prompt_used
|
|
91
|
+
prompt_used: result.result.prompt_used,
|
|
92
|
+
_followup_hint: 'If the user asks for another edit on this output, pass urls[0] back into generate_image_edit as source_images. For targeted ops (upscale/reframe/removebg/enhance_skin) use edit_image instead. Do NOT call generate_image from scratch.'
|
|
84
93
|
}, null, 2)
|
|
85
94
|
}]
|
|
86
95
|
};
|
|
@@ -102,12 +111,13 @@ function registerGenerateTools(server, client) {
|
|
|
102
111
|
reference_images: z.array(z.string()).optional().describe('Array of reference image URLs to guide style/composition of every scene.'),
|
|
103
112
|
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
113
|
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.')
|
|
114
|
+
moodboard_ids: z.array(z.string()).optional().describe('Multiple moodboard IDs when blending styles. Prefer `moodboard_id` for single moodboards.'),
|
|
115
|
+
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
116
|
},
|
|
107
|
-
async ({ prompt, scene_count, model, aspect_ratio, workflow_type, duration, enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids }) => {
|
|
117
|
+
async ({ prompt, scene_count, model, aspect_ratio, workflow_type, duration, enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution }) => {
|
|
108
118
|
const gen = await client.post('/v1/generate/creative-director', {
|
|
109
119
|
prompt, scene_count, model, aspect_ratio, workflow_type, duration,
|
|
110
|
-
enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids
|
|
120
|
+
enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution
|
|
111
121
|
});
|
|
112
122
|
|
|
113
123
|
const result = await pollUntilDone(client, gen.generation_id, {
|
|
@@ -131,7 +141,8 @@ function registerGenerateTools(server, client) {
|
|
|
131
141
|
text: JSON.stringify({
|
|
132
142
|
scenes,
|
|
133
143
|
total_scenes: result.scenes?.length || 0,
|
|
134
|
-
completed_scenes: scenes.length
|
|
144
|
+
completed_scenes: scenes.length,
|
|
145
|
+
_followup_hint: 'Each scene is a separate asset. If the user asks to edit one scene, find that scene by scene_number/title and pass its image_urls[0] (or video_urls[0]) to generate_image_edit / edit_image / edit_video / generate_video_from_video. Do NOT re-run generate_creative_director unless the user explicitly wants a brand-new set.'
|
|
135
146
|
}, null, 2)
|
|
136
147
|
}]
|
|
137
148
|
};
|
|
@@ -139,9 +150,13 @@ function registerGenerateTools(server, client) {
|
|
|
139
150
|
);
|
|
140
151
|
|
|
141
152
|
// ─── generate_video ────────────────────────────────────────
|
|
153
|
+
// NOTE: text-to-video does NOT support Visual DNA — the textToVideoGeneration
|
|
154
|
+
// controller in kolbo-api never reads visualDnaIds. For character-consistent
|
|
155
|
+
// video, use generate_elements (which DOES honor visual_dna_ids) or animate a
|
|
156
|
+
// DNA-locked still via generate_video_from_image.
|
|
142
157
|
server.tool(
|
|
143
158
|
'generate_video',
|
|
144
|
-
'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
|
|
159
|
+
'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 reference images (for style/composition guidance). Does NOT support Visual DNA — for character-consistent video use generate_elements or animate a DNA-locked still via generate_video_from_image. Returns the final video URL when complete.',
|
|
145
160
|
{
|
|
146
161
|
prompt: z.string().describe('Text description of the video to generate'),
|
|
147
162
|
model: z.string().optional().describe('Model identifier. Use list_models type="text_to_video" to see options. Check supported_durations and supported_aspect_ratios.'),
|
|
@@ -149,11 +164,12 @@ function registerGenerateTools(server, client) {
|
|
|
149
164
|
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
165
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
151
166
|
reference_images: z.array(z.string()).optional().describe('Array of image URLs used as visual references (style / composition / subject).'),
|
|
152
|
-
|
|
167
|
+
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 resolution_multipliers to predict cost.'),
|
|
168
|
+
preset_id: z.string().optional().describe('Preset ID from list_presets type="video" to apply a saved motion/style preset to this generation.')
|
|
153
169
|
},
|
|
154
|
-
async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images,
|
|
170
|
+
async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id }) => {
|
|
155
171
|
const gen = await client.post('/v1/generate/video', {
|
|
156
|
-
prompt, model, aspect_ratio, duration, enhance_prompt, reference_images,
|
|
172
|
+
prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id
|
|
157
173
|
});
|
|
158
174
|
|
|
159
175
|
const result = await pollUntilDone(client, gen.generation_id, {
|
|
@@ -169,7 +185,8 @@ function registerGenerateTools(server, client) {
|
|
|
169
185
|
model: result.result.model,
|
|
170
186
|
duration: result.result.duration,
|
|
171
187
|
thumbnail_url: result.result.thumbnail_url,
|
|
172
|
-
prompt_used: result.result.prompt_used
|
|
188
|
+
prompt_used: result.result.prompt_used,
|
|
189
|
+
_followup_hint: 'If the user asks to edit/restyle/extend this video next, pass urls[0] to edit_video (upscale/reframe/face_swap/extend/generate_audio/lipsync/magic_edit) or generate_video_from_video (restyle). Do NOT call generate_video from scratch.'
|
|
173
190
|
}, null, 2)
|
|
174
191
|
}]
|
|
175
192
|
};
|
|
@@ -187,11 +204,12 @@ function registerGenerateTools(server, client) {
|
|
|
187
204
|
aspect_ratio: z.string().optional().describe('Output aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"'),
|
|
188
205
|
duration: z.number().optional().describe('Duration in seconds. Must be a value the chosen model supports. Default: 5'),
|
|
189
206
|
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.')
|
|
207
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to maintain consistency with prior characters / styles.'),
|
|
208
|
+
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 resolution_multipliers to predict cost.')
|
|
191
209
|
},
|
|
192
|
-
async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids }) => {
|
|
210
|
+
async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution }) => {
|
|
193
211
|
const gen = await client.post('/v1/generate/video/from-image', {
|
|
194
|
-
image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids
|
|
212
|
+
image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution
|
|
195
213
|
});
|
|
196
214
|
|
|
197
215
|
const result = await pollUntilDone(client, gen.generation_id, {
|
|
@@ -206,7 +224,8 @@ function registerGenerateTools(server, client) {
|
|
|
206
224
|
urls: result.result.urls,
|
|
207
225
|
model: result.result.model,
|
|
208
226
|
duration: result.result.duration,
|
|
209
|
-
thumbnail_url: result.result.thumbnail_url
|
|
227
|
+
thumbnail_url: result.result.thumbnail_url,
|
|
228
|
+
_followup_hint: 'If the user asks to edit/restyle/extend this video next, pass urls[0] to edit_video or generate_video_from_video. Do NOT re-run generate_video_from_image unless they want a fresh animation from a different source image.'
|
|
210
229
|
}, null, 2)
|
|
211
230
|
}]
|
|
212
231
|
};
|
|
@@ -224,11 +243,12 @@ function registerGenerateTools(server, client) {
|
|
|
224
243
|
instrumental: z.boolean().optional().describe('Generate instrumental only, no vocals. Default: false'),
|
|
225
244
|
lyrics: z.string().optional().describe('Custom lyrics for the song. If omitted, lyrics are generated automatically from the prompt unless instrumental is true.'),
|
|
226
245
|
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')
|
|
246
|
+
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
247
|
+
preset_id: z.string().optional().describe('Preset ID from list_presets type="music" to apply a saved music style preset.')
|
|
228
248
|
},
|
|
229
|
-
async ({ prompt, model, style, instrumental, lyrics, vocal_gender, enhance_prompt }) => {
|
|
249
|
+
async ({ prompt, model, style, instrumental, lyrics, vocal_gender, enhance_prompt, preset_id }) => {
|
|
230
250
|
const gen = await client.post('/v1/generate/music', {
|
|
231
|
-
prompt, model, style, instrumental, lyrics, vocal_gender, enhance_prompt
|
|
251
|
+
prompt, model, style, instrumental, lyrics, vocal_gender, enhance_prompt, preset_id
|
|
232
252
|
});
|
|
233
253
|
|
|
234
254
|
const result = await pollUntilDone(client, gen.generation_id, {
|
|
@@ -290,11 +310,12 @@ function registerGenerateTools(server, client) {
|
|
|
290
310
|
{
|
|
291
311
|
prompt: z.string().describe('Text description of the sound effect (e.g., "thunder clap with rain", "door creaking open", "futuristic UI beep")'),
|
|
292
312
|
model: z.string().optional().describe('Model identifier. Use list_models type="text_to_sound" to see options. Default: elevenlabs-sound-effects-v1'),
|
|
293
|
-
duration: z.number().optional().describe('Duration in seconds. Omit for automatic duration.')
|
|
313
|
+
duration: z.number().optional().describe('Duration in seconds. Omit for automatic duration.'),
|
|
314
|
+
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
315
|
},
|
|
295
|
-
async ({ prompt, model, duration }) => {
|
|
316
|
+
async ({ prompt, model, duration, prompt_influence }) => {
|
|
296
317
|
const gen = await client.post('/v1/generate/sound', {
|
|
297
|
-
prompt, model, duration
|
|
318
|
+
prompt, model, duration, prompt_influence
|
|
298
319
|
});
|
|
299
320
|
|
|
300
321
|
const result = await pollUntilDone(client, gen.generation_id, {
|
|
@@ -377,20 +398,23 @@ function registerGenerateTools(server, client) {
|
|
|
377
398
|
// ─── generate_elements ─────────────────────────────────────
|
|
378
399
|
server.tool(
|
|
379
400
|
'generate_elements',
|
|
380
|
-
'Generate a video from reference elements (images and/or
|
|
401
|
+
'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 elements_max_images / elements_max_videos / elements_max_audio 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
402
|
{
|
|
382
403
|
prompt: z.string().describe('Text description of the desired video / animation'),
|
|
383
|
-
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.). 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.).
|
|
404
|
+
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 elements_max_images / elements_max_videos / elements_max_audio on the model. Omit for Smart Select.'),
|
|
405
|
+
reference_images: z.array(z.string()).optional().describe('Array of public image URLs used as reference elements (product shots, character references, etc.). Check elements_max_images on the chosen model — pass at most that many URLs.'),
|
|
406
|
+
reference_videos: z.array(z.string()).optional().describe('Array of reference video URLs for models that accept video inputs (elements_max_videos > 0). Check elements_max_videos on the chosen model from list_models before passing.'),
|
|
407
|
+
audio_url: z.string().optional().describe('URL of a reference audio track for models that accept audio inputs (elements_max_audio > 0). Check elements_max_audio on the chosen model from list_models before passing.'),
|
|
385
408
|
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
409
|
duration: z.number().optional().describe('Duration in seconds. Default: 5'),
|
|
387
410
|
aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"'),
|
|
388
411
|
motion: z.string().optional().describe('Motion style / intensity hint (optional)'),
|
|
389
412
|
preset_id: z.string().optional().describe('Preset ID from list_presets type="video" (optional)'),
|
|
390
413
|
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.')
|
|
414
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for character/style consistency across outputs.'),
|
|
415
|
+
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.')
|
|
392
416
|
},
|
|
393
|
-
async ({ prompt, model, reference_images, files, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids }) => {
|
|
417
|
+
async ({ prompt, model, reference_images, reference_videos, audio_url, files, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution }) => {
|
|
394
418
|
if (!prompt) throw new Error('prompt is required');
|
|
395
419
|
|
|
396
420
|
let startResponse;
|
|
@@ -407,6 +431,9 @@ function registerGenerateTools(server, client) {
|
|
|
407
431
|
if (enhance_prompt !== undefined) form.append('enhance_prompt', String(enhance_prompt));
|
|
408
432
|
if (visual_dna_ids) form.append('visual_dna_ids', JSON.stringify(visual_dna_ids));
|
|
409
433
|
if (reference_images) form.append('reference_images', JSON.stringify(reference_images));
|
|
434
|
+
if (reference_videos) form.append('reference_videos', JSON.stringify(reference_videos));
|
|
435
|
+
if (audio_url) form.append('audio_url', audio_url);
|
|
436
|
+
if (resolution) form.append('resolution', resolution);
|
|
410
437
|
for (const f of resolved) {
|
|
411
438
|
form.append('files', f.buffer, { filename: f.filename, contentType: f.contentType });
|
|
412
439
|
}
|
|
@@ -414,7 +441,7 @@ function registerGenerateTools(server, client) {
|
|
|
414
441
|
} else {
|
|
415
442
|
// URL-only mode: plain JSON.
|
|
416
443
|
startResponse = await client.post('/v1/generate/elements', {
|
|
417
|
-
prompt, model, reference_images, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids
|
|
444
|
+
prompt, model, reference_images, reference_videos, audio_url, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution
|
|
418
445
|
});
|
|
419
446
|
}
|
|
420
447
|
|
|
@@ -451,9 +478,10 @@ function registerGenerateTools(server, client) {
|
|
|
451
478
|
duration: z.number().optional().describe('Duration in seconds. Default: 5'),
|
|
452
479
|
aspect_ratio: z.string().optional().describe('Aspect ratio (auto-detected from first frame if not provided). Default: "16:9"'),
|
|
453
480
|
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.')
|
|
481
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply.'),
|
|
482
|
+
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.')
|
|
455
483
|
},
|
|
456
|
-
async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids }) => {
|
|
484
|
+
async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids, resolution }) => {
|
|
457
485
|
const urlMode = first_frame_url && last_frame_url;
|
|
458
486
|
const fileMode = first_frame && last_frame;
|
|
459
487
|
if (!urlMode && !fileMode) {
|
|
@@ -478,10 +506,11 @@ function registerGenerateTools(server, client) {
|
|
|
478
506
|
if (aspect_ratio) form.append('aspect_ratio', aspect_ratio);
|
|
479
507
|
if (enhance_prompt !== undefined) form.append('enhance_prompt', String(enhance_prompt));
|
|
480
508
|
if (visual_dna_ids) form.append('visual_dna_ids', JSON.stringify(visual_dna_ids));
|
|
509
|
+
if (resolution) form.append('resolution', resolution);
|
|
481
510
|
startResponse = await client.postMultipart('/v1/generate/first-last-frame', form);
|
|
482
511
|
} else {
|
|
483
512
|
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
|
|
513
|
+
first_frame_url, last_frame_url, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids, resolution
|
|
485
514
|
});
|
|
486
515
|
}
|
|
487
516
|
|
|
@@ -577,17 +606,21 @@ function registerGenerateTools(server, client) {
|
|
|
577
606
|
// ─── generate_video_from_video ─────────────────────────────
|
|
578
607
|
server.tool(
|
|
579
608
|
'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 —
|
|
609
|
+
'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 max_images / max_videos / max_elements on the chosen model before generating. Pass reference_images for models with max_images > 0 (e.g. Kling O1/O3, Aleph, WAN VACE), reference_videos for models with max_videos > 1 (e.g. WAN 2.6 reference-to-video accepts up to 3), and elements for models with max_elements > 0. For animating a still image use generate_video_from_image instead. For text-only → video use generate_video.',
|
|
581
610
|
{
|
|
582
|
-
source_video: z.string().describe('URL or absolute local path to the source video to restyle'),
|
|
611
|
+
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
612
|
prompt: z.string().describe('Text description of the desired restyle / transformation'),
|
|
584
|
-
model: z.string().optional().describe('Model identifier. Use list_models type="video_to_video" to see options. Omit for Smart Select.'),
|
|
613
|
+
model: z.string().optional().describe('Model identifier. Use list_models type="video_to_video" to see options and check max_images / max_videos / max_elements per model. Omit for Smart Select.'),
|
|
585
614
|
aspect_ratio: z.string().optional().describe('Output aspect ratio. Default: matches source'),
|
|
586
615
|
duration: z.number().optional().describe('Duration in seconds (default: matches source)'),
|
|
587
616
|
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.')
|
|
617
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for character/style consistency.'),
|
|
618
|
+
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.'),
|
|
619
|
+
reference_images: z.array(z.string()).optional().describe('Array of reference image URLs for models that support additional image inputs (max_images > 0). Examples: character reference images for Kling O1/O3, style reference for Aleph/gen4_aleph, character image for WAN VACE video-edit. Check max_images on the model from list_models before passing.'),
|
|
620
|
+
reference_videos: z.array(z.string()).optional().describe('Array of additional reference video URLs for models that support multiple video inputs (max_videos > 1). Example: WAN 2.6 reference-to-video accepts 1–3 reference videos. Check max_videos on the model from list_models before passing.'),
|
|
621
|
+
elements: z.array(z.string()).optional().describe('Array of element image URLs for models with max_elements > 0. Elements are used as style or character reference assets alongside the main video. Check max_elements on the model from list_models before passing.')
|
|
589
622
|
},
|
|
590
|
-
async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids }) => {
|
|
623
|
+
async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, reference_images, reference_videos, elements }) => {
|
|
591
624
|
if (!source_video) throw new Error('source_video is required');
|
|
592
625
|
if (!prompt) throw new Error('prompt is required');
|
|
593
626
|
|
|
@@ -595,7 +628,8 @@ function registerGenerateTools(server, client) {
|
|
|
595
628
|
let startResponse;
|
|
596
629
|
if (isUrl) {
|
|
597
630
|
startResponse = await client.post('/v1/generate/video-from-video', {
|
|
598
|
-
video_url: source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids
|
|
631
|
+
video_url: source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution,
|
|
632
|
+
reference_images, reference_videos, elements
|
|
599
633
|
});
|
|
600
634
|
} else {
|
|
601
635
|
const resolved = await resolveToBuffer(source_video, 'video');
|
|
@@ -607,6 +641,10 @@ function registerGenerateTools(server, client) {
|
|
|
607
641
|
if (duration !== undefined) form.append('duration', String(duration));
|
|
608
642
|
if (enhance_prompt !== undefined) form.append('enhance_prompt', String(enhance_prompt));
|
|
609
643
|
if (visual_dna_ids) form.append('visual_dna_ids', JSON.stringify(visual_dna_ids));
|
|
644
|
+
if (resolution) form.append('resolution', resolution);
|
|
645
|
+
if (reference_images) form.append('reference_images', JSON.stringify(reference_images));
|
|
646
|
+
if (reference_videos) form.append('reference_videos', JSON.stringify(reference_videos));
|
|
647
|
+
if (elements) form.append('elements', JSON.stringify(elements));
|
|
610
648
|
startResponse = await client.postMultipart('/v1/generate/video-from-video', form);
|
|
611
649
|
}
|
|
612
650
|
|
|
@@ -720,6 +758,96 @@ function registerGenerateTools(server, client) {
|
|
|
720
758
|
};
|
|
721
759
|
}
|
|
722
760
|
);
|
|
761
|
+
// ─── edit_image ────────────────────────────────────────────
|
|
762
|
+
server.tool(
|
|
763
|
+
'edit_image',
|
|
764
|
+
'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.',
|
|
765
|
+
{
|
|
766
|
+
image_url: z.string().describe('URL of the source image to edit'),
|
|
767
|
+
operation: z.enum(['upscale', 'reframe', 'removebg', 'enhance_skin', 'magic_edit'])
|
|
768
|
+
.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)'),
|
|
769
|
+
model: z.string().optional().describe('Model identifier override. Omit to use the default model for the operation.'),
|
|
770
|
+
scale: z.number().optional().describe('Upscale factor: 2, 3, or 4. Only used when operation="upscale". Default: 2.'),
|
|
771
|
+
aspect_ratio: z.string().optional().describe('Target aspect ratio (e.g., "16:9", "9:16", "1:1"). Required for operation="reframe".'),
|
|
772
|
+
skin_strength: z.enum(['subtle', 'realistic', 'pimple', 'freckle']).optional()
|
|
773
|
+
.describe('Skin enhancement style. Only used when operation="enhance_skin". Default: "realistic".'),
|
|
774
|
+
prompt: z.string().optional().describe('Text instruction for the edit. Required for operation="magic_edit" (e.g., "add sunglasses", "change the sky to sunset").')
|
|
775
|
+
},
|
|
776
|
+
async ({ image_url, operation, model, scale, aspect_ratio, skin_strength, prompt }) => {
|
|
777
|
+
if (operation === 'magic_edit' && !prompt) throw new Error('prompt is required for magic_edit operation');
|
|
778
|
+
if (operation === 'reframe' && !aspect_ratio) throw new Error('aspect_ratio is required for reframe operation');
|
|
779
|
+
|
|
780
|
+
const gen = await client.post('/v1/edit/image', {
|
|
781
|
+
image_url, operation, model, scale, aspect_ratio, skin_strength, prompt
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
const result = await pollUntilDone(client, gen.generation_id, {
|
|
785
|
+
interval: (gen.poll_interval_hint || 5) * 1000,
|
|
786
|
+
timeout: 180000
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
content: [{
|
|
791
|
+
type: 'text',
|
|
792
|
+
text: JSON.stringify({
|
|
793
|
+
urls: result.result?.urls || [],
|
|
794
|
+
edit_type: result.result?.edit_type || null,
|
|
795
|
+
model: result.result?.model || null
|
|
796
|
+
}, null, 2)
|
|
797
|
+
}]
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
// ─── edit_video ────────────────────────────────────────────
|
|
803
|
+
server.tool(
|
|
804
|
+
'edit_video',
|
|
805
|
+
'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.',
|
|
806
|
+
{
|
|
807
|
+
video_url: z.string().describe('URL of the source video to edit'),
|
|
808
|
+
operation: z.enum(['upscale', 'reframe', 'generate_audio', 'remove_watermark', 'face_swap', 'extend', 'magic_edit', 'lipsync'])
|
|
809
|
+
.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)'),
|
|
810
|
+
model: z.string().optional().describe('Model identifier override. Omit to use the default model for the operation.'),
|
|
811
|
+
aspect_ratio: z.string().optional().describe('Target aspect ratio (e.g., "16:9", "9:16"). Required for operation="reframe".'),
|
|
812
|
+
scale: z.number().optional().describe('Upscale factor. Only used when operation="upscale".'),
|
|
813
|
+
prompt: z.string().optional().describe('Text prompt. Required for operation="magic_edit" and "generate_audio". Optional hint for "extend".'),
|
|
814
|
+
image_url: z.string().optional().describe('URL of the reference face image. Required for operation="face_swap".'),
|
|
815
|
+
audio_url: z.string().optional().describe('URL of the audio track to sync. Required for operation="lipsync".'),
|
|
816
|
+
duration: z.number().optional().describe('Seconds of video to generate. Required for operation="extend". Typical range: 1–20.'),
|
|
817
|
+
mode: z.string().optional().describe('Where to extend: "start" or "end". Only used when operation="extend". Default: "end".')
|
|
818
|
+
},
|
|
819
|
+
async ({ video_url, operation, model, aspect_ratio, scale, prompt, image_url, audio_url, duration, mode }) => {
|
|
820
|
+
if (operation === 'magic_edit' && !prompt) throw new Error('prompt is required for magic_edit');
|
|
821
|
+
if (operation === 'generate_audio' && !prompt) throw new Error('prompt is required for generate_audio');
|
|
822
|
+
if (operation === 'reframe' && !aspect_ratio) throw new Error('aspect_ratio is required for reframe');
|
|
823
|
+
if (operation === 'face_swap' && !image_url) throw new Error('image_url (reference face) is required for face_swap');
|
|
824
|
+
if (operation === 'lipsync' && !audio_url) throw new Error('audio_url is required for lipsync');
|
|
825
|
+
if (operation === 'extend' && !duration) throw new Error('duration is required for extend');
|
|
826
|
+
|
|
827
|
+
const gen = await client.post('/v1/edit/video', {
|
|
828
|
+
video_url, operation, model, aspect_ratio, scale, prompt,
|
|
829
|
+
image_url, audio_url, duration, mode
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
const result = await pollUntilDone(client, gen.generation_id, {
|
|
833
|
+
interval: (gen.poll_interval_hint || 8) * 1000,
|
|
834
|
+
timeout: 600000
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
return {
|
|
838
|
+
content: [{
|
|
839
|
+
type: 'text',
|
|
840
|
+
text: JSON.stringify({
|
|
841
|
+
urls: result.result?.urls || [],
|
|
842
|
+
download_url: result.result?.download_url || null,
|
|
843
|
+
edit_type: result.result?.edit_type || null,
|
|
844
|
+
duration: result.result?.duration || null,
|
|
845
|
+
model: result.result?.model || null
|
|
846
|
+
}, null, 2)
|
|
847
|
+
}]
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
);
|
|
723
851
|
}
|
|
724
852
|
|
|
725
853
|
module.exports = { registerGenerateTools };
|
package/src/tools/models.js
CHANGED
|
@@ -21,8 +21,64 @@ function registerModelTools(server, client) {
|
|
|
21
21
|
const withSummary = result.models.filter(m => m.summary && m.summary.trim() !== '');
|
|
22
22
|
const withoutSummary = result.models.filter(m => !m.summary || m.summary.trim() === '');
|
|
23
23
|
|
|
24
|
+
// Format the per-model spec line. The agent NEEDS this — without it,
|
|
25
|
+
// it has to guess `supported_resolutions`/`supported_durations` and
|
|
26
|
+
// either invents values (then the API silently substitutes) or asks
|
|
27
|
+
// the user to clarify what's only knowable from this list.
|
|
28
|
+
const formatSpecs = m => {
|
|
29
|
+
const parts = [];
|
|
30
|
+
|
|
31
|
+
if (Array.isArray(m.supported_resolutions) && m.supported_resolutions.length) {
|
|
32
|
+
const mult = m.resolution_multipliers || {};
|
|
33
|
+
parts.push(
|
|
34
|
+
'resolutions: ' +
|
|
35
|
+
m.supported_resolutions
|
|
36
|
+
.map(r => (mult[r] != null && mult[r] !== 1 ? `${r} (${mult[r]}×)` : r))
|
|
37
|
+
.join(' · ')
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(m.supported_durations) && m.supported_durations.length) {
|
|
42
|
+
const ds = m.supported_durations;
|
|
43
|
+
// Compact ranges like 4-15 if it's a contiguous run.
|
|
44
|
+
const sorted = [...ds].sort((a, b) => a - b);
|
|
45
|
+
const isRange = sorted.length > 2 && sorted.every((v, i) => i === 0 || v - sorted[i - 1] === 1);
|
|
46
|
+
parts.push(`durations: ${isRange ? `${sorted[0]}-${sorted[sorted.length - 1]}s` : sorted.join('/') + 's'}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (Array.isArray(m.supported_aspect_ratios) && m.supported_aspect_ratios.length) {
|
|
50
|
+
parts.push(`aspect: ${m.supported_aspect_ratios.join(', ')}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Elements-type caps (only show when at least one is non-zero)
|
|
54
|
+
const eImg = m.elements_max_images, eVid = m.elements_max_videos, eAud = m.elements_max_audio;
|
|
55
|
+
if ((eImg ?? 0) > 0 || (eVid ?? 0) > 0 || (eAud ?? 0) > 0) {
|
|
56
|
+
parts.push(`elements: ${eImg ?? 0} imgs / ${eVid ?? 0} vids / ${eAud ?? 0} audio`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Video-to-video / multi-input caps
|
|
60
|
+
const mImg = m.max_images, mVid = m.max_videos, mElm = m.max_elements;
|
|
61
|
+
if ((mImg ?? 0) > 0 || (mVid ?? 0) > 0 || (mElm ?? 0) > 0) {
|
|
62
|
+
parts.push(`refs: ${mImg ?? 0} imgs / ${mVid ?? 0} vids / ${mElm ?? 0} elms`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if ((m.max_visual_dna ?? 0) > 0) parts.push(`max_dna: ${m.max_visual_dna}`);
|
|
66
|
+
|
|
67
|
+
// Sound (only show when sound costs more or is generated natively)
|
|
68
|
+
if (m.sound_generation_type === 'native') {
|
|
69
|
+
const mult = m.sound_credit_multiplier && m.sound_credit_multiplier !== 1
|
|
70
|
+
? ` (${m.sound_credit_multiplier}×)`
|
|
71
|
+
: '';
|
|
72
|
+
parts.push(`sound: native${mult}${m.sound_enabled_by_default ? ' on-by-default' : ''}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (m.max_audio_duration != null) parts.push(`audio_max: ${m.max_audio_duration}s`);
|
|
76
|
+
|
|
77
|
+
return parts.length ? `\n ${parts.join(' | ')}` : '';
|
|
78
|
+
};
|
|
79
|
+
|
|
24
80
|
const formatModel = m =>
|
|
25
|
-
`${m.identifier} (${m.name}) - ${m.credit} credits${m.recommended ? ' [RECOMMENDED]' : ''}${m.new_model ? ' [NEW]' : ''}${m.summary ? ` — ${m.summary}` : ''}`;
|
|
81
|
+
`${m.identifier} (${m.name}) - ${m.credit} credits${m.recommended ? ' [RECOMMENDED]' : ''}${m.new_model ? ' [NEW]' : ''}${m.summary ? ` — ${m.summary}` : ''}${formatSpecs(m)}`;
|
|
26
82
|
|
|
27
83
|
const sections = [];
|
|
28
84
|
if (withSummary.length > 0) {
|