@kolbo/mcp 1.113.1 → 1.113.3

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.113.1",
3
+ "version": "1.113.3",
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": {
package/src/apps/index.js CHANGED
@@ -366,6 +366,9 @@ async function modelInfoMap(client) {
366
366
  const DISPLAY_ALIASES = {
367
367
  'flux-3-draft-enhance': { base: 'seedance-2-5', name: 'Seedance 2.5 Draft Enhance' },
368
368
  'video-draft-enhance': { base: 'seedance-2-5', name: 'Seedance 2.5 Draft Enhance' },
369
+ // Billing SKU every ElevenLabs CLONED voice reports; its version shows in the
370
+ // separate "Model V4/V3/V2" chip, so the model chip is just the provider.
371
+ 'fal_elevenlabs_tts': { base: 'eleven_v4', name: 'ElevenLabs' },
369
372
  };
370
373
  // Operations whose engine is picked server-side when no model is passed.
371
374
  const OPERATION_ENGINES = { draft_enhance: 'flux-3-draft-enhance' };
@@ -530,10 +533,16 @@ async function resolveCatalogAspectRatio(client, modelId, requested, type) {
530
533
 
531
534
  // Same shape and TTL as the model catalog above: the voice catalog is stable,
532
535
  // and a per-generation /v1/voices round trip would be paid on every speech card.
533
- const voiceCache = new Map(); // apiBase → { at, byKey }
536
+ // Keyed per caller, not per server: /v1/voices includes the caller's own cloned
537
+ // voices, so one shared entry (the remote connector serves every user from one
538
+ // process) left everyone else's clones unresolved for the whole TTL.
539
+ const voiceCache = new Map(); // apiBase + key hash → { at, byKey }
534
540
 
535
541
  async function voiceInfoMap(client) {
536
- const cacheKey = client.apiBase || 'default';
542
+ const keyHash = client.apiKey
543
+ ? crypto.createHash('sha256').update(String(client.apiKey)).digest('hex').slice(0, 16)
544
+ : 'anon';
545
+ const cacheKey = `${client.apiBase || 'default'}|${keyHash}`;
537
546
  const hit = voiceCache.get(cacheKey);
538
547
  if (hit && Date.now() - hit.at < ICON_TTL_MS) return hit.byKey;
539
548
  const byKey = new Map();
@@ -1201,7 +1201,8 @@ function registerGenerateTools(server, client, options = {}) {
1201
1201
  // the card was labelling those "Smart Select", which is not even a text-to-
1202
1202
  // speech option, and naming the voice the caller typed rather than the one
1203
1203
  // that spoke. Same resolution addDisplayNames does for the polling path.
1204
- const ranVoice = (await voiceInfo(client, result.result?.voice).catch(() => null)) || voiceRecord;
1204
+ const ranVoice = statusVoice(result.result)
1205
+ || (await voiceInfo(client, result.result?.voice).catch(() => null)) || voiceRecord;
1205
1206
  return uiCompleted({
1206
1207
  tool: 'generate_speech', kind: 'audio', gen, client, prompt: text,
1207
1208
  model: result.result?.model || model,
@@ -1314,13 +1315,21 @@ function registerGenerateTools(server, client, options = {}) {
1314
1315
  r.model_name = (info && info.name) || r.model;
1315
1316
  r.model_icon = (info && info.icon) || null;
1316
1317
  }
1317
- if (r.voice) {
1318
+ // The API names a cloned voice itself (voice_name/voice_thumbnail): clones
1319
+ // are not reliably in the cached catalog, so keep the API's answer.
1320
+ if (r.voice && !statusVoice(r)) {
1318
1321
  const v = await voiceInfo(client, r.voice).catch(() => null);
1319
1322
  r.voice_name = (v && v.name) || r.voice;
1320
1323
  r.voice_thumbnail = (v && v.thumbnail) || null;
1321
1324
  }
1322
1325
  }
1323
1326
 
1327
+ function statusVoice(r) {
1328
+ if (!r || !r.voice_name) return null;
1329
+ const id = /^custom_/i.test(String(r.requested_voice_id || '')) ? r.requested_voice_id : r.voice;
1330
+ return { id, name: r.voice_name, thumbnail: r.voice_thumbnail || null };
1331
+ }
1332
+
1324
1333
  // ─── get_generation_status ─────────────────────────────────
1325
1334
  server.tool(
1326
1335
  'get_generation_status',
@@ -1941,6 +1950,18 @@ function registerGenerateTools(server, client, options = {}) {
1941
1950
  if (!source) throw new Error('source is required (URL or absolute local path to image/video)');
1942
1951
  if (!audio) throw new Error('audio is required (URL or absolute local path to audio file)');
1943
1952
 
1953
+ // Upload local files first so the card can show the face source and the
1954
+ // audio from the same CDN URLs the API receives (multipart left the card
1955
+ // with no references at all).
1956
+ ({ source, audio } = await rehostLocalPaths(client, { source, audio, project_id }, { allowLocalFiles: !options.remote }));
1957
+ const isVideoUrl = (u) => /\.(mp4|mov|webm|mkv|avi|m4v)(\?|$)/i.test(u);
1958
+ const httpRef = (u) => (typeof u === 'string' && /^https?:\/\//i.test(u) ? [u] : []);
1959
+ const lipsyncRefs = {
1960
+ reference_images: httpRef(source).filter((u) => !isVideoUrl(u)),
1961
+ reference_videos: httpRef(source).filter(isVideoUrl),
1962
+ reference_audio: httpRef(audio),
1963
+ };
1964
+
1944
1965
  const sourceIsUrl = typeof source === 'string' && /^https?:\/\//i.test(source);
1945
1966
  const audioIsUrl = typeof audio === 'string' && /^https?:\/\//i.test(audio);
1946
1967
 
@@ -1996,8 +2017,7 @@ function registerGenerateTools(server, client, options = {}) {
1996
2017
 
1997
2018
  if (returnsImmediately()) return submittedResult({
1998
2019
  tool: 'generate_lipsync', kind: 'video', gen: startResponse, client, model,
1999
- prompt: text_prompt, settings: { mode: 'lipsync' },
2000
- reference_images: sourceIsUrl && !/\.(mp4|mov|webm|mkv|avi|m4v)(\?|$)/i.test(source) ? [source] : [],
2020
+ prompt: text_prompt, settings: { mode: 'lipsync' }, ...lipsyncRefs,
2001
2021
  });
2002
2022
 
2003
2023
  const poll = await pollOrTimedOut(client, startResponse.generation_id, {
@@ -2009,9 +2029,7 @@ function registerGenerateTools(server, client, options = {}) {
2009
2029
 
2010
2030
  return uiCompleted({
2011
2031
  tool: 'generate_lipsync', kind: 'video', gen: startResponse, client, model,
2012
- prompt: text_prompt, settings: { mode: 'lipsync' },
2013
- reference_images: sourceIsUrl && !/\.(mp4|mov|webm|mkv|avi|m4v)(\?|$)/i.test(source) ? [source] : [],
2014
- reference_videos: sourceIsUrl && /\.(mp4|mov|webm|mkv|avi|m4v)(\?|$)/i.test(source) ? [source] : [],
2032
+ prompt: text_prompt, settings: { mode: 'lipsync' }, ...lipsyncRefs,
2015
2033
  urls: result.result?.urls || [],
2016
2034
  thumbnail_url: result.result?.thumbnail_url || null,
2017
2035
  duration: result.result?.duration || null,