@kolbo/mcp 1.54.0 → 1.55.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.
@@ -30,6 +30,64 @@ const MAX_FILE_BYTES = 500 * 1024 * 1024; // 500 MB — larger than visual_dna b
30
30
  const VISUAL_DNA_MAX_BYTES = 25 * 1024 * 1024; // kept for visual_dna backward-compat
31
31
  const MAX_REDIRECTS = 5;
32
32
 
33
+ // THE single statement of how a local file gets into Kolbo. It is repeated to
34
+ // the model on several surfaces — the server `instructions` block (src/index.js),
35
+ // the media tool descriptions, and both local-path errors below — so it lives
36
+ // here and is imported, not retyped. It was previously pasted in five places and
37
+ // a change to it updated only two, leaving `instructions` teaching the opposite.
38
+ const LOCAL_FILE_ROUTING =
39
+ 'If you are using Kolbo over a remote connector (e.g. claude.ai), local files are not reachable. ' +
40
+ 'DO NOT upload the file yourself with cloud credentials or a shell command — Kolbo has a tool for this. ' +
41
+ 'If you can run shell commands, call `create_upload_ticket` and POST the file to the returned upload_url. ' +
42
+ 'Otherwise call `media_upload_widget` to have the user pick the file, or `upload_media` ' +
43
+ 'when the file IS reachable from where the MCP server runs, then pass the returned https:// URL here. ' +
44
+ 'A URL from `list_media` also works if the asset is already in the library.';
45
+
46
+ // Every tool that takes user media as INPUT. Their descriptions promise "URL or
47
+ // absolute local path" — true on a stdio install, a lie over a remote connector,
48
+ // where the model would read it, see no filesystem, and tell the user Kolbo
49
+ // cannot take their file at all. attachFileInputHints() below appends the route
50
+ // that actually works for the current transport, so the refusal never happens.
51
+ const FILE_INPUT_TOOLS = [
52
+ 'generate_image', 'generate_image_edit', 'generate_creative_director',
53
+ 'generate_video', 'generate_video_from_image', 'generate_video_from_video',
54
+ 'generate_elements', 'generate_first_last_frame', 'generate_lipsync',
55
+ 'generate_3d', 'edit_image', 'edit_video', 'transcribe_audio',
56
+ 'create_visual_dna', 'generate_character_sheet', 'clone_voice',
57
+ 'chat_send_message', 'create_moodboard'
58
+ ];
59
+
60
+ const REMOTE_FILE_HINT =
61
+ ' LOCAL FILE (on the user\'s machine, or attached to this chat)? NEVER reply that you cannot upload files — Kolbo uploads it for you. ' +
62
+ 'Call `media_upload_widget` so the user picks the file (claude.ai web/mobile), or `create_upload_ticket` and POST the file yourself if you can run shell/HTTP commands; ' +
63
+ 'either way you get an https:// URL to pass here. Ignore any "absolute local path" wording in the args below — this server cannot read the caller\'s disk, so a path will fail.';
64
+
65
+ const LOCAL_FILE_HINT =
66
+ ' LOCAL FILE? Absolute local paths work here (server and client share a filesystem). ' +
67
+ 'Never reply that you cannot upload files — for a file you will reference more than once, call `upload_media` first and reuse the returned https:// URL.';
68
+
69
+ /**
70
+ * Append the transport-correct local-file route to every media-input tool's
71
+ * description, post-registration (same pattern as attachToolWidgetMeta).
72
+ * `options.apps === true` is set ONLY by kolbo-api's remote per-request server,
73
+ * so it is a transport signal — not `appsEnabled()`, which is also true for
74
+ * stdio hosts that render widgets but CAN still read local paths.
75
+ */
76
+ function attachFileInputHints(server, options = {}) {
77
+ const hint = options.apps === true ? REMOTE_FILE_HINT : LOCAL_FILE_HINT;
78
+ const registered = server._registeredTools || {};
79
+ for (const name of FILE_INPUT_TOOLS) {
80
+ const t = registered[name];
81
+ if (t && typeof t.description === 'string' && !t.description.includes(hint)) {
82
+ t.description += hint;
83
+ }
84
+ }
85
+ }
86
+
87
+ // Per-kind upload caps advertised to callers when the ticket endpoint omits them.
88
+ // Mirrors MAX_MB in kolbo-api src/modules/mcpConnector/upload.js.
89
+ const DEFAULT_MAX_FILE_MB = { image: 50, video: 500, audio: 200, document: 50 };
90
+
33
91
  function isHttpUrl(s) {
34
92
  return typeof s === 'string' && /^https?:\/\//i.test(s);
35
93
  }
