@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 +8 -0
- package/package.json +1 -1
- package/skill/GENERATED.md +1 -1
- package/skill/SKILL.md +6 -1
- package/skill/VERSION +1 -1
- package/skill/references/workflows/personal-fonts.md +41 -0
- package/src/apps/index.js +4 -0
- package/src/apps/widgets/fontUpload.js +45 -0
- package/src/index.js +4 -1
- package/src/toolAnnotations.js +3 -0
- package/src/tools/fonts.js +49 -0
- package/src/tools/generate.js +10 -6
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
package/skill/GENERATED.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AUTO-GENERATED — do not edit
|
|
2
2
|
|
|
3
|
-
This tree is mirrored from kolbo-code@
|
|
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.
|
|
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.
|
|
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
|
package/src/toolAnnotations.js
CHANGED
|
@@ -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 };
|
package/src/tools/generate.js
CHANGED
|
@@ -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
|
|