@kolbo/mcp 1.87.10 → 1.87.12

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.
@@ -1,113 +1,164 @@
1
- 'use strict';
2
-
3
- const { widgetPage } = require('../html');
4
-
5
- /**
6
- * Plans / upgrade widget — shown by `show_plans`, and by any generation the
7
- * server refused for credits.
8
- *
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
- *
15
- * structuredContent contract:
16
- * {
17
- * widget: 'plans',
18
- * reason: 'insufficient_credits' | 'requested',
19
- * balance, required, shortfall, // when reason === 'insufficient_credits'
20
- * current_plan: { key, name },
21
- * plans: [{ key, name, interval, credits, price, original_price,
22
- * discount_percent, promo_text, currency, top_up_discount }],
23
- * credit_packs: [{ …, subscriber_price, is_subscriber }],
24
- * pricing_url
25
- * }
26
- */
27
-
28
- const BODY = `
29
- <div class="k-card" id="card">
30
- <div class="k-head">
31
- <span class="k-logo" id="logo"></span>
32
- <span class="k-title" id="tool-title">Plans</span>
33
- <span class="k-spacer"></span>
34
- <span class="k-chip" id="balance-chip" style="display:none"></span>
35
- </div>
36
- <div class="k-body">
37
- <div class="k-error" id="notice" style="display:none"></div>
38
- <div id="stage"></div>
39
- <div class="k-actions" id="actions"></div>
40
- </div>
41
- <div class="k-footer">
42
- <span>Powered by <a href="#" id="kolbo-link">Kolbo.AI</a></span>
43
- </div>
44
- </div>
45
- `;
46
-
47
- const SCRIPT = `
48
- var state = null;
49
- var PRICING_EMBED = 'https://app.kolbo.ai/pricing/embed';
50
-
51
- el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
52
- el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink('https://app.kolbo.ai'); };
53
-
54
- function pricingUrl() {
55
- return (state && state.pricing_url) || 'https://app.kolbo.ai/pricing';
56
- }
57
-
58
- function boot(sc) {
59
- if (!sc) return;
60
- state = sc;
61
- el('tool-title').textContent = sc.reason === 'insufficient_credits' ? 'Out of credits' : 'Plans';
62
-
63
- if (sc.balance != null) {
64
- var chip = el('balance-chip');
65
- chip.style.display = '';
66
- chip.innerHTML = ICONS.sparkle + ' ' + esc(String(sc.balance)) + ' credits left';
67
- }
68
-
69
- if (sc.reason === 'insufficient_credits') {
70
- var need = sc.shortfall != null ? sc.shortfall
71
- : (sc.required != null && sc.balance != null ? Math.max(0, sc.required - sc.balance) : null);
72
- el('notice').style.display = '';
73
- el('notice').innerHTML = ICONS.warn + ' ' +
74
- esc(need ? ('That generation needs ' + need + ' more credit' + (need === 1 ? '' : 's') + '.')
75
- : 'That generation needs more credits than you have left.');
76
- }
77
-
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(); };
82
- renderActions();
83
- window.kolbo.notifySize();
84
- }
85
-
86
- function renderActions() {
87
- el('actions').innerHTML =
88
- '<button class="k-btn ghost" id="btn-pricing">See all plans ' + ICONS.open + '</button>';
89
- el('btn-pricing').onclick = function () { window.kolbo.openLink(pricingUrl()); };
90
- }
91
-
92
- window.kolbo.onToolResult(function (result) {
93
- var sc = result.structuredContent || structured(result);
94
- if (sc) boot(sc);
95
- });
96
- window.kolbo.onToolInput(function () {
97
- if (state) return;
98
- el('stage').innerHTML = '<div class="k-skel square" style="min-height:240px"></div>';
99
- window.kolbo.notifySize();
100
- });
101
- window.kolbo.ready(function (ctx) {
102
- var info = ctx && ctx.toolInfo;
103
- if (state || !info) return;
104
- var raw = info.result && (info.result.structuredContent || structured(info.result));
105
- if (raw) boot(raw);
106
- });
107
- `;
108
-
109
- function plansWidgetHtml() {
110
- return widgetPage({ title: 'Kolbo Plans', body: BODY, script: SCRIPT });
111
- }
112
-
113
- module.exports = { plansWidgetHtml };
1
+ 'use strict';
2
+
3
+ const { widgetPage } = require('../html');
4
+
5
+ /**
6
+ * Plans / upgrade widget — shown by `show_plans`, and by any generation the
7
+ * server refused for credits.
8
+ *
9
+ * Rendered NATIVELY from structuredContent. It used to iframe
10
+ * app.kolbo.ai/pricing/embed, which needs `frameDomains` in the widget CSP
11
+ * OpenAI rejected the ChatGPT app for exactly that on 2026-09-06 ("frameDomains
12
+ * is reserved for limited cases where embedding a third-party experience is
13
+ * essential"). Prices here are the live, promo-adjusted numbers the server
14
+ * fetched from /v1/account/plans at call time; Buy/Subscribe open
15
+ * https://app.kolbo.ai/pricing in a new tab — Stripe and the user's session
16
+ * never run inside a host iframe.
17
+ *
18
+ * structuredContent contract:
19
+ * {
20
+ * widget: 'plans',
21
+ * reason: 'insufficient_credits' | 'requested',
22
+ * balance, required, shortfall, // when reason === 'insufficient_credits'
23
+ * current_plan: { key, name },
24
+ * plans: [{ key, name, interval, credits, price, original_price,
25
+ * discount_percent, promo_text, currency, top_up_discount }],
26
+ * credit_packs: [{ …, subscriber_price, is_subscriber }],
27
+ * pricing_url
28
+ * }
29
+ */
30
+
31
+ const BODY = `
32
+ <div class="k-card" id="card">
33
+ <div class="k-head">
34
+ <span class="k-logo" id="logo"></span>
35
+ <span class="k-title" id="tool-title">Plans</span>
36
+ <span class="k-spacer"></span>
37
+ <span class="k-chip" id="balance-chip" style="display:none"></span>
38
+ </div>
39
+ <div class="k-body">
40
+ <div class="k-error" id="notice" style="display:none"></div>
41
+ <div id="stage"></div>
42
+ <div class="k-actions" id="actions"></div>
43
+ </div>
44
+ <div class="k-footer">
45
+ <span>Powered by <a href="#" id="kolbo-link">Kolbo.AI</a></span>
46
+ </div>
47
+ </div>
48
+ `;
49
+
50
+ const SCRIPT = `
51
+ var state = null;
52
+ var interval = 'month';
53
+
54
+ el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
55
+ el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink('https://app.kolbo.ai'); };
56
+
57
+ function pricingUrl() {
58
+ return (state && state.pricing_url) || 'https://app.kolbo.ai/pricing';
59
+ }
60
+ function money(v, currency) {
61
+ if (v == null || isNaN(v)) return '';
62
+ var sym = { usd: '$', eur: '\u20ac', gbp: '\u00a3', ils: '\u20aa' }[String(currency || 'usd').toLowerCase()] || '';
63
+ var n = Number(v);
64
+ return sym + (Math.round(n) === n ? String(n) : n.toFixed(2));
65
+ }
66
+ function fmtCredits(n) { return n == null ? '' : Number(n).toLocaleString('en-US'); }
67
+ function isAnnual(p) { return /^(year|annual|yearly)/i.test(String(p.interval || '')); }
68
+
69
+ function planCard(p, opts) {
70
+ var current = state.current_plan && ((state.current_plan.key && state.current_plan.key === p.key) || (state.current_plan.name && state.current_plan.name === p.name));
71
+ var pricing = '<div class="k-plan-pricing"><span class="k-plan-price">' + esc(money(p.price, p.currency)) + '</span>' +
72
+ (p.original_price != null && p.original_price !== p.price ? '<span class="k-plan-was">' + esc(money(p.original_price, p.currency)) + '</span>' : '') +
73
+ (opts.pack ? '' : '<span class="k-plan-note">/ ' + (isAnnual(p) ? 'year' : 'month') + '</span>') + '</div>';
74
+ var badge = current ? '<span class="k-plan-badge current">Current plan</span>'
75
+ : (p.discount_percent ? '<span class="k-plan-badge">' + esc(String(p.discount_percent)) + '% off</span>' : '');
76
+ var note = p.promo_text ? '<div class="k-plan-note">' + esc(p.promo_text) + '</div>'
77
+ : (opts.pack && p.is_subscriber && p.subscriber_price != null ? '<div class="k-plan-note">Subscriber price applied</div>' : '')
78
+ + (!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>';
80
+ return '<div class="k-plan' + (current ? ' current' : '') + '">' +
81
+ '<div class="k-plan-top"><span class="k-plan-name">' + esc(p.name || p.key || 'Plan') + '</span>' + badge + '</div>' +
82
+ (p.credits != null ? '<div class="k-plan-credits">' + ICONS.sparkle + ' ' + esc(fmtCredits(p.credits)) + ' credits' + (opts.pack ? '' : ' / ' + (isAnnual(p) ? 'year' : 'month')) + '</div>' : '') +
83
+ pricing + note + cta + '</div>';
84
+ }
85
+
86
+ function renderStage() {
87
+ 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; });
89
+ var monthly = plans.filter(function (p) { return !isAnnual(p); });
90
+ var annual = plans.filter(isAnnual);
91
+ var hasToggle = monthly.length && annual.length;
92
+ var shown = hasToggle ? (interval === 'year' ? annual : monthly) : plans;
93
+ var html = '';
94
+ if (hasToggle) {
95
+ html += '<div class="k-plan-toggle">' +
96
+ '<button class="k-toggle-btn' + (interval === 'month' ? ' active' : '') + '" data-interval="month">Monthly</button>' +
97
+ '<button class="k-toggle-btn' + (interval === 'year' ? ' active' : '') + '" data-interval="year">Annual</button></div>';
98
+ }
99
+ if (shown.length) html += '<div class="k-plan-grid">' + shown.map(function (p) { return planCard(p, {}); }).join('') + '</div>';
100
+ if (packs.length) {
101
+ html += '<div class="k-pack-head">Credit packs</div><div class="k-plan-grid">' +
102
+ packs.map(function (p) { return planCard(p, { pack: true }); }).join('') + '</div>';
103
+ }
104
+ if (!html) html = '<div class="k-plan-note">Live plans and prices are on the Kolbo pricing page.</div>';
105
+ el('stage').innerHTML = html;
106
+ var btns = el('stage').querySelectorAll('[data-interval]');
107
+ for (var i = 0; i < btns.length; i++) btns[i].onclick = function () { interval = this.getAttribute('data-interval'); renderStage(); window.kolbo.notifySize(); };
108
+ var opens = el('stage').querySelectorAll('[data-open]');
109
+ for (var j = 0; j < opens.length; j++) opens[j].onclick = function () { window.kolbo.openLink(pricingUrl()); };
110
+ }
111
+
112
+ function boot(sc) {
113
+ if (!sc) return;
114
+ state = sc;
115
+ el('tool-title').textContent = sc.reason === 'insufficient_credits' ? 'Out of credits' : 'Plans';
116
+
117
+ if (sc.balance != null) {
118
+ var chip = el('balance-chip');
119
+ chip.style.display = '';
120
+ chip.innerHTML = ICONS.sparkle + ' ' + esc(String(sc.balance)) + ' credits left';
121
+ }
122
+
123
+ if (sc.reason === 'insufficient_credits') {
124
+ var need = sc.shortfall != null ? sc.shortfall
125
+ : (sc.required != null && sc.balance != null ? Math.max(0, sc.required - sc.balance) : null);
126
+ el('notice').style.display = '';
127
+ el('notice').innerHTML = ICONS.warn + ' ' +
128
+ esc(need ? ('That generation needs ' + need + ' more credit' + (need === 1 ? '' : 's') + '.')
129
+ : 'That generation needs more credits than you have left.');
130
+ }
131
+
132
+ renderStage();
133
+ renderActions();
134
+ window.kolbo.notifySize();
135
+ }
136
+
137
+ function renderActions() {
138
+ el('actions').innerHTML =
139
+ '<button class="k-btn ghost" id="btn-pricing">See all plans ' + ICONS.open + '</button>';
140
+ el('btn-pricing').onclick = function () { window.kolbo.openLink(pricingUrl()); };
141
+ }
142
+
143
+ window.kolbo.onToolResult(function (result) {
144
+ var sc = result.structuredContent || structured(result);
145
+ if (sc) boot(sc);
146
+ });
147
+ window.kolbo.onToolInput(function () {
148
+ if (state) return;
149
+ el('stage').innerHTML = '<div class="k-skel square" style="min-height:240px"></div>';
150
+ window.kolbo.notifySize();
151
+ });
152
+ window.kolbo.ready(function (ctx) {
153
+ var info = ctx && ctx.toolInfo;
154
+ if (state || !info) return;
155
+ var raw = info.result && (info.result.structuredContent || structured(info.result));
156
+ if (raw) boot(raw);
157
+ });
158
+ `;
159
+
160
+ function plansWidgetHtml() {
161
+ return widgetPage({ title: 'Kolbo Plans', body: BODY, script: SCRIPT });
162
+ }
163
+
164
+ module.exports = { plansWidgetHtml };
@@ -56,20 +56,25 @@ const idempotencyKey = z.string()
56
56
  .regex(/^[A-Za-z0-9._:-]+$/)
