@kolbo/mcp 1.87.9 → 1.87.11

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.87.9",
3
+ "version": "1.87.11",
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
@@ -48,10 +48,29 @@ const WIDGET_BUILDERS = {
48
48
  // Widgets are pure functions of source — build once per process.
49
49
  const htmlCache = new Map();
50
50
  function widgetHtml(uri) {
51
+ uri = String(uri).split('?')[0]; // versioned and bare URIs serve the same page
51
52
  if (!htmlCache.has(uri)) htmlCache.set(uri, WIDGET_BUILDERS[uri]());
52
53
  return htmlCache.get(uri);
53
54
  }
54
55
 
56
+ // ChatGPT treats the widget URI as a permanent cache key: it fetched
57
+ // generation.html once per app link and kept serving that snapshot across every
58
+ // later release (1.87.9's ChatGPT fix never reached a card until the URI
59
+ // changed — verified 2026-09-06 via its widget endpoint, where only
60
+ // force_local=true returned the new HTML). OpenAI's own guidance is "treat the
61
+ // resource URI as a cache key; publish a new URI on every change", so the URI
62
+ // every tool declares and returns carries a content hash of the HTML it points
63
+ // at. The bare URI stays registered for hosts holding an older tools/list.
64
+ const crypto = require('crypto');
65
+ const versionCache = new Map();
66
+ function versionedUri(uri) {
67
+ if (!versionCache.has(uri)) {
68
+ const v = crypto.createHash('sha1').update(widgetHtml(uri)).digest('hex').slice(0, 10);
69
+ versionCache.set(uri, uri + '?v=' + v);
70
+ }
71
+ return versionCache.get(uri);
72
+ }
73
+
55
74
  // Hosts apply a deny-by-default CSP to widget iframes — without this
56
75
  // declaration EVERY external asset (generated images/videos on the CDN, model
57
76
  // icons, Google Fonts) is silently blocked. resourceDomains maps to
@@ -145,22 +164,25 @@ function registerApps(server) {
145
164
  [UI.list, 'Kolbo List Widget'],
146
165
  [UI.plans, 'Kolbo Plans Widget'],
147
166
  ]) {
148
- registerAppResource(
149
- server, name, uri,
150
- { mimeType: RESOURCE_MIME_TYPE, _meta: { csp: WIDGET_CSP, ui: { csp: WIDGET_CSP } } },
151
- async () => ({
152
- contents: [{
153
- uri, mimeType: RESOURCE_MIME_TYPE, text: widgetHtml(uri),
154
- _meta: { csp: WIDGET_CSP, ui: { csp: WIDGET_CSP } },
155
- }],
156
- })
157
- );
167
+ for (const u of [versionedUri(uri), uri]) {
168
+ registerAppResource(
169
+ server, name + (u === uri ? ' (unversioned)' : ''), u,
170
+ { mimeType: RESOURCE_MIME_TYPE, _meta: { csp: WIDGET_CSP, ui: { csp: WIDGET_CSP } } },
171
+ async () => ({
172
+ contents: [{
173
+ uri: u, mimeType: RESOURCE_MIME_TYPE, text: widgetHtml(uri),
174
+ _meta: { csp: WIDGET_CSP, ui: { csp: WIDGET_CSP } },
175
+ }],
176
+ })
177
+ );
178
+ }
158
179
  }
159
180
  }
160
181
 
161
182
  /** `_meta` for a tool RESULT (and optionally for tool registration). */
162
183
  function uiMeta(uri) {
163
- return { [RESOURCE_URI_META_KEY]: uri, ui: { resourceUri: uri } };
184
+ const u = versionedUri(uri);
185
+ return { [RESOURCE_URI_META_KEY]: u, ui: { resourceUri: u } };
164
186
  }
165
187
 
166
188
  /**
@@ -740,6 +762,7 @@ module.exports = {
740
762
  TOOL_WIDGETS,
741
763
  registerApps,
742
764
  attachToolWidgetMeta,
765
+ versionedUri,
743
766
  uiMeta,
744
767
  uiResult,
745
768
  listResult,
@@ -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) => {