@@ -184,13 +242,16 @@ async function resolveToBuffer(source, kind, opts = {}) {
184
242
  }
185
243
 
186
244
  if (!path.isAbsolute(source)) {
245
+ // `path.isAbsolute` is platform-specific: on a POSIX server (every remote
246
+ // connector deployment) a valid Windows path like `C:\Users\...` or
247
+ // `\\server\share\...` returns false, so "must be absolute" is a lie that
248
+ // sends the caller off retrying slash variants. `path.win32.isAbsolute`
249
+ // answers the same question with Node's own grammar.
187
250
  throw new Error(
188
- `Local file paths must be absolute: ${source}. ` +
189
- `If you are using Kolbo over a remote connector (e.g. claude.ai), local files are not reachable. ` +
190
- `DO NOT upload the file yourself with cloud credentials or a shell command — Kolbo has a tool for this. ` +
191
- `Call \`media_upload_widget\` to have the user pick the file (remote connectors), or \`upload_media\` ` +
192
- `when the file IS reachable from where the MCP server runs, then pass the returned https:// URL here. ` +
193
- `A URL from \`list_media\` also works if the asset is already in the library.`
251
+ (path.win32.isAbsolute(source)
252
+ ? `This Kolbo server cannot read files off the calling machine, so the Windows path ${source} is unreachable from here. `
253
+ : `Local file paths must be absolute: ${source}. `) +
254
+ LOCAL_FILE_ROUTING
194
255
  );
195
256
  }
196
257
  let stat;
@@ -199,11 +260,7 @@ async function resolveToBuffer(source, kind, opts = {}) {
199
260
  } catch (err) {
200
261
  throw new Error(
201
262
  `Local file not found or unreadable: ${source}. ` +
202
- `If you are using Kolbo over a remote connector (e.g. claude.ai), local file paths are not reachable. ` +
203
- `DO NOT upload the file yourself with cloud credentials or a shell command — Kolbo has a tool for this. ` +
204
- `Call \`media_upload_widget\` to have the user pick the file (remote connectors), or \`upload_media\` ` +
205
- `when the file IS reachable from where the MCP server runs, then pass the returned https:// URL here. ` +
206
- `A URL from \`list_media\` also works if the asset is already in the library.` +
263
+ LOCAL_FILE_ROUTING +
207
264
  (err && err.code ? ` [${err.code}]` : '')
208
265
  );
209
266
  }
@@ -498,9 +555,78 @@ async function uiCompleted(p, textPayload) {
498
555
  return uiResult(UI.generation, textPayload, structured);
499
556
  }
500
557
 
