@kolbo/mcp 1.82.3 → 1.82.6

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.82.3",
3
+ "version": "1.82.6",
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
@@ -97,6 +97,11 @@ const WIDGET_CSP = {
97
97
  connectDomains: [
98
98
  'https://api.kolbo.ai',
99
99
  ],
100
+ // Nested iframes. Empty/omitted → frame-src 'none' and the live pricing
101
+ // embed inside the upgrade card is a blank box.
102
+ frameDomains: [
103
+ 'https://app.kolbo.ai',
104
+ ],
100
105
  };
101
106
 
102
107
  /** Register all Kolbo widget resources on an McpServer. */
package/src/apps/theme.js CHANGED
@@ -183,6 +183,8 @@ html.k-peek-fs .k-peek img, html.k-peek-fs .k-peek video { max-height: calc(100v
183
183
  .k-pack-head { margin: 14px 0 6px; font-size: 11px; font-weight: 700; letter-spacing: 0.04em;
184
184
  text-transform: uppercase; color: var(--text-faint); }
185
185
  .k-pack-row { margin-bottom: 6px; }
186
+ .k-pricing-frame { display: block; width: 100%; height: 560px; border: 0; border-radius: 12px;
187
+ background: #0f0f0f; }
186
188
  /* Visual DNA chips: the character's face, so you can see WHICH DNA is locked in. */
187
189
  .k-dna-face { width: 18px; height: 18px; border-radius: 999px; object-fit: cover; margin-left: -3px; background: var(--border-strong); }
188
190
  .k-dna-stack .k-dna-stack-item { display: inline-flex; }
@@ -794,7 +794,7 @@ function fillBatchCell(sc, i, g) {
794
794
  // the user is meant to watch, and without it the tile was a muted poster with
795
795
  // no way to play, seek or unmute until the whole batch finished and repainted.
796
796
  cell.innerHTML = (sc.kind === 'video'
797
- ? '<video class="k-cell-fill" src="' + esc(u) + '"' + (r.thumbnail_url ? ' poster="' + esc(r.thumbnail_url) + '"' : '') + ' controls playsinline preload="metadata"></video>'
797
+ ? '<video class="k-cell-fill" src="' + esc(playableAt(r, r.urls || [], 0)) + '"' + (r.thumbnail_url ? ' poster="' + esc(r.thumbnail_url) + '"' : '') + ' controls playsinline preload="metadata"></video>'
798
798
  : '<img class="k-cell-fill" src="' + esc(u) + '" alt="">') + cap;
799
799
  window.kolbo.notifySize();
800
800
  }
@@ -862,10 +862,28 @@ function renderImages(sc, urls) {
862
862
  });
863
863
  }
864
864
 
865
+ /**
866
+ * The url to PLAY, which is not always the url to DOWNLOAD.
867
+ *
868
+ * Seedance 2.5 at 1080p returns HEVC Main 10 (10-bit). Chrome and Edge decode HEVC in
869
+ * hardware only — with no software fallback — and Firefox on Windows not at all, so the
870
+ * widget showed a black frame with sound. The API therefore returns playback_urls
871
+ * alongside urls whenever it had to build an H.264 proxy.
872
+ *
873
+ * urls stays the MASTER and is what every download button must keep using: handing
874
+ * someone the 8-bit proxy for a file they paid the 1080p multiplier for is a silent
875
+ * quality downgrade. Backend: kolbo-api/src/services/webPlaybackProxy.js
876
+ */
877
+ function playableAt(src, urls, i) {
878
+ var proxies = (src && src.playback_urls) || [];
879
+ return proxies[i] || (urls || [])[i];
880
+ }
881
+
865
882
  function renderVideo(sc, urls) {
866
883
  // preload="none" behind a poster: the card shows the still until the user
867
884
  // hits play, instead of pulling the video header on mount.
868
- el('stage').innerHTML = '<div class="k-viewer"><video id="main-video" src="' + esc(urls[0]) + '"' +
885
+ // PLAY the proxy when there is one; the download button below keeps urls[0] (master).
886
+ el('stage').innerHTML = '<div class="k-viewer"><video id="main-video" src="' + esc(playableAt(sc, urls, 0)) + '"' +
869
887
  (sc.thumbnail_url ? ' poster="' + esc(sc.thumbnail_url) + '" preload="none"' : ' preload="metadata"') + ' controls playsinline></video>' +
870
888
  dlBtnHTML(urls[0]) + '</div>';
871
889
  wireDlButtons(el('stage'));
@@ -6,11 +6,11 @@ const { widgetPage } = require('../html');
6
6
  * Plans / upgrade widget — shown by `show_plans`, and by any generation the
7
7
  * server refused for credits.
8
8
  *
9
- * The buttons deep-link to app.kolbo.ai/pricing rather than a payment session:
10
- * promo pricing, tax, currency and saved payment methods live there, and a
11
- * checkout link minted on the API-key surface would drift from them the first
12
- * time a campaign changes. The prices below ARE the live promo-adjusted
13
- * numbers from /v1/account/plans, so the card never quotes a stale figure.
9
+ * The live pricing UI lives on app.kolbo.ai. This card iframes
10
+ * `/pricing/embed` (same PlanList / PricingCard as the site) so a price or
11
+ * perk change does not require an MCP republish. Buy/Subscribe on that page
12
+ * open https://app.kolbo.ai/pricing in a new tab Stripe and the user's
13
+ * session must not run inside a third-party host iframe.
14
14
  *
15
15
  * structuredContent contract:
16
16
  * {
@@ -19,8 +19,8 @@ const { widgetPage } = require('../html');
19
19
  * balance, required, shortfall, // when reason === 'insufficient_credits'
20
20
  * current_plan: { key, name },
21
21
  * plans: [{ key, name, interval, credits, price, original_price,
22
- * discount_percent, promo_text, currency }],
23
- * credit_packs: [ same shape ],
22
+ * discount_percent, promo_text, currency, top_up_discount }],
23
+ * credit_packs: [{ …, subscriber_price, is_subscriber }],
24
24
  * pricing_url
25
25
  * }
