@kolbo/mcp 1.115.0 → 1.117.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.115.0",
3
+ "version": "1.117.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": {
@@ -24,7 +24,11 @@ const { widgetPage } = require('../html');
24
24
  * plans: [{ key, name, interval, credits, price, original_price,
25
25
  * discount_percent, promo_text, currency, top_up_discount }],
26
26
  * credit_packs: [{ …, subscriber_price, is_subscriber }],
27
- * pricing_url
27
+ * pricing_url,
28
+ * // insufficient_credits only — leads the card (packs out-convert plans
29
+ * // for users who just ran out); every URL carries MCP attribution tags:
30
+ * recommended_pack: { …pack, covers_shortfall, url } | null,
31
+ * pack_url, home_url
28
32
  * }
29
33
  */
30
34
 
@@ -52,11 +56,15 @@ var state = null;
52
56
  var interval = 'month';
53
57
 
54
58
  el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
55
- el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink('https://app.kolbo.ai'); };
59
+ function homeUrl() { return (state && state.home_url) || 'https://app.kolbo.ai'; }
60
+ el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink(homeUrl()); };
61
+ // Own handler so the shared untagged logo link (html.js) never fires here.
62
+ el('logo').onclick = function (e) { e.stopPropagation(); window.kolbo.openLink(homeUrl()); };
56
63
 
57
64
  function pricingUrl() {
58
65
  return (state && state.pricing_url) || 'https://app.kolbo.ai/pricing';
59
66
  }
