@kolbo/mcp 1.87.14 → 1.88.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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @kolbo/mcp
2
2
 
3
+ ## Personal fonts
4
+
5
+ Browse **My fonts** with `list_fonts({source:"custom"})` (the default), or the read-only **Font collection** with `source:"global"`. Use `source:"all"` to search both. Mix up to three family IDs across both sources in the same generation; inspect actual styles/scripts with `get_font`. Collection fonts do not need uploading and cannot be renamed or deleted by users.
6
+
7
+ Use `list_fonts` / `get_font` to discover My Fonts. Local stdio clients upload one OTF/TTF/WOFF2 (up to 5 MiB) with `upload_font`; remote shell clients use `create_font_upload_ticket`, and browser clients use `font_upload_widget`. Poll `get_font_upload_status` until ready, then pass the returned family ID in `font_ids` to image creation/editing or image-mode Creative Director. `rename_font` and `delete_font` manage the library.
8
+
9
+ Only models reporting `supports_custom_fonts: true` accept these selections. Fonts and backend-generated specimens never go through media upload. See [Personal Fonts](https://docs.kolbo.ai/developer-api/personal-fonts). Availability requires a published client and deployed backend containing these tools; source changes alone do not update installed clients.
10
+
3
11
  Use [Kolbo AI](https://kolbo.ai) as native tools in Claude Code and Claude Desktop via MCP (Model Context Protocol).
4
12
 
5
13
  Generate images, videos, music, speech, sound effects, multi-scene campaigns, and conversational chat — all from natural language in your coding environment. 100+ AI models behind Smart Select routing, with reusable Visual DNA profiles for character/style consistency.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.87.14",
3
+ "version": "1.88.0",
4
4
  "description": "Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/apps/index.js CHANGED
@@ -21,6 +21,7 @@ const { mediaGridWidgetHtml } = require('./widgets/mediaGrid');
21
21
  const { catalogWidgetHtml } = require('./widgets/catalog');
22
22
  const { transcriptWidgetHtml } = require('./widgets/transcript');
23
23
  const { uploadWidgetHtml } = require('./widgets/upload');
24
+ const { fontUploadWidgetHtml } = require('./widgets/fontUpload');
24
25
  const { listWidgetHtml } = require('./widgets/list');
25
26
  const { HOST_MAP } = require('../cdn');
26
27
  const { plansWidgetHtml } = require('./widgets/plans');
@@ -31,6 +32,7 @@ const UI = {
31
32
  catalog: 'ui://kolbo/catalog.html',
32
33
  transcript: 'ui://kolbo/transcript.html',
33
34
  upload: 'ui://kolbo/upload.html',
35
+ fontUpload: 'ui://kolbo/font-upload.html',
34
36
  list: 'ui://kolbo/list.html',
35
37
  plans: 'ui://kolbo/plans.html',
36
38
  };
@@ -41,6 +43,7 @@ const WIDGET_BUILDERS = {
41
43
  [UI.catalog]: catalogWidgetHtml,
42
44
  [UI.transcript]: transcriptWidgetHtml,
43
45
  [UI.upload]: uploadWidgetHtml,
46
+ [UI.fontUpload]: fontUploadWidgetHtml,
44
47
  [UI.list]: listWidgetHtml,
45
48
  [UI.plans]: plansWidgetHtml,
46
49
  };
@@ -732,6 +735,7 @@ const TOOL_WIDGETS = {
732
735
  list_color_palettes: UI.mediaGrid,
733
736
  // upload widget
734
737
  media_upload_widget: UI.upload,
738
+ font_upload_widget: UI.fontUpload,
735
739
  // generic list widget — flat record lists with no natural thumbnail
736
740
  list_projects: UI.list,
737
741
  // Must stay list.html — mapping this to generation.html mounts "Kolbo Generation /
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+ const { widgetPage } = require('../html');
3
+ function fontUploadWidgetHtml() {
4
+ return widgetPage({ title: 'My Fonts', body: '<div class="k-card"><div class="k-head">Upload to My Fonts</div><div class="k-body"><p>One OTF, TTF or WOFF2 file · up to 5 MiB</p><input id="font" type="file" accept=".otf,.ttf,.woff2" aria-label="Choose font"><button id="external" type="button">Open browser upload</button><p id="status" role="status"></p></div></div>', script: `
5
+ var state;
6
+ window.kolbo.onToolResult(function(result) { state = result.structuredContent || structured(result); });
7
+ function endpoint() {
8
+ if (!state) throw new Error('Waiting for upload ticket.');
9
+ var url = new URL(state.upload_url);
10
+ if (url.protocol !== 'https:' || !['api.kolbo.ai','upload-api.kolbo.ai'].includes(url.host) || url.pathname !== '/api/v1/fonts/ticket-upload' || url.search || url.hash || url.username || url.password) throw new Error('Unsupported font upload endpoint. Use a shell upload ticket instead.');
11
+ if (!Number.isFinite(Date.parse(state.expires_at)) || Date.now() >= Date.parse(state.expires_at)) throw new Error('Ticket expired. Open a new font upload card.');
12
+ if (!/^[A-Za-z0-9_-]{43}$/.test(state.token)) throw new Error('Invalid upload ticket. Open a new card.');
13
+ return url;
14
+ }
15
+ el('external').onclick = async function() {
16
+ try {
17
+ var url = endpoint();
18
+ url.pathname = '/api/v1/fonts/upload-ui';
19
+ url.hash = 'ticket=' + encodeURIComponent(state.token);
20
+ await window.kolbo.openLink(url.href);
21
+ el('font').disabled = true;
22
+ this.disabled = true;
23
+ el('status').textContent = 'After uploading in the browser, return with the preparation ID shown there.';
24
+ } catch(error) { el('status').textContent = error.message; }
25
+ };
26
+ el('font').onchange = async function() {
27
+ var file = this.files[0];
28
+ if (!file || !state) return;
29
+ if (!/\\.(otf|ttf|woff2)$/i.test(file.name) || file.size > 5242880 || !file.size) { el('status').textContent = 'Choose a font up to 5 MiB.'; return; }
30
+ this.disabled = true;
31
+ el('external').disabled = true;
32
+ el('status').textContent = 'Uploading font…';
33
+ try {
34
+ var url = endpoint();
35
+ var body = new FormData(); body.append('file', file);
36
+ var response = await fetch(url.href, {method:'POST',headers:{Authorization:'Bearer '+state.token},body:body,credentials:'omit',redirect:'error',signal:AbortSignal.timeout(65000)});
37
+ var result = await response.json();
38
+ if (!response.ok) throw new Error(typeof result.error === 'string' ? result.error : 'Upload failed. Open a new font upload card to retry.');
39
+ el('status').textContent = 'Uploaded. Preparing font…';
40
+ await window.kolbo.updateModelContext('Font uploaded to My Fonts. Poll get_font_upload_status before generating: '+JSON.stringify(result.data));
41
+ } catch(error) { el('status').textContent = error.message; }
42
+ window.kolbo.notifySize();
43
+ };` });
44
+ }
45
+ module.exports = { fontUploadWidgetHtml };
package/src/index.js CHANGED
@@ -66,6 +66,7 @@ const { registerChatTools } = require('./tools/chat');
66
66
  const { registerVisualDnaTools } = require('./tools/visual_dna');
67
67
  const { registerMoodboardTools } = require('./tools/moodboards');
68
68
  const { registerColorPaletteTools } = require('./tools/color_palettes');
69
+ const { registerFontTools } = require('./tools/fonts');
69
70
  const { registerMediaTools } = require('./tools/media');
70
71
  const { registerPresetTools } = require('./tools/presets');
71
72
  const { registerArtifactTools } = require('./tools/artifacts');
@@ -115,6 +116,7 @@ function createServer(opts = {}) {
115
116
  // The single most common failure mode is project confusion — spell out
116
117
  // the project contract here so every client gets it without a skill file.
117
118
  instructions: [
119
+ 'PERSONAL FONTS: use list_fonts/get_font, upload_font for local stdio, create_font_upload_ticket for remote shell, or font_upload_widget for browser uploads. Never use media upload for fonts. Poll get_font_upload_status until ready, then pass up to three family IDs as font_ids to image creation/editing or image-mode Creative Director. Verify supports_custom_fonts in model discovery. The backend prepares private specimens; never render or attach specimens yourself.',
118
120
  'LOCAL FILES: never upload a user file with your own cloud credentials, an S3/Spaces script, or a third-party host — Kolbo owns this. ' + LOCAL_FILE_ROUTING,
119
121
  'PROMPT CONVENTIONS (Kolbo-specific — these change the OUTPUT, not just the metadata):',
120
122
  'A. Visual DNA: passing `visual_dna_ids` is not enough — every DNA in play must ALSO be tagged inside the prompt text as `@Name`, using the DNA name (e.g. "@Kobi walks into frame"). Moodboards are referenced the same way with `#Name`. Resolve names via `list_visual_dnas` / `list_moodboards`.',
@@ -183,6 +185,7 @@ function createServer(opts = {}) {
183
185
  registerVisualDnaTools(server, client, toolOptions);
184
186
  registerMoodboardTools(server, client, toolOptions);
185
187
  registerColorPaletteTools(server, client, toolOptions);
188
+ registerFontTools(server, client, { allowLocalFiles: opts.allowLocalFiles === true && !toolOptions.remote });
186
189
  registerAnalyzeTools(server, client, toolOptions);
187
190
  registerMediaTools(server, client, toolOptions);
188
191
  registerPresetTools(server, client, toolOptions);
@@ -219,7 +222,7 @@ function createServer(opts = {}) {
219
222
  }
220
223
 
221
224
  async function main() {
222
- const server = createServer();
225
+ const server = createServer({ allowLocalFiles: true });
223
226
 
224
227
  // Node kills the process on an unhandled rejection / uncaught exception. In a
225
228
  // long-lived stdio server that is not a stack trace the user ever sees — the
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  const READ_ONLY = [
12
+ 'list_fonts', 'get_font', 'get_font_upload_status',
12
13
  'get_creative_director_status', 'get_generation_status', 'list_models',
13
14
  'check_credits', 'show_plans', 'get_session_usage', 'list_voices',
14
15
  'chat_list_conversations', 'chat_get_messages',
@@ -36,6 +37,7 @@ const OPEN_WORLD_READ_ONLY = [
36
37
  ];
37
38
 
38
39
  const PRIVATE_WRITE = [
40
+ 'upload_font', 'rename_font', 'create_font_upload_ticket', 'font_upload_widget',
39
41
  'media_upload_widget', 'create_upload_ticket', 'upload_media',
40
42
  'favorite_media', 'unfavorite_media',
41
43
  'create_media_folder', 'update_media_folder',
@@ -64,6 +66,7 @@ const PRIVATE_WRITE = [
64
66
  ];
65
67
 
66
68
  const DESTRUCTIVE_WRITE = [
69
+ 'delete_font',
67
70
  // These actions spend credits, enqueue irreversible work, or cancel it.
68
71
  'generate_image', 'generate_image_edit', 'generate_creative_director',
69
72
  'generate_video', 'generate_video_from_image', 'generate_music',
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+ const { z } = require('zod');
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ const FormData = require('form-data');
6
+ const { UI, uiResult } = require('../apps');
7
+ const id = z.string().regex(/^[a-f0-9]{24}$/i);
8
+ const result = value => ({ content: [{ type: 'text', text: JSON.stringify(value) }] });
9
+
10
+ function registerFontTools(server, client, options = {}) {
11
+ server.tool('list_fonts', 'Browse My fonts or the curated Font collection using source. Use ready family IDs from either as font_ids; previews are authenticated, not public media.', {
12
+ source: z.enum(['custom', 'global', 'all']).optional().describe('custom (default): My fonts, global: read-only Font collection, all: both. IDs from either can be combined.'),
13
+ search: z.string().max(120).optional(), cursor: z.string().optional(), limit: z.number().int().min(1).max(50).optional(),
14
+ }, async options => result(await client.get('/v1/fonts?' + new URLSearchParams(Object.entries(options).filter(([, value]) => value !== undefined)))));
15
+ server.tool('get_font', 'Inspect a personal or collection font family by ID, including available styles, scripts, source and collection license.', { font_id: id }, async ({ font_id }) => result(await client.get(`/v1/fonts/${font_id}`)));
16
+ server.tool('get_font_upload_status', 'Poll a font upload until ready or failed. Use its font_id only after ready.', { upload_id: id }, async ({ upload_id }) => result(await client.get(`/v1/fonts/uploads/${upload_id}`)));
17
+ server.tool('rename_font', 'Rename a family in My Fonts.', { font_id: id, name: z.string().min(1).max(120) }, async ({ font_id, name }) => result(await client.patch(`/v1/fonts/${font_id}`, { name })));
18
+ server.tool('delete_font', 'Delete a personal font family from future use. Completed generated images are not deleted.', { font_id: id }, async ({ font_id }) => result(await client.delete(`/v1/fonts/${font_id}`)));
19
+ server.tool('upload_font', 'Upload one LOCAL OTF, TTF or WOFF2 font (up to 5 MiB) directly to My Fonts, not the media library. Local stdio only: remote clients must use create_font_upload_ticket or font_upload_widget. Poll get_font_upload_status until ready.', {
20
+ file_path: z.string(), language: z.string().max(32).optional(),
21
+ }, async ({ file_path, language }) => {
22
+ if (!options.allowLocalFiles) throw new Error('Remote font upload: use create_font_upload_ticket or font_upload_widget; server-local file access is disabled');
23
+ if (!path.isAbsolute(file_path) || !/\.(otf|ttf|woff2)$/i.test(file_path)) throw new Error('Choose an absolute OTF, TTF or WOFF2 path');
24
+ const handle = await fs.open(file_path, 'r');
25
+ try {
26
+ const stat = await handle.stat();
27
+ if (!stat.isFile() || stat.size < 1 || stat.size > 5 * 1024 * 1024) throw new Error('Font must be a regular file up to 5 MiB');
28
+ const form = new FormData();
29
+ const bytes = Buffer.alloc(stat.size);
30
+ let offset = 0;
31
+ while (offset < bytes.length) {
32
+ const read = await handle.read(bytes, offset, bytes.length - offset, offset);
33
+ if (!read.bytesRead) throw new Error('Font changed during upload; choose the file again');
34
+ offset += read.bytesRead;
35
+ }
36
+ form.append('file', bytes, { filename: path.basename(file_path), knownLength: bytes.length });
37
+ if (language) form.append('language', language);
38
+ return result(await client.postMultipart('/v1/fonts', form));
39
+ } finally { await handle.close(); }
40
+ });
41
+ server.tool('create_font_upload_ticket', 'Create a one-use, ten-minute ticket for a remote client with shell access. POST multipart field file to upload_url with Authorization: Bearer <ticket>. Never use upload_media for fonts. Poll returned upload ID with get_font_upload_status.', {}, async () => result(await client.post('/v1/fonts/upload-tickets', {})));
42
+ server.tool('font_upload_widget', 'Show a dedicated font upload picker for browser-only clients. Uploads ONE OTF/TTF/WOFF2 up to 5 MiB into My Fonts. Create another widget for another file or retry. Poll returned upload ID until ready.', {}, async () => {
43
+ const { data } = await client.post('/v1/fonts/upload-tickets', {});
44
+ return uiResult(UI.fontUpload, 'Choose a font in the upload card, then poll get_font_upload_status with the returned upload ID.', {
45
+ widget: 'font-upload', upload_url: data.upload_url, token: data.ticket, expires_at: data.expires_at,
46
+ });
47
+ });
48
+ }
49
+ module.exports = { registerFontTools };
@@ -162,6 +162,7 @@ const ELEMENTS_MAX_UPLOAD_BYTES = 200 * 1024 * 1024;
162
162
  // the card, and not the agent reading the tool result, so a follow-up call
163
163
  // could not reuse the same DNA or preset without re-listing. Carry the ids.
164
164
  const refSettings = (a = {}) => ({
165
+ font_ids: a.font_ids?.length ? a.font_ids : undefined,
165
166
  enhance_prompt: a.enhance_prompt || undefined,
166
167
  web_search: a.enable_web_search || undefined,
167
168
  visual_dna_ids: (a.visual_dna_ids && a.visual_dna_ids.length) ? a.visual_dna_ids : undefined,
@@ -258,6 +259,7 @@ function registerGenerateTools(server, client, options = {}) {
258
259
  {
259
260
  prompt: z.string().optional().describe('Text description of the image to generate. Required unless `prompts` is provided.'),
260
261
  prompts: promptsField('images'),
262
+ font_ids: z.array(z.string().regex(/^[a-f0-9]{24}$/i)).max(3).optional().describe('Ready family IDs from My Fonts (list_fonts/upload_font). Requires supports_custom_fonts=true. State exact text/language/weight in prompt; server prepares private references. Applies to every prompt in a batch.'),
261
263
  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.'),
262
264
  aspect_ratio: z.string().optional().describe(aspectRatioDescribe('1:1')),
263
265
  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.'),
@@ -274,12 +276,12 @@ function registerGenerateTools(server, client, options = {}) {
274
276
  project_id: projectIdField,
275
277
  session_id: sessionIdField
276
278
  },
277
- async ({ prompt, prompts, 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, session_id }) => {
279
+ async ({ prompt, prompts, 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, session_id, font_ids }) => {
278
280
  if (!prompt && !(prompts && prompts.length)) throw new Error('Provide prompt or prompts');
279
281
  model = await canonicalModelId(client, model, 'text_to_img'); // lenient id resolution ("z-image" → "z-image/turbo")
280
282
  aspect_ratio = await resolveCatalogAspectRatio(client, model, aspect_ratio, 'text_to_img');
281
283
  const shared = {
282
- model, aspect_ratio, enhance_prompt,
284
+ model, aspect_ratio, enhance_prompt, font_ids,
283
285
  reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id, session_id
284
286
  };
285
287
 
@@ -342,6 +344,7 @@ function registerGenerateTools(server, client, options = {}) {
342
344
  {
343
345
  prompt: z.string().optional().describe('Description of the edit to apply (e.g., "remove the background", "change the sky to sunset"). Required unless `prompts` is provided.'),
344
346
  prompts: promptsField('edits of the SAME source images'),
347
+ font_ids: z.array(z.string().regex(/^[a-f0-9]{24}$/i)).max(3).optional().describe('Ready My Fonts family IDs. Requires supports_custom_fonts=true. Describe requested typography changes; unrelated text is preserved. Applies to every batch edit.'),
345
348
  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). Many text-to-image ids double as editors: the server auto-routes a base id to its editing variant when source_images is present (e.g. "gpt-image-2" → gpt-image-2/edit, "nano-banana-2" → nano-banana-2-image-editing) — passing the bare id is fine, no need to hunt for the "/edit" suffix yourself. BUT this only works for models that actually have a registered edit variant. For prompt-driven photoreal photo edits (object removal, keep-this-person/remove-the-rest, crowd cleanup, inpainting) the ONLY auto-pick defaults are "nano-banana-2" or "gpt-image-2" (use GPT Image 2 when the image needs readable text). Do NOT auto-pick Flux 2 / flux-2/edit / Flux Klein — those are generate-from-scratch / style models; use them only if the user names Flux. If unsure, confirm the model appears in `list_models type="image_editing"` and choose by the strengths summary — Flux edit variants are named-only.'),
346
349
  source_images: z.array(z.string()).describe('PIXEL-ACCURATE compositing. Array of source images (URLs or absolute local paths) 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.'),
347
350
  reference_images: z.array(z.string()).optional().describe('STYLE/COMPOSITION inspiration, alongside `source_images` on the same call — does NOT embed reference pixels. Use when the edit should follow a look sampled from other images ("re-light this shot like these references"). The pixels that must survive the edit go in `source_images`; these only steer the look. **Cap: `source_images` + `reference_images` together must not exceed `max_reference_images` from list_models for the chosen model.**'),
@@ -359,12 +362,12 @@ function registerGenerateTools(server, client, options = {}) {
359
362
  project_id: projectIdField,
360
363
  session_id: sessionIdField
361
364
  },
362
- async ({ prompt, prompts, model, source_images, reference_images, aspect_ratio, enhance_prompt = false, num_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id, session_id }) => {
365
+ async ({ prompt, prompts, model, source_images, reference_images, aspect_ratio, enhance_prompt = false, num_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id, session_id, font_ids }) => {
363
366
  if (!prompt && !(prompts && prompts.length)) throw new Error('Provide prompt or prompts');
364
367
  model = await canonicalModelId(client, model, 'image_editing'); // lenient id resolution ("z-image" → "z-image/turbo")
365
368
  aspect_ratio = await resolveCatalogAspectRatio(client, model, aspect_ratio, 'image_editing');
366
369
  const shared = {
367
- model, source_images, reference_images, aspect_ratio, enhance_prompt,
370
+ model, source_images, reference_images, aspect_ratio, enhance_prompt, font_ids,
368
371
  visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id, session_id
369
372
  };
370
373
  const settings = imageSettings(shared);
@@ -444,13 +447,14 @@ function registerGenerateTools(server, client, options = {}) {
444
447
  moodboard_id: z.string().optional().describe('A single moodboard ID whose master_prompt and style_guide should shape every scene.'),
445
448
  moodboard_ids: z.array(z.string()).optional().describe('Multiple moodboard IDs when blending styles. Prefer `moodboard_id` for single moodboards.'),
446
449
  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.'),
450
+ font_ids: z.array(z.string().regex(/^[a-f0-9]{24}$/i)).max(3).optional().describe('Image workflows only: ready My Fonts family IDs applied to each scene. Requires supports_custom_fonts=true; do not create specimen references yourself.'),
447
451
  project_id: projectIdField
448
452
  },
449
- 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 }) => {
453
+ 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, font_ids }) => {
450
454
  model = await canonicalModelId(client, model, workflow_type === 'video' ? 'text_to_video' : 'text_to_img'); // lenient id resolution ("z-image" → "z-image/turbo")
451
455
  aspect_ratio = await resolveCatalogAspectRatio(client, model, aspect_ratio, workflow_type === 'video' ? 'text_to_video' : 'text_to_img');
452
456
  const gen = await client.post('/v1/generate/creative-director', {
453
- prompt, scene_count, model, aspect_ratio, workflow_type, duration,
457
+ prompt, scene_count, model, aspect_ratio, workflow_type, duration, font_ids,
454
458
  enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution, project_id
455
459
  });
456
460