@kolbo/mcp 1.105.1 → 1.106.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/package.json +1 -1
- package/src/index.js +2 -0
- package/src/toolAnnotations.js +2 -0
- package/src/tools/generate.js +4 -3
- package/src/tools/trends.js +189 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -69,6 +69,7 @@ const { registerColorPaletteTools } = require('./tools/color_palettes');
|
|
|
69
69
|
const { registerFontTools } = require('./tools/fonts');
|
|
70
70
|
const { registerEditorTools } = require('./tools/editor');
|
|
71
71
|
const { registerFlowTools } = require('./tools/flow');
|
|
72
|
+
const { registerTrendTools } = require('./tools/trends');
|
|
72
73
|
const { registerMediaTools } = require('./tools/media');
|
|
73
74
|
const { registerPresetTools } = require('./tools/presets');
|
|
74
75
|
const { registerArtifactTools } = require('./tools/artifacts');
|
|
@@ -194,6 +195,7 @@ function createServer(opts = {}) {
|
|
|
194
195
|
registerFontTools(server, client, { allowLocalFiles: opts.allowLocalFiles === true && !toolOptions.remote });
|
|
195
196
|
registerEditorTools(server, client);
|
|
196
197
|
registerFlowTools(server, client);
|
|
198
|
+
registerTrendTools(server, client);
|
|
197
199
|
registerAnalyzeTools(server, client, toolOptions);
|
|
198
200
|
registerMediaTools(server, client, toolOptions);
|
|
199
201
|
registerPresetTools(server, client, toolOptions);
|
package/src/toolAnnotations.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
const READ_ONLY = [
|
|
12
12
|
'get_flow_schema', 'list_flow_sessions', 'get_flow_session', 'validate_flow_session',
|
|
13
13
|
'estimate_flow_run', 'get_flow_run', 'list_flow_runs',
|
|
14
|
+
'list_trends', 'get_trend', 'estimate_trend_run', 'get_trend_run', 'list_trend_runs',
|
|
14
15
|
'get_download_status',
|
|
15
16
|
'get_video_editor_schema', 'list_video_editor_sessions', 'get_video_editor_session',
|
|
16
17
|
'list_fonts', 'get_font', 'get_font_upload_status',
|
|
@@ -78,6 +79,7 @@ const PRIVATE_WRITE = [
|
|
|
78
79
|
const DESTRUCTIVE_WRITE = [
|
|
79
80
|
'update_flow_session', 'undo_flow_edit', 'trash_flow_session',
|
|
80
81
|
'run_flow_session', 'cancel_flow_run', 'retry_flow_run',
|
|
82
|
+
'run_trend', 'cancel_trend_run',
|
|
81
83
|
'cancel_download',
|
|
82
84
|
'extend_music', 'cover_music',
|
|
83
85
|
'update_video_editor_session',
|
package/src/tools/generate.js
CHANGED
|
@@ -1081,7 +1081,8 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1081
1081
|
{
|
|
1082
1082
|
text: z.string().describe('The text to convert to speech'),
|
|
1083
1083
|
voice: z.string().optional().describe('Voice ID or display name — MUST come from a `list_voices` result, never constructed. Google/Gemini ids in particular are not validated provider-side: an id that is not in the catalog is silently mapped to another voice (or a default one) and the audio comes back in a voice nobody asked for. Do not pattern-match a locale onto an id you saw for another language. Default: "Rachel"'),
|
|
1084
|
-
model: z.string().optional().describe('Model identifier. Use list_models type="text_to_speech" to see options. Default:
|
|
1084
|
+
model: z.string().optional().describe('Model identifier. Use list_models type="text_to_speech" to see options. Default: eleven_v4 (ElevenLabs V4). ElevenLabs V4/V3 voices follow inline audio tags in the text: delivery ([whispers], [shouting], [sarcastic]), reactions ([laughs], [sighs], [clears throat]), pauses ([pause], [long pause]) and, on V4, free-form directions like [Low, steady voice, restrained urgency]. Tags are always English, even in Hebrew text. SSML such as <break> is NOT supported.'),
|
|
1085
|
+
turbo: z.boolean().optional().describe('ElevenLabs V4 voices ONLY. Turbo: faster generation at half the credits, slightly lower quality. Rejected (TURBO_UNSUPPORTED) for any voice not on ElevenLabs V4. Default false.'),
|
|
1085
1086
|
language: z.string().optional().describe('Language / accent code (e.g., "en-US", "he-IL", "es-ES"). For Google/Gemini voices this is the Accent control (does not translate the text). Default: "en-US"'),
|
|
1086
1087
|
// ── Expressive style / emotion (provider-specific) ──
|
|
1087
1088
|
// Google/Gemini: prefer a named preset id from the Kolbo UI picker; free-form
|
|
@@ -1119,7 +1120,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1119
1120
|
project_id: projectIdField,
|
|
1120
1121
|
session_id: sessionIdField
|
|
1121
1122
|
},
|
|
1122
|
-
async ({ text, voice, model, language, style_instructions_preset_id, style_instructions, style_instructions_label, selected_style, emotion, speaking_speed, similarity_boost, style, use_speaker_boost, variance, tempo, promptBoost, seed, accentControl, voiceTitle, minimax_pitch, minimax_vol, minimax_intensity, minimax_timbre, project_id, session_id }) => {
|
|
1123
|
+
async ({ text, voice, model, turbo, language, style_instructions_preset_id, style_instructions, style_instructions_label, selected_style, emotion, speaking_speed, similarity_boost, style, use_speaker_boost, variance, tempo, promptBoost, seed, accentControl, voiceTitle, minimax_pitch, minimax_vol, minimax_intensity, minimax_timbre, project_id, session_id }) => {
|
|
1123
1124
|
model = await canonicalModelId(client, model, 'text_to_speech'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
1124
1125
|
// Resolve the requested voice against the REAL catalog (cached) so the card
|
|
1125
1126
|
// can show its display name + portrait instead of a raw id, and so an id
|
|
@@ -1132,7 +1133,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1132
1133
|
? `Voice "${voice}" is not in the Kolbo voice catalog. Call list_voices and pass a voice_id it returns — an unrecognised id is NOT rejected, it is silently mapped to a different voice, so the audio will not be the voice you named.`
|
|
1133
1134
|
: null;
|
|
1134
1135
|
const gen = await client.post('/v1/generate/speech', {
|
|
1135
|
-
text, voice, model, language,
|
|
1136
|
+
text, voice, model, turbo, language,
|
|
1136
1137
|
style_instructions_preset_id, style_instructions, style_instructions_label,
|
|
1137
1138
|
selected_style, emotion, speaking_speed,
|
|
1138
1139
|
similarity_boost, style, use_speaker_boost,
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/* ⛔ BACKWARD COMPATIBILITY: Tool names and arg names below are a PUBLIC
|
|
2
|
+
* CONTRACT. Never rename, remove, or break an existing tool/arg — old cached
|
|
3
|
+
* `npx @kolbo/mcp` installs in the wild will break silently. Add new tools or
|
|
4
|
+
* new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
|
|
5
|
+
|
|
6
|
+
// Trends: plug-and-play generations with a hidden recipe. The caller fills the
|
|
7
|
+
// trend's declared inputs (photos, a clip, a name) and gets ONE final result;
|
|
8
|
+
// the steps in between stay private. Motion Library presets are served as
|
|
9
|
+
// trends too (slug "motion/<name>"). Contract: kolbo-api src/modules/trends.
|
|
10
|
+
|
|
11
|
+
const { randomUUID } = require('crypto');
|
|
12
|
+
const { z } = require('zod');
|
|
13
|
+
const { UI, uiResult } = require('../apps');
|
|
14
|
+
|
|
15
|
+
const slugField = z.string().min(1).max(200).regex(/^[\w-]+(\/[\w-]+)?$/)
|
|
16
|
+
.describe('Trend slug from list_trends, e.g. "vanish" or "motion/kolbo-urban-dance-glide".');
|
|
17
|
+
const inputsField = z.record(z.string(), z.string().max(2048))
|
|
18
|
+
.describe('Values for the trend\'s declared inputs, keyed by input `key` from get_trend. Media inputs take an https URL (upload local files with upload_media / create_upload_ticket first). Text inputs take plain text.');
|
|
19
|
+
const resolutionField = z.enum(['480p', '720p', '1080p']).optional()
|
|
20
|
+
.describe('Render resolution for video trends (see resolutions in get_trend). Default: the trend\'s own. Higher costs more; quote it with estimate_trend_run.');
|
|
21
|
+
const projectField = z.string().min(1).max(128).optional()
|
|
22
|
+
.describe('Project to run in. Omit for the default "API Generations" project.');
|
|
23
|
+
|
|
24
|
+
// Keep the slash between "motion" and the preset name; encode each segment.
|
|
25
|
+
const slugPath = slug => String(slug).split('/').map(encodeURIComponent).join('/');
|
|
26
|
+
const unwrap = r => (r && r.status === true && r.data !== undefined ? r.data : r);
|
|
27
|
+
const text = value => ({ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] });
|
|
28
|
+
const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
|
|
29
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
30
|
+
|
|
31
|
+
function trendTile(t) {
|
|
32
|
+
const example = (t.examples || [])[0] || {};
|
|
33
|
+
const poster = t.cover?.image || example.poster || t.source_poster;
|
|
34
|
+
return {
|
|
35
|
+
id: t.slug,
|
|
36
|
+
title: t.name,
|
|
37
|
+
subtitle: t.estimated_credits ? `~${t.estimated_credits} credits` : undefined,
|
|
38
|
+
thumbnail: poster,
|
|
39
|
+
url: t.cover?.video || example.output || poster,
|
|
40
|
+
media_type: t.output_type === 'image' ? 'image' : 'video',
|
|
41
|
+
use_hint: 'Run the "{TITLE}" trend (slug: {ID}) with my media.',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runTile(r) {
|
|
46
|
+
return {
|
|
47
|
+
id: r.run_id,
|
|
48
|
+
title: r.trend_name || r.trend,
|
|
49
|
+
subtitle: r.status === 'completed' ? 'Done' : r.status === 'failed' ? 'Failed' : `${r.status} · ${r.progress || 0}%`,
|
|
50
|
+
thumbnail: (r.output_type === 'image' && r.output?.url) || r.trend_cover,
|
|
51
|
+
url: r.output?.url || r.trend_cover,
|
|
52
|
+
media_type: r.output?.type === 'image' || r.output_type === 'image' ? 'image' : 'video',
|
|
53
|
+
use_hint: 'Show trend run {ID}.',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Poll one run until it finishes or the window closes. A closed window is NOT a
|
|
58
|
+
// failure: the run keeps going server-side and get_trend_run picks it up.
|
|
59
|
+
async function waitForRun(client, runId, maxMs) {
|
|
60
|
+
const deadline = Date.now() + maxMs;
|
|
61
|
+
let run;
|
|
62
|
+
for (;;) {
|
|
63
|
+
run = unwrap(await client.get(`/v1/trends/runs/${encodeURIComponent(runId)}`));
|
|
64
|
+
if (TERMINAL.has(run.status) || Date.now() >= deadline) return run;
|
|
65
|
+
await sleep(5000);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function runResult(run) {
|
|
70
|
+
if (!TERMINAL.has(run.status)) {
|
|
71
|
+
run._hint = `Still running (this is normal — video trends take a few minutes). Call get_trend_run with run_id="${run.run_id}" and wait=true. Do NOT start the trend again.`;
|
|
72
|
+
}
|
|
73
|
+
return text(run);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function registerTrendTools(server, client) {
|
|
77
|
+
server.tool(
|
|
78
|
+
'list_trends',
|
|
79
|
+
'Browse Kolbo Trends: one-click generations with a hidden recipe (e.g. put yourself in a viral clip, turn a photo into an action figure). Includes Motion Library presets (slugs starting "motion/") that restyle or recast a video. Each trend lists the inputs it needs, its output type and an estimated credit cost. Run one with run_trend.',
|
|
80
|
+
{
|
|
81
|
+
family: z.enum(['viral', 'image', 'swap', 'product', 'motion']).optional().describe('Filter by family. "motion" = Motion Library presets only.'),
|
|
82
|
+
search: z.string().max(100).optional().describe('Case-insensitive match on name, description and tags.'),
|
|
83
|
+
include_motion: z.boolean().optional().describe('Include Motion Library presets when no family is set. Default true.'),
|
|
84
|
+
},
|
|
85
|
+
async ({ family, search, include_motion } = {}) => {
|
|
86
|
+
const wantMotion = family === 'motion' || (!family && include_motion !== false);
|
|
87
|
+
const [base, motion] = await Promise.all([
|
|
88
|
+
family === 'motion' ? null : client.get('/v1/trends' + (family ? `?family=${encodeURIComponent(family)}` : '')),
|
|
89
|
+
wantMotion ? client.get('/v1/trends/motion') : null,
|
|
90
|
+
]);
|
|
91
|
+
const baseData = unwrap(base) || {};
|
|
92
|
+
const q = (search || '').trim().toLowerCase();
|
|
93
|
+
const trends = [...(baseData.trends || []), ...((unwrap(motion) || {}).trends || [])]
|
|
94
|
+
.filter(t => !q || [t.name, t.description, ...(t.tags || [])].some(s => s && String(s).toLowerCase().includes(q)));
|
|
95
|
+
const summary = trends.map(t => ({
|
|
96
|
+
slug: t.slug, name: t.name, description: t.description, family: t.family, output_type: t.output_type,
|
|
97
|
+
estimated_credits: t.estimated_credits, free_trial: t.free_trial, badges: t.badges,
|
|
98
|
+
inputs: (t.inputs || []).map(i => ({ key: i.key, kind: i.kind, role: i.role, label: i.label, required: i.required })),
|
|
99
|
+
}));
|
|
100
|
+
return uiResult(UI.mediaGrid, JSON.stringify({ trends: summary, count: summary.length, free_trials_left: baseData.free_trials_left ?? null }, null, 2), {
|
|
101
|
+
widget: 'media-grid',
|
|
102
|
+
title: family === 'motion' ? 'Motion Library' : 'Trends',
|
|
103
|
+
items: trends.slice(0, 300).map(trendTile),
|
|
104
|
+
total: trends.length,
|
|
105
|
+
has_more: trends.length > 300,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
server.tool(
|
|
111
|
+
'get_trend',
|
|
112
|
+
'Get one trend: description, the exact inputs it needs (key, kind: image/video/audio/text/choice, role: what the input is (character/pet/product/location/video/text) - a character input also takes a Visual DNA image URL, required, options), output type/aspect ratio, estimated credits, example results and whether a free trial is available.',
|
|
113
|
+
{ slug: slugField },
|
|
114
|
+
async ({ slug }) => text(unwrap(await client.get(`/v1/trends/${slugPath(slug)}`)))
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
server.tool(
|
|
118
|
+
'estimate_trend_run',
|
|
119
|
+
'Quote a trend run before spending: returns total_credits for these exact inputs (video trends are priced by the source clip length). Free — no credits spent.',
|
|
120
|
+
{ slug: slugField, inputs: inputsField, project_id: projectField, resolution: resolutionField },
|
|
121
|
+
async ({ slug, inputs, project_id, resolution }) => text(unwrap(await client.post(`/v1/trends/${slugPath(slug)}/estimate`, { inputs, ...(project_id && { project_id }), ...(resolution && { resolution }) })))
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
server.tool(
|
|
125
|
+
'run_trend',
|
|
126
|
+
'Run a trend. SPENDS CREDITS (quote with estimate_trend_run first and confirm with the user unless they already agreed). Returns one final result; intermediate steps are private. Waits up to wait_seconds for the result, then returns the run so you can keep checking with get_trend_run. Reuse the same idempotency_key after an uncertain response — a new key starts (and charges) a new run.',
|
|
127
|
+
{
|
|
128
|
+
slug: slugField,
|
|
129
|
+
inputs: inputsField,
|
|
130
|
+
project_id: projectField,
|
|
131
|
+
resolution: resolutionField,
|
|
132
|
+
max_credits: z.number().finite().min(0).optional().describe('Credit ceiling for this run. Default: the trend\'s own ceiling.'),
|
|
133
|
+
free_trial: z.boolean().optional().describe('Use one of the account\'s free trend runs (only trends with free_trial: true).'),
|
|
134
|
+
idempotency_key: z.string().regex(/^[A-Za-z0-9_.:-]{8,100}$/).optional().describe('8–100 chars. Omit to have one generated; pass the SAME key to retry an uncertain request.'),
|
|
135
|
+
wait: z.boolean().optional().describe('Wait for the result. Default true.'),
|
|
136
|
+
wait_seconds: z.number().int().min(0).max(170).optional().describe('How long to wait before returning. Default 150.'),
|
|
137
|
+
},
|
|
138
|
+
async ({ slug, inputs, project_id, resolution, max_credits, free_trial, idempotency_key, wait, wait_seconds }) => {
|
|
139
|
+
const body = { inputs, idempotency_key: idempotency_key || randomUUID(), ...(project_id && { project_id }), ...(resolution && { resolution }), ...(max_credits !== undefined && { max_credits }), ...(free_trial && { free_trial: true }) };
|
|
140
|
+
const started = unwrap(await client.post(`/v1/trends/${slugPath(slug)}/runs`, body));
|
|
141
|
+
started.idempotency_key = body.idempotency_key;
|
|
142
|
+
if (wait === false || TERMINAL.has(started.status)) return runResult(started);
|
|
143
|
+
const run = await waitForRun(client, started.run_id, (wait_seconds ?? 150) * 1000);
|
|
144
|
+
return runResult({ ...run, idempotency_key: body.idempotency_key });
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
server.tool(
|
|
149
|
+
'get_trend_run',
|
|
150
|
+
'Check a trend run: status (running/completed/failed/cancelled), progress %, and the final output URL when done. Pass wait=true to block until it finishes (up to ~2.5 minutes) instead of polling in a loop.',
|
|
151
|
+
{
|
|
152
|
+
run_id: z.string().min(1).max(128),
|
|
153
|
+
wait: z.boolean().optional().describe('Block until the run finishes or ~150s pass.'),
|
|
154
|
+
},
|
|
155
|
+
async ({ run_id, wait }) => runResult(wait ? await waitForRun(client, run_id, 150000) : unwrap(await client.get(`/v1/trends/runs/${encodeURIComponent(run_id)}`)))
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
server.tool(
|
|
159
|
+
'list_trend_runs',
|
|
160
|
+
'List the user\'s recent trend runs in a project (newest first) with status, progress and final output.',
|
|
161
|
+
{
|
|
162
|
+
project_id: projectField,
|
|
163
|
+
limit: z.number().int().min(1).max(100).optional().describe('Default 30.'),
|
|
164
|
+
},
|
|
165
|
+
async ({ project_id, limit } = {}) => {
|
|
166
|
+
const params = new URLSearchParams();
|
|
167
|
+
if (project_id) params.set('project_id', project_id);
|
|
168
|
+
if (limit) params.set('limit', String(limit));
|
|
169
|
+
const qs = params.toString();
|
|
170
|
+
const runs = (unwrap(await client.get('/v1/trends/runs' + (qs ? `?${qs}` : ''))) || {}).runs || [];
|
|
171
|
+
return uiResult(UI.mediaGrid, JSON.stringify({ runs, count: runs.length }, null, 2), {
|
|
172
|
+
widget: 'media-grid',
|
|
173
|
+
title: 'Trend runs',
|
|
174
|
+
items: runs.map(runTile),
|
|
175
|
+
total: runs.length,
|
|
176
|
+
has_more: false,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
server.tool(
|
|
182
|
+
'cancel_trend_run',
|
|
183
|
+
'Cancel a running trend run and release its unspent credits. Free-trial runs cannot be cancelled.',
|
|
184
|
+
{ run_id: z.string().min(1).max(128) },
|
|
185
|
+
async ({ run_id }) => text(unwrap(await client.post(`/v1/trends/runs/${encodeURIComponent(run_id)}/cancel`, {})))
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { registerTrendTools };
|