@kolbo/mcp 1.1.0 → 1.3.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 +14 -1
- package/package.json +9 -3
- package/src/index.js +4 -0
- package/src/polling.js +1 -1
- package/src/tools/_shared.js +212 -0
- package/src/tools/chat.js +13 -12
- package/src/tools/generate.js +415 -62
- package/src/tools/media.js +78 -0
- package/src/tools/models.js +52 -53
- package/src/tools/moodboards.js +3 -1
- package/src/tools/presets.js +36 -0
- package/src/tools/visual_dna.js +15 -78
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
const { z } = require('zod');
|
|
7
|
+
const FormData = require('form-data');
|
|
8
|
+
const { resolveToBuffer } = require('./_shared');
|
|
9
|
+
|
|
10
|
+
function registerMediaTools(server, client) {
|
|
11
|
+
// ─── upload_media ──────────────────────────────────────────
|
|
12
|
+
server.tool(
|
|
13
|
+
'upload_media',
|
|
14
|
+
'Upload a local file (or remote URL) to the user\'s Kolbo media library and get back a stable Kolbo CDN URL. Use this when the user wants to reference a local file in multiple subsequent generation calls — upload once, then pass the returned URL to generate_image / generate_video / visual_dna / etc. Auto-detects media type (image / video / audio) from the file extension. For a single-use reference where you already have a public URL, you can skip this and pass the URL directly to the generation tool.',
|
|
15
|
+
{
|
|
16
|
+
source: z.string().describe('URL or absolute local path to the file to upload. For local files this is the primary mode; for URLs, this re-hosts the file on Kolbo CDN for stability.'),
|
|
17
|
+
description: z.string().optional().describe('Optional description / caption for the uploaded media')
|
|
18
|
+
},
|
|
19
|
+
async ({ source, description }) => {
|
|
20
|
+
if (!source) throw new Error('source is required (URL or absolute local path)');
|
|
21
|
+
|
|
22
|
+
// Even for URL input we download-and-reupload — that's the whole point
|
|
23
|
+
// of upload_media (getting a stable Kolbo-owned URL). For ephemeral
|
|
24
|
+
// pass-through, the generation tools accept URLs directly.
|
|
25
|
+
const kind = /\.(mp4|mov|webm|mkv|avi|m4v)(\?|$)/i.test(source) ? 'video'
|
|
26
|
+
: /\.(mp3|wav|ogg|m4a|flac|aac)(\?|$)/i.test(source) ? 'audio'
|
|
27
|
+
: 'image';
|
|
28
|
+
const resolved = await resolveToBuffer(source, kind);
|
|
29
|
+
|
|
30
|
+
const form = new FormData();
|
|
31
|
+
form.append('file', resolved.buffer, { filename: resolved.filename, contentType: resolved.contentType });
|
|
32
|
+
if (description) form.append('description', description);
|
|
33
|
+
|
|
34
|
+
const result = await client.postMultipart('/v1/media/upload', form);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
content: [{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: JSON.stringify(result.media || result, null, 2)
|
|
40
|
+
}]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// ─── list_media ────────────────────────────────────────────
|
|
46
|
+
server.tool(
|
|
47
|
+
'list_media',
|
|
48
|
+
'List the user\'s uploaded media from their Kolbo media library. Supports filtering by type (image / video / audio) and pagination. Returns items with stable URLs, names, sizes, and upload timestamps. Use this to discover what the user has previously uploaded before deciding whether to create new content.',
|
|
49
|
+
{
|
|
50
|
+
type: z.string().optional().describe('Filter by type: "image" | "video" | "audio". Omit for all types.'),
|
|
51
|
+
page: z.number().optional().describe('Page number (1-indexed). Default: 1'),
|
|
52
|
+
page_size: z.number().optional().describe('Items per page. Default: 20, max 100'),
|
|
53
|
+
search: z.string().optional().describe('Optional full-text search term matched against media names and descriptions')
|
|
54
|
+
},
|
|
55
|
+
async ({ type, page, page_size, search }) => {
|
|
56
|
+
const params = new URLSearchParams();
|
|
57
|
+
if (type) params.set('type', type);
|
|
58
|
+
if (page) params.set('page', String(page));
|
|
59
|
+
if (page_size) params.set('pageSize', String(page_size));
|
|
60
|
+
if (search) params.set('searchTerm', search);
|
|
61
|
+
|
|
62
|
+
const qs = params.toString();
|
|
63
|
+
const result = await client.get(`/v1/media${qs ? '?' + qs : ''}`);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
content: [{
|
|
67
|
+
type: 'text',
|
|
68
|
+
text: JSON.stringify({
|
|
69
|
+
media: result.media || [],
|
|
70
|
+
pagination: result.pagination || null
|
|
71
|
+
}, null, 2)
|
|
72
|
+
}]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { registerMediaTools };
|
package/src/tools/models.js
CHANGED
|
@@ -1,53 +1,52 @@
|
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
module.exports = { registerModelTools };
|
|
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
|
+
const { z } = require('zod');
|
|
7
|
+
|
|
8
|
+
function registerModelTools(server, client) {
|
|
9
|
+
// ─── list_models ───────────────────────────────────────────
|
|
10
|
+
server.tool(
|
|
11
|
+
'list_models',
|
|
12
|
+
'List available AI models on Kolbo. Filter by type to find models for a specific generation type.',
|
|
13
|
+
{
|
|
14
|
+
type: z.string().optional().describe('Filter by type: "image", "video", "video_from_image", "music", "speech", "sound". Omit for all models.')
|
|
15
|
+
},
|
|
16
|
+
async ({ type }) => {
|
|
17
|
+
const path = type ? `/v1/models?type=${encodeURIComponent(type)}` : '/v1/models';
|
|
18
|
+
const result = await client.get(path);
|
|
19
|
+
|
|
20
|
+
// Format for readability
|
|
21
|
+
const summary = result.models.map(m =>
|
|
22
|
+
`${m.identifier} (${m.name}) - ${m.credit} credits${m.recommended ? ' [RECOMMENDED]' : ''}${m.new_model ? ' [NEW]' : ''}`
|
|
23
|
+
).join('\n');
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
content: [{
|
|
27
|
+
type: 'text',
|
|
28
|
+
text: `Available models (${result.count}):\n\n${summary}\n\nUse the "identifier" value as the "model" parameter in generate tools.`
|
|
29
|
+
}]
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// ─── check_credits ─────────────────────────────────────────
|
|
35
|
+
server.tool(
|
|
36
|
+
'check_credits',
|
|
37
|
+
'Check your remaining Kolbo credit balance.',
|
|
38
|
+
{},
|
|
39
|
+
async () => {
|
|
40
|
+
const result = await client.get('/v1/account/credits');
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
content: [{
|
|
44
|
+
type: 'text',
|
|
45
|
+
text: `Credit Balance:\n- Total: ${result.credits.total}\n- Plan credits: ${result.credits.plan_credits}\n- Credit pack: ${result.credits.credit_pack}\n- Redemption: ${result.credits.redemption}`
|
|
46
|
+
}]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { registerModelTools };
|
package/src/tools/moodboards.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* `npx @kolbo/mcp` installs in the wild will break silently. Add new tools or
|
|
4
4
|
* new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
|
|
5
5
|
|
|
6
|
+
const { z } = require('zod');
|
|
7
|
+
|
|
6
8
|
function registerMoodboardTools(server, client) {
|
|
7
9
|
// ─── list_moodboards ───────────────────────────────────────
|
|
8
10
|
server.tool(
|
|
@@ -28,7 +30,7 @@ function registerMoodboardTools(server, client) {
|
|
|
28
30
|
'get_moodboard',
|
|
29
31
|
'Fetch a single moodboard by ID. Returns the full moodboard including master_prompt, style_guide, and all image URLs.',
|
|
30
32
|
{
|
|
31
|
-
moodboard_id:
|
|
33
|
+
moodboard_id: z.string().describe('The moodboard ID')
|
|
32
34
|
},
|
|
33
35
|
async ({ moodboard_id }) => {
|
|
34
36
|
const result = await client.get(`/v1/moodboards/${encodeURIComponent(moodboard_id)}`);
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
const { z } = require('zod');
|
|
7
|
+
|
|
8
|
+
function registerPresetTools(server, client) {
|
|
9
|
+
// ─── list_presets ──────────────────────────────────────────
|
|
10
|
+
server.tool(
|
|
11
|
+
'list_presets',
|
|
12
|
+
'List generation presets across image, video, music, and text-to-video catalogs. Presets bundle a specific prompt template + style direction that can be passed to a generation tool via its `preset_id` arg for a one-shot creative direction. Filter by `type` to narrow to a specific catalog. Returns id, name, description, thumbnail, category, and (for music) audio preview URL.',
|
|
13
|
+
{
|
|
14
|
+
type: z.string().optional().describe('Filter by catalog: "image" | "video" | "music" | "text_to_video". Omit for all.')
|
|
15
|
+
},
|
|
16
|
+
async ({ type }) => {
|
|
17
|
+
const params = new URLSearchParams();
|
|
18
|
+
if (type) params.set('type', type);
|
|
19
|
+
const qs = params.toString();
|
|
20
|
+
const result = await client.get(`/v1/presets${qs ? '?' + qs : ''}`);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
content: [{
|
|
24
|
+
type: 'text',
|
|
25
|
+
text: JSON.stringify({
|
|
26
|
+
presets: result.presets || [],
|
|
27
|
+
count: result.count || 0,
|
|
28
|
+
...(result.warning ? { warning: result.warning } : {})
|
|
29
|
+
}, null, 2)
|
|
30
|
+
}]
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { registerPresetTools };
|
package/src/tools/visual_dna.js
CHANGED
|
@@ -3,78 +3,15 @@
|
|
|
3
3
|
* `npx @kolbo/mcp` installs in the wild will break silently. Add new tools or
|
|
4
4
|
* new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
const path = require('path');
|
|
6
|
+
const { z } = require('zod');
|
|
8
7
|
const FormData = require('form-data');
|
|
8
|
+
const { resolveToBuffer: sharedResolveToBuffer, VISUAL_DNA_MAX_BYTES } = require('./_shared');
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function guessFilename(source, fallbackExt) {
|
|
17
|
-
if (isHttpUrl(source)) {
|
|
18
|
-
try {
|
|
19
|
-
const u = new URL(source);
|
|
20
|
-
const base = path.basename(u.pathname) || `upload${fallbackExt}`;
|
|
21
|
-
return base.includes('.') ? base : `${base}${fallbackExt}`;
|
|
22
|
-
} catch (_) {
|
|
23
|
-
return `upload${fallbackExt}`;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return path.basename(source);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function guessContentType(filename) {
|
|
30
|
-
const ext = path.extname(filename).toLowerCase();
|
|
31
|
-
const map = {
|
|
32
|
-
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
|
|
33
|
-
'.webp': 'image/webp', '.gif': 'image/gif', '.bmp': 'image/bmp',
|
|
34
|
-
'.mp4': 'video/mp4', '.mov': 'video/quicktime', '.webm': 'video/webm',
|
|
35
|
-
'.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.ogg': 'audio/ogg', '.m4a': 'audio/mp4'
|
|
36
|
-
};
|
|
37
|
-
return map[ext] || 'application/octet-stream';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function resolveToBuffer(source, kind) {
|
|
41
|
-
// kind: 'image' | 'video' | 'audio' — used for default filename extension only.
|
|
42
|
-
const defaultExt = kind === 'image' ? '.png' : kind === 'video' ? '.mp4' : '.mp3';
|
|
43
|
-
|
|
44
|
-
if (isHttpUrl(source)) {
|
|
45
|
-
const res = await fetch(source);
|
|
46
|
-
if (!res.ok) throw new Error(`Failed to fetch ${source}: ${res.status}`);
|
|
47
|
-
const contentLen = parseInt(res.headers.get('content-length') || '0', 10);
|
|
48
|
-
if (contentLen && contentLen > MAX_FILE_BYTES) {
|
|
49
|
-
throw new Error(`File at ${source} exceeds 25MB limit`);
|
|
50
|
-
}
|
|
51
|
-
const arrayBuf = await res.arrayBuffer();
|
|
52
|
-
const buffer = Buffer.from(arrayBuf);
|
|
53
|
-
if (buffer.length > MAX_FILE_BYTES) {
|
|
54
|
-
throw new Error(`File at ${source} exceeds 25MB limit`);
|
|
55
|
-
}
|
|
56
|
-
return {
|
|
57
|
-
buffer,
|
|
58
|
-
filename: guessFilename(source, defaultExt),
|
|
59
|
-
contentType: res.headers.get('content-type') || guessContentType(guessFilename(source, defaultExt))
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Local path
|
|
64
|
-
if (!path.isAbsolute(source)) {
|
|
65
|
-
throw new Error(`Local file paths must be absolute: ${source}`);
|
|
66
|
-
}
|
|
67
|
-
const stat = fs.statSync(source);
|
|
68
|
-
if (stat.size > MAX_FILE_BYTES) {
|
|
69
|
-
throw new Error(`File ${source} (${stat.size} bytes) exceeds 25MB limit`);
|
|
70
|
-
}
|
|
71
|
-
const buffer = fs.readFileSync(source);
|
|
72
|
-
const filename = path.basename(source);
|
|
73
|
-
return {
|
|
74
|
-
buffer,
|
|
75
|
-
filename,
|
|
76
|
-
contentType: guessContentType(filename)
|
|
77
|
-
};
|
|
10
|
+
// Visual DNA caps reference media at 25MB per file (stricter than the
|
|
11
|
+
// default _shared.resolveToBuffer cap — DNA profiles only need enough
|
|
12
|
+
// source signal to extract features, not full-quality media).
|
|
13
|
+
function resolveToBuffer(source, kind) {
|
|
14
|
+
return sharedResolveToBuffer(source, kind, { maxBytes: VISUAL_DNA_MAX_BYTES });
|
|
78
15
|
}
|
|
79
16
|
|
|
80
17
|
function registerVisualDnaTools(server, client) {
|
|
@@ -83,12 +20,12 @@ function registerVisualDnaTools(server, client) {
|
|
|
83
20
|
'create_visual_dna',
|
|
84
21
|
'Create a Visual DNA profile from reference media. Each item in images/video/audio can be a public URL or an absolute local file path. Max 4 images, 1 video, 1 audio. Files capped at 25MB each.',
|
|
85
22
|
{
|
|
86
|
-
name:
|
|
87
|
-
dna_type:
|
|
88
|
-
prompt_helper:
|
|
89
|
-
images:
|
|
90
|
-
video:
|
|
91
|
-
audio:
|
|
23
|
+
name: z.string().describe('Name of the Visual DNA profile'),
|
|
24
|
+
dna_type: z.string().optional().describe('Type: "character", "style", "product", "scene". Default: "character"'),
|
|
25
|
+
prompt_helper: z.string().optional().describe('Optional description/notes to guide DNA extraction'),
|
|
26
|
+
images: z.array(z.string()).optional().describe('Array of image sources (URLs or absolute local paths). Max 4.'),
|
|
27
|
+
video: z.string().optional().describe('Optional video source (URL or absolute local path)'),
|
|
28
|
+
audio: z.string().optional().describe('Optional audio source (URL or absolute local path)')
|
|
92
29
|
},
|
|
93
30
|
async ({ name, dna_type, prompt_helper, images, video, audio }) => {
|
|
94
31
|
if (!name || !name.trim()) {
|
|
@@ -160,7 +97,7 @@ function registerVisualDnaTools(server, client) {
|
|
|
160
97
|
'get_visual_dna',
|
|
161
98
|
'Fetch a single Visual DNA profile by ID. Returns the full profile including system_prompt and all reference images.',
|
|
162
99
|
{
|
|
163
|
-
visual_dna_id:
|
|
100
|
+
visual_dna_id: z.string().describe('The Visual DNA profile ID')
|
|
164
101
|
},
|
|
165
102
|
async ({ visual_dna_id }) => {
|
|
166
103
|
const result = await client.get(`/v1/visual-dna/${encodeURIComponent(visual_dna_id)}`);
|
|
@@ -178,7 +115,7 @@ function registerVisualDnaTools(server, client) {
|
|
|
178
115
|
'delete_visual_dna',
|
|
179
116
|
'Delete a Visual DNA profile by ID. Only the owner can delete.',
|
|
180
117
|
{
|
|
181
|
-
visual_dna_id:
|
|
118
|
+
visual_dna_id: z.string().describe('The Visual DNA profile ID to delete')
|
|
182
119
|
},
|
|
183
120
|
async ({ visual_dna_id }) => {
|
|
184
121
|
const result = await client.delete(`/v1/visual-dna/${encodeURIComponent(visual_dna_id)}`);
|