@kolbo/mcp 1.40.0 → 1.42.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.40.0",
3
+ "version": "1.42.0",
4
4
  "description": "Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -122,16 +122,35 @@ function renderGenerating(sc) {
122
122
  schedulePoll(sc);
123
123
  }
124
124
 
125
+ // Poll ceilings so the card never spins "Generating" forever. A generation
126
+ // that genuinely stalls, or a status call that keeps failing (e.g. the record
127
+ // can't be resolved), surfaces an error + Try Again instead of hanging.
128
+ var MAX_POLL_MS = 12 * 60 * 1000; // hard wall-clock ceiling (covers slow 4K video)
129
+ var MAX_POLL_ERRORS = 12; // ~48s of consecutive status-call failures → give up
130
+ var pollStart = 0, pollErrors = 0;
131
+
125
132
  function schedulePoll(sc) {
133
+ if (!pollStart) pollStart = Date.now();
126
134
  clearTimeout(pollTimer);
127
135
  pollTimer = setTimeout(function () { poll(sc); }, 4000);
128
136
  }
129
137
  function poll(sc) {
138
+ if (pollStart && (Date.now() - pollStart) > MAX_POLL_MS) {
139
+ return renderError('This is taking longer than expected and may have stalled. Try again — if it keeps happening the model may be busy. Any finished result will also appear in your Kolbo library.');
140
+ }
130
141
  var args = sc.status_args || { generation_id: sc.generation_id };
131
142
  window.kolbo.callTool(sc.poll_tool || 'get_generation_status', args).then(function (res) {
132
143
  var st = structured(res) || {};
133
144
  var stateName = st.state || st.phase || st.status;
145
+ // A failed status CALL (tool error / not-found / {success:false}) is not a
146
+ // generation state — count it toward the consecutive-error cap so a record
147
+ // that can't be resolved errors out fast instead of polling forever.
148
+ if ((res && res.isError) || st.success === false || (st.error && !stateName)) {
149
+ if (++pollErrors >= MAX_POLL_ERRORS) return renderError(st.error || 'Could not track this generation. Please try again.');
150
+ return schedulePoll(sc);
151
+ }
134
152
  if (stateName === 'completed') {
153
+ pollErrors = 0;
135
154
  var r = st.result || st;
136
155
 
137
156
  var done = Object.assign({}, sc, r, {
@@ -152,9 +171,13 @@ function poll(sc) {
152
171
  } else if (stateName === 'failed' || stateName === 'error' || stateName === 'cancelled') {
153
172
  renderError(st.error || 'Generation ' + stateName);
154
173
  } else {
174
+ pollErrors = 0; // a valid in-progress response resets the failure streak
155
175
  schedulePoll(sc);
156
176
  }
157
- }).catch(function () { schedulePoll(sc); });
177
+ }).catch(function () {
178
+ if (++pollErrors >= MAX_POLL_ERRORS) return renderError('Lost connection while tracking this generation. Please try again.');
179
+ schedulePoll(sc);
180
+ });
158
181
  }
159
182
 
160
183
  /* ---------- results ---------- */
@@ -82,18 +82,31 @@ function boot(sc) {
82
82
  window.kolbo.notifySize();
83
83
  }
84
84
 
85
+ var pollStart = 0, pollErrors = 0;
86
+ var MAX_POLL_MS = 12 * 60 * 1000, MAX_POLL_ERRORS = 12;
85
87
  function poll() {
88
+ if (!pollStart) pollStart = Date.now();
89
+ if ((Date.now() - pollStart) > MAX_POLL_MS) {
90
+ return boot(Object.assign({}, state, { phase: 'failed', error: 'Transcription is taking longer than expected and may have stalled — please try again.' }));
91
+ }
86
92
  window.kolbo.callTool(state.poll_tool || 'get_generation_status', { generation_id: state.generation_id })
87
93
  .then(function (res) {
88
94
  var st = structured(res) || {};
89
95
  var s = st.state || st.phase;
96
+ if ((res && res.isError) || st.success === false || (st.error && !s)) {
97
+ if (++pollErrors >= MAX_POLL_ERRORS) return boot(Object.assign({}, state, { phase: 'failed', error: st.error || 'Could not track this transcription. Please try again.' }));
98
+ pollTimer = setTimeout(poll, 5000); return;
99
+ }
90
100
  if (s === 'completed') {
91
101
  var r = st.result || st;
92
102
  boot(Object.assign({}, state, r, { phase: 'completed', credits_used: st.credits_used }));
93
103
  } else if (s === 'failed' || s === 'cancelled') {
94
104
  boot(Object.assign({}, state, { phase: 'failed', error: st.error }));
95
- } else { pollTimer = setTimeout(poll, 5000); }
96
- }).catch(function () { pollTimer = setTimeout(poll, 5000); });
105
+ } else { pollErrors = 0; pollTimer = setTimeout(poll, 5000); }
106
+ }).catch(function () {
107
+ if (++pollErrors >= MAX_POLL_ERRORS) return boot(Object.assign({}, state, { phase: 'failed', error: 'Lost connection while tracking this transcription. Please try again.' }));
108
+ pollTimer = setTimeout(poll, 5000);
109
+ });
97
110
  }
