@kolbo/mcp 1.6.8 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/package.json +1 -1
- package/src/client.js +42 -12
- package/src/index.js +2 -0
- package/src/tools/_shared.js +32 -1
- package/src/tools/artifacts.js +60 -0
- package/src/tools/chat.js +2 -0
- package/src/tools/generate.js +55 -39
- package/src/tools/media.js +348 -10
- package/src/tools/models.js +134 -18
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ Just ask Claude naturally:
|
|
|
43
43
|
- *"What's in this image?"*
|
|
44
44
|
- *"Create a Visual DNA profile called 'Alex' from these images"*
|
|
45
45
|
|
|
46
|
-
## Available Tools (
|
|
46
|
+
## Available Tools (51)
|
|
47
47
|
|
|
48
48
|
**Generation**
|
|
49
49
|
| Tool | Description |
|
|
@@ -92,7 +92,28 @@ Every generation tool also accepts an optional `resolution` arg. Images use `"1K
|
|
|
92
92
|
| Tool | Description |
|
|
93
93
|
|------|-------------|
|
|
94
94
|
| `upload_media` | Upload a local file (or remote URL) → stable Kolbo CDN URL for reuse |
|
|
95
|
-
| `list_media` | Browse
|
|
95
|
+
| `list_media` | Browse media library — filter by `project_id`, `folder_id`, `type`, `category` (ai / uploaded / edited / favorites / training-lab), `source_type`, `sort`, `search`, pagination |
|
|
96
|
+
| `list_media_folders` | List the user's media folders (owned + shared) — discover `folder_id` values to pass to `list_media` |
|
|
97
|
+
| `create_media_folder` | Create a new folder (name, optional description / color / icon) |
|
|
98
|
+
| `update_media_folder` | Rename / recolor / re-icon a folder (owner only) |
|
|
99
|
+
| `delete_media_folder` | Soft-delete a folder (owner only; items remain in library) |
|
|
100
|
+
| `add_media_to_folder` | Add up to 500 media items to a folder (idempotent) |
|
|
101
|
+
| `remove_media_from_folder` | Remove media items from a folder |
|
|
102
|
+
| `share_media_folder` | Share a folder by user email (owner only) |
|
|
103
|
+
| `unshare_media_folder` | Revoke a user's access to a folder (owner only) |
|
|
104
|
+
| `favorite_media` | Mark a media item as favorited (idempotent) — pass `media_id` from `list_media` |
|
|
105
|
+
| `unfavorite_media` | Remove a media item from favorites (idempotent) — pass `media_id` from `list_media` |
|
|
106
|
+
| `get_media` | Fetch one media item's full details by id |
|
|
107
|
+
| `delete_media` | Soft-delete a media item (30-day trash) |
|
|
108
|
+
| `restore_media` | Restore a trashed item |
|
|
109
|
+
| `permanently_delete_media` | Hard-delete (NOT reversible — confirm with user first) |
|
|
110
|
+
| `move_media` | Re-assign a media item to a different project |
|
|
111
|
+
| `bulk_delete_media` | Soft-delete up to 1000 items in one call |
|
|
112
|
+
| `bulk_restore_media` | Restore up to 1000 trashed items |
|
|
113
|
+
| `bulk_permanently_delete_media` | Hard-delete up to 1000 (NOT reversible) |
|
|
114
|
+
| `bulk_move_media` | Move up to 1000 items to a project (atomic — all-or-nothing) |
|
|
115
|
+
| `move_folder_contents` | Move every item in a folder to a project |
|
|
116
|
+
| `get_media_stats` | Counts + storage bytes per type (optionally per project) |
|
|
96
117
|
|
|
97
118
|
**Discovery & Account**
|
|
98
119
|
| Tool | Description |
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -151,10 +151,12 @@ class KolboClient {
|
|
|
151
151
|
this.apiKey = this._envKey || this._readAuthStore();
|
|
152
152
|
|
|
153
153
|
if (!this.apiKey) {
|
|
154
|
+
// No key in env OR auth store. The Kolbo Code parent process should
|
|
155
|
+
// never spawn us in this state (it injects the key into env after the
|
|
156
|
+
// user signs in). If this fires, the parent will catch it via the
|
|
157
|
+
// [KOLBO_AUTH_MISSING] tag and surface the in-app sign-in flow.
|
|
154
158
|
throw new Error(
|
|
155
|
-
'Kolbo API key not found
|
|
156
|
-
'Fix: Run "kolbo auth login" in the terminal, then restart this editor.\n' +
|
|
157
|
-
'Or: Get an API key at https://app.kolbo.ai/developer and set KOLBO_API_KEY env var.'
|
|
159
|
+
'Kolbo API key not found. Sign in to Kolbo to continue. [KOLBO_AUTH_MISSING]'
|
|
158
160
|
);
|
|
159
161
|
}
|
|
160
162
|
}
|
|
@@ -198,12 +200,21 @@ class KolboClient {
|
|
|
198
200
|
|
|
199
201
|
async _doRequest(method, reqPath, body = null) {
|
|
200
202
|
const url = `${this.baseUrl}${reqPath}`;
|
|
203
|
+
const headers = {
|
|
204
|
+
'X-API-Key': this.apiKey,
|
|
205
|
+
'Content-Type': 'application/json'
|
|
206
|
+
};
|
|
207
|
+
// Stable per-app-launch identifier from the parent process (Kolbo Code
|
|
208
|
+
// sets this in the MCP env when spawning us). kolbo-api tags every
|
|
209
|
+
// CreditUsage record with it so the desktop UI and the get_session_usage
|
|
210
|
+
// tool can aggregate spend without enumerating individual generations.
|
|
211
|
+
const callerSessionId = process.env.KOLBO_CALLER_SESSION_ID;
|
|
212
|
+
if (callerSessionId) {
|
|
213
|
+
headers['X-Kolbo-Caller-Session-Id'] = callerSessionId;
|
|
214
|
+
}
|
|
201
215
|
const options = {
|
|
202
216
|
method,
|
|
203
|
-
headers
|
|
204
|
-
'X-API-Key': this.apiKey,
|
|
205
|
-
'Content-Type': 'application/json'
|
|
206
|
-
}
|
|
217
|
+
headers,
|
|
207
218
|
};
|
|
208
219
|
|
|
209
220
|
if (body) {
|
|
@@ -226,9 +237,15 @@ class KolboClient {
|
|
|
226
237
|
const code = data.code || null;
|
|
227
238
|
let fullMessage = code ? `${message} [${code}]` : message;
|
|
228
239
|
if (response.status === 401) {
|
|
229
|
-
// Tag the response so the retry logic in request() can see it
|
|
240
|
+
// Tag the response so the retry logic in request() can see it AND so
|
|
241
|
+
// the Kolbo Code parent process can intercept this error before the
|
|
242
|
+
// agent sees it — trigger the in-app reconnect flow, refresh the key,
|
|
243
|
+
// and transparently retry the tool call. Never instruct the user to
|
|
244
|
+
// open a terminal: most users run Kolbo Code as a desktop / web app
|
|
245
|
+
// and have no terminal context.
|
|
230
246
|
data._status = 401;
|
|
231
|
-
|
|
247
|
+
data._kolbo_auth_expired = true;
|
|
248
|
+
fullMessage = `${fullMessage} [KOLBO_AUTH_EXPIRED]`;
|
|
232
249
|
}
|
|
233
250
|
throw new KolboApiError(fullMessage, {
|
|
234
251
|
code,
|
|
@@ -252,8 +269,12 @@ class KolboClient {
|
|
|
252
269
|
return this.request('PUT', reqPath, body);
|
|
253
270
|
}
|
|
254
271
|
|
|
255
|
-
async
|
|
256
|
-
return this.request('
|
|
272
|
+
async patch(reqPath, body = null) {
|
|
273
|
+
return this.request('PATCH', reqPath, body);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async delete(reqPath, body = null) {
|
|
277
|
+
return this.request('DELETE', reqPath, body);
|
|
257
278
|
}
|
|
258
279
|
|
|
259
280
|
async postMultipart(reqPath, formData) {
|
|
@@ -270,6 +291,11 @@ class KolboClient {
|
|
|
270
291
|
'X-API-Key': this.apiKey,
|
|
271
292
|
...formData.getHeaders()
|
|
272
293
|
};
|
|
294
|
+
// Same caller-session header as JSON requests — see _doRequest.
|
|
295
|
+
const callerSessionId = process.env.KOLBO_CALLER_SESSION_ID;
|
|
296
|
+
if (callerSessionId) {
|
|
297
|
+
headers['X-Kolbo-Caller-Session-Id'] = callerSessionId;
|
|
298
|
+
}
|
|
273
299
|
|
|
274
300
|
// Serialize form-data to a Buffer before passing to fetch(). Node's
|
|
275
301
|
// built-in fetch (undici) can't consume legacy Node.js streams from
|
|
@@ -298,8 +324,12 @@ class KolboClient {
|
|
|
298
324
|
const code = data.code || null;
|
|
299
325
|
let fullMessage = code ? `${message} [${code}]` : message;
|
|
300
326
|
if (response.status === 401) {
|
|
327
|
+
// Multipart uploads: same auth-expired contract as _doRequest. The
|
|
328
|
+
// Kolbo Code parent process intercepts [KOLBO_AUTH_EXPIRED] and runs
|
|
329
|
+
// the in-app reconnect flow — no terminal command needed.
|
|
301
330
|
data._status = 401;
|
|
302
|
-
|
|
331
|
+
data._kolbo_auth_expired = true;
|
|
332
|
+
fullMessage = `${fullMessage} [KOLBO_AUTH_EXPIRED]`;
|
|
303
333
|
}
|
|
304
334
|
throw new KolboApiError(fullMessage, {
|
|
305
335
|
code,
|
package/src/index.js
CHANGED
|
@@ -67,6 +67,7 @@ const { registerMoodboardTools } = require('./tools/moodboards');
|
|
|
67
67
|
const { registerMediaTools } = require('./tools/media');
|
|
68
68
|
const { registerPresetTools } = require('./tools/presets');
|
|
69
69
|
const { registerAppBuilderTools } = require('./tools/app_builder');
|
|
70
|
+
const { registerArtifactTools } = require('./tools/artifacts');
|
|
70
71
|
|
|
71
72
|
async function main() {
|
|
72
73
|
const client = new KolboClient();
|
|
@@ -85,6 +86,7 @@ async function main() {
|
|
|
85
86
|
registerMediaTools(server, client);
|
|
86
87
|
registerPresetTools(server, client);
|
|
87
88
|
registerAppBuilderTools(server, client);
|
|
89
|
+
registerArtifactTools(server, client);
|
|
88
90
|
|
|
89
91
|
// Start the server with stdio transport
|
|
90
92
|
const transport = new StdioServerTransport();
|
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
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
|
|
8
|
+
function registerArtifactTools(server, client) {
|
|
9
|
+
// ─── publish_html_artifact ─────────────────────────────────────
|
|
10
|
+
server.tool(
|
|
11
|
+
'publish_html_artifact',
|
|
12
|
+
'Publish an HTML page (or SVG / Mermaid diagram) to kolbo.ai and return a public shareable URL. Use this when the user explicitly asks to share, publish, or deploy a built artifact so they can send the URL to someone. The content is hosted at https://sites.kolbo.ai/<slug>; the page is served with restrictive CSP (no fetch/XHR/form-action) so it cannot exfiltrate data. Identical content uploaded twice returns the same URL (server dedup).',
|
|
13
|
+
{
|
|
14
|
+
title: z.string().describe('Human-friendly title for the page (also used to generate the SEO slug). Keep under ~60 chars.'),
|
|
15
|
+
content: z.string().describe('The raw artifact body. For type="html" this is a full HTML document (DOCTYPE + html/head/body). For "svg" it is an <svg> document. For "mermaid" it is the Mermaid source text.'),
|
|
16
|
+
type: z.enum(['html', 'svg', 'mermaid']).optional().describe('Artifact type. Default: "html".'),
|
|
17
|
+
allow_js: z.boolean().optional().describe('Allow inline <script> execution on the published page. Default: false. Required for Tailwind JIT, Chart.js, Three.js, React-from-CDN etc.'),
|
|
18
|
+
},
|
|
19
|
+
async ({ title, content, type, allow_js }) => {
|
|
20
|
+
if (!title || !title.trim()) throw new Error('title is required');
|
|
21
|
+
if (typeof content !== 'string' || !content.length) throw new Error('content is required');
|
|
22
|
+
|
|
23
|
+
const result = await client.post('/artifact/quick-share', {
|
|
24
|
+
title: title.trim(),
|
|
25
|
+
content,
|
|
26
|
+
type: type || 'html',
|
|
27
|
+
allowJs: allow_js === true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const artifact = result?.data || {};
|
|
31
|
+
const slug = artifact.shareableSlug || artifact.shareToken;
|
|
32
|
+
// Compose env-correct URL. sites.kolbo.ai only resolves in prod; for
|
|
33
|
+
// dev/staging we serve straight from the kolbo-api host.
|
|
34
|
+
const apiBase = client.baseUrl || 'https://api.kolbo.ai/api';
|
|
35
|
+
const isProd = /(^|\/\/)api\.kolbo\.ai/i.test(apiBase);
|
|
36
|
+
let url;
|
|
37
|
+
if (isProd) {
|
|
38
|
+
url = artifact.siteUrl || (slug ? `https://sites.kolbo.ai/${slug}` : null);
|
|
39
|
+
}
|
|
40
|
+
if (!url && artifact.shareToken) {
|
|
41
|
+
url = `${apiBase}/shared-artifact-raw/${artifact.shareToken}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
content: [{
|
|
46
|
+
type: 'text',
|
|
47
|
+
text: JSON.stringify({
|
|
48
|
+
url,
|
|
49
|
+
shareToken: artifact.shareToken,
|
|
50
|
+
shareableSlug: slug,
|
|
51
|
+
duplicate: result?.duplicate === true,
|
|
52
|
+
title: artifact.title,
|
|
53
|
+
}),
|
|
54
|
+
}],
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { registerArtifactTools };
|
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 ────────────────────────────────────────
|
|
@@ -16,11 +16,11 @@ function registerGenerateTools(server, client) {
|
|
|
16
16
|
{
|
|
17
17
|
prompt: z.string().describe('Text description of the image to generate'),
|
|
18
18
|
model: z.string().optional().describe('Model identifier. Use list_models type="text_to_img" to see options. Omit for Smart Select.'),
|
|
19
|
-
aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "1:1", "16:9", "9:16"). Default: "1:1"'),
|
|
19
|
+
aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "1:1", "16:9", "9:16"). Must be a value present in the model\'s `supported_aspect_ratios` from list_models — pass an unsupported value and the API rejects. Default: "1:1"'),
|
|
20
20
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: true'),
|
|
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('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 (
|
|
21
|
+
num_images: z.number().optional().describe('Number of images to generate in one call. Default: 1. Note: some models (Midjourney etc.) have a fixed `images_per_request` and ignore this — check list_models.'),
|
|
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. **Cap: pass at most `max_reference_images` URLs from list_models for the chosen model — exceeding it is a deterministic 400.** 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. **Cap: pass at most `max_visual_dna` IDs from list_models — if the field is null/0 or `supports_visual_dna: false`, the model rejects DNA entirely (silently ignored in some paths).** 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). 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
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.'),
|
|
@@ -41,6 +41,7 @@ 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
47
|
prompt_used: result.result.prompt_used,
|
|
@@ -58,11 +59,11 @@ function registerGenerateTools(server, client) {
|
|
|
58
59
|
{
|
|
59
60
|
prompt: z.string().describe('Description of the edit to apply (e.g., "remove the background", "change the sky to sunset")'),
|
|
60
61
|
model: z.string().optional().describe('Model identifier. Use list_models type="image_editing" to see options. Omit for Smart Select.'),
|
|
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.'),
|
|
62
|
-
aspect_ratio: z.string().optional().describe('Output aspect ratio (e.g., "1:1", "16:9", "9:16"). Default: "1:1"'),
|
|
62
|
+
source_images: z.array(z.string()).describe('PIXEL-ACCURATE compositing. Array of source image URLs whose pixel content is composited into the output. **Cap: pass at most `max_reference_images` URLs from list_models for the chosen model — exceeding it is a deterministic 400.** 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") or use @image1/@image2 tags. Add "composite AS-IS, do not redraw or restyle" to lock pixels.'),
|
|
63
|
+
aspect_ratio: z.string().optional().describe('Output aspect ratio (e.g., "1:1", "16:9", "9:16"). Must be in the chosen model\'s `supported_aspect_ratios` from list_models. Default: "1:1"'),
|
|
63
64
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: true'),
|
|
64
65
|
num_images: z.number().optional().describe('Number of output images. Default: 1'),
|
|
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.'),
|
|
66
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Visual DNA profile IDs for character / style / product consistency. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model.** 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.'),
|
|
66
67
|
moodboard_id: z.string().optional().describe('Moodboard ID whose master_prompt and style_guide should be applied.'),
|
|
67
68
|
enable_web_search: z.boolean().optional().describe('Enable web-search grounding. Default: false'),
|
|
68
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.')
|
|
@@ -86,6 +87,7 @@ function registerGenerateTools(server, client) {
|
|
|
86
87
|
content: [{
|
|
87
88
|
type: 'text',
|
|
88
89
|
text: JSON.stringify({
|
|
90
|
+
...creditFields(result),
|
|
89
91
|
urls: result.result.urls,
|
|
90
92
|
model: result.result.model,
|
|
91
93
|
prompt_used: result.result.prompt_used,
|
|
@@ -104,12 +106,12 @@ function registerGenerateTools(server, client) {
|
|
|
104
106
|
prompt: z.string().describe('Creative brief or concept describing the full set of scenes to generate'),
|
|
105
107
|
scene_count: z.number().optional().describe('Number of scenes/images to generate, 1–8. Default: 4. Use this — NOT num_images — to control how many outputs are created.'),
|
|
106
108
|
model: z.string().optional().describe('Model identifier applied to every scene. Omit for Smart Select.'),
|
|
107
|
-
aspect_ratio: z.string().optional().describe('Aspect ratio applied to every scene (e.g., "1:1", "16:9", "9:16"). Default: "1:1"'),
|
|
109
|
+
aspect_ratio: z.string().optional().describe('Aspect ratio applied to every scene (e.g., "1:1", "16:9", "9:16"). Must be in the chosen model\'s `supported_aspect_ratios` from list_models. Default: "1:1"'),
|
|
108
110
|
workflow_type: z.string().optional().describe('"image" (default) or "video"'),
|
|
109
|
-
duration: z.number().optional().describe('Duration in seconds per scene (video mode only). E.g., 5 or 10.'),
|
|
111
|
+
duration: z.number().optional().describe('Duration in seconds per scene (video mode only). Must be a value in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration`. E.g., 5 or 10.'),
|
|
110
112
|
enhance_prompt: z.boolean().optional().describe('Enhance prompts per scene. Default: true'),
|
|
111
|
-
reference_images: z.array(z.string()).optional().describe('Array of reference image URLs to guide style/composition of every scene.'),
|
|
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.'),
|
|
113
|
+
reference_images: z.array(z.string()).optional().describe('Array of reference image URLs to guide style/composition of every scene. **Cap: pass at most `max_reference_images` URLs from list_models for the chosen model.**'),
|
|
114
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply consistently across every scene. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model.** This is the ideal way to keep a character or product looking the same in all scenes of a campaign.'),
|
|
113
115
|
moodboard_id: z.string().optional().describe('A single moodboard ID whose master_prompt and style_guide should shape every scene.'),
|
|
114
116
|
moodboard_ids: z.array(z.string()).optional().describe('Multiple moodboard IDs when blending styles. Prefer `moodboard_id` for single moodboards.'),
|
|
115
117
|
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.')
|
|
@@ -139,6 +141,7 @@ function registerGenerateTools(server, client) {
|
|
|
139
141
|
content: [{
|
|
140
142
|
type: 'text',
|
|
141
143
|
text: JSON.stringify({
|
|
144
|
+
...creditFields(result),
|
|
142
145
|
scenes,
|
|
143
146
|
total_scenes: result.scenes?.length || 0,
|
|
144
147
|
completed_scenes: scenes.length,
|
|
@@ -160,10 +163,10 @@ function registerGenerateTools(server, client) {
|
|
|
160
163
|
{
|
|
161
164
|
prompt: z.string().describe('Text description of the video to generate'),
|
|
162
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.'),
|
|
163
|
-
aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"'),
|
|
164
|
-
duration: z.number().optional().describe('Duration in seconds. Must be a value
|
|
166
|
+
aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "16:9", "9:16", "1:1"). Must be in the chosen model\'s `supported_aspect_ratios` from list_models. Default: "16:9"'),
|
|
167
|
+
duration: z.number().optional().describe('Duration in seconds. Must be a value in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration` (whichever the model exposes). Default: 5'),
|
|
165
168
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
166
|
-
reference_images: z.array(z.string()).optional().describe('Array of image URLs used as visual references (style / composition / subject).'),
|
|
169
|
+
reference_images: z.array(z.string()).optional().describe('Array of image URLs used as visual references (style / composition / subject). **Cap: pass at most `max_reference_images` URLs from list_models for the chosen model — exceeding it is a deterministic 400.**'),
|
|
167
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.'),
|
|
168
171
|
preset_id: z.string().optional().describe('Preset ID from list_presets type="video" to apply a saved motion/style preset to this generation.')
|
|
169
172
|
},
|
|
@@ -181,6 +184,7 @@ function registerGenerateTools(server, client) {
|
|
|
181
184
|
content: [{
|
|
182
185
|
type: 'text',
|
|
183
186
|
text: JSON.stringify({
|
|
187
|
+
...creditFields(result),
|
|
184
188
|
urls: result.result.urls,
|
|
185
189
|
model: result.result.model,
|
|
186
190
|
duration: result.result.duration,
|
|
@@ -201,10 +205,10 @@ function registerGenerateTools(server, client) {
|
|
|
201
205
|
image_url: z.string().describe('URL of the source image to animate'),
|
|
202
206
|
prompt: z.string().describe('Text description of the desired MOTION (e.g., "camera slowly pans right while the character walks forward")'),
|
|
203
207
|
model: z.string().optional().describe('Model identifier. Use list_models type="img_to_video" to see options.'),
|
|
204
|
-
aspect_ratio: z.string().optional().describe('Output aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"'),
|
|
205
|
-
duration: z.number().optional().describe('Duration in seconds. Must be
|
|
208
|
+
aspect_ratio: z.string().optional().describe('Output aspect ratio (e.g., "16:9", "9:16", "1:1"). Must be in the chosen model\'s `supported_aspect_ratios` from list_models. Default: "16:9"'),
|
|
209
|
+
duration: z.number().optional().describe('Duration in seconds. Must be in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration`. Default: 5'),
|
|
206
210
|
enhance_prompt: z.boolean().optional().describe('Enhance the motion prompt. Default: true'),
|
|
207
|
-
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to maintain consistency with prior characters / styles.'),
|
|
211
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to maintain consistency with prior characters / styles. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model; if `supports_visual_dna: false` the model ignores DNA entirely.**'),
|
|
208
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.')
|
|
209
213
|
},
|
|
210
214
|
async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution }) => {
|
|
@@ -221,6 +225,7 @@ function registerGenerateTools(server, client) {
|
|
|
221
225
|
content: [{
|
|
222
226
|
type: 'text',
|
|
223
227
|
text: JSON.stringify({
|
|
228
|
+
...creditFields(result),
|
|
224
229
|
urls: result.result.urls,
|
|
225
230
|
model: result.result.model,
|
|
226
231
|
duration: result.result.duration,
|
|
@@ -260,6 +265,7 @@ function registerGenerateTools(server, client) {
|
|
|
260
265
|
content: [{
|
|
261
266
|
type: 'text',
|
|
262
267
|
text: JSON.stringify({
|
|
268
|
+
...creditFields(result),
|
|
263
269
|
urls: result.result.urls,
|
|
264
270
|
title: result.result.title,
|
|
265
271
|
duration: result.result.duration,
|
|
@@ -294,6 +300,7 @@ function registerGenerateTools(server, client) {
|
|
|
294
300
|
content: [{
|
|
295
301
|
type: 'text',
|
|
296
302
|
text: JSON.stringify({
|
|
303
|
+
...creditFields(result),
|
|
297
304
|
urls: result.result.urls,
|
|
298
305
|
voice: result.result.voice,
|
|
299
306
|
duration: result.result.duration
|
|
@@ -327,6 +334,7 @@ function registerGenerateTools(server, client) {
|
|
|
327
334
|
content: [{
|
|
328
335
|
type: 'text',
|
|
329
336
|
text: JSON.stringify({
|
|
337
|
+
...creditFields(result),
|
|
330
338
|
urls: result.result.urls,
|
|
331
339
|
duration: result.result.duration
|
|
332
340
|
}, null, 2)
|
|
@@ -402,16 +410,16 @@ function registerGenerateTools(server, client) {
|
|
|
402
410
|
{
|
|
403
411
|
prompt: z.string().describe('Text description of the desired video / animation'),
|
|
404
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.'),
|
|
405
|
-
reference_images: z.array(z.string()).optional().describe('Array of public image URLs used as reference elements (product shots, character references, etc.).
|
|
406
|
-
reference_videos: z.array(z.string()).optional().describe('Array of reference video URLs for models that accept video inputs
|
|
407
|
-
audio_url: z.string().optional().describe('URL of a reference audio track
|
|
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.'),
|
|
409
|
-
duration: z.number().optional().describe('
|
|
410
|
-
aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"'),
|
|
413
|
+
reference_images: z.array(z.string()).optional().describe('Array of public image URLs used as reference elements (product shots, character references, etc.). **Cap: pass at most `elements_max_images` URLs from list_models for the chosen model — exceeding it is a deterministic 400.**'),
|
|
414
|
+
reference_videos: z.array(z.string()).optional().describe('Array of reference video URLs for models that accept video inputs. **Cap: pass at most `elements_max_videos` URLs from list_models — if the cap is 0 the model rejects videos.**'),
|
|
415
|
+
audio_url: z.string().optional().describe('URL of a reference audio track. **Audio constraints: `elements_max_audio` from list_models gates whether audio is accepted at all; audio duration must fall within `min_audio_duration`-`max_audio_duration`; format must be in `supported_audio_formats` (if specified).**'),
|
|
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. **Total count across files + reference_images still capped by `elements_max_images`.**'),
|
|
417
|
+
duration: z.number().optional().describe('Output duration in seconds. Must be in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration`. Default: 5'),
|
|
418
|
+
aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "16:9", "9:16", "1:1"). Must be in `supported_aspect_ratios` from list_models. Default: "16:9"'),
|
|
411
419
|
motion: z.string().optional().describe('Motion style / intensity hint (optional)'),
|
|
412
420
|
preset_id: z.string().optional().describe('Preset ID from list_presets type="video" (optional)'),
|
|
413
421
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
414
|
-
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for character/style consistency across outputs.'),
|
|
422
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for character/style consistency across outputs. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model.**'),
|
|
415
423
|
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.')
|
|
416
424
|
},
|
|
417
425
|
async ({ prompt, model, reference_images, reference_videos, audio_url, files, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution }) => {
|
|
@@ -454,6 +462,7 @@ function registerGenerateTools(server, client) {
|
|
|
454
462
|
content: [{
|
|
455
463
|
type: 'text',
|
|
456
464
|
text: JSON.stringify({
|
|
465
|
+
...creditFields(result),
|
|
457
466
|
urls: result.result?.urls || [],
|
|
458
467
|
thumbnail_url: result.result?.thumbnail_url || null,
|
|
459
468
|
duration: result.result?.duration || null,
|
|
@@ -475,10 +484,10 @@ function registerGenerateTools(server, client) {
|
|
|
475
484
|
last_frame: z.string().optional().describe('URL or absolute local path to the last frame (file mode — alternative to last_frame_url)'),
|
|
476
485
|
prompt: z.string().optional().describe('Optional description of the desired motion between the two frames (e.g. "smooth camera dolly in")'),
|
|
477
486
|
model: z.string().optional().describe('Model identifier. Use list_models type="firstlastgenerations" to see options. Omit for Smart Select.'),
|
|
478
|
-
duration: z.number().optional().describe('Duration in seconds. Default: 5'),
|
|
479
|
-
aspect_ratio: z.string().optional().describe('Aspect ratio (auto-detected from first frame if not provided). Default: "16:9"'),
|
|
487
|
+
duration: z.number().optional().describe('Duration in seconds. Must be in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration`. Default: 5'),
|
|
488
|
+
aspect_ratio: z.string().optional().describe('Aspect ratio (auto-detected from first frame if not provided). Must be in `supported_aspect_ratios` from list_models when set. Default: "16:9"'),
|
|
480
489
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
481
|
-
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply.'),
|
|
490
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model; if `supports_visual_dna: false`, DNA is silently ignored.**'),
|
|
482
491
|
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.')
|
|
483
492
|
},
|
|
484
493
|
async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids, resolution }) => {
|
|
@@ -523,6 +532,7 @@ function registerGenerateTools(server, client) {
|
|
|
523
532
|
content: [{
|
|
524
533
|
type: 'text',
|
|
525
534
|
text: JSON.stringify({
|
|
535
|
+
...creditFields(result),
|
|
526
536
|
urls: result.result?.urls || [],
|
|
527
537
|
thumbnail_url: result.result?.thumbnail_url || null,
|
|
528
538
|
duration: result.result?.duration || null,
|
|
@@ -536,10 +546,10 @@ function registerGenerateTools(server, client) {
|
|
|
536
546
|
// ─── generate_lipsync ──────────────────────────────────────
|
|
537
547
|
server.tool(
|
|
538
548
|
'generate_lipsync',
|
|
539
|
-
'Lipsync an audio track to a source image or video. Both `source` (image or video) and `audio` can be provided as URLs or as absolute local file paths. Pass a text_prompt only if the model supports it (some lipsync models do character performance from a prompt). Returns a lipsynced video URL.',
|
|
549
|
+
'Lipsync an audio track to a source image or video. Both `source` (image or video) and `audio` can be provided as URLs or as absolute local file paths. Pass a text_prompt only if the model supports it (some lipsync models do character performance from a prompt). **Validate before submitting: for `lipsync-video` sources, the input video duration must fall within `min_video_duration`-`max_video_duration` from list_models; audio duration must fall within `min_audio_duration`-`max_audio_duration` (and if `audio_max_follows_video_duration: true`, audio is also capped at the video duration); audio format must be in `supported_audio_formats` when specified.** Returns a lipsynced video URL.',
|
|
540
550
|
{
|
|
541
|
-
source: z.string().describe('URL or absolute local path to the source image or video (the face to animate)'),
|
|
542
|
-
audio: z.string().describe('URL or absolute local path to the audio track (the voice to sync to)'),
|
|
551
|
+
source: z.string().describe('URL or absolute local path to the source image or video (the face to animate). For lipsync-video: duration must fall within `min_video_duration`-`max_video_duration` from list_models.'),
|
|
552
|
+
audio: z.string().describe('URL or absolute local path to the audio track (the voice to sync to). Duration must fall within `min_audio_duration`-`max_audio_duration` from list_models; format must be in `supported_audio_formats` (when set).'),
|
|
543
553
|
text_prompt: z.string().optional().describe('Optional text prompt (for performance-capable models)'),
|
|
544
554
|
model: z.string().optional().describe('Model identifier. Use list_models type="lipsync-image" or type="lipsync-video" to see options. Omit for Smart Select.'),
|
|
545
555
|
bounding_box_target: z.array(z.number()).optional().describe('Optional bounding box [x, y, w, h] for multi-face inputs (Hedra Character3 style). Leave empty for single-face.')
|
|
@@ -593,6 +603,7 @@ function registerGenerateTools(server, client) {
|
|
|
593
603
|
content: [{
|
|
594
604
|
type: 'text',
|
|
595
605
|
text: JSON.stringify({
|
|
606
|
+
...creditFields(result),
|
|
596
607
|
urls: result.result?.urls || [],
|
|
597
608
|
thumbnail_url: result.result?.thumbnail_url || null,
|
|
598
609
|
duration: result.result?.duration || null,
|
|
@@ -608,17 +619,17 @@ function registerGenerateTools(server, client) {
|
|
|
608
619
|
'generate_video_from_video',
|
|
609
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.',
|
|
610
621
|
{
|
|
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.'),
|
|
622
|
+
source_video: z.string().describe('URL or absolute local path to the primary source video to restyle. **Source duration must fall within `min_video_duration`-`max_video_duration` from list_models for the chosen model** — videos outside that range are rejected (or silently truncated by some upstream providers). 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.'),
|
|
612
623
|
prompt: z.string().describe('Text description of the desired restyle / transformation'),
|
|
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.'),
|
|
614
|
-
aspect_ratio: z.string().optional().describe('Output aspect ratio. Default: matches source'),
|
|
615
|
-
duration: z.number().optional().describe('
|
|
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 / max_video_duration per model. Omit for Smart Select.'),
|
|
625
|
+
aspect_ratio: z.string().optional().describe('Output aspect ratio. Must be in `supported_aspect_ratios` from list_models when set. Default: matches source'),
|
|
626
|
+
duration: z.number().optional().describe('Output duration in seconds. Must be in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration`. Default: matches source'),
|
|
616
627
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
617
|
-
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for character/style consistency.'),
|
|
628
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for character/style consistency. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model; if `supports_visual_dna: false`, DNA is silently ignored.**'),
|
|
618
629
|
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
|
|
620
|
-
reference_videos: z.array(z.string()).optional().describe('Array of additional reference video URLs for models that support multiple video inputs
|
|
621
|
-
elements: z.array(z.string()).optional().describe('Array of element image URLs
|
|
630
|
+
reference_images: z.array(z.string()).optional().describe('Array of reference image URLs for models that support additional image inputs. **Cap: pass at most `max_images` URLs from list_models — if `max_images === 0` the model does not accept image refs.** Examples: character reference images for Kling O1/O3, style reference for Aleph/gen4_aleph, character image for WAN VACE video-edit.'),
|
|
631
|
+
reference_videos: z.array(z.string()).optional().describe('Array of additional reference video URLs for models that support multiple video inputs. **Cap: pass at most `max_videos` URLs from list_models — if `max_videos <= 1` only the source_video is accepted.** Example: WAN 2.6 reference-to-video accepts 1–3 reference videos.'),
|
|
632
|
+
elements: z.array(z.string()).optional().describe('Array of element image URLs. **Cap: pass at most `max_elements` URLs from list_models — if `max_elements === 0` the model does not accept elements.** Elements are style or character reference assets alongside the main video.')
|
|
622
633
|
},
|
|
623
634
|
async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, reference_images, reference_videos, elements }) => {
|
|
624
635
|
if (!source_video) throw new Error('source_video is required');
|
|
@@ -657,6 +668,7 @@ function registerGenerateTools(server, client) {
|
|
|
657
668
|
content: [{
|
|
658
669
|
type: 'text',
|
|
659
670
|
text: JSON.stringify({
|
|
671
|
+
...creditFields(result),
|
|
660
672
|
urls: result.result?.urls || [],
|
|
661
673
|
thumbnail_url: result.result?.thumbnail_url || null,
|
|
662
674
|
duration: result.result?.duration || null,
|
|
@@ -697,6 +709,7 @@ function registerGenerateTools(server, client) {
|
|
|
697
709
|
content: [{
|
|
698
710
|
type: 'text',
|
|
699
711
|
text: JSON.stringify({
|
|
712
|
+
...creditFields(result),
|
|
700
713
|
text: result.result?.text || '',
|
|
701
714
|
srt_url: result.result?.srt_url || null,
|
|
702
715
|
word_by_word_srt_url: result.result?.word_by_word_srt_url || null,
|
|
@@ -749,6 +762,7 @@ function registerGenerateTools(server, client) {
|
|
|
749
762
|
content: [{
|
|
750
763
|
type: 'text',
|
|
751
764
|
text: JSON.stringify({
|
|
765
|
+
...creditFields(result),
|
|
752
766
|
urls: result.result?.urls || [],
|
|
753
767
|
thumbnail_url: result.result?.thumbnail_url || null,
|
|
754
768
|
mode: result.result?.mode || null,
|
|
@@ -790,6 +804,7 @@ function registerGenerateTools(server, client) {
|
|
|
790
804
|
content: [{
|
|
791
805
|
type: 'text',
|
|
792
806
|
text: JSON.stringify({
|
|
807
|
+
...creditFields(result),
|
|
793
808
|
urls: result.result?.urls || [],
|
|
794
809
|
edit_type: result.result?.edit_type || null,
|
|
795
810
|
model: result.result?.model || null
|
|
@@ -838,6 +853,7 @@ function registerGenerateTools(server, client) {
|
|
|
838
853
|
content: [{
|
|
839
854
|
type: 'text',
|
|
840
855
|
text: JSON.stringify({
|
|
856
|
+
...creditFields(result),
|
|
841
857
|
urls: result.result?.urls || [],
|
|
842
858
|
download_url: result.result?.download_url || null,
|
|
843
859
|
edit_type: result.result?.edit_type || null,
|
package/src/tools/media.js
CHANGED
|
@@ -45,19 +45,29 @@ function registerMediaTools(server, client) {
|
|
|
45
45
|
// ─── list_media ────────────────────────────────────────────
|
|
46
46
|
server.tool(
|
|
47
47
|
'list_media',
|
|
48
|
-
'
|
|
48
|
+
'Browse the user\'s Kolbo media library — both uploaded files AND AI-generated outputs they have saved. Powerful filtering: scope to a single project (`project_id`), a user folder (`folder_id`), a "section" / category (`category`: ai / uploaded / edited / favorites / training-lab), a media type (`type`: image / video / audio), or generation provenance (`source_type`). Combine filters freely. Use this to discover what the user already has before generating something new, to retrieve a specific past creation, or to list everything in a project for downstream batch work.',
|
|
49
49
|
{
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
project_id: z.string().optional().describe('Restrict to a single project (Mongo ObjectId). Use `app_builder_list_projects` to discover IDs. Omit to list across all the user\'s media.'),
|
|
51
|
+
folder_id: z.string().optional().describe('Restrict to a user folder (Mongo ObjectId). Discover folder IDs via `list_media_folders`. Takes precedence over project_id when both are set.'),
|
|
52
|
+
type: z.enum(['image', 'video', 'audio', 'all']).optional().describe('Filter by media type. Default: all types.'),
|
|
53
|
+
category: z.enum(['ai', 'uploaded', 'edited', 'favorites', 'training-lab', 'all']).optional().describe('Filter by "section" (matches the Kolbo desktop app sidebar): `ai` = AI-generated, `uploaded` = files the user uploaded, `edited` = AI-edited variants, `favorites` = items the user starred, `training-lab` = training-lab assets. Default: all sections.'),
|
|
54
|
+
source_type: z.enum(['uploaded', 'generated', 'chat-generated']).optional().describe('Lower-level provenance filter. Use `category` for the common case; use `source_type` for fine-grained distinction (e.g. only chat-generated images).'),
|
|
55
|
+
sort: z.enum(['created_desc', 'created_asc', 'name_asc', 'name_desc']).optional().describe('Sort order. Default: created_desc (newest first).'),
|
|
56
|
+
page: z.number().optional().describe('1-indexed page number. Default: 1'),
|
|
57
|
+
page_size: z.number().optional().describe('Items per page. Default: 50, max 200.'),
|
|
58
|
+
search: z.string().optional().describe('Free-text match against filename + original prompt.')
|
|
54
59
|
},
|
|
55
|
-
async ({ type, page, page_size, search }) => {
|
|
60
|
+
async ({ project_id, folder_id, type, category, source_type, sort, page, page_size, search }) => {
|
|
56
61
|
const params = new URLSearchParams();
|
|
57
|
-
if (
|
|
58
|
-
if (
|
|
59
|
-
if (
|
|
60
|
-
if (
|
|
62
|
+
if (project_id) params.set('project_id', project_id);
|
|
63
|
+
if (folder_id) params.set('folder_id', folder_id);
|
|
64
|
+
if (type) params.set('type', type);
|
|
65
|
+
if (category) params.set('category', category);
|
|
66
|
+
if (source_type) params.set('source_type', source_type);
|
|
67
|
+
if (sort) params.set('sort', sort);
|
|
68
|
+
if (page) params.set('page', String(page));
|
|
69
|
+
if (page_size) params.set('page_size', String(page_size));
|
|
70
|
+
if (search) params.set('search', search);
|
|
61
71
|
|
|
62
72
|
const qs = params.toString();
|
|
63
73
|
const result = await client.get(`/v1/media${qs ? '?' + qs : ''}`);
|
|
@@ -73,6 +83,334 @@ function registerMediaTools(server, client) {
|
|
|
73
83
|
};
|
|
74
84
|
}
|
|
75
85
|
);
|
|
86
|
+
|
|
87
|
+
// ─── favorite_media ────────────────────────────────────────
|
|
88
|
+
server.tool(
|
|
89
|
+
'favorite_media',
|
|
90
|
+
'Mark a media item as a favorite for the user. Idempotent — calling on an already-favorited item is a no-op. Requires the media `id` from `list_media`. After favoriting, the item shows up in `list_media` with `category=favorites` and in the desktop app sidebar\'s Favorites section. Use this when the user explicitly says "favorite this", "save this to favorites", "star this", or similar.',
|
|
91
|
+
{
|
|
92
|
+
media_id: z.string().describe('The MediaLibraryItem id (returned as `id` from `list_media`).')
|
|
93
|
+
},
|
|
94
|
+
async ({ media_id }) => {
|
|
95
|
+
const result = await client.post(`/v1/media/${encodeURIComponent(media_id)}/favorite`, {});
|
|
96
|
+
return {
|
|
97
|
+
content: [{
|
|
98
|
+
type: 'text',
|
|
99
|
+
text: JSON.stringify(result, null, 2)
|
|
100
|
+
}]
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// ─── unfavorite_media ──────────────────────────────────────
|
|
106
|
+
server.tool(
|
|
107
|
+
'unfavorite_media',
|
|
108
|
+
'Remove a media item from the user\'s favorites. Idempotent — calling on an item that isn\'t favorited is a no-op. Requires the media `id` from `list_media`. Use this when the user says "unfavorite", "remove from favorites", "unstar", or similar.',
|
|
109
|
+
{
|
|
110
|
+
media_id: z.string().describe('The MediaLibraryItem id (returned as `id` from `list_media`).')
|
|
111
|
+
},
|
|
112
|
+
async ({ media_id }) => {
|
|
113
|
+
const result = await client.delete(`/v1/media/${encodeURIComponent(media_id)}/favorite`);
|
|
114
|
+
return {
|
|
115
|
+
content: [{
|
|
116
|
+
type: 'text',
|
|
117
|
+
text: JSON.stringify(result, null, 2)
|
|
118
|
+
}]
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// ─── list_media_folders ────────────────────────────────────
|
|
124
|
+
server.tool(
|
|
125
|
+
'list_media_folders',
|
|
126
|
+
'List the user\'s media folders (their own + folders shared with them). Folders are user-scoped and can span multiple projects — they\'re a way for the user to group media across the library independent of project structure. Use this to discover folder IDs to pass into `list_media` via `folder_id`, or to show the user what folders exist before suggesting where to look.',
|
|
127
|
+
{},
|
|
128
|
+
async () => {
|
|
129
|
+
const result = await client.get('/v1/media/folders');
|
|
130
|
+
return {
|
|
131
|
+
content: [{
|
|
132
|
+
type: 'text',
|
|
133
|
+
text: JSON.stringify({
|
|
134
|
+
folders: result.folders || [],
|
|
135
|
+
count: result.count || 0
|
|
136
|
+
}, null, 2)
|
|
137
|
+
}]
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// ─── create_media_folder ───────────────────────────────────
|
|
143
|
+
server.tool(
|
|
144
|
+
'create_media_folder',
|
|
145
|
+
'Create a new media folder for the user. Folders are user-scoped (span all projects) and useful for grouping related assets. Returns the new folder id — pass it as `folder_id` to `list_media`, `add_media_to_folder`, etc.',
|
|
146
|
+
{
|
|
147
|
+
name: z.string().describe('Folder name (1–100 characters).'),
|
|
148
|
+
description: z.string().optional().describe('Optional description (up to 500 characters).'),
|
|
149
|
+
color: z.string().optional().describe('Optional hex color like "#3B82F6" for UI tinting. Default: Kolbo blue.'),
|
|
150
|
+
icon: z.string().optional().describe('Optional Lucide icon name (e.g. "folder", "star", "image"). Default: "folder".')
|
|
151
|
+
},
|
|
152
|
+
async ({ name, description, color, icon }) => {
|
|
153
|
+
const result = await client.post('/v1/media/folders', { name, description, color, icon });
|
|
154
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.folder || result, null, 2) }] };
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// ─── update_media_folder ───────────────────────────────────
|
|
159
|
+
server.tool(
|
|
160
|
+
'update_media_folder',
|
|
161
|
+
'Rename a folder or update its color / icon / description. Owner only. Any subset of fields may be provided — fields omitted are left unchanged.',
|
|
162
|
+
{
|
|
163
|
+
folder_id: z.string().describe('Folder id from `list_media_folders` or `create_media_folder`.'),
|
|
164
|
+
name: z.string().optional().describe('New folder name (1–100 characters).'),
|
|
165
|
+
description: z.string().optional().describe('New description (up to 500 characters). Pass "" to clear.'),
|
|
166
|
+
color: z.string().optional().describe('New hex color like "#3B82F6".'),
|
|
167
|
+
icon: z.string().optional().describe('New Lucide icon name.')
|
|
168
|
+
},
|
|
169
|
+
async ({ folder_id, name, description, color, icon }) => {
|
|
170
|
+
const body = {};
|
|
171
|
+
if (name !== undefined) body.name = name;
|
|
172
|
+
if (description !== undefined) body.description = description;
|
|
173
|
+
if (color !== undefined) body.color = color;
|
|
174
|
+
if (icon !== undefined) body.icon = icon;
|
|
175
|
+
const result = await client.put(`/v1/media/folders/${encodeURIComponent(folder_id)}`, body);
|
|
176
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.folder || result, null, 2) }] };
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// ─── delete_media_folder ───────────────────────────────────
|
|
181
|
+
server.tool(
|
|
182
|
+
'delete_media_folder',
|
|
183
|
+
'Delete a folder (soft delete — items inside are detached but NOT deleted from the user\'s media library). Owner only. ALWAYS ask the user to confirm before calling this — folder deletion is not surfaced in any "undo" flow.',
|
|
184
|
+
{
|
|
185
|
+
folder_id: z.string().describe('Folder id to delete.')
|
|
186
|
+
},
|
|
187
|
+
async ({ folder_id }) => {
|
|
188
|
+
const result = await client.delete(`/v1/media/folders/${encodeURIComponent(folder_id)}`);
|
|
189
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// ─── add_media_to_folder ───────────────────────────────────
|
|
194
|
+
server.tool(
|
|
195
|
+
'add_media_to_folder',
|
|
196
|
+
'Add one or more media items to a folder. Caller must own the folder or be a shared member. Idempotent — items already in the folder are skipped silently. Up to 500 items per call.',
|
|
197
|
+
{
|
|
198
|
+
folder_id: z.string().describe('Target folder id.'),
|
|
199
|
+
media_ids: z.array(z.string()).describe('Array of MediaLibraryItem ids (from `list_media`). Up to 500.')
|
|
200
|
+
},
|
|
201
|
+
async ({ folder_id, media_ids }) => {
|
|
202
|
+
const result = await client.post(
|
|
203
|
+
`/v1/media/folders/${encodeURIComponent(folder_id)}/items`,
|
|
204
|
+
{ media_ids }
|
|
205
|
+
);
|
|
206
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// ─── remove_media_from_folder ──────────────────────────────
|
|
211
|
+
server.tool(
|
|
212
|
+
'remove_media_from_folder',
|
|
213
|
+
'Remove one or more media items from a folder. Caller must own the folder or be a shared member. Items themselves remain in the library. Up to 500 items per call.',
|
|
214
|
+
{
|
|
215
|
+
folder_id: z.string().describe('Folder id.'),
|
|
216
|
+
media_ids: z.array(z.string()).describe('Array of MediaLibraryItem ids to remove from the folder.')
|
|
217
|
+
},
|
|
218
|
+
async ({ folder_id, media_ids }) => {
|
|
219
|
+
const result = await client.delete(
|
|
220
|
+
`/v1/media/folders/${encodeURIComponent(folder_id)}/items`,
|
|
221
|
+
{ media_ids }
|
|
222
|
+
);
|
|
223
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// ─── share_media_folder ────────────────────────────────────
|
|
228
|
+
server.tool(
|
|
229
|
+
'share_media_folder',
|
|
230
|
+
'Share a folder with one or more users by email. Owner only. Users must already have a Kolbo account; emails not found are returned in `not_found`. Shared members can list folder contents, add and remove items, but cannot delete the folder or reshare it.',
|
|
231
|
+
{
|
|
232
|
+
folder_id: z.string().describe('Folder id to share.'),
|
|
233
|
+
user_emails: z.array(z.string()).describe('Array of email addresses to grant access to. Up to 50 per call.')
|
|
234
|
+
},
|
|
235
|
+
async ({ folder_id, user_emails }) => {
|
|
236
|
+
const result = await client.post(
|
|
237
|
+
`/v1/media/folders/${encodeURIComponent(folder_id)}/share`,
|
|
238
|
+
{ user_emails }
|
|
239
|
+
);
|
|
240
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// ─── get_media ─────────────────────────────────────────────
|
|
245
|
+
server.tool(
|
|
246
|
+
'get_media',
|
|
247
|
+
'Fetch one media item\'s full details by id. Returns the same shape as items in `list_media` plus extra metadata. Use this when the user references a specific item ("tell me about this generation", "what prompt did I use for [item]").',
|
|
248
|
+
{
|
|
249
|
+
media_id: z.string().describe('MediaLibraryItem id (from `list_media`). Generation ids are also accepted as a fallback.')
|
|
250
|
+
},
|
|
251
|
+
async ({ media_id }) => {
|
|
252
|
+
const result = await client.get(`/v1/media/${encodeURIComponent(media_id)}`);
|
|
253
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.media || result, null, 2) }] };
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// ─── delete_media ──────────────────────────────────────────
|
|
258
|
+
server.tool(
|
|
259
|
+
'delete_media',
|
|
260
|
+
'Soft-delete a media item — moves it to the user\'s trash where it can be restored for 30 days. Owner only. Idempotent. Use this for "delete this image / video / song" — NOT for `permanently_delete_media`, which is irreversible.',
|
|
261
|
+
{
|
|
262
|
+
media_id: z.string().describe('MediaLibraryItem id to soft-delete.')
|
|
263
|
+
},
|
|
264
|
+
async ({ media_id }) => {
|
|
265
|
+
const result = await client.delete(`/v1/media/${encodeURIComponent(media_id)}`);
|
|
266
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// ─── restore_media ─────────────────────────────────────────
|
|
271
|
+
server.tool(
|
|
272
|
+
'restore_media',
|
|
273
|
+
'Restore a soft-deleted (trashed) media item back to the user\'s active library. Owner only. Use after `delete_media` if the user changes their mind, or when the user explicitly asks "restore [item] from trash".',
|
|
274
|
+
{
|
|
275
|
+
media_id: z.string().describe('MediaLibraryItem id to restore from trash.')
|
|
276
|
+
},
|
|
277
|
+
async ({ media_id }) => {
|
|
278
|
+
const result = await client.post(`/v1/media/${encodeURIComponent(media_id)}/restore`, {});
|
|
279
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// ─── permanently_delete_media ──────────────────────────────
|
|
284
|
+
server.tool(
|
|
285
|
+
'permanently_delete_media',
|
|
286
|
+
'PERMANENTLY delete a media item — removes it from MongoDB, deletes the file from S3, removes from all folders, and deletes the source generation record. NOT REVERSIBLE — there is no recovery flow. Owner only. ALWAYS ask the user to explicitly confirm before calling this; use `delete_media` for normal "delete" intent.',
|
|
287
|
+
{
|
|
288
|
+
media_id: z.string().describe('MediaLibraryItem id to permanently delete. Cannot be undone.')
|
|
289
|
+
},
|
|
290
|
+
async ({ media_id }) => {
|
|
291
|
+
const result = await client.delete(`/v1/media/${encodeURIComponent(media_id)}/permanent`);
|
|
292
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// ─── move_media ────────────────────────────────────────────
|
|
297
|
+
server.tool(
|
|
298
|
+
'move_media',
|
|
299
|
+
'Move a media item to a different project. Caller must own the item AND have access to the target project. Items in shared projects from other members cannot be moved by you. Use this when the user says "move this to project X" or wants to reorganize.',
|
|
300
|
+
{
|
|
301
|
+
media_id: z.string().describe('MediaLibraryItem id to move.'),
|
|
302
|
+
project_id: z.string().describe('Target project id (use `app_builder_list_projects` to discover ids).')
|
|
303
|
+
},
|
|
304
|
+
async ({ media_id, project_id }) => {
|
|
305
|
+
const result = await client.patch(
|
|
306
|
+
`/v1/media/${encodeURIComponent(media_id)}/project`,
|
|
307
|
+
{ project_id }
|
|
308
|
+
);
|
|
309
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// ─── bulk_delete_media ─────────────────────────────────────
|
|
314
|
+
server.tool(
|
|
315
|
+
'bulk_delete_media',
|
|
316
|
+
'Soft-delete up to 1000 media items in one call. Items go to trash (30-day recovery). Owner only — items not owned by the user are silently skipped (count returned in response). Use this for "clean up all my old [type]" or "delete the failed generations from yesterday".',
|
|
317
|
+
{
|
|
318
|
+
media_ids: z.array(z.string()).describe('Array of MediaLibraryItem ids. Up to 1000 per call.')
|
|
319
|
+
},
|
|
320
|
+
async ({ media_ids }) => {
|
|
321
|
+
const result = await client.post('/v1/media/bulk/delete', { media_ids });
|
|
322
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// ─── bulk_restore_media ────────────────────────────────────
|
|
327
|
+
server.tool(
|
|
328
|
+
'bulk_restore_media',
|
|
329
|
+
'Restore up to 1000 trashed media items at once. Owner only. Returns the count restored and how many ids weren\'t in trash (already active or not owned).',
|
|
330
|
+
{
|
|
331
|
+
media_ids: z.array(z.string()).describe('Array of trashed MediaLibraryItem ids to restore. Up to 1000.')
|
|
332
|
+
},
|
|
333
|
+
async ({ media_ids }) => {
|
|
334
|
+
const result = await client.post('/v1/media/bulk/restore', { media_ids });
|
|
335
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
336
|
+
}
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// ─── bulk_permanently_delete_media ─────────────────────────
|
|
340
|
+
server.tool(
|
|
341
|
+
'bulk_permanently_delete_media',
|
|
342
|
+
'PERMANENTLY delete up to 1000 media items. NOT REVERSIBLE — removes from MongoDB, S3, folders, and source generation records. Owner only. ALWAYS confirm with the user before calling; this is the bulk equivalent of `permanently_delete_media`.',
|
|
343
|
+
{
|
|
344
|
+
media_ids: z.array(z.string()).describe('Array of MediaLibraryItem ids to permanently delete. Up to 1000. Cannot be undone.')
|
|
345
|
+
},
|
|
346
|
+
async ({ media_ids }) => {
|
|
347
|
+
const result = await client.post('/v1/media/bulk/permanent', { media_ids });
|
|
348
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// ─── bulk_move_media ───────────────────────────────────────
|
|
353
|
+
server.tool(
|
|
354
|
+
'bulk_move_media',
|
|
355
|
+
'Move up to 1000 media items to a different project in a single call. Caller must own ALL items AND have access to the target project — if any item isn\'t owned by the caller, the entire operation is rejected (atomic).',
|
|
356
|
+
{
|
|
357
|
+
media_ids: z.array(z.string()).describe('Array of MediaLibraryItem ids to move. Up to 1000.'),
|
|
358
|
+
project_id: z.string().describe('Target project id.')
|
|
359
|
+
},
|
|
360
|
+
async ({ media_ids, project_id }) => {
|
|
361
|
+
const result = await client.post('/v1/media/bulk/move', { media_ids, project_id });
|
|
362
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// ─── move_folder_contents ──────────────────────────────────
|
|
367
|
+
server.tool(
|
|
368
|
+
'move_folder_contents',
|
|
369
|
+
'Move every media item inside a folder to a different project. Caller must own ALL items in the folder AND have access to the target project. Shared folder members cannot use this — only the item owner can move items between projects.',
|
|
370
|
+
{
|
|
371
|
+
folder_id: z.string().describe('Folder id whose contents will be moved.'),
|
|
372
|
+
project_id: z.string().describe('Target project id.')
|
|
373
|
+
},
|
|
374
|
+
async ({ folder_id, project_id }) => {
|
|
375
|
+
const result = await client.post(
|
|
376
|
+
`/v1/media/folders/${encodeURIComponent(folder_id)}/move-contents`,
|
|
377
|
+
{ project_id }
|
|
378
|
+
);
|
|
379
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// ─── get_media_stats ───────────────────────────────────────
|
|
384
|
+
server.tool(
|
|
385
|
+
'get_media_stats',
|
|
386
|
+
'Get counts and total storage size of the user\'s media (or a specific project\'s media). Returns `{ total, images, videos, audio, total_size_bytes }`. Use this for "how many videos do I have", "what\'s my storage usage", or before bulk operations to estimate scope.',
|
|
387
|
+
{
|
|
388
|
+
project_id: z.string().optional().describe('Optional project id to scope stats to one project. Omit for the user\'s personal library across all projects.')
|
|
389
|
+
},
|
|
390
|
+
async ({ project_id }) => {
|
|
391
|
+
const params = new URLSearchParams();
|
|
392
|
+
if (project_id) params.set('project_id', project_id);
|
|
393
|
+
const qs = params.toString();
|
|
394
|
+
const result = await client.get(qs ? `/v1/media/stats?${qs}` : '/v1/media/stats');
|
|
395
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.stats || result, null, 2) }] };
|
|
396
|
+
}
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
// ─── unshare_media_folder ──────────────────────────────────
|
|
400
|
+
server.tool(
|
|
401
|
+
'unshare_media_folder',
|
|
402
|
+
'Revoke a single user\'s access to a folder. Owner only. The user keeps any media they uploaded — only the folder access is removed.',
|
|
403
|
+
{
|
|
404
|
+
folder_id: z.string().describe('Folder id.'),
|
|
405
|
+
user_id: z.string().describe('User id to revoke (from the folder\'s `shared_with` array).')
|
|
406
|
+
},
|
|
407
|
+
async ({ folder_id, user_id }) => {
|
|
408
|
+
const result = await client.delete(
|
|
409
|
+
`/v1/media/folders/${encodeURIComponent(folder_id)}/share/${encodeURIComponent(user_id)}`
|
|
410
|
+
);
|
|
411
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
412
|
+
}
|
|
413
|
+
);
|
|
76
414
|
}
|
|
77
415
|
|
|
78
416
|
module.exports = { registerMediaTools };
|
package/src/tools/models.js
CHANGED
|
@@ -9,14 +9,28 @@ function registerModelTools(server, client) {
|
|
|
9
9
|
// ─── list_models ───────────────────────────────────────────
|
|
10
10
|
server.tool(
|
|
11
11
|
'list_models',
|
|
12
|
-
'List available AI models on Kolbo. Filter by type to
|
|
12
|
+
'List available AI models on Kolbo. Filter by `type` to narrow to a generation type, and pass `format: "json"` to get the raw model documents (every constraint field, useful for programmatic comparison / cap validation before submitting a generation). Default `format: "text"` returns the human-readable summary.',
|
|
13
13
|
{
|
|
14
|
-
type: z.string().optional().describe('Filter by DB type name: "text_to_img", "image_editing", "text_to_video", "img_to_video", "draw_to_video", "video_to_video", "elements", "firstlastgenerations", "lipsync-image", "lipsync-video", "music_gen", "text_to_speech", "text_to_sound", "stt", "text". Legacy aliases also accepted: "image", "image_edit", "video", "video_from_image", "video_from_video", "music", "speech", "sound", "chat", "lipsync" (both lipsync types), "three_d" (all 3D types), "first_last_frame", "transcription". Omit for all models.')
|
|
14
|
+
type: z.string().optional().describe('Filter by DB type name: "text_to_img", "image_editing", "text_to_video", "img_to_video", "draw_to_video", "video_to_video", "elements", "firstlastgenerations", "lipsync-image", "lipsync-video", "music_gen", "text_to_speech", "text_to_sound", "stt", "text". Legacy aliases also accepted: "image", "image_edit", "video", "video_from_image", "video_from_video", "music", "speech", "sound", "chat", "lipsync" (both lipsync types), "three_d" (all 3D types), "first_last_frame", "transcription". Omit for all models.'),
|
|
15
|
+
format: z.enum(['text', 'json']).optional().describe('Output format. "text" (default) returns a human-readable summary with the most-used caps. "json" returns the raw model documents from the API — use this when you need to programmatically verify caps (max_reference_images, max_visual_dna, max_video_duration, supported_aspect_ratios, etc.) before passing an array/value that might exceed a model-specific limit. The JSON form is the source of truth; the text form is a convenience preview.')
|
|
15
16
|
},
|
|
16
|
-
async ({ type }) => {
|
|
17
|
+
async ({ type, format }) => {
|
|
17
18
|
const path = type ? `/v1/models?type=${encodeURIComponent(type)}` : '/v1/models';
|
|
18
19
|
const result = await client.get(path);
|
|
19
20
|
|
|
21
|
+
// JSON mode — return the raw API documents unchanged. This is the
|
|
22
|
+
// authoritative shape; every constraint the agent might need to validate
|
|
23
|
+
// a request lives here (durations, reference caps, audio/video min/max,
|
|
24
|
+
// resolution multipliers, supports_* flags, prompt-length limits, etc.).
|
|
25
|
+
if (format === 'json') {
|
|
26
|
+
return {
|
|
27
|
+
content: [{
|
|
28
|
+
type: 'text',
|
|
29
|
+
text: JSON.stringify({ count: result.count, models: result.models }, null, 2)
|
|
30
|
+
}]
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
// Split into auto-selectable (has summary) and named-only (no summary)
|
|
21
35
|
const withSummary = result.models.filter(m => m.summary && m.summary.trim() !== '');
|
|
22
36
|
const withoutSummary = result.models.filter(m => !m.summary || m.summary.trim() === '');
|
|
@@ -25,8 +39,26 @@ function registerModelTools(server, client) {
|
|
|
25
39
|
// it has to guess `supported_resolutions`/`supported_durations` and
|
|
26
40
|
// either invents values (then the API silently substitutes) or asks
|
|
27
41
|
// the user to clarify what's only knowable from this list.
|
|
42
|
+
//
|
|
43
|
+
// Rendering rule: emit a line for EVERY known constraint that is
|
|
44
|
+
// applicable for this model's type — even when the value is 0 / null.
|
|
45
|
+
// Hiding "0 cap" lines used to mean the agent couldn't distinguish
|
|
46
|
+
// "this model rejects DNA" (cap = 0) from "I don't know" (field
|
|
47
|
+
// missing). Now an explicit `max_dna: 0 (DNA not supported)` says the
|
|
48
|
+
// model says no, and absence means the API doesn't expose the field.
|
|
28
49
|
const formatSpecs = m => {
|
|
29
50
|
const parts = [];
|
|
51
|
+
const types = Array.isArray(m.types) ? m.types : [];
|
|
52
|
+
const isVideoType = types.some(t =>
|
|
53
|
+
['text_to_video', 'img_to_video', 'video_to_video', 'elements',
|
|
54
|
+
'firstlastgenerations', 'lipsync-image', 'lipsync-video', 'draw_to_video'].includes(t)
|
|
55
|
+
);
|
|
56
|
+
const isElements = types.includes('elements');
|
|
57
|
+
const isV2V = types.includes('video_to_video');
|
|
58
|
+
const isLipsyncVideo = types.includes('lipsync-video');
|
|
59
|
+
const isLipsyncImage = types.includes('lipsync-image');
|
|
60
|
+
const isImageEdit = types.includes('image_editing');
|
|
61
|
+
const isImage = types.includes('text_to_img') || isImageEdit;
|
|
30
62
|
|
|
31
63
|
if (Array.isArray(m.supported_resolutions) && m.supported_resolutions.length) {
|
|
32
64
|
const mult = m.resolution_multipliers || {};
|
|
@@ -38,33 +70,70 @@ function registerModelTools(server, client) {
|
|
|
38
70
|
);
|
|
39
71
|
}
|
|
40
72
|
|
|
73
|
+
// Output durations (video gen output, not source video)
|
|
41
74
|
if (Array.isArray(m.supported_durations) && m.supported_durations.length) {
|
|
42
75
|
const ds = m.supported_durations;
|
|
43
|
-
// Compact ranges like 4-15 if it's a contiguous run.
|
|
44
76
|
const sorted = [...ds].sort((a, b) => a - b);
|
|
45
77
|
const isRange = sorted.length > 2 && sorted.every((v, i) => i === 0 || v - sorted[i - 1] === 1);
|
|
46
78
|
parts.push(`durations: ${isRange ? `${sorted[0]}-${sorted[sorted.length - 1]}s` : sorted.join('/') + 's'}`);
|
|
79
|
+
} else if (isVideoType && (m.min_output_duration != null || m.max_output_duration != null)) {
|
|
80
|
+
parts.push(`duration_range: ${m.min_output_duration ?? '?'}-${m.max_output_duration ?? '?'}s${m.default_duration != null ? ` (default ${m.default_duration}s)` : ''}`);
|
|
47
81
|
}
|
|
48
82
|
|
|
49
|
-
|
|
50
|
-
|
|
83
|
+
// Aspect ratios — prefer per-type override if set
|
|
84
|
+
const ratios = m.supported_aspect_ratios_by_type
|
|
85
|
+
? Object.entries(m.supported_aspect_ratios_by_type).map(([t, arr]) => `${t}: ${arr.join('/')}`)
|
|
86
|
+
: null;
|
|
87
|
+
if (ratios) {
|
|
88
|
+
parts.push(`aspect (per-type): ${ratios.join(' | ')}`);
|
|
89
|
+
} else if (Array.isArray(m.supported_aspect_ratios) && m.supported_aspect_ratios.length) {
|
|
90
|
+
parts.push(`aspect: ${m.supported_aspect_ratios.join(', ')}${m.default_aspect_ratio ? ` (default ${m.default_aspect_ratio})` : ''}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Reference-input caps — show the slot relevant for this model family.
|
|
94
|
+
// The same conceptual "max reference images" lives under THREE field
|
|
95
|
+
// names depending on the model type. Be explicit about which is which
|
|
96
|
+
// so the agent reads the right one.
|
|
97
|
+
if (isImage || isImageEdit) {
|
|
98
|
+
parts.push(`max_reference_images: ${m.max_reference_images ?? 0}${(m.max_reference_images ?? 0) === 0 ? ' (no refs)' : ''}`);
|
|
99
|
+
}
|
|
100
|
+
if (isElements) {
|
|
101
|
+
parts.push(`elements caps: imgs=${m.elements_max_images ?? 0} · vids=${m.elements_max_videos ?? 0} · audio=${m.elements_max_audio ?? 0}`);
|
|
102
|
+
}
|
|
103
|
+
if (isV2V) {
|
|
104
|
+
parts.push(`v2v ref caps: imgs=${m.max_images ?? 0} · vids=${m.max_videos ?? 0} · elements=${m.max_elements ?? 0} · audio=${m.max_audio ?? 0}`);
|
|
51
105
|
}
|
|
52
106
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
107
|
+
// Visual DNA cap — always show for image / elements / video, even if 0.
|
|
108
|
+
// Use the authoritative supports_visual_dna flag when available; fall
|
|
109
|
+
// back to inferring from cap > 0 for older API responses.
|
|
110
|
+
const dnaSupported = typeof m.supports_visual_dna === 'boolean'
|
|
111
|
+
? m.supports_visual_dna
|
|
112
|
+
: (m.max_visual_dna ?? 0) > 0;
|
|
113
|
+
if (isImage || isVideoType) {
|
|
114
|
+
const cap = m.max_visual_dna;
|
|
115
|
+
if (dnaSupported && cap != null && cap > 0) parts.push(`max_visual_dna: ${cap}`);
|
|
116
|
+
else if (dnaSupported && cap == null) parts.push('visual_dna: supported (no cap published — confirm before passing >3)');
|
|
117
|
+
else parts.push('visual_dna: not supported');
|
|
57
118
|
}
|
|
58
119
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
120
|
+
// Source-video duration constraints — only matter for tools that take
|
|
121
|
+
// an INPUT video (lipsync-video, video_to_video).
|
|
122
|
+
if (isLipsyncVideo || isV2V) {
|
|
123
|
+
if (m.min_video_duration != null || m.max_video_duration != null) {
|
|
124
|
+
parts.push(`source_video: ${m.min_video_duration ?? '?'}-${m.max_video_duration ?? '?'}s`);
|
|
125
|
+
}
|
|
63
126
|
}
|
|
64
127
|
|
|
65
|
-
|
|
128
|
+
// Audio input — lipsync, elements, music-driven flows.
|
|
129
|
+
if (m.max_audio_duration != null || m.min_audio_duration != null) {
|
|
130
|
+
parts.push(`audio_input: ${m.min_audio_duration ?? '?'}-${m.max_audio_duration ?? '?'}s${m.audio_max_follows_video_duration ? ' (max follows video)' : ''}`);
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(m.supported_audio_formats) && m.supported_audio_formats.length) {
|
|
133
|
+
parts.push(`audio_formats: ${m.supported_audio_formats.join('/')}`);
|
|
134
|
+
}
|
|
66
135
|
|
|
67
|
-
//
|
|
136
|
+
// Native sound generation (video models that emit synced audio)
|
|
68
137
|
if (m.sound_generation_type === 'native') {
|
|
69
138
|
const mult = m.sound_credit_multiplier && m.sound_credit_multiplier !== 1
|
|
70
139
|
? ` (${m.sound_credit_multiplier}×)`
|
|
@@ -72,7 +141,22 @@ function registerModelTools(server, client) {
|
|
|
72
141
|
parts.push(`sound: native${mult}${m.sound_enabled_by_default ? ' on-by-default' : ''}`);
|
|
73
142
|
}
|
|
74
143
|
|
|
75
|
-
|
|
144
|
+
// Prompt constraints
|
|
145
|
+
if (m.requires_prompt === false) parts.push('prompt: optional');
|
|
146
|
+
if (m.min_prompt_length != null || m.max_prompt_length != null) {
|
|
147
|
+
parts.push(`prompt_length: ${m.min_prompt_length ?? 0}-${m.max_prompt_length ?? '∞'} chars`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Upload cap (when present)
|
|
151
|
+
if (m.max_file_size != null) {
|
|
152
|
+
const mb = Math.round(m.max_file_size / (1024 * 1024));
|
|
153
|
+
parts.push(`max_file_size: ${mb}MB`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Images-per-request (Midjourney-style fixed-N output)
|
|
157
|
+
if (m.images_per_request != null && m.images_per_request !== 1) {
|
|
158
|
+
parts.push(`images_per_request: ${m.images_per_request}`);
|
|
159
|
+
}
|
|
76
160
|
|
|
77
161
|
return parts.length ? `\n ${parts.join(' | ')}` : '';
|
|
78
162
|
};
|
|
@@ -91,7 +175,7 @@ function registerModelTools(server, client) {
|
|
|
91
175
|
return {
|
|
92
176
|
content: [{
|
|
93
177
|
type: 'text',
|
|
94
|
-
text: `Available models (${result.count}):\n\n${sections.join('\n\n')}\n\nUse the "identifier" value as the "model" parameter in generate tools.`
|
|
178
|
+
text: `Available models (${result.count}):\n\n${sections.join('\n\n')}\n\nUse the "identifier" value as the "model" parameter in generate tools. For programmatic cap validation, re-call with format: "json".`
|
|
95
179
|
}]
|
|
96
180
|
};
|
|
97
181
|
}
|
|
@@ -113,6 +197,38 @@ function registerModelTools(server, client) {
|
|
|
113
197
|
};
|
|
114
198
|
}
|
|
115
199
|
);
|
|
200
|
+
|
|
201
|
+
// ─── get_session_usage ─────────────────────────────────────
|
|
202
|
+
// Real, multiplier-adjusted credit spend tagged with the caller's
|
|
203
|
+
// X-Kolbo-Caller-Session-Id (set automatically by the parent process —
|
|
204
|
+
// no need to pass it). Use this to give the user an honest "you've spent
|
|
205
|
+
// X credits in this app session" instead of estimating from base credits.
|
|
206
|
+
server.tool(
|
|
207
|
+
'get_session_usage',
|
|
208
|
+
'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.',
|
|
209
|
+
{},
|
|
210
|
+
async () => {
|
|
211
|
+
try {
|
|
212
|
+
const r = await client.get('/credit-usage/by-caller-session');
|
|
213
|
+
// The endpoint returns { message, data: { total, count, by_tool, by_model, recent[] } }
|
|
214
|
+
return {
|
|
215
|
+
content: [{
|
|
216
|
+
type: 'text',
|
|
217
|
+
text: JSON.stringify(r.data || r, null, 2)
|
|
218
|
+
}]
|
|
219
|
+
};
|
|
220
|
+
} catch (err) {
|
|
221
|
+
// 400 from the endpoint means no caller-session-id was forwarded —
|
|
222
|
+
// surface a clear hint instead of a generic API error.
|
|
223
|
+
const hint = err?.status === 400
|
|
224
|
+
? '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.'
|
|
225
|
+
: err?.message || 'Failed to fetch session usage';
|
|
226
|
+
return {
|
|
227
|
+
content: [{ type: 'text', text: JSON.stringify({ error: hint }, null, 2) }]
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
);
|
|
116
232
|
}
|
|
117
233
|
|
|
118
234
|
module.exports = { registerModelTools };
|