@kolbo/mcp 1.6.5 → 1.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/client.js +22 -4
- package/src/tools/_shared.js +32 -1
- package/src/tools/app_builder.js +1 -2
- package/src/tools/chat.js +2 -0
- package/src/tools/generate.js +56 -28
- package/src/tools/models.js +89 -1
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -198,12 +198,21 @@ class KolboClient {
|
|
|
198
198
|
|
|
199
199
|
async _doRequest(method, reqPath, body = null) {
|
|
200
200
|
const url = `${this.baseUrl}${reqPath}`;
|
|
201
|
+
const headers = {
|
|
202
|
+
'X-API-Key': this.apiKey,
|
|
203
|
+
'Content-Type': 'application/json'
|
|
204
|
+
};
|
|
205
|
+
// Stable per-app-launch identifier from the parent process (Kolbo Code
|
|
206
|
+
// sets this in the MCP env when spawning us). kolbo-api tags every
|
|
207
|
+
// CreditUsage record with it so the desktop UI and the get_session_usage
|
|
208
|
+
// tool can aggregate spend without enumerating individual generations.
|
|
209
|
+
const callerSessionId = process.env.KOLBO_CALLER_SESSION_ID;
|
|
210
|
+
if (callerSessionId) {
|
|
211
|
+
headers['X-Kolbo-Caller-Session-Id'] = callerSessionId;
|
|
212
|
+
}
|
|
201
213
|
const options = {
|
|
202
214
|
method,
|
|
203
|
-
headers
|
|
204
|
-
'X-API-Key': this.apiKey,
|
|
205
|
-
'Content-Type': 'application/json'
|
|
206
|
-
}
|
|
215
|
+
headers,
|
|
207
216
|
};
|
|
208
217
|
|
|
209
218
|
if (body) {
|
|
@@ -248,6 +257,10 @@ class KolboClient {
|
|
|
248
257
|
return this.request('GET', reqPath);
|
|
249
258
|
}
|
|
250
259
|
|
|
260
|
+
async put(reqPath, body = null) {
|
|
261
|
+
return this.request('PUT', reqPath, body);
|
|
262
|
+
}
|
|
263
|
+
|
|
251
264
|
async delete(reqPath) {
|
|
252
265
|
return this.request('DELETE', reqPath);
|
|
253
266
|
}
|
|
@@ -266,6 +279,11 @@ class KolboClient {
|
|
|
266
279
|
'X-API-Key': this.apiKey,
|
|
267
280
|
...formData.getHeaders()
|
|
268
281
|
};
|
|
282
|
+
// Same caller-session header as JSON requests — see _doRequest.
|
|
283
|
+
const callerSessionId = process.env.KOLBO_CALLER_SESSION_ID;
|
|
284
|
+
if (callerSessionId) {
|
|
285
|
+
headers['X-Kolbo-Caller-Session-Id'] = callerSessionId;
|
|
286
|
+
}
|
|
269
287
|
|
|
270
288
|
// Serialize form-data to a Buffer before passing to fetch(). Node's
|
|
271
289
|
// built-in fetch (undici) can't consume legacy Node.js streams from
|
package/src/tools/_shared.js
CHANGED
|
@@ -200,6 +200,36 @@ async function resolveToBuffer(source, kind, opts = {}) {
|
|
|
200
200
|
};
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Extract real, multiplier-adjusted credit cost from a polled getStatus
|
|
205
|
+
* response. kolbo-api returns `credits_used` (final number deducted) and
|
|
206
|
+
* `credits_breakdown` (per-CreditUsage detail) when the generation is
|
|
207
|
+
* complete. Returns `{}` when the API didn't include them so spreading
|
|
208
|
+
* the result into a tool's response object is a no-op (forward-compatible
|
|
209
|
+
* with old kolbo-api versions).
|
|
210
|
+
*
|
|
211
|
+
* Usage in every generation tool:
|
|
212
|
+
* return {
|
|
213
|
+
* content: [{ type: 'text', text: JSON.stringify({
|
|
214
|
+
* urls: result.result.urls,
|
|
215
|
+
* model: result.result.model,
|
|
216
|
+
* ...creditFields(result), // adds credits_used + credits_breakdown
|
|
217
|
+
* _followup_hint: '...',
|
|
218
|
+
* }, null, 2) }]
|
|
219
|
+
* };
|
|
220
|
+
*/
|
|
221
|
+
function creditFields(polledResult) {
|
|
222
|
+
if (!polledResult) return {};
|
|
223
|
+
const out = {};
|
|
224
|
+
if (typeof polledResult.credits_used === 'number') {
|
|
225
|
+
out.credits_used = polledResult.credits_used;
|
|
226
|
+
}
|
|
227
|
+
if (Array.isArray(polledResult.credits_breakdown) && polledResult.credits_breakdown.length) {
|
|
228
|
+
out.credits_breakdown = polledResult.credits_breakdown;
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
|
|
203
233
|
module.exports = {
|
|
204
234
|
MAX_FILE_BYTES,
|
|
205
235
|
VISUAL_DNA_MAX_BYTES,
|
|
@@ -208,5 +238,6 @@ module.exports = {
|
|
|
208
238
|
safeFetch,
|
|
209
239
|
guessFilename,
|
|
210
240
|
guessContentType,
|
|
211
|
-
resolveToBuffer
|
|
241
|
+
resolveToBuffer,
|
|
242
|
+
creditFields,
|
|
212
243
|
};
|
package/src/tools/app_builder.js
CHANGED
|
@@ -119,8 +119,7 @@ function registerAppBuilderTools(server, client) {
|
|
|
119
119
|
edit_prompt: z.string().describe('Natural language instruction describing the change to make.')
|
|
120
120
|
},
|
|
121
121
|
async ({ session_id, generation_id, edit_prompt }) => {
|
|
122
|
-
await client.
|
|
123
|
-
'PUT',
|
|
122
|
+
await client.put(
|
|
124
123
|
`/app-builder/generation/${encodeURIComponent(session_id)}/${encodeURIComponent(generation_id)}`,
|
|
125
124
|
{ editPrompt: edit_prompt }
|
|
126
125
|
);
|
package/src/tools/chat.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const { z } = require('zod');
|
|
7
7
|
const { pollUntilDone } = require('../polling');
|
|
8
|
+
const { creditFields } = require('./_shared');
|
|
8
9
|
|
|
9
10
|
function registerChatTools(server, client) {
|
|
10
11
|
// ─── chat_send_message ─────────────────────────────────────
|
|
@@ -49,6 +50,7 @@ function registerChatTools(server, client) {
|
|
|
49
50
|
content: [{
|
|
50
51
|
type: 'text',
|
|
51
52
|
text: JSON.stringify({
|
|
53
|
+
...creditFields(result),
|
|
52
54
|
session_id: gen.session_id,
|
|
53
55
|
message_id: gen.message_id,
|
|
54
56
|
model: r.model || gen.model,
|
package/src/tools/generate.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
const { z } = require('zod');
|
|
7
7
|
const FormData = require('form-data');
|
|
8
8
|
const { pollUntilDone } = require('../polling');
|
|
9
|
-
const { resolveToBuffer } = require('./_shared');
|
|
9
|
+
const { resolveToBuffer, creditFields } = require('./_shared');
|
|
10
10
|
|
|
11
11
|
function registerGenerateTools(server, client) {
|
|
12
12
|
// ─── generate_image ────────────────────────────────────────
|
|
@@ -19,11 +19,11 @@ 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
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
|
|
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
27
|
preset_id: z.string().optional().describe('Preset ID from list_presets type="image" to apply a saved style preset to this generation.')
|
|
28
28
|
},
|
|
29
29
|
async ({ prompt, model, aspect_ratio, enhance_prompt, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, preset_id }) => {
|
|
@@ -41,9 +41,11 @@ function registerGenerateTools(server, client) {
|
|
|
41
41
|
content: [{
|
|
42
42
|
type: 'text',
|
|
43
43
|
text: JSON.stringify({
|
|
44
|
+
...creditFields(result),
|
|
44
45
|
urls: result.result.urls,
|
|
45
46
|
model: result.result.model,
|
|
46
|
-
prompt_used: result.result.prompt_used
|
|
47
|
+
prompt_used: result.result.prompt_used,
|
|
48
|
+
_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.'
|
|
47
49
|
}, null, 2)
|
|
48
50
|
}]
|
|
49
51
|
};
|
|
@@ -57,11 +59,11 @@ function registerGenerateTools(server, client) {
|
|
|
57
59
|
{
|
|
58
60
|
prompt: z.string().describe('Description of the edit to apply (e.g., "remove the background", "change the sky to sunset")'),
|
|
59
61
|
model: z.string().optional().describe('Model identifier. Use list_models type="image_editing" to see options. Omit for Smart Select.'),
|
|
60
|
-
source_images: z.array(z.string()).describe('Array of source image URLs
|
|
62
|
+
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.'),
|
|
61
63
|
aspect_ratio: z.string().optional().describe('Output aspect ratio (e.g., "1:1", "16:9", "9:16"). Default: "1:1"'),
|
|
62
64
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: true'),
|
|
63
65
|
num_images: z.number().optional().describe('Number of output images. Default: 1'),
|
|
64
|
-
visual_dna_ids: z.array(z.string()).optional().describe('
|
|
66
|
+
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.'),
|
|
65
67
|
moodboard_id: z.string().optional().describe('Moodboard ID whose master_prompt and style_guide should be applied.'),
|
|
66
68
|
enable_web_search: z.boolean().optional().describe('Enable web-search grounding. Default: false'),
|
|
67
69
|
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.')
|
|
@@ -72,18 +74,24 @@ function registerGenerateTools(server, client) {
|
|
|
72
74
|
visual_dna_ids, moodboard_id, enable_web_search, resolution
|
|
73
75
|
});
|
|
74
76
|
|
|
77
|
+
// Multi-source compositing or DNA-anchored edits routinely exceed 120s
|
|
78
|
+
// server-side. Extend the polling window in those cases to avoid forcing
|
|
79
|
+
// every call into the timeout-and-recover path via get_generation_status.
|
|
80
|
+
const heavy = (source_images && source_images.length > 1) || (visual_dna_ids && visual_dna_ids.length > 0);
|
|
75
81
|
const result = await pollUntilDone(client, gen.generation_id, {
|
|
76
82
|
interval: (gen.poll_interval_hint || 3) * 1000,
|
|
77
|
-
timeout: 120000
|
|
83
|
+
timeout: heavy ? 240000 : 120000
|
|
78
84
|
});
|
|
79
85
|
|
|
80
86
|
return {
|
|
81
87
|
content: [{
|
|
82
88
|
type: 'text',
|
|
83
89
|
text: JSON.stringify({
|
|
90
|
+
...creditFields(result),
|
|
84
91
|
urls: result.result.urls,
|
|
85
92
|
model: result.result.model,
|
|
86
|
-
prompt_used: result.result.prompt_used
|
|
93
|
+
prompt_used: result.result.prompt_used,
|
|
94
|
+
_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.'
|
|
87
95
|
}, null, 2)
|
|
88
96
|
}]
|
|
89
97
|
};
|
|
@@ -133,9 +141,11 @@ function registerGenerateTools(server, client) {
|
|
|
133
141
|
content: [{
|
|
134
142
|
type: 'text',
|
|
135
143
|
text: JSON.stringify({
|
|
144
|
+
...creditFields(result),
|
|
136
145
|
scenes,
|
|
137
146
|
total_scenes: result.scenes?.length || 0,
|
|
138
|
-
completed_scenes: scenes.length
|
|
147
|
+
completed_scenes: scenes.length,
|
|
148
|
+
_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.'
|
|
139
149
|
}, null, 2)
|
|
140
150
|
}]
|
|
141
151
|
};
|
|
@@ -143,9 +153,13 @@ function registerGenerateTools(server, client) {
|
|
|
143
153
|
);
|
|
144
154
|
|
|
145
155
|
// ─── generate_video ────────────────────────────────────────
|
|
156
|
+
// NOTE: text-to-video does NOT support Visual DNA — the textToVideoGeneration
|
|
157
|
+
// controller in kolbo-api never reads visualDnaIds. For character-consistent
|
|
158
|
+
// video, use generate_elements (which DOES honor visual_dna_ids) or animate a
|
|
159
|
+
// DNA-locked still via generate_video_from_image.
|
|
146
160
|
server.tool(
|
|
147
161
|
'generate_video',
|
|
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
|
|
162
|
+
'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.',
|
|
149
163
|
{
|
|
150
164
|
prompt: z.string().describe('Text description of the video to generate'),
|
|
151
165
|
model: z.string().optional().describe('Model identifier. Use list_models type="text_to_video" to see options. Check supported_durations and supported_aspect_ratios.'),
|
|
@@ -153,13 +167,12 @@ function registerGenerateTools(server, client) {
|
|
|
153
167
|
duration: z.number().optional().describe('Duration in seconds. Must be a value the chosen model supports — check supported_durations from list_models. Default: 5'),
|
|
154
168
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
155
169
|
reference_images: z.array(z.string()).optional().describe('Array of image URLs used as visual references (style / composition / subject).'),
|
|
156
|
-
|
|
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.'),
|
|
170
|
+
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.'),
|
|
158
171
|
preset_id: z.string().optional().describe('Preset ID from list_presets type="video" to apply a saved motion/style preset to this generation.')
|
|
159
172
|
},
|
|
160
|
-
async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images,
|
|
173
|
+
async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id }) => {
|
|
161
174
|
const gen = await client.post('/v1/generate/video', {
|
|
162
|
-
prompt, model, aspect_ratio, duration, enhance_prompt, reference_images,
|
|
175
|
+
prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id
|
|
163
176
|
});
|
|
164
177
|
|
|
165
178
|
const result = await pollUntilDone(client, gen.generation_id, {
|
|
@@ -171,11 +184,13 @@ function registerGenerateTools(server, client) {
|
|
|
171
184
|
content: [{
|
|
172
185
|
type: 'text',
|
|
173
186
|
text: JSON.stringify({
|
|
187
|
+
...creditFields(result),
|
|
174
188
|
urls: result.result.urls,
|
|
175
189
|
model: result.result.model,
|
|
176
190
|
duration: result.result.duration,
|
|
177
191
|
thumbnail_url: result.result.thumbnail_url,
|
|
178
|
-
prompt_used: result.result.prompt_used
|
|
192
|
+
prompt_used: result.result.prompt_used,
|
|
193
|
+
_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.'
|
|
179
194
|
}, null, 2)
|
|
180
195
|
}]
|
|
181
196
|
};
|
|
@@ -194,7 +209,7 @@ function registerGenerateTools(server, client) {
|
|
|
194
209
|
duration: z.number().optional().describe('Duration in seconds. Must be a value the chosen model supports. Default: 5'),
|
|
195
210
|
enhance_prompt: z.boolean().optional().describe('Enhance the motion prompt. Default: true'),
|
|
196
211
|
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
|
|
212
|
+
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.')
|
|
198
213
|
},
|
|
199
214
|
async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution }) => {
|
|
200
215
|
const gen = await client.post('/v1/generate/video/from-image', {
|
|
@@ -210,10 +225,12 @@ function registerGenerateTools(server, client) {
|
|
|
210
225
|
content: [{
|
|
211
226
|
type: 'text',
|
|
212
227
|
text: JSON.stringify({
|
|
228
|
+
...creditFields(result),
|
|
213
229
|
urls: result.result.urls,
|
|
214
230
|
model: result.result.model,
|
|
215
231
|
duration: result.result.duration,
|
|
216
|
-
thumbnail_url: result.result.thumbnail_url
|
|
232
|
+
thumbnail_url: result.result.thumbnail_url,
|
|
233
|
+
_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.'
|
|
217
234
|
}, null, 2)
|
|
218
235
|
}]
|
|
219
236
|
};
|
|
@@ -248,6 +265,7 @@ function registerGenerateTools(server, client) {
|
|
|
248
265
|
content: [{
|
|
249
266
|
type: 'text',
|
|
250
267
|
text: JSON.stringify({
|
|
268
|
+
...creditFields(result),
|
|
251
269
|
urls: result.result.urls,
|
|
252
270
|
title: result.result.title,
|
|
253
271
|
duration: result.result.duration,
|
|
@@ -282,6 +300,7 @@ function registerGenerateTools(server, client) {
|
|
|
282
300
|
content: [{
|
|
283
301
|
type: 'text',
|
|
284
302
|
text: JSON.stringify({
|
|
303
|
+
...creditFields(result),
|
|
285
304
|
urls: result.result.urls,
|
|
286
305
|
voice: result.result.voice,
|
|
287
306
|
duration: result.result.duration
|
|
@@ -315,6 +334,7 @@ function registerGenerateTools(server, client) {
|
|
|
315
334
|
content: [{
|
|
316
335
|
type: 'text',
|
|
317
336
|
text: JSON.stringify({
|
|
337
|
+
...creditFields(result),
|
|
318
338
|
urls: result.result.urls,
|
|
319
339
|
duration: result.result.duration
|
|
320
340
|
}, null, 2)
|
|
@@ -386,13 +406,13 @@ function registerGenerateTools(server, client) {
|
|
|
386
406
|
// ─── generate_elements ─────────────────────────────────────
|
|
387
407
|
server.tool(
|
|
388
408
|
'generate_elements',
|
|
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
|
|
409
|
+
'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.',
|
|
390
410
|
{
|
|
391
411
|
prompt: z.string().describe('Text description of the desired video / animation'),
|
|
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
|
|
393
|
-
reference_images: z.array(z.string()).optional().describe('Array of public image URLs used as reference elements (product shots, character references, etc.). Check
|
|
394
|
-
reference_videos: z.array(z.string()).optional().describe('Array of reference video URLs for models that accept video inputs (
|
|
395
|
-
audio_url: z.string().optional().describe('URL of a reference audio track for models that accept audio inputs (
|
|
412
|
+
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.'),
|
|
413
|
+
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.'),
|
|
414
|
+
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.'),
|
|
415
|
+
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.'),
|
|
396
416
|
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.'),
|
|
397
417
|
duration: z.number().optional().describe('Duration in seconds. Default: 5'),
|
|
398
418
|
aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"'),
|
|
@@ -442,6 +462,7 @@ function registerGenerateTools(server, client) {
|
|
|
442
462
|
content: [{
|
|
443
463
|
type: 'text',
|
|
444
464
|
text: JSON.stringify({
|
|
465
|
+
...creditFields(result),
|
|
445
466
|
urls: result.result?.urls || [],
|
|
446
467
|
thumbnail_url: result.result?.thumbnail_url || null,
|
|
447
468
|
duration: result.result?.duration || null,
|
|
@@ -511,6 +532,7 @@ function registerGenerateTools(server, client) {
|
|
|
511
532
|
content: [{
|
|
512
533
|
type: 'text',
|
|
513
534
|
text: JSON.stringify({
|
|
535
|
+
...creditFields(result),
|
|
514
536
|
urls: result.result?.urls || [],
|
|
515
537
|
thumbnail_url: result.result?.thumbnail_url || null,
|
|
516
538
|
duration: result.result?.duration || null,
|
|
@@ -581,6 +603,7 @@ function registerGenerateTools(server, client) {
|
|
|
581
603
|
content: [{
|
|
582
604
|
type: 'text',
|
|
583
605
|
text: JSON.stringify({
|
|
606
|
+
...creditFields(result),
|
|
584
607
|
urls: result.result?.urls || [],
|
|
585
608
|
thumbnail_url: result.result?.thumbnail_url || null,
|
|
586
609
|
duration: result.result?.duration || null,
|
|
@@ -594,19 +617,19 @@ function registerGenerateTools(server, client) {
|
|
|
594
617
|
// ─── generate_video_from_video ─────────────────────────────
|
|
595
618
|
server.tool(
|
|
596
619
|
'generate_video_from_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
|
|
620
|
+
'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.',
|
|
598
621
|
{
|
|
599
622
|
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.'),
|
|
600
623
|
prompt: z.string().describe('Text description of the desired restyle / transformation'),
|
|
601
|
-
model: z.string().optional().describe('Model identifier. Use list_models type="video_to_video" to see options and check
|
|
624
|
+
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.'),
|
|
602
625
|
aspect_ratio: z.string().optional().describe('Output aspect ratio. Default: matches source'),
|
|
603
626
|
duration: z.number().optional().describe('Duration in seconds (default: matches source)'),
|
|
604
627
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
605
628
|
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for character/style consistency.'),
|
|
606
629
|
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 (
|
|
608
|
-
reference_videos: z.array(z.string()).optional().describe('Array of additional reference video URLs for models that support multiple video inputs (
|
|
609
|
-
elements: z.array(z.string()).optional().describe('Array of element image URLs for models with
|
|
630
|
+
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.'),
|
|
631
|
+
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.'),
|
|
632
|
+
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.')
|
|
610
633
|
},
|
|
611
634
|
async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, reference_images, reference_videos, elements }) => {
|
|
612
635
|
if (!source_video) throw new Error('source_video is required');
|
|
@@ -645,6 +668,7 @@ function registerGenerateTools(server, client) {
|
|
|
645
668
|
content: [{
|
|
646
669
|
type: 'text',
|
|
647
670
|
text: JSON.stringify({
|
|
671
|
+
...creditFields(result),
|
|
648
672
|
urls: result.result?.urls || [],
|
|
649
673
|
thumbnail_url: result.result?.thumbnail_url || null,
|
|
650
674
|
duration: result.result?.duration || null,
|
|
@@ -685,6 +709,7 @@ function registerGenerateTools(server, client) {
|
|
|
685
709
|
content: [{
|
|
686
710
|
type: 'text',
|
|
687
711
|
text: JSON.stringify({
|
|
712
|
+
...creditFields(result),
|
|
688
713
|
text: result.result?.text || '',
|
|
689
714
|
srt_url: result.result?.srt_url || null,
|
|
690
715
|
word_by_word_srt_url: result.result?.word_by_word_srt_url || null,
|
|
@@ -737,6 +762,7 @@ function registerGenerateTools(server, client) {
|
|
|
737
762
|
content: [{
|
|
738
763
|
type: 'text',
|
|
739
764
|
text: JSON.stringify({
|
|
765
|
+
...creditFields(result),
|
|
740
766
|
urls: result.result?.urls || [],
|
|
741
767
|
thumbnail_url: result.result?.thumbnail_url || null,
|
|
742
768
|
mode: result.result?.mode || null,
|
|
@@ -778,6 +804,7 @@ function registerGenerateTools(server, client) {
|
|
|
778
804
|
content: [{
|
|
779
805
|
type: 'text',
|
|
780
806
|
text: JSON.stringify({
|
|
807
|
+
...creditFields(result),
|
|
781
808
|
urls: result.result?.urls || [],
|
|
782
809
|
edit_type: result.result?.edit_type || null,
|
|
783
810
|
model: result.result?.model || null
|
|
@@ -826,6 +853,7 @@ function registerGenerateTools(server, client) {
|
|
|
826
853
|
content: [{
|
|
827
854
|
type: 'text',
|
|
828
855
|
text: JSON.stringify({
|
|
856
|
+
...creditFields(result),
|
|
829
857
|
urls: result.result?.urls || [],
|
|
830
858
|
download_url: result.result?.download_url || null,
|
|
831
859
|
edit_type: result.result?.edit_type || null,
|
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) {
|
|
@@ -57,6 +113,38 @@ function registerModelTools(server, client) {
|
|
|
57
113
|
};
|
|
58
114
|
}
|
|
59
115
|
);
|
|
116
|
+
|
|
117
|
+
// ─── get_session_usage ─────────────────────────────────────
|
|
118
|
+
// Real, multiplier-adjusted credit spend tagged with the caller's
|
|
119
|
+
// X-Kolbo-Caller-Session-Id (set automatically by the parent process —
|
|
120
|
+
// no need to pass it). Use this to give the user an honest "you've spent
|
|
121
|
+
// X credits in this app session" instead of estimating from base credits.
|
|
122
|
+
server.tool(
|
|
123
|
+
'get_session_usage',
|
|
124
|
+
'Fetch real, multiplier-adjusted credit spend for the current Kolbo Code app session. Use when the user asks "how much did I spend?" or before/after a large bulk job so you can quote actual cost (not an estimate from base credits). Returns total + per-tool breakdown + per-model breakdown + a recent list. The caller-session-id is forwarded automatically by the MCP HTTP client.',
|
|
125
|
+
{},
|
|
126
|
+
async () => {
|
|
127
|
+
try {
|
|
128
|
+
const r = await client.get('/credit-usage/by-caller-session');
|
|
129
|
+
// The endpoint returns { message, data: { total, count, by_tool, by_model, recent[] } }
|
|
130
|
+
return {
|
|
131
|
+
content: [{
|
|
132
|
+
type: 'text',
|
|
133
|
+
text: JSON.stringify(r.data || r, null, 2)
|
|
134
|
+
}]
|
|
135
|
+
};
|
|
136
|
+
} catch (err) {
|
|
137
|
+
// 400 from the endpoint means no caller-session-id was forwarded —
|
|
138
|
+
// surface a clear hint instead of a generic API error.
|
|
139
|
+
const hint = err?.status === 400
|
|
140
|
+
? 'No caller-session-id was forwarded. Ensure the parent process (Kolbo Code / desktop sidecar) sets KOLBO_CALLER_SESSION_ID in this MCP\'s env, or call again after at least one media generation has fired.'
|
|
141
|
+
: err?.message || 'Failed to fetch session usage';
|
|
142
|
+
return {
|
|
143
|
+
content: [{ type: 'text', text: JSON.stringify({ error: hint }, null, 2) }]
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
);
|
|
60
148
|
}
|
|
61
149
|
|
|
62
150
|
module.exports = { registerModelTools };
|