@kolbo/mcp 1.0.0 → 1.1.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.
@@ -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,198 @@
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 fs = require('fs');
7
+ const path = require('path');
8
+ const FormData = require('form-data');
9
+
10
+ const MAX_FILE_BYTES = 25 * 1024 * 1024; // 25 MB per file
11
+
12
+ function isHttpUrl(s) {
13
+ return typeof s === 'string' && /^https?:\/\//i.test(s);
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
+ };
78
+ }
79
+
80
+ function registerVisualDnaTools(server, client) {
81
+ // ─── create_visual_dna ─────────────────────────────────────
82
+ server.tool(
83
+ 'create_visual_dna',
84
+ '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
+ {
86
+ name: { type: 'string', description: 'Name of the Visual DNA profile' },
87
+ dna_type: { type: 'string', description: 'Type: "character", "style", "product", "scene". Default: "character"' },
88
+ prompt_helper: { type: 'string', description: 'Optional description/notes to guide DNA extraction' },
89
+ images: { type: 'array', description: 'Array of image sources (URLs or absolute local paths). Max 4.' },
90
+ video: { type: 'string', description: 'Optional video source (URL or absolute local path)' },
91
+ audio: { type: 'string', description: 'Optional audio source (URL or absolute local path)' }
92
+ },
93
+ async ({ name, dna_type, prompt_helper, images, video, audio }) => {
94
+ if (!name || !name.trim()) {
95
+ throw new Error('name is required');
96
+ }
97
+
98
+ const imageList = Array.isArray(images) ? images.filter(Boolean) : [];
99
+ if (imageList.length > 4) {
100
+ throw new Error('Maximum 4 images allowed');
101
+ }
102
+ if (imageList.length === 0 && !video && !audio) {
103
+ throw new Error('At least one media reference (image, video, or audio) is required');
104
+ }
105
+
106
+ // Resolve all sources to buffers in parallel.
107
+ const [imageFiles, videoFile, audioFile] = await Promise.all([
108
+ Promise.all(imageList.map(src => resolveToBuffer(src, 'image'))),
109
+ video ? resolveToBuffer(video, 'video') : Promise.resolve(null),
110
+ audio ? resolveToBuffer(audio, 'audio') : Promise.resolve(null)
111
+ ]);
112
+
113
+ const form = new FormData();
114
+ form.append('name', name);
115
+ if (dna_type) form.append('dnaType', dna_type);
116
+ if (prompt_helper) form.append('promptHelper', prompt_helper);
117
+
118
+ for (const f of imageFiles) {
119
+ form.append('images', f.buffer, { filename: f.filename, contentType: f.contentType });
120
+ }
121
+ if (videoFile) {
122
+ form.append('videos', videoFile.buffer, { filename: videoFile.filename, contentType: videoFile.contentType });
123
+ }
124
+ if (audioFile) {
125
+ form.append('audio', audioFile.buffer, { filename: audioFile.filename, contentType: audioFile.contentType });
126
+ }
127
+
128
+ const result = await client.postMultipart('/v1/visual-dna', form);
129
+
130
+ return {
131
+ content: [{
132
+ type: 'text',
133
+ text: JSON.stringify(result.visual_dna || result, null, 2)
134
+ }]
135
+ };
136
+ }
137
+ );
138
+
139
+ // ─── list_visual_dnas ──────────────────────────────────────
140
+ server.tool(
141
+ 'list_visual_dnas',
142
+ 'List your Visual DNA profiles. Returns id, name, type, and thumbnail for each.',
143
+ {},
144
+ async () => {
145
+ const result = await client.get('/v1/visual-dna');
146
+ return {
147
+ content: [{
148
+ type: 'text',
149
+ text: JSON.stringify({
150
+ visual_dnas: result.visual_dnas || [],
151
+ count: result.count || 0
152
+ }, null, 2)
153
+ }]
154
+ };
155
+ }
156
+ );
157
+
158
+ // ─── get_visual_dna ────────────────────────────────────────
159
+ server.tool(
160
+ 'get_visual_dna',
161
+ 'Fetch a single Visual DNA profile by ID. Returns the full profile including system_prompt and all reference images.',
162
+ {
163
+ visual_dna_id: { type: 'string', description: 'The Visual DNA profile ID' }
164
+ },
165
+ async ({ visual_dna_id }) => {
166
+ const result = await client.get(`/v1/visual-dna/${encodeURIComponent(visual_dna_id)}`);
167
+ return {
168
+ content: [{
169
+ type: 'text',
170
+ text: JSON.stringify(result.visual_dna || result, null, 2)
171
+ }]
172
+ };
173
+ }
174
+ );
175
+
176
+ // ─── delete_visual_dna ─────────────────────────────────────
177
+ server.tool(
178
+ 'delete_visual_dna',
179
+ 'Delete a Visual DNA profile by ID. Only the owner can delete.',
180
+ {
181
+ visual_dna_id: { type: 'string', description: 'The Visual DNA profile ID to delete' }
182
+ },
183
+ async ({ visual_dna_id }) => {
184
+ const result = await client.delete(`/v1/visual-dna/${encodeURIComponent(visual_dna_id)}`);
185
+ return {
186
+ content: [{
187
+ type: 'text',
188
+ text: JSON.stringify({
189
+ success: true,
190
+ message: result.message || 'Visual DNA deleted'
191
+ }, null, 2)
192
+ }]
193
+ };
194
+ }
195
+ );
196
+ }
197
+
198
+ module.exports = { registerVisualDnaTools };