@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 +1 -1
- package/src/apps/index.js +34 -11
- package/src/tools/blender.js +62 -57
package/package.json
CHANGED
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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,
|
package/src/tools/blender.js
CHANGED
|
@@ -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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
101
|
-
z.object({ op:
|
|
102
|
-
z.object({ op:
|
|
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:
|
|
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:
|
|
111
|
-
z.object({ op:
|
|
112
|
-
z.object({ op:
|
|
113
|
-
z.object({ op:
|
|
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:
|
|
116
|
-
|
|
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:
|
|
129
|
+
z.object({ op: op('material.assign'), object: name(), material: name(), replace: z.boolean().optional() }).strict(),
|
|
125
130
|
z.object({
|
|
126
|
-
op:
|
|
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:
|
|
140
|
+
z.object({ op: op('world.set_color'), color: color() }).strict(),
|
|
136
141
|
z.object({
|
|
137
|
-
op:
|
|
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:
|
|
144
|
-
z.object({ op:
|
|
145
|
-
z.object({ op:
|
|
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:
|
|
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:
|
|
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) => {
|