57
57
  .optional()
58
58
  .describe('Optional replay-safe key. Reuse it only when retrying the same command.');
59
- const name = noControls(128);
60
- const boundedNumber = z.number().finite().min(-MAX_ABSOLUTE_NUMBER).max(MAX_ABSOLUTE_NUMBER);
61
- const vector = z.tuple([boundedNumber, boundedNumber, boundedNumber]);
62
- const colorComponent = z.number().finite().min(0).max(1);
63
- const color = z.union([
64
- z.tuple([colorComponent, colorComponent, colorComponent]),
65
- z.tuple([colorComponent, colorComponent, colorComponent, colorComponent]),
66
- ]);
67
- const propertyScalar = z.union([
59
+ // Every shared piece below is a FACTORY, not a shared instance. zod-to-json-schema
60
+ // de-duplicates repeated zod instances into JSON-pointer `$ref`s, and this tool
61
+ // reuses name/vector/color dozens of times inside one discriminated union, so the
62
+ // emitted inputSchema carried 54 `$ref`s, draft-07 tuple `items: [...]` arrays and
63
+ // `const` discriminators. ChatGPT's app "Scan Tools" rejects all three
64
+ // ("Invalid MCP tool schema for tool 'blender_apply_operations'", 2026-09-06),
65
+ // which blocks re-scanning the whole Kolbo app. Fresh instances + fixed-length
66
+ // arrays + single-value enums keep the schema inside the plain subset every host
67
+ // accepts. Validation semantics are unchanged.
68
+ const name = () => noControls(128);
69
+ const num = () => z.number().finite().min(-MAX_ABSOLUTE_NUMBER).max(MAX_ABSOLUTE_NUMBER);
70
+ const vector = () => z.array(num()).length(3);
71
+ const color = () => z.array(z.number().finite().min(0).max(1)).min(3).max(4);
72
+ const propertyScalar = () => z.union([
68
73
  noControls(256),
69
- boundedNumber,
74
+ num(),
70
75
  z.boolean(),
71
76
  ]);
72
- const properties = z.record(z.union([propertyScalar, z.array(propertyScalar).max(16)]))
77
+ const properties = () => z.record(z.union([propertyScalar(), z.array(propertyScalar()).max(16)]))
73
78
  .refine((value) => Object.keys(value).length <= 30, 'A modifier may contain at most 30 properties.')
74
79
  .refine(
75
80
  (value) => Object.keys(value).every((key) => SAFE_PROPERTY_KEY.test(key) && !BLOCKED_PROPERTY_KEYS.has(key)),
@@ -79,81 +84,81 @@ const properties = z.record(z.union([propertyScalar, z.array(propertyScalar).max
79
84
  (value) => Buffer.byteLength(JSON.stringify(value), 'utf8') <= 16 * 1024,
80
85
  'Modifier properties must be at most 16 KiB as JSON.',
81
86
  );
82
- const transform = {
83
- location: vector.optional(),
84
- rotation_euler: vector.optional(),
85
- scale: vector.optional(),
86
- };
87
+ const transform = () => ({
88
+ location: vector().optional(),
89
+ rotation_euler: vector().optional(),
90
+ scale: vector().optional(),
91
+ });
92
+ const op = (value) => z.enum([value]);
87
93
  const operation = z.discriminatedUnion('op', [
88
94
  z.object({
89
- op: z.literal('object.create'),
90
- name: name.optional(),
95
+ op: op('object.create'),
96
+ name: name().optional(),
91
97
  type: z.enum(['CUBE', 'SPHERE', 'CYLINDER', 'CONE', 'PLANE', 'EMPTY', 'CAMERA', 'LIGHT']),
92
98
  light_type: z.enum(['POINT', 'SUN', 'SPOT', 'AREA']).optional(),
93
99
  energy: z.number().finite().min(0).max(MAX_ABSOLUTE_NUMBER).optional(),
94
- color: color.optional(),
100
+ color: color().optional(),
95
101
  shadow_soft_size: z.number().finite().min(0).max(MAX_ABSOLUTE_NUMBER).optional(),
96
102
  lens: z.number().finite().min(1).max(10_000).optional(),
97
- collection: name.optional(),
98
- ...transform,
103
+ collection: name().optional(),
104
+ ...transform(),
99
105
  }).strict(),
100
- z.object({ op: z.literal('object.transform'), object: name, ...transform }).strict(),
101
- z.object({ op: z.literal('object.rename'), object: name, new_name: name }).strict(),
102
- z.object({ op: z.literal('object.delete'), object: name }).strict(),
106
+ z.object({ op: op('object.transform'), object: name(), ...transform() }).strict(),
107
+ z.object({ op: op('object.rename'), object: name(), new_name: name() }).strict(),
108
+ z.object({ op: op('object.delete'), object: name() }).strict(),
103
109
  z.object({
104
- op: z.literal('object.duplicate'),
105
- object: name,
106
- new_name: name.optional(),
107
- collection: name.optional(),
108
- ...transform,
110
+ op: op('object.duplicate'),
111
+ object: name(),
112
+ new_name: name().optional(),
113
+ collection: name().optional(),
114
+ ...transform(),
109
115
  }).strict(),
110
- z.object({ op: z.literal('object.set_parent'), object: name, parent: name.nullable().optional() }).strict(),
111
- z.object({ op: z.literal('collection.create'), name, parent: name.nullable().optional() }).strict(),
112
- z.object({ op: z.literal('collection.delete'), collection: name }).strict(),
113
- z.object({ op: z.literal('collection.link_object'), collection: name, object: name }).strict(),
116
+ z.object({ op: op('object.set_parent'), object: name(), parent: name().nullable().optional() }).strict(),
117
+ z.object({ op: op('collection.create'), name: name(), parent: name().nullable().optional() }).strict(),
118
+ z.object({ op: op('collection.delete'), collection: name() }).strict(),
119
+ z.object({ op: op('collection.link_object'), collection: name(), object: name() }).strict(),
114
120
  z.object({
115
- op: z.literal('material.create'),
116
- name,
117
- base_color: color.optional(),
121
+ op: op('material.create'), name: name(),
122
+ base_color: color().optional(),
118
123
  roughness: z.number().min(0).max(1).optional(),
119
124
  metallic: z.number().min(0).max(1).optional(),
120
125
  alpha: z.number().min(0).max(1).optional(),
121
- emission_color: color.optional(),
126
+ emission_color: color().optional(),
122
127
  emission_strength: z.number().finite().min(0).max(MAX_ABSOLUTE_NUMBER).optional(),
123
128
  }).strict(),
124
- z.object({ op: z.literal('material.assign'), object: name, material: name, replace: z.boolean().optional() }).strict(),
129
+ z.object({ op: op('material.assign'), object: name(), material: name(), replace: z.boolean().optional() }).strict(),
125
130
  z.object({
126
- op: z.literal('material.set_principled'),
127
- material: name,
128
- base_color: color.optional(),
131
+ op: op('material.set_principled'),
132
+ material: name(),
133
+ base_color: color().optional(),
129
134
  roughness: z.number().min(0).max(1).optional(),
130
135
  metallic: z.number().min(0).max(1).optional(),
131
136
  alpha: z.number().min(0).max(1).optional(),
132
- emission_color: color.optional(),
137
+ emission_color: color().optional(),
133
138
  emission_strength: z.number().finite().min(0).max(MAX_ABSOLUTE_NUMBER).optional(),
134
139
  }).strict(),
135
- z.object({ op: z.literal('world.set_color'), color }).strict(),
140
+ z.object({ op: op('world.set_color'), color: color() }).strict(),
136
141
  z.object({
137
- op: z.literal('modifier.add'),
138
- object: name,
142
+ op: op('modifier.add'),
143
+ object: name(),
139
144
  type: z.string().regex(SAFE_MODIFIER_TYPE),
140
- name: name.optional(),
141
- properties: properties.optional(),
145
+ name: name().optional(),
146
+ properties: properties().optional(),
142
147
  }).strict(),
143
- z.object({ op: z.literal('modifier.configure'), object: name, modifier: name, properties }).strict(),
144
- z.object({ op: z.literal('modifier.remove'), object: name, modifier: name }).strict(),
145
- z.object({ op: z.literal('camera.set_active'), object: name }).strict(),
148
+ z.object({ op: op('modifier.configure'), object: name(), modifier: name(), properties: properties() }).strict(),
149
+ z.object({ op: op('modifier.remove'), object: name(), modifier: name() }).strict(),
150
+ z.object({ op: op('camera.set_active'), object: name() }).strict(),
146
151
  z.object({
147
- op: z.literal('animation.keyframe_insert'),
148
- object: name,
149
- data_path: name,
152
+ op: op('animation.keyframe_insert'),
153
+ object: name(),
154
+ data_path: name(),
150
155
  frame: z.number().int().min(-1_048_574).max(1_048_574),
151
156
  index: z.number().int().min(-1).max(1024).optional(),
152
157
  }).strict(),
153
158
  z.object({
154
- op: z.literal('animation.delete_keyframe'),
155
- object: name,
156
- data_path: name,
159
+ op: op('animation.delete_keyframe'),
160
+ object: name(),
161
+ data_path: name(),
157
162
  frame: z.number().int().min(-1_048_574).max(1_048_574),
158
163
  index: z.number().int().min(-1).max(1024).optional(),
159
164
  }).strict(),
@@ -316,7 +321,7 @@ function registerBlenderTools(server, client) {
316
321
  .optional(),
317
322
  kind: z.enum(['model', '3d', 'glb', 'image', 'video']).optional(),
318
323
  import_mode: z.enum(['plane', 'active_material', 'world', 'sequencer', 'collection']).optional(),
319
- name: name.optional(),
324
+ name: name().optional(),
320
325
  idempotency_key: idempotencyKey,
321
326
  },
322
327
  async (args) => {