98
111
 
99
112
  window.kolbo.onToolResult(function (result) {
@@ -90,7 +90,8 @@ function boot(sc) {
90
90
  var exts = [];
91
91
  kinds.forEach(function (k) { if (KINDS[k]) exts = exts.concat(KINDS[k].exts); });
92
92
  el('picker').setAttribute('accept', exts.map(function (e) { return '.' + e; }).join(','));
93
- el('accept-hint').textContent = kinds.join(' · ') + ' — up to ' + (sc.max_files || 10) + ' files';
93
+ var maxN = sc.max_files || 10;
94
+ el('accept-hint').textContent = kinds.join(' · ') + (maxN === 1 ? ' — one file' : ' — up to ' + maxN + ' files');
94
95
  if (expired()) return showExpired();
95
96
  window.kolbo.notifySize();
96
97
  }
@@ -105,7 +105,7 @@ function registerGenerateTools(server, client, options = {}) {
105
105
  'THE tool for ANY prompt-driven / content edit of an existing image — changing the scene ("make it night", "change the sky to sunset"), adding/removing/replacing objects, restyling, recoloring, compositing, or any "edit this image to…" request. This is the image-editing equivalent of generate_image and runs on strong dedicated editing models (nano-banana-2, gpt-image-2). Provide the source image URL(s) in `source_images` and the instruction in `prompt`. Supports Visual DNA profiles and moodboards for style-consistent edits. Do NOT use `edit_image` for these — that tool is only for mechanical enhancements (upscale/reframe/remove-background/skin). For a brand-new image from scratch, use generate_image. Returns the edited image URL(s) when complete.',
106
106
  {
107
107
  prompt: z.string().describe('Description of the edit to apply (e.g., "remove the background", "change the sky to sunset")'),
108
- model: z.string().optional().describe('Model identifier — REQUIRED in practice: pick a specific editing model, do NOT omit (omitting = Smart Select auto-pick, which we avoid). Strong current default: "nano-banana-2" (best general prompt-driven editor) or "gpt-image-2" for photoreal edits. Call list_models type="image_editing" to see all options and pick per the user\'s intent.'),
108
+ model: z.string().optional().describe('Model identifier — REQUIRED in practice: pick a specific IMAGE-EDITING model, do NOT omit (omitting = Smart Select auto-pick, which we avoid). Strong current defaults: "nano-banana-pro/edit" (best general prompt editor), "gpt-image/1.5-image-to-image" (photoreal), or "flux-2/edit". NOTE: text-to-image ids like "nano-banana-2"/"gpt-image-2" are NOT editors — don\'t use them here. Call list_models type="image_editing" to see all options and pick per the user\'s intent.'),
109
109
  source_images: z.array(z.string()).describe('PIXEL-ACCURATE compositing. Array of source image URLs whose pixel content is composited into the output. **Cap: pass at most `max_reference_images` URLs from list_models for the chosen model — exceeding it is a deterministic 400.** Three modes the model auto-detects from input shape: (1) Single image → edit/transform that image. (2) Multiple images, one base + others → composite the others into the base. (3) Multiple images with no clear base → generate a new scene that pixel-accurately embeds the supplied images at positions described in the prompt. Mode 3 is the canonical pattern for thumbnails / branded compositions where exact-pixel logo + face fidelity matter. Refer to source images in the prompt by ordinal position ("FIRST source image", "SECOND source image") or use @image1/@image2 tags. Add "composite AS-IS, do not redraw or restyle" to lock pixels.'),
110
110
  aspect_ratio: z.string().optional().describe('Output aspect ratio (e.g., "1:1", "16:9", "9:16"). Must be in the chosen model\'s `supported_aspect_ratios` from list_models. Default: "1:1"'),
111
111
  enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: true'),
@@ -295,7 +295,7 @@ function registerGenerateTools(server, client, options = {}) {
295
295
  'Generate a video from a text prompt using Kolbo AI. For animating an existing still image into motion, use generate_video_from_image instead. For a coordinated multi-scene video campaign, use generate_creative_director with workflow_type="video". Supports reference images (for style/composition guidance). Does NOT support Visual DNA — for character-consistent video use generate_elements or animate a DNA-locked still via generate_video_from_image. Returns the final video URL when complete.',
296
296
  {
297
297
  prompt: z.string().describe('Text description of the video to generate'),
298
- model: z.string().optional().describe('Model identifier. Use list_models type="text_to_video" to see options. Check supported_durations and supported_aspect_ratios.'),
298
+ model: z.string().optional().describe('Model identifier — pick a SPECIFIC model, do NOT omit (omitting = Smart Select auto-pick, which we avoid). Strong current defaults: "seedance-2" (versatile) or "veo3" (Veo 3.1, cinematic + native audio); the Kling family (call list_models for exact ids like kling-video/v3/pro/text-to-video) is strongest for motion. Call list_models type="text_to_video" to see all options + check supported_durations / supported_aspect_ratios, and choose per the user\'s intent.'),
299
299
  aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "16:9", "9:16", "1:1"). Must be in the chosen model\'s `supported_aspect_ratios` from list_models. Default: "16:9"'),
300
300
  duration: z.number().optional().describe('Duration in seconds. Must be a value in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration` (whichever the model exposes). Default: 5'),
301
301
  enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
@@ -346,7 +346,7 @@ function registerGenerateTools(server, client, options = {}) {
346
346
  {
347
347
  image_url: z.string().describe('URL of the source image to animate'),
348
348
  prompt: z.string().describe('Text description of the desired MOTION (e.g., "camera slowly pans right while the character walks forward")'),
349
- model: z.string().optional().describe('Model identifier. Use list_models type="img_to_video" to see options.'),
349
+ model: z.string().optional().describe('Model identifier — pick a SPECIFIC model, do NOT omit (omitting = Smart Select auto-pick, which we avoid). Strong current defaults: "seedance-2" (versatile) or "veo3" (Veo 3.1, cinematic + native audio); the Kling family (call list_models for exact ids like kling-video/v3/pro/image-to-video) is strongest for motion. Call list_models type="img_to_video" to see all options and choose per the user\'s intent.'),
350
350
  aspect_ratio: z.string().optional().describe('Output aspect ratio (e.g., "16:9", "9:16", "1:1"). Must be in the chosen model\'s `supported_aspect_ratios` from list_models. Default: "16:9"'),
351
351
  duration: z.number().optional().describe('Duration in seconds. Must be in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration`. Default: 5'),
352
352
  enhance_prompt: z.boolean().optional().describe('Enhance the motion prompt. Default: true'),
@@ -18,7 +18,7 @@ function registerMediaTools(server, client, options = {}) {
18
18
  {
19
19
  purpose: z.string().optional().describe('Short title shown on the card, e.g. "Upload the photo to animate". Helps the user know what to drop.'),
20
20
  media_types: z.array(z.enum(['image', 'video', 'audio', 'document'])).optional().describe('Restrict which file kinds the widget accepts. Omit to accept all types.'),
21
- max_files: z.number().optional().describe('Maximum number of files (default 10, max 20).'),
21
+ max_files: z.number().optional().describe('Maximum number of files the user may upload (default 10, max 20). LEAVE UNSET in almost all cases so the user can drop multiple files — only set this (e.g. to 1) if the task genuinely requires exactly one file. Do not restrict to 1 just because the current step uses one image; the user may want to upload several.'),
22
22
  project_id: z.string().optional().describe('Project ObjectId to file the uploads into (resolve names via `list_projects`).')
23
23
  },
24
24
  async ({ purpose, media_types, max_files, project_id }) => {