@kolbo/mcp 1.88.3 → 1.89.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 +8 -2
- package/package.json +1 -1
- package/skill/GENERATED.md +1 -1
- package/skill/SKILL.md +6 -4
- package/skill/VERSION +1 -1
- package/skill/references/models/gpt-image.md +4 -3
- package/skill/references/workflows/cost-and-validation.md +4 -0
- package/skill/references/workflows/media-library.md +4 -0
- package/src/apps/bridge.js +53 -19
- package/src/apps/html.js +2 -0
- package/src/apps/index.js +8 -8
- package/src/apps/theme.js +58 -10
- package/src/apps/widgets/generation.js +13 -4
- package/src/apps/widgets/list.js +67 -25
- package/src/apps/widgets/mediaGrid.js +148 -79
- package/src/index.js +269 -267
- package/src/toolAnnotations.js +11 -9
- package/src/tools/_shared.js +10 -5
- package/src/tools/agents.js +51 -25
- package/src/tools/chat.js +3 -1
- package/src/tools/editor.js +22 -0
- package/src/tools/generate.js +18 -6
- package/src/tools/media.js +10 -10
- package/src/tools/models.js +11 -5
- package/src/tools/visual_dna.js +1 -1
package/src/toolAnnotations.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* or non-boolean entries whenever the registered tool surface changes.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
const READ_ONLY = [
|
|
12
|
-
'list_fonts', 'get_font', 'get_font_upload_status',
|
|
11
|
+
const READ_ONLY = [
|
|
12
|
+
'list_fonts', 'get_font', 'get_font_upload_status',
|
|
13
13
|
'get_creative_director_status', 'get_generation_status', 'list_models',
|
|
14
14
|
'check_credits', 'show_plans', 'get_session_usage', 'list_voices',
|
|
15
15
|
'chat_list_conversations', 'chat_get_messages',
|
|
@@ -21,7 +21,7 @@ const READ_ONLY = [
|
|
|
21
21
|
'list_projects', 'get_project', 'list_sessions', 'list_project_context', 'get_project_profile',
|
|
22
22
|
'list_project_assets',
|
|
23
23
|
'list_session_generations',
|
|
24
|
-
'list_agents', 'list_docs', 'get_doc',
|
|
24
|
+
'list_agents', 'list_skills', 'list_docs', 'get_doc',
|
|
25
25
|
'get_review_storage_usage', 'list_review_assets', 'get_review_asset',
|
|
26
26
|
'list_review_collections', 'list_review_comments', 'list_review_share_links',
|
|
27
27
|
'search_music_library', 'analyze_script_for_music', 'browse_music_library',
|
|
@@ -36,8 +36,9 @@ const OPEN_WORLD_READ_ONLY = [
|
|
|
36
36
|
'blender_get_command_status',
|
|
37
37
|
];
|
|
38
38
|
|
|
39
|
-
const PRIVATE_WRITE = [
|
|
40
|
-
'upload_font', 'rename_font', 'create_font_upload_ticket', 'font_upload_widget',
|
|
39
|
+
const PRIVATE_WRITE = [
|
|
40
|
+
'upload_font', 'rename_font', 'create_font_upload_ticket', 'font_upload_widget',
|
|
41
|
+
'create_video_editor_session',
|
|
41
42
|
'media_upload_widget', 'create_upload_ticket', 'upload_media',
|
|
42
43
|
'favorite_media', 'unfavorite_media',
|
|
43
44
|
'create_media_folder', 'update_media_folder',
|
|
@@ -56,7 +57,7 @@ const PRIVATE_WRITE = [
|
|
|
56
57
|
'create_project', 'duplicate_project', 'update_project',
|
|
57
58
|
'archive_project', 'unarchive_project', 'add_project_context',
|
|
58
59
|
'link_project_asset', 'unlink_project_asset',
|
|
59
|
-
'create_agent',
|
|
60
|
+
'create_agent', 'create_skill',
|
|
60
61
|
'create_doc',
|
|
61
62
|
'create_review_asset', 'update_review_asset', 'add_review_version',
|
|
62
63
|
'set_review_status', 'create_review_collection', 'update_review_collection',
|
|
@@ -65,8 +66,9 @@ const PRIVATE_WRITE = [
|
|
|
65
66
|
'import_stock_asset',
|
|
66
67
|
];
|
|
67
68
|
|
|
68
|
-
const DESTRUCTIVE_WRITE = [
|
|
69
|
-
'delete_font',
|
|
69
|
+
const DESTRUCTIVE_WRITE = [
|
|
70
|
+
'delete_font',
|
|
71
|
+
'export_video_editor_session',
|
|
70
72
|
// These actions spend credits, enqueue irreversible work, or cancel it.
|
|
71
73
|
'generate_image', 'generate_image_edit', 'generate_creative_director',
|
|
72
74
|
'generate_video', 'generate_video_from_image', 'generate_music',
|
|
@@ -89,7 +91,7 @@ const DESTRUCTIVE_WRITE = [
|
|
|
89
91
|
'delete_session',
|
|
90
92
|
'delete_project_context', 'regenerate_project_profile',
|
|
91
93
|
'update_project_asset',
|
|
92
|
-
'update_agent', 'delete_agent', 'update_doc', 'delete_doc',
|
|
94
|
+
'update_agent', 'delete_agent', 'update_skill', 'delete_skill', 'update_doc', 'delete_doc',
|
|
93
95
|
'delete_review_asset', 'delete_review_collection',
|
|
94
96
|
'edit_review_comment', 'delete_review_comment',
|
|
95
97
|
];
|
package/src/tools/_shared.js
CHANGED
|
@@ -990,7 +990,11 @@ async function uiCompleted(p, textPayload, extraContent) {
|
|
|
990
990
|
// `settings` wiped the resolution / aspect / DNA chips off the finished
|
|
991
991
|
// card. Every reader already does `sc.settings || {}`.
|
|
992
992
|
...(settings ? { settings } : {}),
|
|
993
|
-
|
|
993
|
+
...(Array.isArray(p.visual_dnas)
|
|
994
|
+
? { visual_dnas: p.visual_dnas }
|
|
995
|
+
: Array.isArray(settings?.visual_dna_ids)
|
|
996
|
+
? { visual_dnas: await resolveVisualDnas(p.client, settings.visual_dna_ids) }
|
|
997
|
+
: {}),
|
|
994
998
|
moodboards: await resolveMoodboards(p.client, moodboardIds(settings)),
|
|
995
999
|
...mediaRefs(p),
|
|
996
1000
|
urls: preferOwnedUrls(p.urls),
|
|
@@ -1058,11 +1062,12 @@ const MAX_TEXT_CHARS = 20000;
|
|
|
1058
1062
|
* @param {object} opts
|
|
1059
1063
|
* @param {string[]} opts.fields keys to keep per row, in order (others dropped)
|
|
1060
1064
|
* @param {number} [opts.cap] max rows to include (default 50)
|
|
1061
|
-
* @param {number} [opts.total] true total, so the model knows more exist
|
|
1065
|
+
* @param {number} [opts.total] true total, so the model knows more exist
|
|
1066
|
+
* @param {number} [opts.maxChars] text budget; Infinity for bounded server pages that must remain complete
|
|
1062
1067
|
* @param {object} [opts.extra] extra top-level keys to merge in
|
|
1063
1068
|
* @param {string} [opts.note] guidance on how to fetch the rest
|
|
1064
1069
|
*/
|
|
1065
|
-
function compactList(items, { fields, cap = 50, total, extra, note } = {}) {
|
|
1070
|
+
function compactList(items, { fields, cap = 50, total, extra, note, maxChars = MAX_TEXT_CHARS } = {}) {
|
|
1066
1071
|
const rows = Array.isArray(items) ? items : [];
|
|
1067
1072
|
const kept = rows.slice(0, cap).map((row) => {
|
|
1068
1073
|
if (!row || typeof row !== 'object') return row;
|
|
@@ -1085,11 +1090,11 @@ function compactList(items, { fields, cap = 50, total, extra, note } = {}) {
|
|
|
1085
1090
|
}
|
|
1086
1091
|
|
|
1087
1092
|
let text = JSON.stringify(payload);
|
|
1088
|
-
if (text.length >
|
|
1093
|
+
if (text.length > maxChars) {
|
|
1089
1094
|
// Still too big even trimmed (very long descriptions). Halve until it fits
|
|
1090
1095
|
// rather than returning something the host will truncate at a random byte.
|
|
1091
1096
|
let n = kept.length;
|
|
1092
|
-
while (n > 1 && text.length >
|
|
1097
|
+
while (n > 1 && text.length > maxChars) {
|
|
1093
1098
|
n = Math.floor(n / 2);
|
|
1094
1099
|
payload.items = kept.slice(0, n);
|
|
1095
1100
|
payload.count = n;
|
package/src/tools/agents.js
CHANGED
|
@@ -6,15 +6,39 @@
|
|
|
6
6
|
const { z } = require('zod');
|
|
7
7
|
const { listResult } = require('../apps');
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Skills rename (2026-09-09). The product surface is called Skills; the original tools
|
|
11
|
+
* were called *_agent. Because a published tool name can never be withdrawn (commandment
|
|
12
|
+
* above), every tool is registered TWICE from one definition:
|
|
13
|
+
*
|
|
14
|
+
* list_skill s/create_skill/update_skill/delete_skill — current vocabulary
|
|
15
|
+
* list_agents/create_agent/update_agent/delete_agent — original, still supported
|
|
16
|
+
*
|
|
17
|
+
* Same handler, same behaviour. Only the tool name, the id ARG name (`skill_id` vs the
|
|
18
|
+
* original `agent_id`) and the prose differ — the legacy arg name must keep working
|
|
19
|
+
* exactly as published, so it is not renamed, only shadowed by the new one.
|
|
20
|
+
*
|
|
21
|
+
* The server route is `/v1/skills`, which the backend also serves at `/v1/agents`.
|
|
22
|
+
*/
|
|
9
23
|
function registerAgentTools(server, client) {
|
|
10
|
-
|
|
24
|
+
registerSkillCrud(server, client, { noun: 'skill', idArg: 'skill_id', suffix: 'skill', plural: 'skills' });
|
|
25
|
+
registerSkillCrud(server, client, { noun: 'agent', idArg: 'agent_id', suffix: 'agent', plural: 'agents' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function registerSkillCrud(server, client, { noun, idArg, suffix, plural }) {
|
|
29
|
+
const legacyNote = noun === 'agent'
|
|
30
|
+
? ' NOTE: "agent" is the original name for this feature; the product now calls them Skills. New integrations should prefer list_skills/create_skill/update_skill/delete_skill — these keep working unchanged.'
|
|
31
|
+
: '';
|
|
32
|
+
const idDesc = `${noun[0].toUpperCase() + noun.slice(1)} id (from list_${plural}).`;
|
|
33
|
+
|
|
34
|
+
// ─── list_skills / list_agents ─────────────────────────────
|
|
11
35
|
server.tool(
|
|
12
|
-
|
|
13
|
-
|
|
36
|
+
`list_${plural}`,
|
|
37
|
+
`List the user's custom ${plural} (personal + any platform/org preset ${plural} visible to them). A ${noun} is a reusable, named persona for the chat tool — its \`description\` is the system instruction the model adopts. Use this to resolve a ${noun} NAME the user mentioned into its id, or to show what ${plural} exist. Returns id, name, description, emoji, is_global. Personal ${plural} (is_global:false) are editable/deletable; platform presets are not.${legacyNote}`,
|
|
14
38
|
{ search: z.string().optional().describe('Optional case-insensitive name filter.') },
|
|
15
39
|
async ({ search }) => {
|
|
16
40
|
const qs = search ? `?search=${encodeURIComponent(search)}` : '';
|
|
17
|
-
const result = await client.get(`/v1/
|
|
41
|
+
const result = await client.get(`/v1/skills${qs}`);
|
|
18
42
|
const agents = result.agents || [];
|
|
19
43
|
const text = JSON.stringify({
|
|
20
44
|
agents,
|
|
@@ -23,26 +47,26 @@ function registerAgentTools(server, client) {
|
|
|
23
47
|
|
|
24
48
|
return listResult(text, {
|
|
25
49
|
widget: 'list',
|
|
26
|
-
title: 'Your Agents',
|
|
50
|
+
title: noun === 'skill' ? 'Your Skills' : 'Your Agents',
|
|
27
51
|
items: agents.map(a => ({
|
|
28
52
|
id: a.id,
|
|
29
53
|
title: (a.emoji ? a.emoji + ' ' : '') + a.name,
|
|
30
54
|
subtitle: a.description,
|
|
31
55
|
badge: a.is_global ? 'preset' : null,
|
|
32
|
-
use_hint:
|
|
56
|
+
use_hint: `Use my "{TITLE}" ${noun} (${idArg}: {ID}) for this conversation.`
|
|
33
57
|
})),
|
|
34
58
|
total: agents.length
|
|
35
59
|
});
|
|
36
60
|
}
|
|
37
61
|
);
|
|
38
62
|
|
|
39
|
-
// ─── create_agent
|
|
63
|
+
// ─── create_skill / create_agent ───────────────────────────
|
|
40
64
|
server.tool(
|
|
41
|
-
|
|
42
|
-
|
|
65
|
+
`create_${suffix}`,
|
|
66
|
+
`Create a reusable custom ${noun} (a named persona for the chat tool). The \`description\` IS the ${noun}'s system instruction — write it as the persona + behavior you want ("You are a senior creative director. Turn any brief into a structured shot list…"). Use when the user wants a persistent, reusable assistant ("make me a creative-director ${noun}", "set up a support-triage bot"). For a ONE-OFF persona on a single conversation, pass \`system_prompt\` to chat_send_message instead — no need to create a ${noun}. Plan limits apply (server rejects when the ${noun} cap is reached).${legacyNote}`,
|
|
43
67
|
{
|
|
44
|
-
name: z.string().optional().describe(
|
|
45
|
-
description: z.string().describe(
|
|
68
|
+
name: z.string().optional().describe(`${noun[0].toUpperCase() + noun.slice(1)} name. If omitted, a name is generated from the description.`),
|
|
69
|
+
description: z.string().describe(`The ${noun} persona + instructions (max 2000 chars). This becomes the system prompt the model adopts in every conversation that uses the ${noun}.`),
|
|
46
70
|
emoji: z.string().optional().describe('Optional emoji avatar (auto-picked if omitted).'),
|
|
47
71
|
thumbnail: z.string().optional().describe('Optional thumbnail image URL.')
|
|
48
72
|
},
|
|
@@ -51,40 +75,42 @@ function registerAgentTools(server, client) {
|
|
|
51
75
|
if (name !== undefined) body.name = name;
|
|
52
76
|
if (emoji !== undefined) body.emoji = emoji;
|
|
53
77
|
if (thumbnail !== undefined) body.thumbnail = thumbnail;
|
|
54
|
-
const result = await client.post('/v1/
|
|
55
|
-
return { content: [{ type: 'text', text: JSON.stringify({ agent: result.agent, _hint:
|
|
78
|
+
const result = await client.post('/v1/skills', body);
|
|
79
|
+
return { content: [{ type: 'text', text: JSON.stringify({ agent: result.agent, _hint: `Reuse this ${noun} by selecting it in the chat tool. Its description is the system instruction applied to every conversation using it.` }, null, 2) }] };
|
|
56
80
|
}
|
|
57
81
|
);
|
|
58
82
|
|
|
59
|
-
// ─── update_agent
|
|
83
|
+
// ─── update_skill / update_agent ───────────────────────────
|
|
60
84
|
server.tool(
|
|
61
|
-
|
|
62
|
-
|
|
85
|
+
`update_${suffix}`,
|
|
86
|
+
`Edit a custom ${noun} in place: name, description (persona/instructions), or emoji/thumbnail. NEVER delete and recreate a ${noun} to change its persona — conversations already reference this id. Only personal ${plural} you own can be edited — platform presets are protected. Resolve the id with list_${plural} first.${legacyNote}`,
|
|
63
87
|
{
|
|
64
|
-
|
|
88
|
+
[idArg]: z.string().describe(idDesc),
|
|
65
89
|
name: z.string().optional().describe('New name.'),
|
|
66
90
|
description: z.string().optional().describe('New persona/instructions (replaces the old description; max 2000 chars).'),
|
|
67
91
|
emoji: z.string().optional().describe('New emoji avatar.'),
|
|
68
92
|
thumbnail: z.string().optional().describe('New thumbnail image URL.')
|
|
69
93
|
},
|
|
70
|
-
async (
|
|
94
|
+
async (args) => {
|
|
95
|
+
const id = args[idArg];
|
|
96
|
+
const { name, description, emoji, thumbnail } = args;
|
|
71
97
|
const body = {};
|
|
72
98
|
if (name !== undefined) body.name = name;
|
|
73
99
|
if (description !== undefined) body.description = description;
|
|
74
100
|
if (emoji !== undefined) body.emoji = emoji;
|
|
75
101
|
if (thumbnail !== undefined) body.thumbnail = thumbnail;
|
|
76
|
-
const result = await client.put(`/v1/
|
|
102
|
+
const result = await client.put(`/v1/skills/${encodeURIComponent(id)}`, body);
|
|
77
103
|
return { content: [{ type: 'text', text: JSON.stringify({ agent: result.agent }, null, 2) }] };
|
|
78
104
|
}
|
|
79
105
|
);
|
|
80
106
|
|
|
81
|
-
// ─── delete_agent
|
|
107
|
+
// ─── delete_skill / delete_agent ───────────────────────────
|
|
82
108
|
server.tool(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
{
|
|
86
|
-
async (
|
|
87
|
-
const result = await client.delete(`/v1/
|
|
109
|
+
`delete_${suffix}`,
|
|
110
|
+
`Delete a custom ${noun} you own. Platform preset ${plural} cannot be deleted. This removes the ${noun} config only — it does not touch any conversations that used it.${legacyNote}`,
|
|
111
|
+
{ [idArg]: z.string().describe(idDesc) },
|
|
112
|
+
async (args) => {
|
|
113
|
+
const result = await client.delete(`/v1/skills/${encodeURIComponent(args[idArg])}`);
|
|
88
114
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
89
115
|
}
|
|
90
116
|
);
|
package/src/tools/chat.js
CHANGED
|
@@ -20,11 +20,12 @@ function registerChatTools(server, client) {
|
|
|
20
20
|
web_search: z.boolean().optional().describe('Enable web search for this message. Default: false'),
|
|
21
21
|
deep_think: z.boolean().optional().describe('Enable deep think (extended reasoning). Default: false'),
|
|
22
22
|
thinking_level: z.string().optional().describe('Thinking effort ID from list_models type="text" thinkingLevels. The server uses the model catalog default when omitted or invalid. Separate from legacy deep_think; safeguards take precedence.'),
|
|
23
|
+
routing_mode: z.enum(['fast', 'balanced', 'smart']).optional().describe('Auto routing mode. Use fast, balanced, or smart when model is Auto; omitted uses the server default (balanced).'),
|
|
23
24
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
|
|
24
25
|
media_urls: z.array(z.string()).optional().describe('Public URLs of images, videos, or audio files to analyze. The model auto-routes to a vision-capable model when media is present. For a local file, get a URL first via the LOCAL FILE route in this tool\'s description.'),
|
|
25
26
|
project_id: projectIdField
|
|
26
27
|
},
|
|
27
|
-
async ({ message, model, session_id, system_prompt, web_search, deep_think, thinking_level, enhance_prompt = false, media_urls, project_id }) => {
|
|
28
|
+
async ({ message, model, session_id, system_prompt, web_search, deep_think, thinking_level, routing_mode, enhance_prompt = false, media_urls, project_id }) => {
|
|
28
29
|
// Every generate_* tool resolves its model this way; chat was the one
|
|
29
30
|
// `model` arg that went straight to the API, which has no fuzzy matching.
|
|
30
31
|
// So the display names list_models hands back ("Claude Fable 5") came
|
|
@@ -40,6 +41,7 @@ function registerChatTools(server, client) {
|
|
|
40
41
|
web_search,
|
|
41
42
|
deep_think,
|
|
42
43
|
...(thinking_level !== undefined ? { thinking_level } : {}),
|
|
44
|
+
...(routing_mode !== undefined ? { routing_mode } : {}),
|
|
43
45
|
enhance_prompt,
|
|
44
46
|
media_urls,
|
|
45
47
|
project_id
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { z } = require('zod');
|
|
3
|
+
|
|
4
|
+
function registerEditorTools(server, client) {
|
|
5
|
+
server.tool('create_video_editor_session',
|
|
6
|
+
'Create an editable timeline from existing Kolbo-hosted images/video, audio layers and text. No new media generation. Requires project_id with edit access. Returns session_id and editor_url. Preserve the returned ID; creation is not a polling operation.',
|
|
7
|
+
{
|
|
8
|
+
project_id: z.string().min(1), name: z.string().min(1).max(120),
|
|
9
|
+
format: z.enum(['16:9', '9:16', '1:1', '4:5', '21:9']).optional(),
|
|
10
|
+
clips: z.array(z.object({ url: z.string().url(), name: z.string().optional(), duration_ms: z.number().finite().min(0).max(1800000).optional(), trim_start_ms: z.number().finite().min(0).max(1800000).optional(), trim_end_ms: z.number().finite().min(0).max(1800000).optional(), volume: z.number().finite().min(0).max(2).optional(), muted: z.boolean().optional() })).min(1).max(60),
|
|
11
|
+
audio: z.array(z.object({ url: z.string().url(), name: z.string().optional(), start_ms: z.number().finite().min(0).max(1800000).optional(), duration_ms: z.number().finite().min(0).max(1800000).optional(), volume: z.number().finite().min(0).max(2).optional(), fade_in_ms: z.number().finite().min(0).max(1800000).optional(), fade_out_ms: z.number().finite().min(0).max(1800000).optional() })).max(16).optional(),
|
|
12
|
+
texts: z.array(z.object({ content: z.string().min(1).max(2000), start_ms: z.number().finite().min(0).max(1800000).optional(), duration_ms: z.number().finite().min(0).max(1800000).optional(), font_size: z.number().finite().min(8).max(512).optional(), color: z.string().optional(), vertical: z.enum(['top', 'middle', 'bottom']).optional() })).max(120).optional(),
|
|
13
|
+
},
|
|
14
|
+
async args => ({ content: [{ type: 'text', text: JSON.stringify(await client.post('/v1/editor/sessions', args)) }] })
|
|
15
|
+
);
|
|
16
|
+
server.tool('export_video_editor_session',
|
|
17
|
+
'Export an owned Video Editor timeline to MP4. Identical snapshots reuse the same job. If pending, call again with the returned job_id; do not recreate the timeline. retry_failed explicitly retries a terminal failed/cancelled snapshot and cannot be combined with job_id. This exports media to the library, not a public website.',
|
|
18
|
+
{ session_id: z.string().min(1), quality: z.enum(['480p', '720p', '1080p']).optional(), job_id: z.string().min(1).max(100).optional(), retry_failed: z.boolean().optional() },
|
|
19
|
+
async args => ({ content: [{ type: 'text', text: JSON.stringify(await client.post('/v1/editor/exports', args)) }] })
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
module.exports = { registerEditorTools };
|
package/src/tools/generate.js
CHANGED
|
@@ -182,6 +182,8 @@ const imageSettings = (a = {}) => ({
|
|
|
182
182
|
resolution: a.resolution,
|
|
183
183
|
aspect_ratio: a.aspect_ratio,
|
|
184
184
|
quality: a.quality,
|
|
185
|
+
background: a.background,
|
|
186
|
+
output_format: a.output_format,
|
|
185
187
|
...refSettings(a),
|
|
186
188
|
});
|
|
187
189
|
|
|
@@ -269,20 +271,25 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
269
271
|
moodboard_id: z.string().optional().describe('Moodboard ID (from list_moodboards / get_moodboard) whose master_prompt and style_guide should be applied to this generation.'),
|
|
270
272
|
enable_web_search: z.boolean().optional().describe('Enable web-search grounding for the prompt (useful for current events, brand references, real-world accuracy). Default: false'),
|
|
271
273
|
resolution: z.string().optional().describe('Image resolution tier: "1K" (~1024px), "2K" (Full HD), "3K" (QHD), or "4K" (UHD). Model-dependent — call list_models and read supported_resolutions on the chosen model. Read resolution_multipliers on the same model to predict credit cost. Omit to use the model default.'),
|
|
272
|
-
quality: z.string().optional().describe('Quality tier for models that support it (e.g. "low", "medium", "high", "auto"). Check list_models → supported_qualities on the chosen model. "auto" is normalised to "medium" on gpt-image-2. Omit to use the model default.'),
|
|
274
|
+
quality: z.string().optional().describe('Quality tier for models that support it (e.g. "low", "medium", "high", "xhigh", "max", "auto"). Check list_models → supported_qualities on the chosen model. "auto" is normalised to "medium" on gpt-image-2. Omit to use the model default.'),
|
|
275
|
+
background: z.enum(['auto', 'opaque', 'transparent']).optional().describe('Output background. Before passing "transparent", call list_models for the chosen image model and require supports_transparent_background=true. Use PNG or WebP (PNG is selected by default when omitted). Kolbo forwards the setting and appends the exact phrase "no background" once to the effective prompt; prompt wording alone is not sufficient.'),
|
|
276
|
+
output_format: z.enum(['png', 'jpeg', 'webp']).optional().describe('Output file format on supported OpenAI image models. Default: png.'),
|
|
277
|
+
output_compression: z.number().int().min(0).max(100).optional().describe('JPEG/WebP output compression quality (0-100); not applicable to PNG.'),
|
|
278
|
+
moderation: z.enum(['auto', 'low']).optional().describe('Moderation setting on supported OpenAI image models. Default: auto.'),
|
|
279
|
+
mask_image_url: z.string().url().optional().describe('Optional public PNG mask URL for supported OpenAI edits with source/reference images. Transparent regions indicate areas to edit.'),
|
|
273
280
|
preset_id: z.string().optional().describe('Exact preset ID from list_presets type="image". If the user requests any image preset, resolve it with list_presets and pass it here; do not omit it.'),
|
|
274
281
|
cinematic: CINEMATIC_SCHEMA,
|
|
275
282
|
skip_color_palette: z.boolean().optional().describe('Opt this single call OUT of the account\'s active Color DNA palette (see list_color_palettes / activate_color_palette). By default, if the user has an active palette it strict-grades every generation automatically — pass true only when the user explicitly wants this one image ungraded.'),
|
|
276
283
|
project_id: projectIdField,
|
|
277
284
|
session_id: sessionIdField
|
|
278
285
|
},
|
|
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 }) => {
|
|
286
|
+
async ({ prompt, prompts, model, aspect_ratio, enhance_prompt = false, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, background, output_format, output_compression, moderation, mask_image_url, preset_id, cinematic, skip_color_palette, project_id, session_id, font_ids }) => {
|
|
280
287
|
if (!prompt && !(prompts && prompts.length)) throw new Error('Provide prompt or prompts');
|
|
281
288
|
model = await canonicalModelId(client, model, 'text_to_img'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
282
289
|
aspect_ratio = await resolveCatalogAspectRatio(client, model, aspect_ratio, 'text_to_img');
|
|
283
290
|
const shared = {
|
|
284
291
|
model, aspect_ratio, enhance_prompt, font_ids,
|
|
285
|
-
reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id, session_id
|
|
292
|
+
reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, background, output_format, output_compression, moderation, mask_image_url, preset_id, cinematic, skip_color_palette, project_id, session_id
|
|
286
293
|
};
|
|
287
294
|
|
|
288
295
|
// Batch mode: N different prompts, one widget owning all generation ids.
|
|
@@ -355,20 +362,25 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
355
362
|
moodboard_id: z.string().optional().describe('Moodboard ID whose master_prompt and style_guide should be applied.'),
|
|
356
363
|
enable_web_search: z.boolean().optional().describe('Enable web-search grounding. Default: false'),
|
|
357
364
|
resolution: z.string().optional().describe('Image resolution tier: "1K" / "2K" / "3K" / "4K". Model-dependent — call list_models and read supported_resolutions. Default: "1K" for most edit models.'),
|
|
358
|
-
quality: z.string().optional().describe('Quality tier for edit models that support it (e.g. "low", "medium", "high", "auto"). Check list_models → supported_qualities on the chosen model. "auto" is normalised to "medium" on gpt-image-2. Omit to use the model default.'),
|
|
365
|
+
quality: z.string().optional().describe('Quality tier for edit models that support it (e.g. "low", "medium", "high", "xhigh", "max", "auto"). Check list_models → supported_qualities on the chosen model. "auto" is normalised to "medium" on gpt-image-2. Omit to use the model default.'),
|
|
366
|
+
background: z.enum(['auto', 'opaque', 'transparent']).optional().describe('Output background. Before passing "transparent", call list_models for the chosen image-edit model and require supports_transparent_background=true. Use PNG or WebP (PNG is selected by default when omitted). Kolbo forwards the setting and appends the exact phrase "no background" once to the effective prompt; prompt wording alone is not sufficient.'),
|
|
367
|
+
output_format: z.enum(['png', 'jpeg', 'webp']).optional().describe('Output file format on supported OpenAI image models. Default: png.'),
|
|
368
|
+
output_compression: z.number().int().min(0).max(100).optional().describe('JPEG/WebP output compression quality (0-100); not applicable to PNG.'),
|
|
369
|
+
moderation: z.enum(['auto', 'low']).optional().describe('Moderation setting on supported OpenAI image models. Default: auto.'),
|
|
370
|
+
mask_image_url: z.string().url().optional().describe('Optional public PNG mask URL for supported OpenAI edits with source/reference images. Transparent regions indicate areas to edit.'),
|
|
359
371
|
preset_id: z.string().optional().describe('Exact preset ID from list_presets type="image_edit" to apply an image-editing preset. If the user requests a preset, resolve and pass it; do not silently omit it.'),
|
|
360
372
|
cinematic: CINEMATIC_SCHEMA,
|
|
361
373
|
skip_color_palette: z.boolean().optional().describe('Opt this single call OUT of the account\'s active Color DNA palette (see list_color_palettes / activate_color_palette). By default, if the user has an active palette it strict-grades every generation automatically — pass true only when the user explicitly wants this one edit ungraded.'),
|
|
362
374
|
project_id: projectIdField,
|
|
363
375
|
session_id: sessionIdField
|
|
364
376
|
},
|
|
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 }) => {
|
|
377
|
+
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, background, output_format, output_compression, moderation, mask_image_url, preset_id, cinematic, skip_color_palette, project_id, session_id, font_ids }) => {
|
|
366
378
|
if (!prompt && !(prompts && prompts.length)) throw new Error('Provide prompt or prompts');
|
|
367
379
|
model = await canonicalModelId(client, model, 'image_editing'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
368
380
|
aspect_ratio = await resolveCatalogAspectRatio(client, model, aspect_ratio, 'image_editing');
|
|
369
381
|
const shared = {
|
|
370
382
|
model, source_images, reference_images, aspect_ratio, enhance_prompt, font_ids,
|
|
371
|
-
visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id, session_id
|
|
383
|
+
visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, background, output_format, output_compression, moderation, mask_image_url, preset_id, cinematic, skip_color_palette, project_id, session_id
|
|
372
384
|
};
|
|
373
385
|
const settings = imageSettings(shared);
|
|
374
386
|
|
package/src/tools/media.js
CHANGED
|
@@ -9,10 +9,8 @@ const { resolveToBuffer, DEFAULT_MAX_FILE_MB, compactList } = require('./_shared
|
|
|
9
9
|
const { ownedUrl } = require('./owned-url');
|
|
10
10
|
const { UI, uiResult, listResult } = require('../apps');
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// count, so a capped grid can never be mistaken for "that's everything".
|
|
15
|
-
const GRID_CAP = 24;
|
|
12
|
+
// Server pagination bounds the page. Never slice it again: both text readers
|
|
13
|
+
// and the widget advance by the server's page size and would skip hidden rows.
|
|
16
14
|
|
|
17
15
|
async function mintUploadTicket(client) {
|
|
18
16
|
const ticket = await client.post('/v1/media/upload-ticket', {});
|
|
@@ -272,7 +270,8 @@ function registerMediaTools(server, client, options = {}) {
|
|
|
272
270
|
// locates an item; get_media returns one in full.
|
|
273
271
|
const text = compactList(media, {
|
|
274
272
|
fields: ['id', 'filename', 'media_type', 'url', 'thumbnail_url', 'size', 'project_id', 'created_at'],
|
|
275
|
-
cap:
|
|
273
|
+
cap: media.length,
|
|
274
|
+
maxChars: Infinity,
|
|
276
275
|
total: pagination ? (pagination.total_items != null ? pagination.total_items : pagination.total) : media.length,
|
|
277
276
|
extra: pagination ? { pagination } : undefined,
|
|
278
277
|
note: 'Narrow with `type`, `category`, `project_id`, `folder_id`, or `search`; get_media returns one item in full.',
|
|
@@ -289,7 +288,7 @@ function registerMediaTools(server, client, options = {}) {
|
|
|
289
288
|
const totalItems = pagination
|
|
290
289
|
? (pagination.total_items != null ? pagination.total_items : pagination.total)
|
|
291
290
|
: null;
|
|
292
|
-
const items = media.
|
|
291
|
+
const items = media.map((m) => ({
|
|
293
292
|
id: m.id,
|
|
294
293
|
title: m.filename,
|
|
295
294
|
subtitle: m.media_type + (m.size ? ' · ' + Math.round(m.size / 1024) + 'KB' : ''),
|
|
@@ -302,8 +301,9 @@ function registerMediaTools(server, client, options = {}) {
|
|
|
302
301
|
widget: 'media-grid',
|
|
303
302
|
title: 'Media Library',
|
|
304
303
|
items,
|
|
305
|
-
total: totalItems != null ? totalItems :
|
|
306
|
-
|
|
304
|
+
total: totalItems != null && totalItems >= 0 ? totalItems : null,
|
|
305
|
+
...(typeof pagination?.has_next === 'boolean' ? { has_next: pagination.has_next } : {}),
|
|
306
|
+
shown: media.length,
|
|
307
307
|
// Everything "Load more" needs to fetch page N+1 ITSELF. The button used
|
|
308
308
|
// to send a chat message asking the model to run the next page, on the
|
|
309
309
|
// belief that a widget cannot invoke a tool — it can
|
|
@@ -311,8 +311,8 @@ function registerMediaTools(server, client, options = {}) {
|
|
|
311
311
|
// with). Worse, the payload carried no page and no filters, so the model
|
|
312
312
|
// could not reconstruct the query either and typically re-ran page 1.
|
|
313
313
|
page_tool: 'list_media',
|
|
314
|
-
page: page || 1,
|
|
315
|
-
page_size: page_size || 50,
|
|
314
|
+
page: pagination?.page || page || 1,
|
|
315
|
+
page_size: pagination?.page_size || page_size || 50,
|
|
316
316
|
query: { project_id, folder_id, type, category, source_type, sort, search }
|
|
317
317
|
});
|
|
318
318
|
}
|
package/src/tools/models.js
CHANGED
|
@@ -49,7 +49,9 @@ function modelChips(m) {
|
|
|
49
49
|
const ds = [...m.supported_durations].sort((a, b) => a - b);
|
|
50
50
|
chips.push(ds.length > 1 ? `${ds[0]}-${ds[ds.length - 1]}s` : `${ds[0]}s`);
|
|
51
51
|
}
|
|
52
|
-
if (m.
|
|
52
|
+
if (m.supports_custom_fonts) chips.unshift('Fonts');
|
|
53
|
+
if (m.supports_transparent_background) chips.unshift('No BG');
|
|
54
|
+
if (m.supports_visual_dna) chips.push('DNA');
|
|
53
55
|
if (m.new_model || m.newModel) chips.push('NEW');
|
|
54
56
|
return chips.slice(0, 3);
|
|
55
57
|
}
|
|
@@ -101,7 +103,9 @@ function buildCatalogStructured(models, type, compact) {
|
|
|
101
103
|
|
|
102
104
|
// One row per model — every identifier, nothing else. ~90 bytes/model, so the
|
|
103
105
|
// whole 400+ model catalog fits in a payload an agent can actually read.
|
|
104
|
-
const identifierRow = (m) => ({
|
|
106
|
+
const identifierRow = (m) => ({
|
|
107
|
+
supports_custom_fonts: m.supports_custom_fonts === true,
|
|
108
|
+
supports_transparent_background: m.supports_transparent_background === true,
|
|
105
109
|
identifier: m.identifier,
|
|
106
110
|
name: m.name,
|
|
107
111
|
types: m.types,
|
|
@@ -190,8 +194,9 @@ function registerModelTools(server, client, options = {}) {
|
|
|
190
194
|
// "this model rejects DNA" (cap = 0) from "I don't know" (field
|
|
191
195
|
// missing). Now an explicit `max_dna: 0 (DNA not supported)` says the
|
|
192
196
|
// model says no, and absence means the API doesn't expose the field.
|
|
193
|
-
const formatSpecs = m => {
|
|
194
|
-
const parts = [];
|
|
197
|
+
const formatSpecs = m => {
|
|
198
|
+
const parts = [];
|
|
199
|
+
parts.push('supports_custom_fonts: ' + (m.supports_custom_fonts === true));
|
|
195
200
|
if (m.haveThinking && Array.isArray(m.thinkingLevels) && m.thinkingLevels.length) {
|
|
196
201
|
parts.push(`thinking_level: ${m.thinkingLevels.map(level => level.id).join('/')} (default ${m.thinkingDefault})`);
|
|
197
202
|
}
|
|
@@ -205,7 +210,8 @@ function registerModelTools(server, client, options = {}) {
|
|
|
205
210
|
const isLipsyncVideo = types.includes('lipsync-video');
|
|
206
211
|
const isLipsyncImage = types.includes('lipsync-image');
|
|
207
212
|
const isImageEdit = types.includes('image_editing');
|
|
208
|
-
const isImage = types.includes('text_to_img') || isImageEdit;
|
|
213
|
+
const isImage = types.includes('text_to_img') || isImageEdit;
|
|
214
|
+
if (isImage) parts.push('supports_transparent_background: ' + (m.supports_transparent_background === true));
|
|
209
215
|
|
|
210
216
|
if (Array.isArray(m.supported_resolutions) && m.supported_resolutions.length) {
|
|
211
217
|
const mult = m.resolution_multipliers || {};
|
package/src/tools/visual_dna.js
CHANGED
|
@@ -177,7 +177,7 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
177
177
|
// ─── get_visual_dna ────────────────────────────────────────
|
|
178
178
|
server.tool(
|
|
179
179
|
'get_visual_dna',
|
|
180
|
-
'Fetch a single Visual DNA profile by ID. Returns the full
|
|
180
|
+
'Fetch a single Visual DNA profile by ID. Returns the full stored description, character attributes, and all reference images. Internal extraction system prompts are not exposed. To fetch a teammate\'s Visual DNA that lives in a shared project, pass project_id (you need edit+ on it).',
|
|
181
181
|
{
|
|
182
182
|
visual_dna_id: z.string().describe('The Visual DNA profile ID'),
|
|
183
183
|
project_id: projectScopeReadField
|