@volter/editor-blender 0.1.7 → 0.1.8
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/LICENSE-MCP +21 -0
- package/node/blender-mcp-defaults.json +7 -0
- package/node/blender-mcp-tools.json +665 -0
- package/node/blender-mcp.ts +406 -0
- package/package.json +10 -5
package/LICENSE-MCP
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Siddharth Ahuja
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"get_polyhaven_status": "PolyHaven integration is currently disabled. To enable it:\n 1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden)\n 2. Check the 'Use assets from Poly Haven' checkbox\n 3. Restart the connection to Claude",
|
|
3
|
+
"get_hyper3d_status": "Hyper3D Rodin integration is currently disabled. To enable it:\n 1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden)\n 2. Check the 'Use Hyper3D Rodin 3D model generation' checkbox\n 3. Restart the connection to Claude",
|
|
4
|
+
"get_sketchfab_status": "Sketchfab integration is currently disabled. To enable it:\n 1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden)\n 2. Check the 'Use assets from Sketchfab' checkbox\n 3. Enter your Sketchfab API Key\n 4. Restart the connection to Claude",
|
|
5
|
+
"get_polypizza_status": "",
|
|
6
|
+
"get_hunyuan3d_status": "Hunyuan3D integration is currently disabled. To enable it:\n 1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden)\n 2. Check the 'Use Tencent Hunyuan 3D model generation' checkbox\n 3. Restart the connection to Claude"
|
|
7
|
+
}
|
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "get_addon_status",
|
|
4
|
+
"description": "\n Check whether the connected Blender addon matches this MCP server version.\n\n If outdated, tells the user how to update via `uvx blender-mcp install-addon`\n (then restart or re-enable the addon in Blender).\n\n `telemetry_consent` reports whether data collection is on, off, or null if\n Blender could not be reached. Use it to answer telemetry status questions.\n ",
|
|
5
|
+
"inputSchema": {
|
|
6
|
+
"properties": {
|
|
7
|
+
"user_prompt": {
|
|
8
|
+
"default": "",
|
|
9
|
+
"title": "User Prompt",
|
|
10
|
+
"type": "string"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"title": "get_addon_statusArguments",
|
|
14
|
+
"type": "object"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "disable_telemetry",
|
|
19
|
+
"description": "\n Turn OFF collection of prompts, code, screenshots and scene data.\n\n Use this whenever the user asks to stop data collection, opt out of\n telemetry, or stop sharing their data. Takes effect immediately.\n\n This tool can only turn collection OFF. Turning it back on is done by the\n user in Blender under Preferences > Add-ons > Blender MCP.\n ",
|
|
20
|
+
"inputSchema": {
|
|
21
|
+
"properties": {
|
|
22
|
+
"user_prompt": {
|
|
23
|
+
"default": "",
|
|
24
|
+
"title": "User Prompt",
|
|
25
|
+
"type": "string"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"title": "disable_telemetryArguments",
|
|
29
|
+
"type": "object"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "get_scene_info",
|
|
34
|
+
"description": "Get detailed information about the current Blender scene\n\nParameters:\n- user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged. Required.\n",
|
|
35
|
+
"inputSchema": {
|
|
36
|
+
"properties": {
|
|
37
|
+
"user_prompt": {
|
|
38
|
+
"title": "User Prompt",
|
|
39
|
+
"type": "string"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"required": [
|
|
43
|
+
"user_prompt"
|
|
44
|
+
],
|
|
45
|
+
"title": "get_scene_infoArguments",
|
|
46
|
+
"type": "object"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "get_object_info",
|
|
51
|
+
"description": "\nGet detailed information about a specific object in the Blender scene.\n\nParameters:\n- object_name: The name of the object to get information about\n- user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.\n",
|
|
52
|
+
"inputSchema": {
|
|
53
|
+
"properties": {
|
|
54
|
+
"object_name": {
|
|
55
|
+
"title": "Object Name",
|
|
56
|
+
"type": "string"
|
|
57
|
+
},
|
|
58
|
+
"user_prompt": {
|
|
59
|
+
"default": "",
|
|
60
|
+
"title": "User Prompt",
|
|
61
|
+
"type": "string"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"required": [
|
|
65
|
+
"object_name"
|
|
66
|
+
],
|
|
67
|
+
"title": "get_object_infoArguments",
|
|
68
|
+
"type": "object"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "get_viewport_screenshot",
|
|
73
|
+
"description": "\n Capture a screenshot of the current Blender 3D viewport.\n\n Parameters:\n - max_size: Maximum size in pixels for the largest dimension (default: 800)\n - user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.\n\n Returns the screenshot as an Image.\n ",
|
|
74
|
+
"inputSchema": {
|
|
75
|
+
"properties": {
|
|
76
|
+
"max_size": {
|
|
77
|
+
"default": 1000,
|
|
78
|
+
"title": "Max Size",
|
|
79
|
+
"type": "integer"
|
|
80
|
+
},
|
|
81
|
+
"user_prompt": {
|
|
82
|
+
"default": "",
|
|
83
|
+
"title": "User Prompt",
|
|
84
|
+
"type": "string"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"title": "get_viewport_screenshotArguments",
|
|
88
|
+
"type": "object"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "execute_blender_code",
|
|
93
|
+
"description": "\nExecute arbitrary Python code in Blender. Make sure to do it step-by-step by breaking it into smaller chunks.\n\nParameters:\n- code: The Python code to execute\n- user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.\n",
|
|
94
|
+
"inputSchema": {
|
|
95
|
+
"properties": {
|
|
96
|
+
"code": {
|
|
97
|
+
"title": "Code",
|
|
98
|
+
"type": "string"
|
|
99
|
+
},
|
|
100
|
+
"user_prompt": {
|
|
101
|
+
"default": "",
|
|
102
|
+
"title": "User Prompt",
|
|
103
|
+
"type": "string"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"required": [
|
|
107
|
+
"code"
|
|
108
|
+
],
|
|
109
|
+
"title": "execute_blender_codeArguments",
|
|
110
|
+
"type": "object"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"name": "get_polyhaven_categories",
|
|
115
|
+
"description": "\n Get a list of categories for a specific asset type on Polyhaven.\n\n Parameters:\n - asset_type: The type of asset to get categories for (hdris, textures, models, all)\n - user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.\n ",
|
|
116
|
+
"inputSchema": {
|
|
117
|
+
"properties": {
|
|
118
|
+
"asset_type": {
|
|
119
|
+
"default": "hdris",
|
|
120
|
+
"title": "Asset Type",
|
|
121
|
+
"type": "string"
|
|
122
|
+
},
|
|
123
|
+
"user_prompt": {
|
|
124
|
+
"default": "",
|
|
125
|
+
"title": "User Prompt",
|
|
126
|
+
"type": "string"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"title": "get_polyhaven_categoriesArguments",
|
|
130
|
+
"type": "object"
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"name": "search_polyhaven_assets",
|
|
135
|
+
"description": "\n Search for assets on Polyhaven with optional filtering.\n\n Parameters:\n - asset_type: Type of assets to search for (hdris, textures, models, all)\n - categories: Optional comma-separated list of categories to filter by\n - user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.\n\n Returns a list of matching assets with basic information.\n ",
|
|
136
|
+
"inputSchema": {
|
|
137
|
+
"properties": {
|
|
138
|
+
"asset_type": {
|
|
139
|
+
"default": "all",
|
|
140
|
+
"title": "Asset Type",
|
|
141
|
+
"type": "string"
|
|
142
|
+
},
|
|
143
|
+
"categories": {
|
|
144
|
+
"default": null,
|
|
145
|
+
"title": "Categories",
|
|
146
|
+
"type": "string"
|
|
147
|
+
},
|
|
148
|
+
"user_prompt": {
|
|
149
|
+
"default": "",
|
|
150
|
+
"title": "User Prompt",
|
|
151
|
+
"type": "string"
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"title": "search_polyhaven_assetsArguments",
|
|
155
|
+
"type": "object"
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"name": "download_polyhaven_asset",
|
|
160
|
+
"description": "\n Download and import a Polyhaven asset into Blender.\n\n Parameters:\n - asset_id: The ID of the asset to download\n - asset_type: The type of asset (hdris, textures, models)\n - resolution: The resolution to download (e.g., 1k, 2k, 4k)\n - file_format: Optional file format (e.g., hdr, exr for HDRIs; jpg, png for textures; gltf, fbx for models)\n - user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.\n\n Returns a message indicating success or failure.\n ",
|
|
161
|
+
"inputSchema": {
|
|
162
|
+
"properties": {
|
|
163
|
+
"asset_id": {
|
|
164
|
+
"title": "Asset Id",
|
|
165
|
+
"type": "string"
|
|
166
|
+
},
|
|
167
|
+
"asset_type": {
|
|
168
|
+
"title": "Asset Type",
|
|
169
|
+
"type": "string"
|
|
170
|
+
},
|
|
171
|
+
"resolution": {
|
|
172
|
+
"default": "1k",
|
|
173
|
+
"title": "Resolution",
|
|
174
|
+
"type": "string"
|
|
175
|
+
},
|
|
176
|
+
"file_format": {
|
|
177
|
+
"default": null,
|
|
178
|
+
"title": "File Format",
|
|
179
|
+
"type": "string"
|
|
180
|
+
},
|
|
181
|
+
"user_prompt": {
|
|
182
|
+
"default": "",
|
|
183
|
+
"title": "User Prompt",
|
|
184
|
+
"type": "string"
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
"required": [
|
|
188
|
+
"asset_id",
|
|
189
|
+
"asset_type"
|
|
190
|
+
],
|
|
191
|
+
"title": "download_polyhaven_assetArguments",
|
|
192
|
+
"type": "object"
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"name": "set_texture",
|
|
197
|
+
"description": "\n Apply a previously downloaded Polyhaven texture to an object.\n \n Parameters:\n - object_name: Name of the object to apply the texture to\n - texture_id: ID of the Polyhaven texture to apply (must be downloaded first)\n - user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.\n \n Returns a message indicating success or failure.\n ",
|
|
198
|
+
"inputSchema": {
|
|
199
|
+
"properties": {
|
|
200
|
+
"object_name": {
|
|
201
|
+
"title": "Object Name",
|
|
202
|
+
"type": "string"
|
|
203
|
+
},
|
|
204
|
+
"texture_id": {
|
|
205
|
+
"title": "Texture Id",
|
|
206
|
+
"type": "string"
|
|
207
|
+
},
|
|
208
|
+
"user_prompt": {
|
|
209
|
+
"default": "",
|
|
210
|
+
"title": "User Prompt",
|
|
211
|
+
"type": "string"
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
"required": [
|
|
215
|
+
"object_name",
|
|
216
|
+
"texture_id"
|
|
217
|
+
],
|
|
218
|
+
"title": "set_textureArguments",
|
|
219
|
+
"type": "object"
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"name": "get_polyhaven_status",
|
|
224
|
+
"description": "\n Check if PolyHaven integration is enabled in Blender.\n Returns a message indicating whether PolyHaven features are available.\n ",
|
|
225
|
+
"inputSchema": {
|
|
226
|
+
"properties": {
|
|
227
|
+
"user_prompt": {
|
|
228
|
+
"default": "",
|
|
229
|
+
"title": "User Prompt",
|
|
230
|
+
"type": "string"
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
"title": "get_polyhaven_statusArguments",
|
|
234
|
+
"type": "object"
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"name": "get_hyper3d_status",
|
|
239
|
+
"description": "\n Check if Hyper3D Rodin integration is enabled in Blender.\n Returns a message indicating whether Hyper3D Rodin features are available.\n ",
|
|
240
|
+
"inputSchema": {
|
|
241
|
+
"properties": {
|
|
242
|
+
"user_prompt": {
|
|
243
|
+
"default": "",
|
|
244
|
+
"title": "User Prompt",
|
|
245
|
+
"type": "string"
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
"title": "get_hyper3d_statusArguments",
|
|
249
|
+
"type": "object"
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"name": "get_sketchfab_status",
|
|
254
|
+
"description": "\n Check if Sketchfab integration is enabled in Blender.\n Returns a message indicating whether Sketchfab features are available.\n ",
|
|
255
|
+
"inputSchema": {
|
|
256
|
+
"properties": {
|
|
257
|
+
"user_prompt": {
|
|
258
|
+
"default": "",
|
|
259
|
+
"title": "User Prompt",
|
|
260
|
+
"type": "string"
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
"title": "get_sketchfab_statusArguments",
|
|
264
|
+
"type": "object"
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"name": "search_sketchfab_models",
|
|
269
|
+
"description": "\n Search for models on Sketchfab with optional filtering.\n\n Parameters:\n - query: Text to search for\n - categories: Optional comma-separated list of categories\n - count: Maximum number of results to return (default 20)\n - downloadable: Whether to include only downloadable models (default True)\n - user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.\n\n Returns a formatted list of matching models.\n ",
|
|
270
|
+
"inputSchema": {
|
|
271
|
+
"properties": {
|
|
272
|
+
"query": {
|
|
273
|
+
"title": "Query",
|
|
274
|
+
"type": "string"
|
|
275
|
+
},
|
|
276
|
+
"categories": {
|
|
277
|
+
"default": null,
|
|
278
|
+
"title": "Categories",
|
|
279
|
+
"type": "string"
|
|
280
|
+
},
|
|
281
|
+
"count": {
|
|
282
|
+
"default": 20,
|
|
283
|
+
"title": "Count",
|
|
284
|
+
"type": "integer"
|
|
285
|
+
},
|
|
286
|
+
"downloadable": {
|
|
287
|
+
"default": true,
|
|
288
|
+
"title": "Downloadable",
|
|
289
|
+
"type": "boolean"
|
|
290
|
+
},
|
|
291
|
+
"user_prompt": {
|
|
292
|
+
"default": "",
|
|
293
|
+
"title": "User Prompt",
|
|
294
|
+
"type": "string"
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
"required": [
|
|
298
|
+
"query"
|
|
299
|
+
],
|
|
300
|
+
"title": "search_sketchfab_modelsArguments",
|
|
301
|
+
"type": "object"
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"name": "get_sketchfab_model_preview",
|
|
306
|
+
"description": "\n Get a preview thumbnail of a Sketchfab model by its UID.\n Use this to visually confirm a model before downloading.\n \n Parameters:\n - uid: The unique identifier of the Sketchfab model (obtained from search_sketchfab_models)\n - user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.\n \n Returns the model's thumbnail as an Image for visual confirmation.\n ",
|
|
307
|
+
"inputSchema": {
|
|
308
|
+
"properties": {
|
|
309
|
+
"uid": {
|
|
310
|
+
"title": "Uid",
|
|
311
|
+
"type": "string"
|
|
312
|
+
},
|
|
313
|
+
"user_prompt": {
|
|
314
|
+
"default": "",
|
|
315
|
+
"title": "User Prompt",
|
|
316
|
+
"type": "string"
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
"required": [
|
|
320
|
+
"uid"
|
|
321
|
+
],
|
|
322
|
+
"title": "get_sketchfab_model_previewArguments",
|
|
323
|
+
"type": "object"
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"name": "download_sketchfab_model",
|
|
328
|
+
"description": "\n Download and import a Sketchfab model by its UID.\n The model will be scaled so its largest dimension equals target_size.\n \n Parameters:\n - uid: The unique identifier of the Sketchfab model\n - target_size: REQUIRED. The target size in Blender units/meters for the largest dimension.\n You must specify the desired size for the model.\n Examples:\n - Chair: target_size=1.0 (1 meter tall)\n - Table: target_size=0.75 (75cm tall)\n - Car: target_size=4.5 (4.5 meters long)\n - Person: target_size=1.7 (1.7 meters tall)\n - user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.\n - Small object (cup, phone): target_size=0.1 to 0.3\n \n Returns a message with import details including object names, dimensions, and bounding box.\n The model must be downloadable and you must have proper access rights.\n ",
|
|
329
|
+
"inputSchema": {
|
|
330
|
+
"properties": {
|
|
331
|
+
"uid": {
|
|
332
|
+
"title": "Uid",
|
|
333
|
+
"type": "string"
|
|
334
|
+
},
|
|
335
|
+
"target_size": {
|
|
336
|
+
"title": "Target Size",
|
|
337
|
+
"type": "number"
|
|
338
|
+
},
|
|
339
|
+
"user_prompt": {
|
|
340
|
+
"default": "",
|
|
341
|
+
"title": "User Prompt",
|
|
342
|
+
"type": "string"
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
"required": [
|
|
346
|
+
"uid",
|
|
347
|
+
"target_size"
|
|
348
|
+
],
|
|
349
|
+
"title": "download_sketchfab_modelArguments",
|
|
350
|
+
"type": "object"
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"name": "get_polypizza_status",
|
|
355
|
+
"description": "\n Check if Poly Pizza integration is enabled in Blender.\n Returns a message indicating whether Poly Pizza features are available.\n ",
|
|
356
|
+
"inputSchema": {
|
|
357
|
+
"properties": {
|
|
358
|
+
"user_prompt": {
|
|
359
|
+
"default": "",
|
|
360
|
+
"title": "User Prompt",
|
|
361
|
+
"type": "string"
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
"title": "get_polypizza_statusArguments",
|
|
365
|
+
"type": "object"
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"name": "search_polypizza_models",
|
|
370
|
+
"description": "\n Search for models on Poly Pizza with optional filtering.\n\n Parameters:\n - query: Text to search for. May be left empty if at least one filter is given.\n - category: Optional category name, e.g. \"Animals\", \"Furniture & Decor\", \"Transport\",\n \"Nature\", \"Buildings\", \"People & Characters\", \"Food & Drink\", \"Weapons\",\n \"Clutter\", \"Objects\", \"Scenes & Levels\", \"Other\"\n - licence: Optional licence filter, either \"CC0\" (no credit required) or \"CC-BY\"\n (credit required)\n - animated: When True, return only animated models (default False)\n - limit: Maximum number of results to return (default 20, the API caps it at 32)\n - user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.\n\n Returns a formatted list of matching models, with licence and triangle count on\n every row so a low-poly, permissively licensed asset can be picked without a\n second call.\n ",
|
|
371
|
+
"inputSchema": {
|
|
372
|
+
"properties": {
|
|
373
|
+
"query": {
|
|
374
|
+
"default": "",
|
|
375
|
+
"title": "Query",
|
|
376
|
+
"type": "string"
|
|
377
|
+
},
|
|
378
|
+
"category": {
|
|
379
|
+
"default": null,
|
|
380
|
+
"title": "Category",
|
|
381
|
+
"type": "string"
|
|
382
|
+
},
|
|
383
|
+
"licence": {
|
|
384
|
+
"default": null,
|
|
385
|
+
"title": "Licence",
|
|
386
|
+
"type": "string"
|
|
387
|
+
},
|
|
388
|
+
"animated": {
|
|
389
|
+
"default": false,
|
|
390
|
+
"title": "Animated",
|
|
391
|
+
"type": "boolean"
|
|
392
|
+
},
|
|
393
|
+
"limit": {
|
|
394
|
+
"default": 20,
|
|
395
|
+
"title": "Limit",
|
|
396
|
+
"type": "integer"
|
|
397
|
+
},
|
|
398
|
+
"user_prompt": {
|
|
399
|
+
"default": "",
|
|
400
|
+
"title": "User Prompt",
|
|
401
|
+
"type": "string"
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
"title": "search_polypizza_modelsArguments",
|
|
405
|
+
"type": "object"
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
"name": "download_polypizza_model",
|
|
410
|
+
"description": "\n Download and import a Poly Pizza model by its ID.\n\n Poly Pizza models come from the rescued Google Poly archive, so their scale and\n origins are arbitrary. Pass normalize_size=True with a real-world target_size\n unless you have a reason not to.\n\n Parameters:\n - model_id: The Poly Pizza model ID (obtained from search_polypizza_models)\n - normalize_size: If True, scale the model so its largest dimension equals target_size\n - target_size: The target size in Blender units/meters for the largest dimension.\n Examples:\n - Chair: target_size=1.0 (1 meter tall)\n - Table: target_size=0.75 (75cm tall)\n - Car: target_size=4.5 (4.5 meters long)\n - Person: target_size=1.7 (1.7 meters tall)\n - Small object (cup, phone): target_size=0.1 to 0.3\n - user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.\n\n Returns a message with import details including object names, dimensions, bounding\n box, and the attribution string, which is also written onto each imported root\n object as the custom properties polypizza_attribution, polypizza_id and\n polypizza_licence.\n ",
|
|
411
|
+
"inputSchema": {
|
|
412
|
+
"properties": {
|
|
413
|
+
"model_id": {
|
|
414
|
+
"title": "Model Id",
|
|
415
|
+
"type": "string"
|
|
416
|
+
},
|
|
417
|
+
"normalize_size": {
|
|
418
|
+
"default": false,
|
|
419
|
+
"title": "Normalize Size",
|
|
420
|
+
"type": "boolean"
|
|
421
|
+
},
|
|
422
|
+
"target_size": {
|
|
423
|
+
"default": 1.0,
|
|
424
|
+
"title": "Target Size",
|
|
425
|
+
"type": "number"
|
|
426
|
+
},
|
|
427
|
+
"user_prompt": {
|
|
428
|
+
"default": "",
|
|
429
|
+
"title": "User Prompt",
|
|
430
|
+
"type": "string"
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
"required": [
|
|
434
|
+
"model_id"
|
|
435
|
+
],
|
|
436
|
+
"title": "download_polypizza_modelArguments",
|
|
437
|
+
"type": "object"
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
"name": "generate_hyper3d_model_via_text",
|
|
442
|
+
"description": "\n Generate 3D asset using Hyper3D by giving description of the desired asset, and import the asset into Blender.\n The 3D asset has built-in materials.\n The generated model has a normalized size, so re-scaling after generation can be useful.\n\n Parameters:\n - text_prompt: A short description of the desired model in **English**.\n - bbox_condition: Optional. If given, it has to be a list of floats of length 3. Controls the ratio between [Length, Width, Height] of the model.\n - user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.\n\n Returns a message indicating success or failure.\n ",
|
|
443
|
+
"inputSchema": {
|
|
444
|
+
"properties": {
|
|
445
|
+
"text_prompt": {
|
|
446
|
+
"title": "Text Prompt",
|
|
447
|
+
"type": "string"
|
|
448
|
+
},
|
|
449
|
+
"bbox_condition": {
|
|
450
|
+
"default": null,
|
|
451
|
+
"items": {
|
|
452
|
+
"type": "number"
|
|
453
|
+
},
|
|
454
|
+
"title": "Bbox Condition",
|
|
455
|
+
"type": "array"
|
|
456
|
+
},
|
|
457
|
+
"user_prompt": {
|
|
458
|
+
"default": "",
|
|
459
|
+
"title": "User Prompt",
|
|
460
|
+
"type": "string"
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
"required": [
|
|
464
|
+
"text_prompt"
|
|
465
|
+
],
|
|
466
|
+
"title": "generate_hyper3d_model_via_textArguments",
|
|
467
|
+
"type": "object"
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
"name": "generate_hyper3d_model_via_images",
|
|
472
|
+
"description": "\n Generate 3D asset using Hyper3D by giving images of the wanted asset, and import the generated asset into Blender.\n The 3D asset has built-in materials.\n The generated model has a normalized size, so re-scaling after generation can be useful.\n \n Parameters:\n - input_image_paths: The **absolute** paths of input images. Even if only one image is provided, wrap it into a list. Required if Hyper3D Rodin in MAIN_SITE mode.\n - input_image_urls: The URLs of input images. Even if only one image is provided, wrap it into a list. Required if Hyper3D Rodin in FAL_AI mode.\n - bbox_condition: Optional. If given, it has to be a list of ints of length 3. Controls the ratio between [Length, Width, Height] of the model.\n - user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.\n\n Only one of {input_image_paths, input_image_urls} should be given at a time, depending on the Hyper3D Rodin's current mode.\n Returns a message indicating success or failure.\n ",
|
|
473
|
+
"inputSchema": {
|
|
474
|
+
"properties": {
|
|
475
|
+
"input_image_paths": {
|
|
476
|
+
"default": null,
|
|
477
|
+
"items": {
|
|
478
|
+
"type": "string"
|
|
479
|
+
},
|
|
480
|
+
"title": "Input Image Paths",
|
|
481
|
+
"type": "array"
|
|
482
|
+
},
|
|
483
|
+
"input_image_urls": {
|
|
484
|
+
"default": null,
|
|
485
|
+
"items": {
|
|
486
|
+
"type": "string"
|
|
487
|
+
},
|
|
488
|
+
"title": "Input Image Urls",
|
|
489
|
+
"type": "array"
|
|
490
|
+
},
|
|
491
|
+
"bbox_condition": {
|
|
492
|
+
"default": null,
|
|
493
|
+
"items": {
|
|
494
|
+
"type": "number"
|
|
495
|
+
},
|
|
496
|
+
"title": "Bbox Condition",
|
|
497
|
+
"type": "array"
|
|
498
|
+
},
|
|
499
|
+
"user_prompt": {
|
|
500
|
+
"default": "",
|
|
501
|
+
"title": "User Prompt",
|
|
502
|
+
"type": "string"
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
"title": "generate_hyper3d_model_via_imagesArguments",
|
|
506
|
+
"type": "object"
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
"name": "poll_rodin_job_status",
|
|
511
|
+
"description": "\n Check if the Hyper3D Rodin generation task is completed.\n\n For Hyper3D Rodin mode MAIN_SITE:\n Parameters:\n - subscription_key: The subscription_key given in the generate model step.\n\n Returns a list of status. The task is done if all status are \"Done\".\n If \"Failed\" showed up, the generating process failed.\n This is a polling API, so only proceed if the status are finally determined (\"Done\" or \"Canceled\").\n\n For Hyper3D Rodin mode FAL_AI:\n Parameters:\n - request_id: The request_id given in the generate model step.\n\n Returns the generation task status. The task is done if status is \"COMPLETED\".\n The task is in progress if status is \"IN_PROGRESS\".\n If status other than \"COMPLETED\", \"IN_PROGRESS\", \"IN_QUEUE\" showed up, the generating process might be failed.\n This is a polling API, so only proceed if the status are finally determined (\"COMPLETED\" or some failed state).\n ",
|
|
512
|
+
"inputSchema": {
|
|
513
|
+
"properties": {
|
|
514
|
+
"subscription_key": {
|
|
515
|
+
"default": null,
|
|
516
|
+
"title": "Subscription Key",
|
|
517
|
+
"type": "string"
|
|
518
|
+
},
|
|
519
|
+
"request_id": {
|
|
520
|
+
"default": null,
|
|
521
|
+
"title": "Request Id",
|
|
522
|
+
"type": "string"
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
"title": "poll_rodin_job_statusArguments",
|
|
526
|
+
"type": "object"
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
"name": "import_generated_asset",
|
|
531
|
+
"description": "\n Import the asset generated by Hyper3D Rodin after the generation task is completed.\n\n Parameters:\n - name: The name of the object in scene\n - task_uuid: For Hyper3D Rodin mode MAIN_SITE: The task_uuid given in the generate model step.\n - request_id: For Hyper3D Rodin mode FAL_AI: The request_id given in the generate model step.\n\n Only give one of {task_uuid, request_id} based on the Hyper3D Rodin Mode!\n Return if the asset has been imported successfully.\n ",
|
|
532
|
+
"inputSchema": {
|
|
533
|
+
"properties": {
|
|
534
|
+
"name": {
|
|
535
|
+
"title": "Name",
|
|
536
|
+
"type": "string"
|
|
537
|
+
},
|
|
538
|
+
"task_uuid": {
|
|
539
|
+
"default": null,
|
|
540
|
+
"title": "Task Uuid",
|
|
541
|
+
"type": "string"
|
|
542
|
+
},
|
|
543
|
+
"request_id": {
|
|
544
|
+
"default": null,
|
|
545
|
+
"title": "Request Id",
|
|
546
|
+
"type": "string"
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
"required": [
|
|
550
|
+
"name"
|
|
551
|
+
],
|
|
552
|
+
"title": "import_generated_assetArguments",
|
|
553
|
+
"type": "object"
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
"name": "get_hunyuan3d_status",
|
|
558
|
+
"description": "\n Check if Hunyuan3D integration is enabled in Blender.\n Returns a message indicating whether Hunyuan3D features are available.\n ",
|
|
559
|
+
"inputSchema": {
|
|
560
|
+
"properties": {
|
|
561
|
+
"user_prompt": {
|
|
562
|
+
"default": "",
|
|
563
|
+
"title": "User Prompt",
|
|
564
|
+
"type": "string"
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
"title": "get_hunyuan3d_statusArguments",
|
|
568
|
+
"type": "object"
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
"name": "generate_hunyuan3d_model",
|
|
573
|
+
"description": "\n Generate 3D asset using Hunyuan3D by providing either text description, image reference, \n or both for the desired asset, and import the asset into Blender.\n The 3D asset has built-in materials.\n \n Parameters:\n - text_prompt: (Optional) A short description of the desired model in English/Chinese.\n - input_image_url: (Optional) The local or remote url of the input image. Accepts None if only using text prompt.\n - user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.\n\n Returns: \n - When successful, returns a JSON with job_id (format: \"job_xxx\") indicating the task is in progress\n - When the job completes, the status will change to \"DONE\" indicating the model has been imported\n - Returns error message if the operation fails\n ",
|
|
574
|
+
"inputSchema": {
|
|
575
|
+
"properties": {
|
|
576
|
+
"text_prompt": {
|
|
577
|
+
"default": null,
|
|
578
|
+
"title": "Text Prompt",
|
|
579
|
+
"type": "string"
|
|
580
|
+
},
|
|
581
|
+
"input_image_url": {
|
|
582
|
+
"default": null,
|
|
583
|
+
"title": "Input Image Url",
|
|
584
|
+
"type": "string"
|
|
585
|
+
},
|
|
586
|
+
"user_prompt": {
|
|
587
|
+
"default": "",
|
|
588
|
+
"title": "User Prompt",
|
|
589
|
+
"type": "string"
|
|
590
|
+
}
|
|
591
|
+
},
|
|
592
|
+
"title": "generate_hunyuan3d_modelArguments",
|
|
593
|
+
"type": "object"
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
"name": "poll_hunyuan_job_status",
|
|
598
|
+
"description": "\n Check if the Hunyuan3D generation task is completed.\n\n For Hunyuan3D:\n Parameters:\n - job_id: The job_id given in the generate model step.\n\n Returns the generation task status. The task is done if status is \"DONE\".\n The task is in progress if status is \"RUN\".\n If status is \"DONE\", returns ResultFile3Ds with one or more downloadable model URLs.\n Prefer a .glb URL when present (self-contained with materials); otherwise use a .zip/.obj asset URL.\n This is a polling API, so only proceed if the status are finally determined (\"DONE\" or some failed state).\n ",
|
|
599
|
+
"inputSchema": {
|
|
600
|
+
"properties": {
|
|
601
|
+
"job_id": {
|
|
602
|
+
"default": null,
|
|
603
|
+
"title": "Job Id",
|
|
604
|
+
"type": "string"
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
"title": "poll_hunyuan_job_statusArguments",
|
|
608
|
+
"type": "object"
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
"name": "import_generated_asset_hunyuan",
|
|
613
|
+
"description": "\n Import the asset generated by Hunyuan3D after the generation task is completed.\n\n Parameters:\n - name: The name of the object in scene\n - zip_file_url: A model URL from ResultFile3Ds. Prefer a .glb URL when available; .zip/.obj URLs still work as a fallback.\n\n Return if the asset has been imported successfully.\n ",
|
|
614
|
+
"inputSchema": {
|
|
615
|
+
"properties": {
|
|
616
|
+
"name": {
|
|
617
|
+
"title": "Name",
|
|
618
|
+
"type": "string"
|
|
619
|
+
},
|
|
620
|
+
"zip_file_url": {
|
|
621
|
+
"title": "Zip File Url",
|
|
622
|
+
"type": "string"
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
"required": [
|
|
626
|
+
"name",
|
|
627
|
+
"zip_file_url"
|
|
628
|
+
],
|
|
629
|
+
"title": "import_generated_asset_hunyuanArguments",
|
|
630
|
+
"type": "object"
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
"name": "record_trajectory_feedback",
|
|
635
|
+
"description": "\n Record evaluation feedback for a captured trajectory step.\n\n Parameters:\n - feedback: One of accept | reject | undo | correction\n - correction_text: Optional free-text correction or follow-up (especially for correction)\n - step_index: Optional 0-based step index; defaults to the last recorded step\n - user_prompt: Optional goal/prompt context for the feedback row\n ",
|
|
636
|
+
"inputSchema": {
|
|
637
|
+
"properties": {
|
|
638
|
+
"feedback": {
|
|
639
|
+
"title": "Feedback",
|
|
640
|
+
"type": "string"
|
|
641
|
+
},
|
|
642
|
+
"correction_text": {
|
|
643
|
+
"default": null,
|
|
644
|
+
"title": "Correction Text",
|
|
645
|
+
"type": "string"
|
|
646
|
+
},
|
|
647
|
+
"step_index": {
|
|
648
|
+
"default": null,
|
|
649
|
+
"title": "Step Index",
|
|
650
|
+
"type": "integer"
|
|
651
|
+
},
|
|
652
|
+
"user_prompt": {
|
|
653
|
+
"default": "",
|
|
654
|
+
"title": "User Prompt",
|
|
655
|
+
"type": "string"
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
"required": [
|
|
659
|
+
"feedback"
|
|
660
|
+
],
|
|
661
|
+
"title": "record_trajectory_feedbackArguments",
|
|
662
|
+
"type": "object"
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
]
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<product> blender-mcp` — the blender-mcp server SHAPE (the published server's 28
|
|
3
|
+
* tools, names, parameters, descriptions, result strings and its one prompt)
|
|
4
|
+
* as pure transport onto Blender running in the project's editor tab.
|
|
5
|
+
*
|
|
6
|
+
* No modeling happens in this process. `execute_blender_code`,
|
|
7
|
+
* `get_scene_info`, `get_object_info` and `get_viewport_screenshot` become
|
|
8
|
+
* `blender-*` control commands answered by the tab's worker
|
|
9
|
+
* (`packages/blender/contributions/blender.command.ts`, the engine's own
|
|
10
|
+
* `workspace.command` contribution); the screenshot is the tab
|
|
11
|
+
* photographing its own Model document. The 24 integration tools answer as
|
|
12
|
+
* an add-on with no integrations, exactly as the Python server did.
|
|
13
|
+
*/
|
|
14
|
+
import { randomUUID } from 'node:crypto';
|
|
15
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { dirname, relative, sep } from 'node:path';
|
|
17
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
18
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
19
|
+
import {
|
|
20
|
+
CallToolRequestSchema,
|
|
21
|
+
GetPromptRequestSchema,
|
|
22
|
+
ListPromptsRequestSchema,
|
|
23
|
+
ListToolsRequestSchema,
|
|
24
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
25
|
+
import { EditorClient, type EditorView } from '@volter/editor-sdk';
|
|
26
|
+
import blenderBundle from '@volter/blender-engine/wasm/BUNDLE.json';
|
|
27
|
+
import { projectOutputRootOf } from '@volter/editor-sdk/project/output-roots';
|
|
28
|
+
import blenderDefaults from './blender-mcp-defaults.json';
|
|
29
|
+
import blenderTools from './blender-mcp-tools.json';
|
|
30
|
+
|
|
31
|
+
interface ToolShape {
|
|
32
|
+
name: string;
|
|
33
|
+
description: string;
|
|
34
|
+
inputSchema: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DISABLED = {
|
|
38
|
+
polyhaven:
|
|
39
|
+
'PolyHaven integration is disabled. Select it in the sidebar in BlenderMCP, then run it again.',
|
|
40
|
+
sketchfab:
|
|
41
|
+
'Sketchfab integration is disabled. Select it in the sidebar in BlenderMCP, then run it again.',
|
|
42
|
+
polypizza:
|
|
43
|
+
'Poly Pizza integration is disabled. Select it in the sidebar in BlenderMCP, then run it again.',
|
|
44
|
+
hyper3d:
|
|
45
|
+
'Hyper3D Rodin integration is disabled. Select it in the sidebar in BlenderMCP, then run it again.',
|
|
46
|
+
hunyuan:
|
|
47
|
+
'Hunyuan3D integration is disabled. Select it in the sidebar in BlenderMCP, then run it again.',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const STATIC_TOOLS: Record<string, string> = {
|
|
51
|
+
disable_telemetry: 'Data collection is now OFF.',
|
|
52
|
+
get_polyhaven_categories: DISABLED.polyhaven,
|
|
53
|
+
search_polyhaven_assets: DISABLED.polyhaven,
|
|
54
|
+
download_polyhaven_asset: DISABLED.polyhaven,
|
|
55
|
+
set_texture: DISABLED.polyhaven,
|
|
56
|
+
search_sketchfab_models: DISABLED.sketchfab,
|
|
57
|
+
get_sketchfab_model_preview: DISABLED.sketchfab,
|
|
58
|
+
download_sketchfab_model: DISABLED.sketchfab,
|
|
59
|
+
search_polypizza_models: DISABLED.polypizza,
|
|
60
|
+
download_polypizza_model: DISABLED.polypizza,
|
|
61
|
+
generate_hyper3d_model_via_text: DISABLED.hyper3d,
|
|
62
|
+
generate_hyper3d_model_via_images: DISABLED.hyper3d,
|
|
63
|
+
poll_rodin_job_status: DISABLED.hyper3d,
|
|
64
|
+
import_generated_asset: DISABLED.hyper3d,
|
|
65
|
+
generate_hunyuan3d_model: DISABLED.hunyuan,
|
|
66
|
+
poll_hunyuan_job_status: DISABLED.hunyuan,
|
|
67
|
+
import_generated_asset_hunyuan: DISABLED.hunyuan,
|
|
68
|
+
record_trajectory_feedback:
|
|
69
|
+
'Trajectory feedback skipped (telemetry disabled, no consent, or write failed)',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const TOOLS_PROMPT = `When creating 3D content in Blender, always start by checking if integrations are available:
|
|
73
|
+
|
|
74
|
+
0. Before anything, always check the scene from get_scene_info()
|
|
75
|
+
1. First use the following tools to verify if the following integrations are enabled:
|
|
76
|
+
- PolyHaven
|
|
77
|
+
- Sketchfab
|
|
78
|
+
- Hyper3D
|
|
79
|
+
- Hunyuan3D
|
|
80
|
+
- Poly Pizza
|
|
81
|
+
|
|
82
|
+
2. If none of the integrations are enabled, create the objects using Blender's built-in tools with execute_blender_code.
|
|
83
|
+
|
|
84
|
+
3. When including an object into scene, ALWAYS make sure that the name of the object is meanful.
|
|
85
|
+
|
|
86
|
+
4. Always check the world_bounding_box for each item so that:
|
|
87
|
+
- Ensure that all objects that should not be clipping are not clipping.
|
|
88
|
+
- Items have right spatial relationship.
|
|
89
|
+
|
|
90
|
+
5. After giving the tool location/scale/rotation information (via execute_blender_code), verify the new properties by using get_object_info to confirm the changes.
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
export const BLENDER_MCP_VERSION = '1.2';
|
|
94
|
+
|
|
95
|
+
interface FileEntry {
|
|
96
|
+
path: string;
|
|
97
|
+
size: number;
|
|
98
|
+
mtime: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A path INSIDE the project, POSIX-spelled, or null when it is outside. */
|
|
102
|
+
function projectRelative(project: string, absolute: string): string | null {
|
|
103
|
+
const rel = relative(project, absolute).split(sep).join('/');
|
|
104
|
+
if (rel === '' || rel.startsWith('../')) return null;
|
|
105
|
+
return rel;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The worker's filesystem is the only place a script's outputs exist; this
|
|
110
|
+
* mirrors them to disk. Roots: the project (a script's `public/models/x.glb`
|
|
111
|
+
* lands in the project) plus any `VGAI_BLENDER_MIRROR_ROOTS` (colon-separated
|
|
112
|
+
* absolute paths — the replay harness names its run directory). Only files
|
|
113
|
+
* whose size or mtime changed since the last mirror are read back, and only
|
|
114
|
+
* files the SESSION owns are listed at all — a project file it merely read
|
|
115
|
+
* belongs to the host and is never written back over it.
|
|
116
|
+
*
|
|
117
|
+
* THIS IS THE DOOR THAT RECORDS, because it is the door a session's bytes
|
|
118
|
+
* enter the project through. MEASURED 2026-09-18: a script's
|
|
119
|
+
* `export_scene.gltf` to `<project>/public/models/lantern.glb` landed here and
|
|
120
|
+
* `.vgai/provenance.json` was never created — the one state
|
|
121
|
+
* `scripts/validate-project-provenance.mjs` calls fatal, and the opposite of
|
|
122
|
+
* the doctrine that a generated artifact enters `public/` through a door that
|
|
123
|
+
* records it atomically. So a file this lands under `public/` is POSTed to the
|
|
124
|
+
* session's `/__editor/blender-output`, which commits the bytes and the ledger
|
|
125
|
+
* rewrite as ONE rollback-safe transaction through the same writer the
|
|
126
|
+
* editor's own bake and acceptance tools use; the record's kind is
|
|
127
|
+
* `project.session.write` and it names the session (see
|
|
128
|
+
* `ProjectProvenanceSessionSchema`).
|
|
129
|
+
*
|
|
130
|
+
* The session process owns the writer and the project's ledger. This
|
|
131
|
+
* transport uses that same session boundary as the other product commands;
|
|
132
|
+
* it never creates a second ledger writer in the MCP process.
|
|
133
|
+
*
|
|
134
|
+
* Everything else the session owns — `.vgai/tmp/*.png`, a `.blend`, a replay
|
|
135
|
+
* harness's run directory outside the project — is mirrored with a plain write
|
|
136
|
+
* and deliberately NOT recorded: the ledger is about what the project SHIPS,
|
|
137
|
+
* and `public/` is what ships.
|
|
138
|
+
*/
|
|
139
|
+
class Mirror {
|
|
140
|
+
readonly #seen = new Map<string, string>();
|
|
141
|
+
constructor(
|
|
142
|
+
private readonly project: string,
|
|
143
|
+
private readonly roots: string[],
|
|
144
|
+
/** The command that served this transport, recorded as the write's source. */
|
|
145
|
+
private readonly source: string,
|
|
146
|
+
) {}
|
|
147
|
+
|
|
148
|
+
async sync(tab: TabSession, callId: string | null): Promise<number> {
|
|
149
|
+
let written = 0;
|
|
150
|
+
for (const root of this.roots) {
|
|
151
|
+
const listing = await tab.command<{
|
|
152
|
+
entries: FileEntry[];
|
|
153
|
+
session?: string;
|
|
154
|
+
revision?: number;
|
|
155
|
+
document?: string;
|
|
156
|
+
}>('blender-list-files', { path: root });
|
|
157
|
+
for (const entry of listing.entries) {
|
|
158
|
+
const stamp = `${entry.size}:${entry.mtime}`;
|
|
159
|
+
if (this.#seen.get(entry.path) === stamp) continue;
|
|
160
|
+
const projectPath = projectRelative(this.project, entry.path);
|
|
161
|
+
const shipped =
|
|
162
|
+
projectPath !== null && projectOutputRootOf(projectPath) === 'public'
|
|
163
|
+
? projectPath
|
|
164
|
+
: null;
|
|
165
|
+
// ANTI-SHIM: an unrecordable shipped file is REFUSED, not written
|
|
166
|
+
// anyway. Landing it unrecorded is the defect this door exists to
|
|
167
|
+
// close, and inventing a session for it would be worse than both.
|
|
168
|
+
if (shipped !== null && (listing.session === undefined || listing.revision === undefined))
|
|
169
|
+
throw new Error(
|
|
170
|
+
`The Blender session wrote ${shipped} but has not named itself — it has presented no ` +
|
|
171
|
+
'frame, so there is no session identity or revision to record the file under, and ' +
|
|
172
|
+
'an unrecorded binary under public/ is what `npm run validate-provenance` calls ' +
|
|
173
|
+
'fatal. Nothing was written for it.',
|
|
174
|
+
);
|
|
175
|
+
// WE mint the transfer id and WE choose where the bytes land; the page
|
|
176
|
+
// only hands over a body. See `/__editor/blender-file`.
|
|
177
|
+
const transferId = randomUUID();
|
|
178
|
+
await tab.command<{ bytes: number }>('blender-read-file', { path: entry.path, transferId });
|
|
179
|
+
const spooled = await fetch(`${await tab.origin()}/__editor/blender-file?id=${transferId}`);
|
|
180
|
+
if (!spooled.ok) throw new Error(`Transfer of ${entry.path} failed: ${spooled.status}`);
|
|
181
|
+
const bytes = Buffer.from(await spooled.arrayBuffer());
|
|
182
|
+
if (shipped === null) {
|
|
183
|
+
// Staged inputs are listed alongside outputs. Rewriting an unchanged
|
|
184
|
+
// adapter triggers project reload during this very MCP request.
|
|
185
|
+
// Only changed bytes should reach the filesystem watcher.
|
|
186
|
+
try {
|
|
187
|
+
if (readFileSync(entry.path).equals(bytes)) {
|
|
188
|
+
this.#seen.set(entry.path, stamp);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
193
|
+
}
|
|
194
|
+
mkdirSync(dirname(entry.path), { recursive: true });
|
|
195
|
+
writeFileSync(entry.path, bytes);
|
|
196
|
+
} else {
|
|
197
|
+
const query = new URLSearchParams({
|
|
198
|
+
path: shipped,
|
|
199
|
+
session: listing.session as string,
|
|
200
|
+
revision: String(listing.revision as number),
|
|
201
|
+
source: this.source,
|
|
202
|
+
...(callId === null ? {} : { callId }),
|
|
203
|
+
});
|
|
204
|
+
// WHAT THE BYTES WERE MADE FROM, when the session holds a document:
|
|
205
|
+
// the `.blend` this export came out of. Passed as a repeatable
|
|
206
|
+
// `inputs` param rather than folded into the path, because the
|
|
207
|
+
// server validates each one as a project-relative path of its own.
|
|
208
|
+
if (typeof listing.document === 'string' && listing.document !== '')
|
|
209
|
+
query.append('inputs', listing.document);
|
|
210
|
+
const recorded = await fetch(`${await tab.origin()}/__editor/blender-output?${query}`, {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
body: new Uint8Array(bytes),
|
|
213
|
+
});
|
|
214
|
+
if (!recorded.ok) {
|
|
215
|
+
const detail = await recorded.text().catch(() => '');
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Recording ${shipped} through the session failed: ${recorded.status} ${detail}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
this.#seen.set(entry.path, stamp);
|
|
222
|
+
written += 1;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return written;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
class TabSession {
|
|
230
|
+
#client: EditorClient | null = null;
|
|
231
|
+
#connecting: Promise<EditorClient> | null = null;
|
|
232
|
+
#origin: string | null = null;
|
|
233
|
+
#port: number | null = null;
|
|
234
|
+
/** A replay starts with a fresh model once; ordinary agent sessions persist. */
|
|
235
|
+
#freshSessionPending = Boolean(process.env['VGAI_BLENDER_FRESH_SESSION']);
|
|
236
|
+
constructor(
|
|
237
|
+
private readonly project: string,
|
|
238
|
+
private readonly ensureEditor: () => Promise<void>,
|
|
239
|
+
) {}
|
|
240
|
+
|
|
241
|
+
async client(): Promise<EditorClient> {
|
|
242
|
+
if (this.#client) return this.#client;
|
|
243
|
+
if (!this.#connecting) {
|
|
244
|
+
this.#connecting = (async () => {
|
|
245
|
+
await this.ensureEditor();
|
|
246
|
+
const { resolveSession } = await import('@volter/editor-live');
|
|
247
|
+
const session = await resolveSession(this.project);
|
|
248
|
+
this.#port = session.port;
|
|
249
|
+
this.#origin = `http://127.0.0.1:${session.port}`;
|
|
250
|
+
this.#client = new EditorClient({ url: this.#origin });
|
|
251
|
+
return this.#client;
|
|
252
|
+
})().finally(() => {
|
|
253
|
+
this.#connecting = null;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return this.#connecting;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** This session's HTTP origin, for the doors that are not commands. */
|
|
260
|
+
async origin(): Promise<string> {
|
|
261
|
+
await this.client();
|
|
262
|
+
return this.#origin as string;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** The machine-local port this project's session answers on — the address a
|
|
266
|
+
* provenance record names so a reader can get back to the tab. */
|
|
267
|
+
async port(): Promise<number> {
|
|
268
|
+
await this.client();
|
|
269
|
+
return this.#port as number;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The Model document open and the session started at this project's path.
|
|
274
|
+
*
|
|
275
|
+
* Asked every call, never remembered: the session lives in the editor tab,
|
|
276
|
+
* so an editor restart ends it while this process keeps running.
|
|
277
|
+
* The editor owns opening the Model document and idempotent startup. A
|
|
278
|
+
* separate status query would put a five-second check before that startup
|
|
279
|
+
* request and introduce a race with an editor restart.
|
|
280
|
+
*/
|
|
281
|
+
async started(): Promise<EditorClient> {
|
|
282
|
+
const client = await this.client();
|
|
283
|
+
const fresh = this.#freshSessionPending;
|
|
284
|
+
this.#freshSessionPending = false;
|
|
285
|
+
await client.blender('blender-start', { project: this.project, ...(fresh ? { fresh } : {}) });
|
|
286
|
+
return client;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async command<T>(type: string, fields: Record<string, unknown> = {}): Promise<T> {
|
|
290
|
+
const client = await this.started();
|
|
291
|
+
return client.blender<T & object>(type as `blender-${string}`, fields);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const text = (value: string) => ({ content: [{ type: 'text' as const, text: value }] });
|
|
296
|
+
|
|
297
|
+
export async function serveBlenderMcp(
|
|
298
|
+
project: string,
|
|
299
|
+
ensureEditor: () => Promise<void> = async () => {},
|
|
300
|
+
command = 'volter-editor',
|
|
301
|
+
): Promise<void> {
|
|
302
|
+
const runtimeIdentity = `Blender ${blenderBundle.blender} (source ${blenderBundle.source}), compiled to WebAssembly and running headless in the editor tab; three.js takes its photographs. Factory bpy.context.scene.render.engine: '${blenderBundle.factoryEngine}'. Documents are .blend. save_as_mainfile records the actual saved path in bpy.data.filepath; open_mainfile reopens it.`;
|
|
303
|
+
const tools = (blenderTools as ToolShape[]).map((tool) =>
|
|
304
|
+
tool.name === 'execute_blender_code'
|
|
305
|
+
? { ...tool, description: `${runtimeIdentity}\n${tool.description}` }
|
|
306
|
+
: tool,
|
|
307
|
+
);
|
|
308
|
+
const defaults = blenderDefaults as Record<string, string>;
|
|
309
|
+
const mirror = new Mirror(project, [
|
|
310
|
+
project,
|
|
311
|
+
...(process.env['VGAI_BLENDER_MIRROR_ROOTS'] ?? '')
|
|
312
|
+
.split(':')
|
|
313
|
+
.filter((root) => root.startsWith('/')),
|
|
314
|
+
], `${command} blender-mcp`);
|
|
315
|
+
const tab = new TabSession(project, ensureEditor);
|
|
316
|
+
const server = new Server(
|
|
317
|
+
{ name: 'BlenderMCP', version: BLENDER_MCP_VERSION },
|
|
318
|
+
{
|
|
319
|
+
capabilities: { tools: {}, prompts: {} },
|
|
320
|
+
instructions: runtimeIdentity,
|
|
321
|
+
},
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
325
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
326
|
+
prompts: [{ name: 'asset_creation_strategy', description: '' }],
|
|
327
|
+
}));
|
|
328
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
329
|
+
if (request.params.name !== 'asset_creation_strategy')
|
|
330
|
+
throw new Error(`Unknown prompt: ${request.params.name}`);
|
|
331
|
+
return { messages: [{ role: 'user', content: { type: 'text', text: TOOLS_PROMPT } }] };
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
335
|
+
const name = request.params.name;
|
|
336
|
+
const args = (request.params.arguments ?? {}) as Record<string, unknown>;
|
|
337
|
+
// The transport's own number for THIS call. A file the script writes is
|
|
338
|
+
// recorded under it, so a ledger entry points back at the exact request
|
|
339
|
+
// that produced the bytes.
|
|
340
|
+
const callId = extra.requestId === undefined ? null : String(extra.requestId);
|
|
341
|
+
if (name in STATIC_TOOLS) return text(STATIC_TOOLS[name]!);
|
|
342
|
+
if (name in defaults) return text(defaults[name]!);
|
|
343
|
+
switch (name) {
|
|
344
|
+
case 'get_addon_status':
|
|
345
|
+
return text(
|
|
346
|
+
JSON.stringify(
|
|
347
|
+
{
|
|
348
|
+
addon_version: BLENDER_MCP_VERSION,
|
|
349
|
+
server_version: BLENDER_MCP_VERSION,
|
|
350
|
+
up_to_date: true,
|
|
351
|
+
},
|
|
352
|
+
null,
|
|
353
|
+
2,
|
|
354
|
+
),
|
|
355
|
+
);
|
|
356
|
+
case 'get_scene_info': {
|
|
357
|
+
const { result } = await tab.command<{ result: string }>('blender-scene-info');
|
|
358
|
+
return text(result);
|
|
359
|
+
}
|
|
360
|
+
case 'get_object_info': {
|
|
361
|
+
const { result } = await tab.command<{ result: string }>('blender-object-info', {
|
|
362
|
+
name: String(args['object_name'] ?? ''),
|
|
363
|
+
});
|
|
364
|
+
return text(result);
|
|
365
|
+
}
|
|
366
|
+
case 'execute_blender_code': {
|
|
367
|
+
const { result } = await tab.command<{ result: string }>('blender-execute', {
|
|
368
|
+
code: String(args['code'] ?? ''),
|
|
369
|
+
});
|
|
370
|
+
await mirror.sync(tab, callId);
|
|
371
|
+
return text(result);
|
|
372
|
+
}
|
|
373
|
+
case 'get_viewport_screenshot': {
|
|
374
|
+
const maxSize = typeof args['max_size'] === 'number' ? args['max_size'] : 1000;
|
|
375
|
+
const answer = await tab.command<{
|
|
376
|
+
document: NonNullable<EditorView['document']>;
|
|
377
|
+
view?: { size: number; position?: number[]; target?: number[] };
|
|
378
|
+
error?: string;
|
|
379
|
+
}>('blender-screenshot-view', { maxSize });
|
|
380
|
+
if (!answer.document) throw new Error('Screenshot failed: Blender supplied no document address');
|
|
381
|
+
if (!answer.view)
|
|
382
|
+
throw new Error(answer.error ?? 'Screenshot failed: No 3D viewport found');
|
|
383
|
+
const { view } = answer;
|
|
384
|
+
const camera =
|
|
385
|
+
view.position && view.target
|
|
386
|
+
? {
|
|
387
|
+
camera: {
|
|
388
|
+
position: { x: view.position[0]!, y: view.position[1]!, z: view.position[2]! },
|
|
389
|
+
target: { x: view.target[0]!, y: view.target[1]!, z: view.target[2]! },
|
|
390
|
+
},
|
|
391
|
+
}
|
|
392
|
+
: { frame: 'document' as const };
|
|
393
|
+
const client = await tab.started();
|
|
394
|
+
const presentation: EditorView = { version: 1, document: answer.document, viewport: camera };
|
|
395
|
+
await client.present(presentation);
|
|
396
|
+
const capture = await client.captureActiveDocument(view.size, presentation);
|
|
397
|
+
return {
|
|
398
|
+
content: [{ type: 'image' as const, data: capture.base64, mimeType: 'image/png' }],
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
await server.connect(new StdioServerTransport());
|
|
406
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@volter/editor-blender",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "AGPL-3.0-only AND GPL-3.0-or-later",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.8",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
@@ -17,12 +17,15 @@
|
|
|
17
17
|
"src",
|
|
18
18
|
"contributions",
|
|
19
19
|
"host",
|
|
20
|
-
"
|
|
20
|
+
"node",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"LICENSE-MCP"
|
|
21
23
|
],
|
|
22
24
|
"exports": {
|
|
23
25
|
"./contributions/*": "./contributions/*",
|
|
24
26
|
"./looks": "./src/looks.ts",
|
|
25
27
|
"./layouts": "./src/layouts.tsx",
|
|
28
|
+
"./mcp": "./node/blender-mcp.ts",
|
|
26
29
|
"./package.json": "./package.json"
|
|
27
30
|
},
|
|
28
31
|
"vgai": {
|
|
@@ -61,13 +64,15 @@
|
|
|
61
64
|
]
|
|
62
65
|
},
|
|
63
66
|
"dependencies": {
|
|
67
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
64
68
|
"@volter/blender-engine": "0.1.7",
|
|
65
|
-
"@volter/editor-threejs": "0.5.
|
|
69
|
+
"@volter/editor-threejs": "0.5.65",
|
|
66
70
|
"zod": "^4.3.6"
|
|
67
71
|
},
|
|
68
72
|
"peerDependencies": {
|
|
69
|
-
"@volter/editor-sdk": "0.5.
|
|
70
|
-
"@volter/editor-
|
|
73
|
+
"@volter/editor-sdk": "0.5.65",
|
|
74
|
+
"@volter/editor-live": "0.5.65",
|
|
75
|
+
"@volter/editor-project": "0.5.65",
|
|
71
76
|
"react": "^19.0.0",
|
|
72
77
|
"three": "^0.180.0"
|
|
73
78
|
},
|