26
26
  */
@@ -35,10 +35,6 @@ const BODY = `
35
35
  </div>
36
36
  <div class="k-body">
37
37
  <div class="k-error" id="notice" style="display:none"></div>
38
- <div class="k-plan-toggle" id="toggle" style="display:none">
39
- <button type="button" class="k-toggle-btn" data-interval="month">Monthly</button>
40
- <button type="button" class="k-toggle-btn" data-interval="year">Annual</button>
41
- </div>
42
38
  <div id="stage"></div>
43
39
  <div class="k-actions" id="actions"></div>
44
40
  </div>
@@ -50,7 +46,7 @@ const BODY = `
50
46
 
51
47
  const SCRIPT = `
52
48
  var state = null;
53
- var interval = 'year'; // annual first — it is the better per-credit deal
49
+ var PRICING_EMBED = 'https://app.kolbo.ai/pricing/embed';
54
50
 
55
51
  el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
56
52
  el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink('https://app.kolbo.ai'); };
@@ -58,17 +54,6 @@ el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openL
58
54
  function pricingUrl() {
59
55
  return (state && state.pricing_url) || 'https://app.kolbo.ai/pricing';
60
56
  }
61
- function money(amount, currency) {
62
- if (amount == null) return '';
63
- var sym = String(currency || 'usd').toLowerCase() === 'usd' ? '$' : '';
64
- var n = Math.round(Number(amount) * 100) / 100;
65
- return sym + n + (sym ? '' : ' ' + String(currency || '').toUpperCase());
66
- }
67
- function perMonthNote(plan) {
68
- if (plan.interval !== 'year' || plan.price == null) return '';
69
- var monthly = Math.round((Number(plan.price) / 12) * 100) / 100;
70
- return money(monthly, plan.currency) + '/mo, billed annually';
71
- }
72
57
 
73
58
  function boot(sc) {
74
59
  if (!sc) return;
@@ -90,86 +75,12 @@ function boot(sc) {
90
75
  : 'That generation needs more credits than you have left.');
91
76
  }
92
77
 
93
- var plans = Array.isArray(sc.plans) ? sc.plans : [];
94
- var intervals = {};
95
- plans.forEach(function (p) { if (p.interval) intervals[p.interval] = true; });
96
- if (intervals.month && intervals.year) {
97
- el('toggle').style.display = '';
98
- if (!intervals[interval]) interval = intervals.year ? 'year' : 'month';
99
- } else {
100
- interval = intervals.year ? 'year' : 'month';
101
- }
102
- wireToggle();
103
- render();
104
- window.kolbo.notifySize();
105
- }
106
-
107
- function wireToggle() {
108
- Array.prototype.forEach.call(document.querySelectorAll('.k-toggle-btn'), function (b) {
109
- b.onclick = function () { interval = b.getAttribute('data-interval'); render(); window.kolbo.notifySize(); };
110
- });
111
- }
112
-
113
- function render() {
114
- Array.prototype.forEach.call(document.querySelectorAll('.k-toggle-btn'), function (b) {
115
- b.classList.toggle('active', b.getAttribute('data-interval') === interval);
116
- });
117
-
118
- var plans = (state.plans || []).filter(function (p) { return p.interval === interval; });
119
- // Cheapest first so the ladder reads left-to-right. A zero-price plan is not
120
- // an upgrade path, so it never takes a card slot.
121
- plans = plans.filter(function (p) { return Number(p.price) > 0; });
122
- plans.sort(function (a, b) { return (a.price || 0) - (b.price || 0); });
123
-
124
- var current = state.current_plan && state.current_plan.key;
125
- var html = plans.length
126
- ? '<div class="k-plan-grid">' + plans.map(function (p) { return planCard(p, current); }).join('') + '</div>'
127
- : '<div class="k-empty">Plan details are on the pricing page</div>';
128
-
129
- var packs = (state.credit_packs || []).filter(function (p) { return Number(p.price) > 0; });
130
- if (packs.length) {
131
- packs.sort(function (a, b) { return (a.price || 0) - (b.price || 0); });
132
- html += '<div class="k-pack-head">One-time credit packs</div>' +
133
- packs.slice(0, 4).map(function (p) {
134
- return '<div class="k-audio-row k-pack-row"><div class="k-audio-meta">' +
135
- '<div class="k-audio-title">' + esc(p.name || '') + '</div>' +
136
- (p.credits != null ? '<div class="k-audio-sub">' + esc(String(p.credits)) + ' credits</div>' : '') +
137
- '</div><span class="k-chip">' + esc(money(p.price, p.currency)) + '</span></div>';
138
- }).join('');
139
- }
140
-
141
- el('stage').innerHTML = html;
142
- Array.prototype.forEach.call(el('stage').querySelectorAll('[data-buy]'), function (b) {
143
- b.onclick = function () { window.kolbo.openLink(pricingUrl()); };
144
- });
78
+ el('stage').innerHTML = '<iframe class="k-pricing-frame" src="' + PRICING_EMBED +
79
+ '" title="Kolbo plans" referrerpolicy="no-referrer-when-downgrade"></iframe>';
80
+ var frame = el('stage').querySelector('iframe');
81
+ if (frame) frame.onload = function () { window.kolbo.notifySize(); };
145
82
  renderActions();
146
- }
147
-
148
- function planCard(p, currentKey) {
149
- var isCurrent = currentKey && p.key === currentKey;
150
- var badges = '';
151
- if (p.discount_percent) badges += '<span class="k-plan-badge">' + esc(String(p.discount_percent)) + '% OFF</span>';
152
- if (isCurrent) badges += '<span class="k-plan-badge current">Current</span>';
153
-
154
- var price = '<span class="k-plan-price">' + esc(money(p.price, p.currency)) + '</span>';
155
- if (p.original_price && p.original_price > p.price) {
156
- price = '<span class="k-plan-was">' + esc(money(p.original_price, p.currency)) + '</span> ' + price;
157
- }
158
- var note = perMonthNote(p);
159
-
160
- return '<div class="k-plan' + (isCurrent ? ' current' : '') + '">' +
161
- '<div class="k-plan-top"><span class="k-plan-name">' + esc(p.name || p.key || '') + '</span>' + badges + '</div>' +
162
- (p.credits != null
163
- ? '<div class="k-plan-credits">' + ICONS.sparkle + ' ' + esc(String(p.credits)) + ' credits' +
164
- (p.interval === 'month' ? '/mo' : p.interval === 'year' ? '/yr' : '') + '</div>'
165
- : '') +
166
- '<div class="k-plan-pricing">' + price + '</div>' +
167
- (note ? '<div class="k-plan-note">' + esc(note) + '</div>' : '') +
168
- (p.promo_text ? '<div class="k-plan-note">' + esc(p.promo_text) + '</div>' : '') +
169
- (isCurrent
170
- ? '<button class="k-btn" disabled>Your plan</button>'
171
- : '<button class="k-btn primary" data-buy="' + esc(p.key || '') + '">Get ' + esc(p.name || 'plan') + '</button>') +
172
- '</div>';
83
+ window.kolbo.notifySize();
173
84
  }
174
85
 
175
86
  function renderActions() {
@@ -184,8 +95,7 @@ window.kolbo.onToolResult(function (result) {
184
95
  });
185
96
  window.kolbo.onToolInput(function () {
186
97
  if (state) return;
187
- el('stage').innerHTML = '<div class="k-gen-grid n2"><div class="k-skel square" style="min-height:120px"></div>'
188
- + '<div class="k-skel square" style="min-height:120px"></div></div>';
98
+ el('stage').innerHTML = '<div class="k-skel square" style="min-height:240px"></div>';
189
99
  window.kolbo.notifySize();
190
100
  });
191
101
  window.kolbo.ready(function (ctx) {
@@ -89,7 +89,7 @@ async function pollBatch(client, batch, { interval, timeout }, toolName) {
89
89
  const polls = await Promise.all(batch.ids.map((id) => pollOrTimedOut(client, id, { interval, timeout })));
90
90
  const generations = polls.map((p, i) => p.timedOut
91
91
  ? { prompt: batch.ok[i].prompt, generation_id: batch.ids[i], status: 'processing', note: 'Still running — call get_generation_status with wait=true to collect it.' }
92
- : { prompt: batch.ok[i].prompt, generation_id: batch.ids[i], status: 'completed', ...creditFields(polls[i].result), urls: p.result.result.urls });
92
+ : { prompt: batch.ok[i].prompt, generation_id: batch.ids[i], status: 'completed', ...creditFields(polls[i].result), urls: p.result.result.urls, playback_urls: p.result.result.playback_urls });
93
93
  const text = JSON.stringify({
94
94
  batch: true,
95
95
  session_id: batch.ok[0].gen.session_id,
@@ -290,11 +290,13 @@ function registerGenerateTools(server, client, options = {}) {
290
290
  // input arg is only what the caller happened to name.
291
291
  reference_images: result.result.reference_images || reference_images,
292
292
  urls: result.result.urls,
293
+ playback_urls: result.result.playback_urls,
293
294
  credits_used: creditFields(result).credits_used,
294
295
  }, JSON.stringify({
295
296
  ...creditFields(result),
296
297
  session_id: gen.session_id,
297
298
  urls: result.result.urls,
299
+ playback_urls: result.result.playback_urls,
298
300
  model: result.result.model,
299
301
  prompt_used: result.result.prompt_used,
300
302
  _followup_hint: 'If the user asks to edit/change/modify this image next (scene, lighting, objects, style, color — any content edit), pass urls[0] to generate_image_edit. Use edit_image ONLY for mechanical ops (upscale/reframe/removebg/enhance_skin). Do NOT call generate_image again.'
@@ -380,11 +382,13 @@ function registerGenerateTools(server, client, options = {}) {
380
382
  reference_images: result.result.reference_images
381
383
  || [...(source_images || []), ...(reference_images || [])],
382
384
  urls: result.result.urls,
385
+ playback_urls: result.result.playback_urls,
383
386
  credits_used: creditFields(result).credits_used,
384
387
  }, JSON.stringify({
385
388
  ...creditFields(result),
386
389
  session_id: gen.session_id,
387
390
  urls: result.result.urls,
391
+ playback_urls: result.result.playback_urls,
388
392
  model: result.result.model,
389
393
  prompt_used: result.result.prompt_used,
390
394
  _followup_hint: 'If the user asks for another edit on this output, pass urls[0] back into generate_image_edit as source_images. For targeted ops (upscale/reframe/removebg/enhance_skin) use edit_image instead. Do NOT call generate_image from scratch.'
@@ -627,6 +631,7 @@ function registerGenerateTools(server, client, options = {}) {
627
631
  settings: videoSettings(shared),
628
632
  reference_images,
629
633
  urls: result.result.urls,
634
+ playback_urls: result.result.playback_urls,
630
635
  thumbnail_url: result.result.thumbnail_url,
631
636
  duration: result.result.duration,
632
637
  credits_used: creditFields(result).credits_used,
@@ -634,6 +639,7 @@ function registerGenerateTools(server, client, options = {}) {
634
639
  ...creditFields(result),
635
640
  session_id: gen.session_id,
636
641
  urls: result.result.urls,
642
+ playback_urls: result.result.playback_urls,
637
643
  model: result.result.model,
638
644
  duration: result.result.duration,
639
645
  thumbnail_url: result.result.thumbnail_url,
@@ -714,6 +720,7 @@ function registerGenerateTools(server, client, options = {}) {
714
720
  settings: videoSettings({ duration, resolution, aspect_ratio, enhance_prompt, visual_dna_ids }),
715
721
  reference_images: [image_url],
716
722
  urls: result.result.urls,
723
+ playback_urls: result.result.playback_urls,
717
724
  thumbnail_url: result.result.thumbnail_url,
718
725
  duration: result.result.duration,
719
726
  credits_used: creditFields(result).credits_used,
@@ -721,6 +728,7 @@ function registerGenerateTools(server, client, options = {}) {
721
728
  ...creditFields(result),
722
729
  session_id: gen.session_id,
723
730
  urls: result.result.urls,
731
+ playback_urls: result.result.playback_urls,
724
732
  model: result.result.model,
725
733
  duration: result.result.duration,
726
734
  thumbnail_url: result.result.thumbnail_url,
@@ -781,6 +789,7 @@ function registerGenerateTools(server, client, options = {}) {
781
789
  tool: 'generate_music', kind: 'audio', gen, client, model: model || 'Suno', prompt,
782
790
  settings: { mode: instrumental ? 'instrumental' : (style || undefined), preset_id },
783
791
  urls: result.result.urls,
792
+ playback_urls: result.result.playback_urls,
784
793
  title: result.result.title,
785
794
  duration: result.result.duration,
786
795
  credits_used: creditFields(result).credits_used,
@@ -788,6 +797,7 @@ function registerGenerateTools(server, client, options = {}) {
788
797
  ...creditFields(result),
789
798
  session_id: gen.session_id,
790
799
  urls: result.result.urls,
800
+ playback_urls: result.result.playback_urls,
791
801
  title: result.result.title,
792
802
  duration: result.result.duration,
793
803
  lyrics: result.result.lyrics
@@ -899,12 +909,14 @@ function registerGenerateTools(server, client, options = {}) {
899
909
  language,
900
910
  },
901
911
  urls: result.result.urls,
912
+ playback_urls: result.result.playback_urls,
902
913
  duration: result.result.duration,
903
914
  credits_used: creditFields(result).credits_used,
904
915
  }, JSON.stringify({
905
916
  ...creditFields(result),
906
917
  session_id: gen.session_id,
907
918
  urls: result.result.urls,
919
+ playback_urls: result.result.playback_urls,
908
920
  voice: result.result.voice,
909
921
  duration: result.result.duration,
910
922
  ...(unknownVoice ? { _warning: unknownVoice } : {})
@@ -964,12 +976,14 @@ function registerGenerateTools(server, client, options = {}) {
964
976
  settings: { duration },
965
977
  reference_images: seed_reference_image_url ? [seed_reference_image_url] : [],
966
978
  urls: result.result.urls,
979
+ playback_urls: result.result.playback_urls,
967
980
  duration: result.result.duration,
968
981
  credits_used: creditFields(result).credits_used,
969
982
  }, JSON.stringify({
970
983
  ...creditFields(result),
971
984
  session_id: gen.session_id,
972
985
  urls: result.result.urls,
986
+ playback_urls: result.result.playback_urls,
973
987
  duration: result.result.duration
974
988
  }, null, 2));
975
989
  }