@kolbo/mcp 1.87.14 → 1.88.1

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.1",
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": {
@@ -1,6 +1,6 @@
1
1
  # AUTO-GENERATED — do not edit
2
2
 
3
- This tree is mirrored from kolbo-code@d4250c0, the single source of truth.
3
+ This tree is mirrored from kolbo-code@06fbd86, the single source of truth.
4
4
  Canonical source: packages/opencode/skills/kolbo/
5
5
  Distribution: .github/workflows/sync-skill-to-plugin.yml
6
6
 
package/skill/SKILL.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- version: 0.9.13
2
+ version: 0.9.14
3
3
  name: kolbo
4
4
  description: |
5
5
  Generate, edit, analyze, and direct creative media through Kolbo AI: images,
@@ -96,6 +96,7 @@ For multi-scene / batch work this pairs with `generate_creative_director` (see b
96
96
  | **Split a soundtrack into layers** — remove/isolate speech, strip narration, instrumental bed, stems for dubbing | `references/workflows/audio-stems.md` |
97
97
  | **Scrape brand/product info** before generating + persist as `.kolbo/brand-kits/<slug>.md` | `references/workflows/research-first.md` |
98
98
  | Browse, manage, or present existing **media library** items | `references/workflows/media-library.md` |
99
+ | Upload, pick, or reuse a **personal font** (My Fonts / Font collection, `font_ids`) | `references/workflows/personal-fonts.md` |
99
100
  | Run a **client review / approval loop** — share a cut for feedback, timestamped comments, versions (v1→v2), approve / request-changes, guest links | `references/workflows/review-collections.md` |
100
101
  | Confirm **cost** or validate **resolution / aspect / duration** against model caps | `references/workflows/cost-and-validation.md` |
101
102
  | Hit an **auth / MCP / 429** issue | `references/workflows/troubleshooting.md` |
@@ -105,6 +106,10 @@ Each `references/models/*.md` mirrors the matching skill prompt in `kolbo-api/sr
105
106
 
106
107
  ## Available MCP Tools
107
108
 
109
+ For personal font uploads, font selection, or reuse, read `references/workflows/personal-fonts.md`. Use the dedicated My Fonts tools; never the media-upload path or agent-rendered specimens.
110
+
111
+ Font tools (when exposed by the installed MCP): `list_fonts`, `get_font`, `upload_font`, `get_font_upload_status`, `create_font_upload_ticket`, `font_upload_widget`, `rename_font`, `delete_font`. Image creation/editing and image-mode Creative Director accept `font_ids` only on models with `supports_custom_fonts`.
112
+
108
113
  ### Generation
109
114
  | Tool | Description |
110
115
  |------|-------------|
package/skill/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.13
1
+ 0.9.14
@@ -0,0 +1,41 @@
1
+ # Personal fonts (My Fonts)
2
+
3
+ ## Curated font collection
4
+
5
+ The picker separates **My fonts** (account uploads) from **Font collection** (ready-to-use, read-only families). Use `list_fonts({source: "global"})` to browse the collection, `source: "custom"` for personal uploads (the default), or `source: "all"` to search both. Results are paginated; inspect the returned family IDs with `get_font` for available styles and actual script coverage. Never invent IDs or promise every language/weight for every family.
6
+
7
+ Collection families use the same `font_ids` generation path. Up to three families can be mixed across both sources. Do not upload or copy a collection font into My fonts, and do not try to rename/delete collection entries. Server-side SDK: `fonts.list({source: "global"})`. REST: `GET /api/v1/fonts?source=global`. Family metadata identifies `source: "custom" | "global"`; collection inspection also includes license information. Preserve licenses when redistributing font files. Availability still depends on the deployed collection and backend/client versions.
8
+
9
+
10
+ Use this workflow when the user supplies OTF, TTF or WOFF2 files, names a personal font, or wants to reuse fonts from a previous image.
11
+
12
+ ## Discover and upload
13
+
14
+ Check the connected tool inventory first. Older MCP installations may not expose font tools yet; report that mismatch rather than pretending an upload succeeded or substituting media upload.
15
+
16
+ - Existing font: `list_fonts({search: "Birzia"})`, then `get_font({font_id})` for styles/scripts.
17
+ - Local stdio: `upload_font({file_path: "<absolute path to font>"})`.
18
+ - Remote connector with shell: `create_font_upload_ticket({})`; POST multipart field `file` to its exact `upload_url` with `Authorization: Bearer <ticket>`. Do not follow redirects or expose the ticket in chat, logs, or saved production notes.
19
+ - Browser-only: `font_upload_widget({})`. The user chooses the file.
20
+ - Upload one file per request, at most 5 MiB. Upload the requested weights separately; the backend groups matching families.
21
+ - Inspect the returned upload ID with `get_font_upload_status({upload_id})`. Wait between checks, stop on ready/failed, and report pending after a bounded wait. This is font preparation, not `get_generation_status`; do not assume it supports `wait`.
22
+ - Use the returned `font_id`, never the upload ID. A failed scan/preparation is not permission to bypass validation or upload the font as media.
23
+ - `rename_font({font_id,name})` preserves identity. `delete_font({font_id})` only when requested; deletion prevents future use without deleting completed images.
24
+
25
+ ## Generate
26
+
27
+ Verify `supports_custom_fonts: true` in current `list_models` JSON for the actual image creation/editing model, even if the user named it. GPT Image 2 is initially supported, but do not hardcode the pipeline to that name. Missing capability means unsupported; never silently drop fonts or swap a named model.
28
+
29
+ Pass up to three ready family IDs as `font_ids` to `generate_image`, `generate_image_edit`, or image-mode `generate_creative_director`. Batch prompts share the selected families. Keep `visual_dna_ids`, exact DNA @names, and project/session bindings as normal.
30
+
31
+ State exact requested copy in the prompt, unchanged in its original language. The backend infers language and chooses uploaded styles; bold/italic or per-family assignments can be described naturally. Do not require a language selector. Do not promise an unavailable style or unsupported glyphs. Selecting a font alone does not request new text. For edits, specify what typography changes and preserve unrelated existing text.
32
+
33
+ The backend renders internal specimens. Do NOT render specimens, attach them as `reference_images`, use `upload_media` for fonts, or expose internal specimen URLs. Font files/previews belong to My Fonts, not the media library. Reuse selected IDs from generation metadata, rechecking deleted/unavailable families instead of silently removing them.
34
+
35
+ Font upload/preparation has no separate credit charge; normal image generation remains billable under the existing approval rules. No automatic paid regeneration to improve typography.
36
+
37
+ ## SDK / REST
38
+
39
+ The account-authenticated server-side SDK exports `createFontClient`: `list`, `get`, `upload(Blob, filename)`, `status`, `rename`, `delete`, `createUploadTicket`, and `grantToApp`. Keep account API keys on the server. The dedicated REST root is `/api/v1/fonts`; multipart upload is POST to that root. App end-user credentials do not grant access to an owner's personal library; use explicit app font grants. Image SDK calls use the same optional `font_ids`.
40
+
41
+ Availability depends on the installed MCP/SDK and deployed backend versions. Do not describe a local source change as a published release.
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