@kolbo/mcp 1.0.0 → 1.2.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 +59 -11
- package/package.json +14 -5
- package/src/client.js +85 -2
- package/src/index.js +68 -0
- package/src/polling.js +32 -6
- package/src/tools/_shared.js +212 -0
- package/src/tools/chat.js +135 -0
- package/src/tools/generate.js +723 -224
- package/src/tools/media.js +77 -0
- package/src/tools/models.js +5 -0
- package/src/tools/moodboards.js +45 -0
- package/src/tools/presets.js +34 -0
- package/src/tools/visual_dna.js +134 -0
|
@@ -0,0 +1,77 @@
|
|
|
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 FormData = require('form-data');
|
|
7
|
+
const { resolveToBuffer } = require('./_shared');
|
|
8
|
+
|
|
9
|
+
function registerMediaTools(server, client) {
|
|
10
|
+
// ─── upload_media ──────────────────────────────────────────
|
|
11
|
+
server.tool(
|
|
12
|
+
'upload_media',
|
|
13
|
+
'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.',
|
|
14
|
+
{
|
|
15
|
+
source: { type: 'string', description: '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.' },
|
|
16
|
+
description: { type: 'string', description: 'Optional description / caption for the uploaded media' }
|
|
17
|
+
},
|
|
18
|
+
async ({ source, description }) => {
|
|
19
|
+
if (!source) throw new Error('source is required (URL or absolute local path)');
|
|
20
|
+
|
|
21
|
+
// Even for URL input we download-and-reupload — that's the whole point
|
|
22
|
+
// of upload_media (getting a stable Kolbo-owned URL). For ephemeral
|
|
23
|
+
// pass-through, the generation tools accept URLs directly.
|
|
24
|
+
const kind = /\.(mp4|mov|webm|mkv|avi|m4v)(\?|$)/i.test(source) ? 'video'
|
|
25
|
+
: /\.(mp3|wav|ogg|m4a|flac|aac)(\?|$)/i.test(source) ? 'audio'
|
|
26
|
+
: 'image';
|
|
27
|
+
const resolved = await resolveToBuffer(source, kind);
|
|
28
|
+
|
|
29
|
+
const form = new FormData();
|
|
30
|
+
form.append('file', resolved.buffer, { filename: resolved.filename, contentType: resolved.contentType });
|
|
31
|
+
if (description) form.append('description', description);
|
|
32
|
+
|
|
33
|
+
const result = await client.postMultipart('/v1/media/upload', form);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
content: [{
|
|
37
|
+
type: 'text',
|
|
38
|
+
text: JSON.stringify(result.media || result, null, 2)
|
|
39
|
+
}]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// ─── list_media ────────────────────────────────────────────
|
|
45
|
+
server.tool(
|
|
46
|
+
'list_media',
|
|
47
|
+
'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.',
|
|
48
|
+
{
|
|
49
|
+
type: { type: 'string', description: 'Filter by type: "image" | "video" | "audio". Omit for all types.' },
|
|
50
|
+
page: { type: 'number', description: 'Page number (1-indexed). Default: 1' },
|
|
51
|
+
page_size: { type: 'number', description: 'Items per page. Default: 20, max 100' },
|
|
52
|
+
search: { type: 'string', description: 'Optional full-text search term matched against media names and descriptions' }
|
|
53
|
+
},
|
|
54
|
+
async ({ type, page, page_size, search }) => {
|
|
55
|
+
const params = new URLSearchParams();
|
|
56
|
+
if (type) params.set('type', type);
|
|
57
|
+
if (page) params.set('page', String(page));
|
|
58
|
+
if (page_size) params.set('pageSize', String(page_size));
|
|
59
|
+
if (search) params.set('searchTerm', search);
|
|
60
|
+
|
|
61
|
+
const qs = params.toString();
|
|
62
|
+
const result = await client.get(`/v1/media${qs ? '?' + qs : ''}`);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
content: [{
|
|
66
|
+
type: 'text',
|
|
67
|
+
text: JSON.stringify({
|
|
68
|
+
media: result.media || [],
|
|
69
|
+
pagination: result.pagination || null
|
|
70
|
+
}, null, 2)
|
|
71
|
+
}]
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { registerMediaTools };
|
package/src/tools/models.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
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
|
+
|
|
1
6
|
function registerModelTools(server, client) {
|
|
2
7
|
// ─── list_models ───────────────────────────────────────────
|
|
3
8
|
server.tool(
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
function registerMoodboardTools(server, client) {
|
|
7
|
+
// ─── list_moodboards ───────────────────────────────────────
|
|
8
|
+
server.tool(
|
|
9
|
+
'list_moodboards',
|
|
10
|
+
'List moodboards available to you: your own, system presets, and any organization moodboards. Returns id, name, master_prompt, thumbnail, and image URLs for each.',
|
|
11
|
+
{},
|
|
12
|
+
async () => {
|
|
13
|
+
const result = await client.get('/v1/moodboards');
|
|
14
|
+
return {
|
|
15
|
+
content: [{
|
|
16
|
+
type: 'text',
|
|
17
|
+
text: JSON.stringify({
|
|
18
|
+
moodboards: result.moodboards || [],
|
|
19
|
+
count: result.count || 0
|
|
20
|
+
}, null, 2)
|
|
21
|
+
}]
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// ─── get_moodboard ─────────────────────────────────────────
|
|
27
|
+
server.tool(
|
|
28
|
+
'get_moodboard',
|
|
29
|
+
'Fetch a single moodboard by ID. Returns the full moodboard including master_prompt, style_guide, and all image URLs.',
|
|
30
|
+
{
|
|
31
|
+
moodboard_id: { type: 'string', description: 'The moodboard ID' }
|
|
32
|
+
},
|
|
33
|
+
async ({ moodboard_id }) => {
|
|
34
|
+
const result = await client.get(`/v1/moodboards/${encodeURIComponent(moodboard_id)}`);
|
|
35
|
+
return {
|
|
36
|
+
content: [{
|
|
37
|
+
type: 'text',
|
|
38
|
+
text: JSON.stringify(result.moodboard || result, null, 2)
|
|
39
|
+
}]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { registerMoodboardTools };
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
function registerPresetTools(server, client) {
|
|
7
|
+
// ─── list_presets ──────────────────────────────────────────
|
|
8
|
+
server.tool(
|
|
9
|
+
'list_presets',
|
|
10
|
+
'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.',
|
|
11
|
+
{
|
|
12
|
+
type: { type: 'string', description: 'Filter by catalog: "image" | "video" | "music" | "text_to_video". Omit for all.' }
|
|
13
|
+
},
|
|
14
|
+
async ({ type }) => {
|
|
15
|
+
const params = new URLSearchParams();
|
|
16
|
+
if (type) params.set('type', type);
|
|
17
|
+
const qs = params.toString();
|
|
18
|
+
const result = await client.get(`/v1/presets${qs ? '?' + qs : ''}`);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
content: [{
|
|
22
|
+
type: 'text',
|
|
23
|
+
text: JSON.stringify({
|
|
24
|
+
presets: result.presets || [],
|
|
25
|
+
count: result.count || 0,
|
|
26
|
+
...(result.warning ? { warning: result.warning } : {})
|
|
27
|
+
}, null, 2)
|
|
28
|
+
}]
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { registerPresetTools };
|
|
@@ -0,0 +1,134 @@
|
|
|
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 FormData = require('form-data');
|
|
7
|
+
const { resolveToBuffer: sharedResolveToBuffer, VISUAL_DNA_MAX_BYTES } = require('./_shared');
|
|
8
|
+
|
|
9
|
+
// Visual DNA caps reference media at 25MB per file (stricter than the
|
|
10
|
+
// default _shared.resolveToBuffer cap — DNA profiles only need enough
|
|
11
|
+
// source signal to extract features, not full-quality media).
|
|
12
|
+
function resolveToBuffer(source, kind) {
|
|
13
|
+
return sharedResolveToBuffer(source, kind, { maxBytes: VISUAL_DNA_MAX_BYTES });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function registerVisualDnaTools(server, client) {
|
|
17
|
+
// ─── create_visual_dna ─────────────────────────────────────
|
|
18
|
+
server.tool(
|
|
19
|
+
'create_visual_dna',
|
|
20
|
+
'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.',
|
|
21
|
+
{
|
|
22
|
+
name: { type: 'string', description: 'Name of the Visual DNA profile' },
|
|
23
|
+
dna_type: { type: 'string', description: 'Type: "character", "style", "product", "scene". Default: "character"' },
|
|
24
|
+
prompt_helper: { type: 'string', description: 'Optional description/notes to guide DNA extraction' },
|
|
25
|
+
images: { type: 'array', description: 'Array of image sources (URLs or absolute local paths). Max 4.' },
|
|
26
|
+
video: { type: 'string', description: 'Optional video source (URL or absolute local path)' },
|
|
27
|
+
audio: { type: 'string', description: 'Optional audio source (URL or absolute local path)' }
|
|
28
|
+
},
|
|
29
|
+
async ({ name, dna_type, prompt_helper, images, video, audio }) => {
|
|
30
|
+
if (!name || !name.trim()) {
|
|
31
|
+
throw new Error('name is required');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const imageList = Array.isArray(images) ? images.filter(Boolean) : [];
|
|
35
|
+
if (imageList.length > 4) {
|
|
36
|
+
throw new Error('Maximum 4 images allowed');
|
|
37
|
+
}
|
|
38
|
+
if (imageList.length === 0 && !video && !audio) {
|
|
39
|
+
throw new Error('At least one media reference (image, video, or audio) is required');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Resolve all sources to buffers in parallel.
|
|
43
|
+
const [imageFiles, videoFile, audioFile] = await Promise.all([
|
|
44
|
+
Promise.all(imageList.map(src => resolveToBuffer(src, 'image'))),
|
|
45
|
+
video ? resolveToBuffer(video, 'video') : Promise.resolve(null),
|
|
46
|
+
audio ? resolveToBuffer(audio, 'audio') : Promise.resolve(null)
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const form = new FormData();
|
|
50
|
+
form.append('name', name);
|
|
51
|
+
if (dna_type) form.append('dnaType', dna_type);
|
|
52
|
+
if (prompt_helper) form.append('promptHelper', prompt_helper);
|
|
53
|
+
|
|
54
|
+
for (const f of imageFiles) {
|
|
55
|
+
form.append('images', f.buffer, { filename: f.filename, contentType: f.contentType });
|
|
56
|
+
}
|
|
57
|
+
if (videoFile) {
|
|
58
|
+
form.append('videos', videoFile.buffer, { filename: videoFile.filename, contentType: videoFile.contentType });
|
|
59
|
+
}
|
|
60
|
+
if (audioFile) {
|
|
61
|
+
form.append('audio', audioFile.buffer, { filename: audioFile.filename, contentType: audioFile.contentType });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = await client.postMultipart('/v1/visual-dna', form);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
content: [{
|
|
68
|
+
type: 'text',
|
|
69
|
+
text: JSON.stringify(result.visual_dna || result, null, 2)
|
|
70
|
+
}]
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// ─── list_visual_dnas ──────────────────────────────────────
|
|
76
|
+
server.tool(
|
|
77
|
+
'list_visual_dnas',
|
|
78
|
+
'List your Visual DNA profiles. Returns id, name, type, and thumbnail for each.',
|
|
79
|
+
{},
|
|
80
|
+
async () => {
|
|
81
|
+
const result = await client.get('/v1/visual-dna');
|
|
82
|
+
return {
|
|
83
|
+
content: [{
|
|
84
|
+
type: 'text',
|
|
85
|
+
text: JSON.stringify({
|
|
86
|
+
visual_dnas: result.visual_dnas || [],
|
|
87
|
+
count: result.count || 0
|
|
88
|
+
}, null, 2)
|
|
89
|
+
}]
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// ─── get_visual_dna ────────────────────────────────────────
|
|
95
|
+
server.tool(
|
|
96
|
+
'get_visual_dna',
|
|
97
|
+
'Fetch a single Visual DNA profile by ID. Returns the full profile including system_prompt and all reference images.',
|
|
98
|
+
{
|
|
99
|
+
visual_dna_id: { type: 'string', description: 'The Visual DNA profile ID' }
|
|
100
|
+
},
|
|
101
|
+
async ({ visual_dna_id }) => {
|
|
102
|
+
const result = await client.get(`/v1/visual-dna/${encodeURIComponent(visual_dna_id)}`);
|
|
103
|
+
return {
|
|
104
|
+
content: [{
|
|
105
|
+
type: 'text',
|
|
106
|
+
text: JSON.stringify(result.visual_dna || result, null, 2)
|
|
107
|
+
}]
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// ─── delete_visual_dna ─────────────────────────────────────
|
|
113
|
+
server.tool(
|
|
114
|
+
'delete_visual_dna',
|
|
115
|
+
'Delete a Visual DNA profile by ID. Only the owner can delete.',
|
|
116
|
+
{
|
|
117
|
+
visual_dna_id: { type: 'string', description: 'The Visual DNA profile ID to delete' }
|
|
118
|
+
},
|
|
119
|
+
async ({ visual_dna_id }) => {
|
|
120
|
+
const result = await client.delete(`/v1/visual-dna/${encodeURIComponent(visual_dna_id)}`);
|
|
121
|
+
return {
|
|
122
|
+
content: [{
|
|
123
|
+
type: 'text',
|
|
124
|
+
text: JSON.stringify({
|
|
125
|
+
success: true,
|
|
126
|
+
message: result.message || 'Visual DNA deleted'
|
|
127
|
+
}, null, 2)
|
|
128
|
+
}]
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { registerVisualDnaTools };
|