558
+ // ─── Text-payload budget ─────────────────────────────────────────────────────
559
+ //
560
+ // Whatever a tool returns as TEXT is what the model actually reads, and hosts
561
+ // reject or spill-to-disk anything much past this. A list tool that dumps every
562
+ // field of every row blows it instantly: list_presets measured 632,919 chars,
563
+ // get_stock_categories 257,817, list_voices 190,286 — all pretty-printed with
564
+ // `JSON.stringify(x, null, 2)` and no cap. The widget path already slims rows to
565
+ // id/title/thumbnail; the text path has to do the same or the tool is unusable
566
+ // on exactly the text hosts (Claude Code, Cursor, Codex) that depend on it.
567
+ const MAX_TEXT_CHARS = 20000;
568
+
569
+ /**
570
+ * Build a compact text payload for a list-shaped result.
571
+ *
572
+ * @param {object[]} items rows from the API
573
+ * @param {object} opts
574
+ * @param {string[]} opts.fields keys to keep per row, in order (others dropped)
575
+ * @param {number} [opts.cap] max rows to include (default 50)
576
+ * @param {number} [opts.total] true total, so the model knows more exist
577
+ * @param {object} [opts.extra] extra top-level keys to merge in
578
+ * @param {string} [opts.note] guidance on how to fetch the rest
579
+ */
580
+ function compactList(items, { fields, cap = 50, total, extra, note } = {}) {
581
+ const rows = Array.isArray(items) ? items : [];
582
+ const kept = rows.slice(0, cap).map((row) => {
583
+ if (!row || typeof row !== 'object') return row;
584
+ const out = {};
585
+ for (const f of fields || Object.keys(row)) {
586
+ // Drop empties — a row of nulls costs tokens and tells the model nothing.
587
+ if (row[f] !== undefined && row[f] !== null && row[f] !== '') out[f] = row[f];
588
+ }
589
+ return out;
590
+ });
591
+
592
+ const payload = { ...(extra || {}), items: kept, count: kept.length };
593
+ if (total != null) payload.total = total;
594
+ const omitted = Math.max(rows.length - kept.length, 0);
595
+ if (omitted > 0 || (total != null && total > kept.length)) {
596
+ payload._truncated = {
597
+ omitted_from_this_page: omitted,
598
+ hint: note || 'Narrow with the tool\'s filter args, or page for more.',
599
+ };
600
+ }
601
+
602
+ let text = JSON.stringify(payload);
603
+ if (text.length > MAX_TEXT_CHARS) {
604
+ // Still too big even trimmed (very long descriptions). Halve until it fits
605
+ // rather than returning something the host will truncate at a random byte.
606
+ let n = kept.length;
607
+ while (n > 1 && text.length > MAX_TEXT_CHARS) {
608
+ n = Math.floor(n / 2);
609
+ payload.items = kept.slice(0, n);
610
+ payload.count = n;
611
+ payload._truncated = {
612
+ omitted_from_this_page: rows.length - n,
613
+ hint: note || 'Result was too large for one response; narrow with filter args.',
614
+ };
615
+ text = JSON.stringify(payload);
616
+ }
617
+ }
618
+ return text;
619
+ }
620
+
501
621
  module.exports = {
502
622
  MAX_FILE_BYTES,
623
+ MAX_TEXT_CHARS,
624
+ compactList,
503
625
  VISUAL_DNA_MAX_BYTES,
626
+ LOCAL_FILE_ROUTING,
627
+ FILE_INPUT_TOOLS,
628
+ attachFileInputHints,
629
+ DEFAULT_MAX_FILE_MB,
504
630
  isHttpUrl,
505
631
  assertSafeUrl,
506
632
  safeFetch,
@@ -4,26 +4,44 @@
4
4
  * new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
5
5
 
6
6
  const { z } = require('zod');
7
+ const { resolveToBuffer } = require('./_shared');
8
+
9
+ // An HTML/SVG/Mermaid document is text — cap well below the media limit.
10
+ const MAX_ARTIFACT_BYTES = 5 * 1024 * 1024;
7
11
 
8
12
  function registerArtifactTools(server, client) {
9
13
  // ─── publish_html_artifact ─────────────────────────────────────
10
14
  server.tool(
11
15
  '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). To update a previously-published page in place (keeping the same URL), pass the `share_token` returned from the prior publish — the old content is preserved in version history.',
16
+ '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. Pass `file_path` instead of `content` whenever the artifact already exists as a file — the server reads it, which avoids re-emitting the whole document into the tool call. Identical content uploaded twice returns the same URL (server dedup). To update a previously-published page in place (keeping the same URL), pass the `share_token` returned from the prior publish — the old content is preserved in version history.',
13
17
  {
14
18
  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.'),
19
+ content: z.string().optional().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. Omit this when passing `file_path`.'),
20
+ file_path: z.string().optional().describe('ALTERNATIVE to `content`: an ABSOLUTE local path (or https:// URL) to the artifact file. Strongly preferred for anything sizeable — the server reads the file itself, so you do not have to re-emit the whole document into this tool call. Local paths only work when the file is reachable from where the MCP server runs (local stdio installs), not over a remote connector.'),
16
21
  type: z.enum(['html', 'svg', 'mermaid']).optional().describe('Artifact type. Default: "html".'),
17
22
  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
23
  share_token: z.string().optional().describe('Optional. Pass the `shareToken` returned from a previous publish to update that artifact in place. The public URL stays the same and the old content is moved into version history. Omit this on the first publish.'),
19
24
  },
20
- async ({ title, content, type, allow_js, share_token }) => {
25
+ async ({ title, content, file_path, type, allow_js, share_token }) => {
21
26
  if (!title || !title.trim()) throw new Error('title is required');
22
- if (typeof content !== 'string' || !content.length) throw new Error('content is required');
27
+
28
+ const hasContent = typeof content === 'string' && content.length > 0;
29
+ const hasPath = typeof file_path === 'string' && file_path.trim().length > 0;
30
+ if (!hasContent && !hasPath) throw new Error('one of `content` or `file_path` is required');
31
+ if (hasContent && hasPath) throw new Error('pass either `content` or `file_path`, not both');
32
+
33
+ let body_content = content;
34
+ if (hasPath) {
35
+ // resolveToBuffer gives us the absolute-path check, the SSRF guard for
36
+ // https:// sources, and the remote-connector error message for free.
37
+ const { buffer } = await resolveToBuffer(file_path.trim(), 'html', { maxBytes: MAX_ARTIFACT_BYTES });
38
+ body_content = buffer.toString('utf8');
39
+ if (!body_content.trim()) throw new Error(`File is empty: ${file_path}`);
40
+ }
23
41
 
24
42
  const body = {
25
43
  title: title.trim(),
26
- content,
44
+ content: body_content,
27
45
  type: type || 'html',
28
46
  allowJs: allow_js === true,
29
47
  };
package/src/tools/chat.js CHANGED
@@ -18,11 +18,11 @@ function registerChatTools(server, client) {
18
18
  system_prompt: z.string().optional().describe('System prompt for the conversation. Only applied when creating a new session.'),
19
19
  web_search: z.boolean().optional().describe('Enable web search for this message. Default: false'),
20
20
  deep_think: z.boolean().optional().describe('Enable deep think (extended reasoning). Default: false'),
21
- enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
22
- media_urls: z.array(z.string()).optional().describe('Public URLs of images, videos, or audio files to analyze. The model auto-routes to a vision-capable model when media is present. Use upload_media first to get a public URL for local files.'),
21
+ enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
22
+ media_urls: z.array(z.string()).optional().describe('Public URLs of images, videos, or audio files to analyze. The model auto-routes to a vision-capable model when media is present. For a local file, get a URL first via the LOCAL FILE route in this tool\'s description.'),
23
23
  project_id: projectIdField
24
24
  },
25
- async ({ message, model, session_id, system_prompt, web_search, deep_think, enhance_prompt, media_urls, project_id }) => {
25
+ async ({ message, model, session_id, system_prompt, web_search, deep_think, enhance_prompt = false, media_urls, project_id }) => {
26
26
  const gen = await client.post('/v1/chat', {
27
27
  message,
28
28
  model,
@@ -54,7 +54,7 @@ function registerGenerateTools(server, client, options = {}) {
54
54
  prompt: z.string().describe('Text description of the image to generate'),
55
55
  model: z.string().optional().describe('Model identifier — REQUIRED in practice: pick a specific model, do NOT omit (omitting = Smart Select auto-pick, which we avoid). Strong current defaults: "nano-banana-2" (versatile, text rendering, multilingual) or "gpt-image-2" (photoreal, infographics). Call list_models type="text_to_img" to see all options and pick per the user\'s intent.'),
56
56
  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"'),
57
- enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: true'),
57
+ enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
58
58
  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.'),
59
59
  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.'),
60
60
  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.'),
@@ -67,7 +67,7 @@ function registerGenerateTools(server, client, options = {}) {
67
67
  skip_color_palette: z.boolean().optional().describe('Opt this single call OUT of the account\'s active Color DNA palette (see list_color_palettes / activate_color_palette). By default, if the user has an active palette it strict-grades every generation automatically — pass true only when the user explicitly wants this one image ungraded.'),
68
68
  project_id: projectIdField
69
69
  },
70
- async ({ prompt, model, aspect_ratio, enhance_prompt, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id }) => {
70
+ async ({ prompt, model, aspect_ratio, enhance_prompt = false, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id }) => {
71
71
  model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
72
72
  const gen = await client.post('/v1/generate/image', {
73
73
  prompt, model, aspect_ratio, enhance_prompt, num_images,
@@ -112,7 +112,7 @@ function registerGenerateTools(server, client, options = {}) {
112
112
  model: z.string().optional().describe('Model identifier — REQUIRED in practice: pick a specific IMAGE-EDITING model, do NOT omit (omitting = Smart Select auto-pick, which we avoid). Strong current defaults: "nano-banana-pro/edit" (best general prompt editor), "gpt-image/1.5-image-to-image" (photoreal), or "flux-2/edit". NOTE: text-to-image ids like "nano-banana-2"/"gpt-image-2" are NOT editors — don\'t use them here. Call list_models type="image_editing" to see all options and pick per the user\'s intent.'),
113
113
  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.'),
114
114
  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"'),
115
- enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: true'),
115
+ enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
116
116
  num_images: z.number().optional().describe('Number of output images. Default: 1'),
117
117
  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.'),
118
118
  moodboard_id: z.string().optional().describe('Moodboard ID whose master_prompt and style_guide should be applied.'),
@@ -122,7 +122,7 @@ function registerGenerateTools(server, client, options = {}) {
122
122
  skip_color_palette: z.boolean().optional().describe('Opt this single call OUT of the account\'s active Color DNA palette (see list_color_palettes / activate_color_palette). By default, if the user has an active palette it strict-grades every generation automatically — pass true only when the user explicitly wants this one edit ungraded.'),
123
123
  project_id: projectIdField
124
124
  },
125
- async ({ prompt, model, source_images, aspect_ratio, enhance_prompt, num_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, cinematic, skip_color_palette, project_id }) => {
125
+ async ({ prompt, model, source_images, aspect_ratio, enhance_prompt = false, num_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, cinematic, skip_color_palette, project_id }) => {
126
126
  model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
127
127
  const gen = await client.post('/v1/generate/image-edit', {
128
128
  prompt, model, source_images, aspect_ratio, enhance_prompt, num_images,
@@ -173,7 +173,7 @@ function registerGenerateTools(server, client, options = {}) {
173
173
  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"'),
174
174
  workflow_type: z.string().optional().describe('"image" (default) or "video"'),
175
175
  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.'),
176
- enhance_prompt: z.boolean().optional().describe('Enhance prompts per scene. Default: true'),
176
+ enhance_prompt: z.boolean().optional().describe('Enhance prompts per scene. Default: false — only pass true if the user explicitly asks to enhance/improve the prompts.'),
177
177
  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.**'),
178
178
  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.'),
179
179
  moodboard_id: z.string().optional().describe('A single moodboard ID whose master_prompt and style_guide should shape every scene.'),
@@ -181,7 +181,7 @@ function registerGenerateTools(server, client, options = {}) {
181
181
  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.'),
182
182
  project_id: projectIdField
183
183
  },
184
- async ({ prompt, scene_count, model, aspect_ratio, workflow_type, duration, enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution, project_id }) => {
184
+ async ({ prompt, scene_count, model, aspect_ratio, workflow_type, duration, enhance_prompt = false, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution, project_id }) => {
185
185
  model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
186
186
  const gen = await client.post('/v1/generate/creative-director', {
187
187
  prompt, scene_count, model, aspect_ratio, workflow_type, duration,
@@ -324,7 +324,7 @@ function registerGenerateTools(server, client, options = {}) {
324
324
  model: z.string().optional().describe('Model identifier — pick a SPECIFIC model, do NOT omit (omitting = Smart Select auto-pick, which we avoid). Strong current defaults: "seedance-2" (versatile) or "veo3" (Veo 3.1, cinematic + native audio); the Kling family (call list_models for exact ids like kling-video/v3/pro/text-to-video) is strongest for motion. Call list_models type="text_to_video" to see all options + check supported_durations / supported_aspect_ratios, and choose per the user\'s intent.'),
325
325
  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"'),
326
326
  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'),
327
- enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
327
+ enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
328
328
  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.**'),
329
329
  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.'),
330
330
  preset_id: z.string().optional().describe('Preset ID from list_presets type="video" to apply a saved motion/style preset to this generation.'),
@@ -332,7 +332,7 @@ function registerGenerateTools(server, client, options = {}) {
332
332
  skip_color_palette: z.boolean().optional().describe('Opt this single call OUT of the account\'s active Color DNA palette (see list_color_palettes / activate_color_palette). By default, if the user has an active palette it strict-grades every generation automatically — pass true only when the user explicitly wants this one video ungraded.'),
333
333
  project_id: projectIdField
334
334
  },
335
- async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, sound_enabled, skip_color_palette, project_id }) => {
335
+ async ({ prompt, model, aspect_ratio, duration, enhance_prompt = false, reference_images, resolution, preset_id, sound_enabled, skip_color_palette, project_id }) => {
336
336
  model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
337
337
  const gen = await client.post('/v1/generate/video', {
338
338
  prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, sound_enabled, skip_color_palette, project_id
@@ -381,14 +381,14 @@ function registerGenerateTools(server, client, options = {}) {
381
381
  model: z.string().optional().describe('Model identifier — pick a SPECIFIC model, do NOT omit (omitting = Smart Select auto-pick, which we avoid). Strong current defaults: "seedance-2" (versatile) or "veo3" (Veo 3.1, cinematic + native audio); the Kling family (call list_models for exact ids like kling-video/v3/pro/image-to-video) is strongest for motion. Call list_models type="img_to_video" to see all options and choose per the user\'s intent.'),
382
382
  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"'),
383
383
  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'),
384
- enhance_prompt: z.boolean().optional().describe('Enhance the motion prompt. Default: true'),
384
+ enhance_prompt: z.boolean().optional().describe('Enhance the motion prompt. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
385
385
  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.**'),
386
386
  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.'),
387
387
  sound_enabled: z.boolean().optional().describe('Enable (`true`) or disable (`false`) AI-generated synced audio on the output video. Only honored by models with `sound_generation_type: "native"` from list_models (e.g. Veo 3.1 Lite, Kling V3 4K, PixVerse V6, Kling 2.6/v3). On `sound_generation_type: "none"` models the flag has no effect. Omit to use the model\'s `sound_enabled_by_default`. Pass `false` when the user says no sound / silent / mute / without audio. Enabling sound may apply `sound_credit_multiplier` to cost.'),
388
388
  skip_color_palette: z.boolean().optional().describe('Opt this single call OUT of the account\'s active Color DNA palette (see list_color_palettes / activate_color_palette). By default, if the user has an active palette it strict-grades every generation automatically — pass true only when the user explicitly wants this one video ungraded.'),
389
389
  project_id: projectIdField
390
390
  },
391
- async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled, skip_color_palette, project_id }) => {
391
+ async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt = false, visual_dna_ids, resolution, sound_enabled, skip_color_palette, project_id }) => {
392
392
  model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
393
393
  const gen = await client.post('/v1/generate/video/from-image', {
394
394
  image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled, skip_color_palette, project_id
@@ -439,7 +439,7 @@ function registerGenerateTools(server, client, options = {}) {
439
439
  vocal_gender: z.string().optional().describe('Preferred vocal gender: "male" or "female". Only applies when instrumental is false.'),
440
440
  negative_tags: z.string().optional().describe('Styles / sounds to EXCLUDE, comma-separated (e.g. "heavy metal, screaming, distortion"). Suno.'),
441
441
  duration_seconds: z.number().optional().describe('Target song length in seconds (length-capable models like ElevenLabs Music). Clamped 5–300. Omit for the model default.'),
442
- enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
442
+ enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
443
443
  preset_id: z.string().optional().describe('Preset ID from list_presets type="music" to apply a saved music style preset.'),
444
444
  // ── Suno fine controls ──
445
445
  style_weight: z.number().optional().describe('Suno: how strongly the style/genre is applied, 0–1.'),
@@ -451,7 +451,7 @@ function registerGenerateTools(server, client, options = {}) {
451
451
  singing_voice_id: z.string().optional().describe('Custom cloned singing-voice id (must be owned by the caller).'),
452
452
  project_id: projectIdField
453
453
  },
454
- async ({ prompt, model, style, title, instrumental, lyrics, vocal_gender, negative_tags, duration_seconds, enhance_prompt, preset_id, style_weight, weirdness, audio_weight, persona_id, use_composition_plan, singing_dna_id, singing_voice_id, project_id }) => {
454
+ async ({ prompt, model, style, title, instrumental, lyrics, vocal_gender, negative_tags, duration_seconds, enhance_prompt = false, preset_id, style_weight, weirdness, audio_weight, persona_id, use_composition_plan, singing_dna_id, singing_voice_id, project_id }) => {
455
455
  model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
456
456
  const gen = await client.post('/v1/generate/music', {
457
457
  prompt, model, style, title, instrumental, lyrics, vocal_gender, negative_tags,
@@ -757,18 +757,19 @@ function registerGenerateTools(server, client, options = {}) {
757
757
  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. Pick a SPECIFIC model — do NOT omit (omitting = Smart Select auto-pick, which we avoid); call list_models for this type and choose the model that best fits the user\'s intent.'),
758
758
  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.**'),
759
759
  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.**'),
760
+ reference_audio_urls: z.array(z.string()).optional().describe('Array of reference audio URLs for models that accept audio inputs. **Cap: pass at most `elements_max_audio` URLs from list_models.** `audio_url` remains supported as the legacy single-track form.'),
760
761
  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).**'),
761
762
  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`.**'),
762
763
  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'),
763
764
  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"'),
764
765
  motion: z.string().optional().describe('Motion style / intensity hint (optional)'),
765
766
  preset_id: z.string().optional().describe('Preset ID from list_presets type="video" (optional)'),
766
- enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
767
+ enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
767
768
  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.**'),
768
769
  resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.'),
769
770
  project_id: projectIdField
770
771
  },
771
- async ({ prompt, model, reference_images, reference_videos, audio_url, files, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution, project_id }) => {
772
+ async ({ prompt, model, reference_images, reference_videos, reference_audio_urls, audio_url, files, duration, aspect_ratio, motion, preset_id, enhance_prompt = false, visual_dna_ids, resolution, project_id }) => {
772
773
  model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
773
774
  if (!prompt) throw new Error('prompt is required');
774
775
 
@@ -787,6 +788,7 @@ function registerGenerateTools(server, client, options = {}) {
787
788
  if (visual_dna_ids) form.append('visual_dna_ids', JSON.stringify(visual_dna_ids));
788
789
  if (reference_images) form.append('reference_images', JSON.stringify(reference_images));
789
790
  if (reference_videos) form.append('reference_videos', JSON.stringify(reference_videos));
791
+ if (reference_audio_urls) form.append('reference_audio_urls', JSON.stringify(reference_audio_urls));
790
792
  if (audio_url) form.append('audio_url', audio_url);
791
793
  if (resolution) form.append('resolution', resolution);
792
794
  if (project_id) form.append('project_id', project_id);
@@ -797,7 +799,7 @@ function registerGenerateTools(server, client, options = {}) {
797
799
  } else {
798
800
  // URL-only mode: plain JSON.
799
801
  startResponse = await client.post('/v1/generate/elements', {
800
- prompt, model, reference_images, reference_videos, audio_url, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution, project_id
802
+ prompt, model, reference_images, reference_videos, reference_audio_urls, audio_url, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution, project_id
801
803
  });
802
804
  }
803
805
 
@@ -842,12 +844,12 @@ function registerGenerateTools(server, client, options = {}) {
842
844
  model: z.string().optional().describe('Model identifier. Use list_models type="firstlastgenerations" to see options. Pick a SPECIFIC model — do NOT omit (omitting = Smart Select auto-pick, which we avoid); call list_models for this type and choose the model that best fits the user\'s intent.'),
843
845
  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'),
844
846
  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"'),
845
- enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
847
+ enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
846
848
  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.**'),
847
849
  resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.'),
848
850
  project_id: projectIdField
849
851
  },
850
- async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids, resolution, project_id }) => {
852
+ async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt = false, visual_dna_ids, resolution, project_id }) => {
851
853
  model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
852
854
  const urlMode = first_frame_url && last_frame_url;
853
855
  const fileMode = first_frame && last_frame;
@@ -1032,7 +1034,7 @@ function registerGenerateTools(server, client, options = {}) {
1032
1034
  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. Pick a SPECIFIC model — do NOT omit (omitting = Smart Select auto-pick, which we avoid); call list_models for this type and choose the model that best fits the user\'s intent.'),
1033
1035
  aspect_ratio: z.string().optional().describe('Output aspect ratio. Must be in `supported_aspect_ratios` from list_models when set. Default: matches source'),
1034
1036
  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'),
1035
- enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
1037
+ enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
1036
1038
  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.**'),
1037
1039
  resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.'),
1038
1040
  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.'),
@@ -1058,7 +1060,7 @@ function registerGenerateTools(server, client, options = {}) {
1058
1060
  }).optional().describe('VEED Subtitles only: style overrides. Any omitted field keeps the preset default. Best supported by Basic presets.'),
1059
1061
  project_id: projectIdField
1060
1062
  },
1061
- async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, reference_images, reference_videos, elements, preset, source_language, translation_language, srt_content, srt_file_url, vocabulary, customization, project_id }) => {
1063
+ async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt = false, visual_dna_ids, resolution, reference_images, reference_videos, elements, preset, source_language, translation_language, srt_content, srt_file_url, vocabulary, customization, project_id }) => {
1062
1064
  model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
1063
1065
  if (!source_video) throw new Error('source_video is required');
1064
1066