67
+ function packUrl() { return (state && state.pack_url) || pricingUrl(); }
60
68
  function money(v, currency) {
61
69
  if (v == null || isNaN(v)) return '';
62
70
  var sym = { usd: '$', eur: '\u20ac', gbp: '\u00a3', ils: '\u20aa' }[String(currency || 'usd').toLowerCase()] || '';
@@ -76,7 +84,9 @@ function planCard(p, opts) {
76
84
  var note = p.promo_text ? '<div class="k-plan-note">' + esc(p.promo_text) + '</div>'
77
85
  : (opts.pack && p.is_subscriber && p.subscriber_price != null ? '<div class="k-plan-note">Subscriber price applied</div>' : '')
78
86
  + (!opts.pack && p.top_up_discount ? '<div class="k-plan-note">Cheaper credit packs included</div>' : '');
79
- var cta = current ? '' : '<button class="k-btn ' + (opts.pack ? 'ghost' : 'primary') + '" data-open="1">' + (opts.pack ? 'Buy pack' : 'Upgrade') + ' ' + ICONS.open + '</button>';
87
+ if (opts.lead) note += '<div class="k-plan-note">One-time top-up, no subscription</div>';
88
+ var primary = opts.pack ? opts.lead : !state.recommended_pack;
89
+ var cta = current ? '' : '<button class="k-btn ' + (primary ? 'primary' : 'ghost') + '" data-open="' + (opts.pack ? 'pack' : 'plan') + '">' + (opts.pack ? 'Buy pack' : 'Upgrade') + ' ' + ICONS.open + '</button>';
80
90
  return '<div class="k-plan' + (current ? ' current' : '') + '">' +
81
91
  '<div class="k-plan-top"><span class="k-plan-name">' + esc(p.name || p.key || 'Plan') + '</span>' + badge + '</div>' +
82
92
  (p.credits != null ? '<div class="k-plan-credits">' + ICONS.sparkle + ' ' + esc(fmtCredits(p.credits)) + ' credits' + (opts.pack ? '' : ' / ' + (isAnnual(p) ? 'year' : 'month')) + '</div>' : '') +
@@ -85,12 +95,17 @@ function planCard(p, opts) {
85
95
 
86
96
  function renderStage() {
87
97
  var plans = (state.plans || []).filter(function (p) { return p && (p.price != null || p.credits != null); });
88
- var packs = (state.credit_packs || []).filter(function (p) { return p && p.price != null; });
98
+ var lead = state.recommended_pack;
99
+ var packs = (state.credit_packs || []).filter(function (p) { return p && p.price != null && !(lead && lead.key != null && p.key === lead.key); });
89
100
  var monthly = plans.filter(function (p) { return !isAnnual(p); });
90
101
  var annual = plans.filter(isAnnual);
91
102
  var hasToggle = monthly.length && annual.length;
92
103
  var shown = hasToggle ? (interval === 'year' ? annual : monthly) : plans;
93
104
  var html = '';
105
+ if (lead) {
106
+ html += '<div class="k-pack-head">Quickest fix</div><div class="k-plan-grid">' + planCard(lead, { pack: true, lead: true }) + '</div>' +
107
+ '<div class="k-pack-head">Or upgrade your plan</div>';
108
+ }
94
109
  if (hasToggle) {
95
110
  html += '<div class="k-plan-toggle">' +
96
111
  '<button class="k-toggle-btn' + (interval === 'month' ? ' active' : '') + '" data-interval="month">Monthly</button>' +
@@ -98,7 +113,7 @@ function renderStage() {
98
113
  }
99
114
  if (shown.length) html += '<div class="k-plan-grid">' + shown.map(function (p) { return planCard(p, {}); }).join('') + '</div>';
100
115
  if (packs.length) {
101
- html += '<div class="k-pack-head">Credit packs</div><div class="k-plan-grid">' +
116
+ html += '<div class="k-pack-head">' + (lead ? 'Other credit packs' : 'Credit packs') + '</div><div class="k-plan-grid">' +
102
117
  packs.map(function (p) { return planCard(p, { pack: true }); }).join('') + '</div>';
103
118
  }
104
119
  if (!html) html = '<div class="k-plan-note">Live plans and prices are on the Kolbo pricing page.</div>';
@@ -106,7 +121,7 @@ function renderStage() {
106
121
  var btns = el('stage').querySelectorAll('[data-interval]');
107
122
  for (var i = 0; i < btns.length; i++) btns[i].onclick = function () { interval = this.getAttribute('data-interval'); renderStage(); window.kolbo.notifySize(); };
108
123
  var opens = el('stage').querySelectorAll('[data-open]');
109
- for (var j = 0; j < opens.length; j++) opens[j].onclick = function () { window.kolbo.openLink(pricingUrl()); };
124
+ for (var j = 0; j < opens.length; j++) opens[j].onclick = function () { window.kolbo.openLink(this.getAttribute('data-open') === 'pack' ? packUrl() : pricingUrl()); };
110
125
  }
111
126
 
112
127
  function boot(sc) {
@@ -1163,12 +1163,44 @@ function compactList(items, { fields, cap = 50, total, extra, note, maxChars = M
1163
1163
  return text;
1164
1164
  }
1165
1165
 
1166
+ // Every link out of the out-of-credits card carries the same tags so MCP
1167
+ // purchases are attributable. kolbo-map's /pricing reads only `category`
1168
+ // (month | annual | one_time) — no per-pack preselect exists, so the pack link
1169
+ // lands on the credit-pack tab.
1170
+ function upgradeCardUrl(path, content, query = {}) {
1171
+ const url = new URL(path, APP_BASE_URL);
1172
+ const params = {
1173
+ ...query,
1174
+ utm_source: 'mcp',
1175
+ utm_medium: 'upgrade_card',
1176
+ utm_campaign: 'out_of_credits',
1177
+ utm_content: content,
1178
+ // kolbo-map's stable funnel-join fields (ANALYTICS_ATTRIBUTION_KEYS).
1179
+ attribution_link_id: `mcp_upgrade_card_${content}`,
1180
+ attribution_trigger: 'mcp_insufficient_credits',
1181
+ upgrade_entry: 'mcp_upgrade_card',
1182
+ };
1183
+ for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
1184
+ return url.toString();
1185
+ }
1186
+
1187
+ // Smallest pack that covers the shortfall; the largest when none does; the
1188
+ // smallest when the shortfall is unknown.
1189
+ function packForShortfall(packs, shortfall) {
1190
+ const sized = (packs || [])
1191
+ .filter((p) => p && Number.isFinite(p.credits) && p.price != null)
1192
+ .sort((a, b) => a.credits - b.credits);
1193
+ if (!sized.length) return null;
1194
+ return sized.find((p) => !(shortfall > 0) || p.credits >= shortfall) || sized[sized.length - 1];
1195
+ }
1196
+
1166
1197
  /**
1167
1198
  * Turn a 403 INSUFFICIENT_CREDITS into the plans/upgrade card.
1168
1199
  *
1169
1200
  * Without this the user just sees the raw API sentence and has to go find the
1170
- * pricing page themselves. The card states the shortfall, shows live
1171
- * promo-adjusted plans, and links to app.kolbo.ai/pricing.
1201
+ * pricing page themselves. The card states the shortfall, leads with the
1202
+ * credit pack that covers it, shows live promo-adjusted plans below, and links
1203
+ * to app.kolbo.ai/pricing with MCP attribution tags.
1172
1204
  *
1173
1205
  * Returns null for any other error so callers can rethrow untouched. The plan
1174
1206
  * fetch is best-effort: if it fails we still return a card carrying the
@@ -1209,30 +1241,56 @@ async function insufficientCreditsResult(client, err, options = {}) {
1209
1241
 
1210
1242
  const balance = err.data?.balance ?? data?.credits?.total;
1211
1243
  const required = err.data?.required;
1244
+ const shortfall = (Number.isFinite(balance) && Number.isFinite(required))
1245
+ ? Math.max(0, required - balance) : undefined;
1246
+ const credit_packs = data?.credit_packs || [];
1247
+ const pack = packForShortfall(credit_packs, shortfall);
1248
+ const pack_url = upgradeCardUrl('/pricing', 'credit_pack', { category: 'one_time' });
1212
1249
  const structured = {
1213
1250
  widget: 'plans',
1214
1251
  reason: 'insufficient_credits',
1215
1252
  balance,
1216
1253
  required,
1217
- shortfall: (Number.isFinite(balance) && Number.isFinite(required))
1218
- ? Math.max(0, required - balance) : undefined,
1254
+ shortfall,
1219
1255
  current_plan: data?.current_plan || null,
1256
+ // Leads the card: out of credits on the web app, packs convert 56% vs
1257
+ // plans 17.5%. Plans stay below as the alternative.
1258
+ recommended_pack: pack ? {
1259
+ ...pack,
1260
+ // Only when the shortfall is known (and `required` is the API's lower
1261
+ // bound, so this is a floor, not a promise).
1262
+ covers_shortfall: shortfall > 0 && pack.credits >= shortfall,
1263
+ url: pack_url,
1264
+ } : null,
1220
1265
  plans: data?.plans || [],
1221
- credit_packs: data?.credit_packs || [],
1222
- pricing_url: err.data?.pricing_url || data?.pricing_url || 'https://app.kolbo.ai/pricing',
1266
+ credit_packs,
1267
+ // MCP-tagged, not the API's sdk-tagged link: every way out of this card
1268
+ // must be attributable to it.
1269
+ pricing_url: upgradeCardUrl('/pricing', 'plans'),
1270
+ pack_url,
1271
+ home_url: upgradeCardUrl('/', 'home'),
1223
1272
  };
1224
1273
 
1225
1274
  // structuredContent shadows the text on widget hosts, so the refusal reason
1226
1275
  // has to be inside the object too — an agent that only reads structured
1227
1276
  // content must still understand that nothing was generated.
1228
1277
  structured.error = err.message;
1229
- structured._hint = 'The generation did NOT run and nothing was charged. An upgrade card is shown to the user. Tell them they are out of credits and point at the card; do not retry the generation, and do not attempt to purchase anything for them.';
1278
+ structured._hint = 'The generation did NOT run and nothing was charged. An upgrade card is shown to the user. Tell them they are out of credits and point at the card; '
1279
+ + (pack ? 'the quickest fix is the one-time credit pack it leads with (recommended_pack), with plans as the alternative; ' : '')
1280
+ + 'do not retry the generation, and do not attempt to purchase anything for them.';
1230
1281
 
1231
1282
  const text = JSON.stringify({
1232
1283
  error: err.message,
1233
1284
  code: 'INSUFFICIENT_CREDITS',
1234
1285
  balance,
1235
1286
  required,
1287
+ recommended_pack: structured.recommended_pack && {
1288
+ name: pack.name,
1289
+ credits: pack.credits,
1290
+ price: pack.price,
1291
+ currency: pack.currency,
1292
+ url: pack_url,
1293
+ },
1236
1294
  pricing_url: structured.pricing_url,
1237
1295
  _hint: structured._hint,
1238
1296
  }, null, 2);
@@ -115,6 +115,17 @@ const identifierRow = (m) => ({
115
115
  ...(m.new_model ? { new_model: true } : {}),
116
116
  });
117
117
 
118
+ // upgrade_entry credits a purchase to the surface that sent the user (kolbo-map
119
+ // markUpgradeEntryFromUrl). Set server-side so the card AND the text link carry it;
120
+ // a tag already on the URL is kept.
121
+ function withUpgradeEntry(raw, entry) {
122
+ try {
123
+ const u = new URL(raw);
124
+ if (!u.searchParams.has('upgrade_entry')) u.searchParams.set('upgrade_entry', entry);
125
+ return u.toString();
126
+ } catch { return raw; }
127
+ }
128
+
118
129
  function registerModelTools(server, client, options = {}) {
119
130
  const ui = () => appsEnabled(server, options);
120
131
  // ─── list_models ───────────────────────────────────────────
@@ -516,7 +527,7 @@ function registerModelTools(server, client, options = {}) {
516
527
  current_plan: data?.current_plan || null,
517
528
  plans: data?.plans || [],
518
529
  credit_packs: data?.credit_packs || [],
519
- pricing_url: data?.pricing_url || 'https://app.kolbo.ai/pricing',
530
+ pricing_url: withUpgradeEntry(data?.pricing_url || 'https://app.kolbo.ai/pricing', 'mcp_plans_card'),
520
531
  };
521
532
  // structuredContent SHADOWS the text on widget hosts, so everything the
522
533
  // agent needs to talk about pricing has to live in the object above.
@@ -533,4 +544,4 @@ function registerModelTools(server, client, options = {}) {
533
544
  );
534
545
  }
535
546
 
536
- module.exports = { registerModelTools };
547
+ module.exports = { registerModelTools, withUpgradeEntry };