iflow-mcp_cwahlfeldt_blender-mcp 0.1.0__py3-none-any.whl

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.
blender/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # blender module initialization
blender/executor.py ADDED
@@ -0,0 +1,160 @@
1
+ import subprocess
2
+ import os
3
+ import tempfile
4
+ from pathlib import Path
5
+ import logging
6
+
7
+ class BlenderExecutor:
8
+ """Class to handle script execution in Blender"""
9
+
10
+ def __init__(self, blender_path=None, timeout=60):
11
+ """
12
+ Initialize the BlenderExecutor
13
+
14
+ Args:
15
+ blender_path (str, optional): Path to the Blender executable. If None, will try to find it.
16
+ timeout (int, optional): Timeout for script execution in seconds
17
+ """
18
+ self.blender_path = blender_path or self._find_blender_executable()
19
+ self.timeout = timeout
20
+ self.temp_dir = Path(tempfile.gettempdir()) / "blender_mcp"
21
+ self._setup()
22
+
23
+ def _setup(self):
24
+ """Setup the executor, create necessary directories"""
25
+ os.makedirs(self.temp_dir, exist_ok=True)
26
+
27
+ def _find_blender_executable(self):
28
+ """Find Blender executable path"""
29
+ # Try common locations
30
+ common_paths = [
31
+ "blender", # If it's in PATH
32
+ "/usr/bin/blender",
33
+ "/usr/local/bin/blender",
34
+ "/Applications/Blender.app/Contents/MacOS/Blender",
35
+ "C:\\Program Files\\Blender Foundation\\Blender\\blender.exe",
36
+ ]
37
+
38
+ for path in common_paths:
39
+ try:
40
+ # Just check if we can run it with --version
41
+ subprocess.run([path, "--version"],
42
+ stdout=subprocess.PIPE,
43
+ stderr=subprocess.PIPE,
44
+ timeout=5)
45
+ return path
46
+ except (subprocess.SubprocessError, FileNotFoundError):
47
+ continue
48
+
49
+ # If we couldn't find it, default to "blender" and hope it's in PATH
50
+ return "blender"
51
+
52
+ def execute(self, script_name, script_content, blend_file=None):
53
+ """
54
+ Execute a script in Blender
55
+
56
+ Args:
57
+ script_name (str): Name of the script
58
+ script_content (str): Content of the script to execute
59
+ blend_file (str, optional): Path to a .blend file to use
60
+
61
+ Returns:
62
+ str: Output of the script execution
63
+ """
64
+ # Create a temporary script file
65
+ script_file = self.temp_dir / f"{script_name}.py"
66
+ output_file = self.temp_dir / f"{script_name}.out"
67
+
68
+ # Prepare variables to avoid f-string backslash issues
69
+ output_file_str = str(output_file)
70
+ newlines = "\n\n"
71
+
72
+ # Write the script content with output capture
73
+ with open(script_file, "w") as f:
74
+ # Add code to capture stdout and stderr
75
+ indented_script = script_content.replace('\n', '\n ')
76
+ wrapped_script = f"""
77
+ import sys
78
+ import io
79
+ import traceback
80
+
81
+ # Redirect stdout and stderr
82
+ old_stdout = sys.stdout
83
+ old_stderr = sys.stderr
84
+ stdout_buffer = io.StringIO()
85
+ stderr_buffer = io.StringIO()
86
+ sys.stdout = stdout_buffer
87
+ sys.stderr = stderr_buffer
88
+
89
+ try:
90
+ # Execute the script
91
+ {indented_script}
92
+ except Exception as e:
93
+ print(f"Error executing script: {{e}}")
94
+ traceback.print_exc()
95
+ finally:
96
+ # Restore stdout and stderr
97
+ sys.stdout = old_stdout
98
+ sys.stderr = old_stderr
99
+
100
+ # Write output to file
101
+ with open(r"{output_file_str}", "w") as output:
102
+ output.write(stdout_buffer.getvalue())
103
+ output.write("{newlines}")
104
+ output.write(stderr_buffer.getvalue())
105
+ """
106
+ f.write(wrapped_script)
107
+
108
+ try:
109
+ # Run Blender with the script
110
+ cmd = [
111
+ self.blender_path,
112
+ "--background", # Run without GUI
113
+ ]
114
+
115
+ # Add blend file if provided
116
+ if blend_file:
117
+ if os.path.exists(blend_file):
118
+ cmd.extend(["--file", blend_file])
119
+ else:
120
+ return f"Error: Blend file not found: {blend_file}"
121
+
122
+ # Add Python script
123
+ cmd.extend(["--python", str(script_file)])
124
+
125
+ process = subprocess.run(
126
+ cmd,
127
+ stdout=subprocess.PIPE,
128
+ stderr=subprocess.PIPE,
129
+ timeout=self.timeout,
130
+ text=True
131
+ )
132
+
133
+ # Check if the output file exists
134
+ if output_file.exists():
135
+ with open(output_file, "r") as f:
136
+ script_output = f.read()
137
+ else:
138
+ script_output = "No output captured from script."
139
+
140
+ # Add Blender's stdout/stderr if there was an error
141
+ if process.returncode != 0:
142
+ script_output += "\n\nBlender process output:\n"
143
+ script_output += f"STDOUT: {process.stdout}\n"
144
+ script_output += f"STDERR: {process.stderr}"
145
+
146
+ return script_output
147
+
148
+ except subprocess.TimeoutExpired:
149
+ return f"Script execution timed out after {self.timeout} seconds"
150
+ except Exception as e:
151
+ return f"Error executing script: {str(e)}"
152
+ finally:
153
+ # Clean up temp files
154
+ try:
155
+ if script_file.exists():
156
+ script_file.unlink()
157
+ if output_file.exists():
158
+ output_file.unlink()
159
+ except Exception as e:
160
+ logging.warning(f"Failed to clean up temp files: {e}")
blender/uv_tools.py ADDED
@@ -0,0 +1,323 @@
1
+ import os
2
+ import json
3
+ from pathlib import Path
4
+ import tempfile
5
+
6
+ class UVTools:
7
+ """Tools for UV mapping operations in Blender"""
8
+
9
+ def __init__(self, executor):
10
+ """
11
+ Initialize UV tools with a BlenderExecutor
12
+
13
+ Args:
14
+ executor: BlenderExecutor instance for running scripts
15
+ """
16
+ self.executor = executor
17
+ self.temp_dir = Path(tempfile.gettempdir()) / "blender_mcp_uv"
18
+ self._setup()
19
+
20
+ def _setup(self):
21
+ """Setup the UV tools, create necessary directories"""
22
+ os.makedirs(self.temp_dir, exist_ok=True)
23
+
24
+ def unwrap_object(self, object_name, method="ANGLE_BASED", blend_file=None):
25
+ """
26
+ Unwrap a specific object in a blend file
27
+
28
+ Args:
29
+ object_name (str): Name of the object to unwrap
30
+ method (str): Unwrapping method (ANGLE_BASED, CONFORMAL, or SMART_PROJECT)
31
+ blend_file (str): Path to the blend file
32
+
33
+ Returns:
34
+ dict: Result of the operation
35
+ """
36
+ if blend_file is None or not os.path.exists(blend_file):
37
+ return {"error": f"Blend file not found: {blend_file}"}
38
+
39
+ script = f"""
40
+ import bpy
41
+ import json
42
+
43
+ result = {{"success": False, "message": ""}}
44
+
45
+ # Check if the object exists
46
+ if "{object_name}" not in bpy.data.objects:
47
+ result["message"] = f"Object '{object_name}' not found in the blend file"
48
+ else:
49
+ obj = bpy.data.objects["{object_name}"]
50
+
51
+ # Check if it's a mesh
52
+ if obj.type != 'MESH':
53
+ result["message"] = f"Object '{object_name}' is not a mesh (type: {{obj.type}})"
54
+ else:
55
+ # Select only this object and make it active
56
+ bpy.ops.object.select_all(action='DESELECT')
57
+ obj.select_set(True)
58
+ bpy.context.view_layer.objects.active = obj
59
+
60
+ # Go to edit mode
61
+ bpy.ops.object.mode_set(mode='EDIT')
62
+
63
+ # Select all faces
64
+ bpy.ops.mesh.select_all(action='SELECT')
65
+
66
+ # Perform the unwrap operation
67
+ try:
68
+ method = "{method}"
69
+ if method == "SMART_PROJECT":
70
+ bpy.ops.uv.smart_project(angle_limit=66, island_margin=0.02)
71
+ else:
72
+ # ANGLE_BASED or CONFORMAL
73
+ bpy.ops.uv.unwrap(method=method, margin=0.02)
74
+
75
+ result["success"] = True
76
+ result["message"] = f"Successfully unwrapped {{obj.name}} using {{method}} method"
77
+
78
+ # Get UV stats
79
+ me = obj.data
80
+ if me.uv_layers:
81
+ result["uv_layers"] = [layer.name for layer in me.uv_layers]
82
+ result["active_uv"] = me.uv_layers.active.name if me.uv_layers.active else None
83
+ result["uv_count"] = len(me.uv_layers)
84
+ else:
85
+ result["message"] += "\\nNo UV layers found after unwrapping. This may indicate an error."
86
+ except Exception as e:
87
+ result["message"] = f"Error during unwrapping: {{str(e)}}"
88
+
89
+ # Return to object mode
90
+ bpy.ops.object.mode_set(mode='OBJECT')
91
+
92
+ # Save the file
93
+ bpy.ops.wm.save_mainfile()
94
+
95
+ # Print the result as JSON for parsing
96
+ print(json.dumps(result))
97
+ """
98
+
99
+ # Execute the script
100
+ output = self.executor.execute("unwrap_object", script, blend_file)
101
+
102
+ # Try to parse the JSON result
103
+ try:
104
+ # Extract JSON from the output (it may contain other print statements)
105
+ lines = output.strip().split('\n')
106
+ for line in lines:
107
+ try:
108
+ result = json.loads(line)
109
+ if isinstance(result, dict) and "success" in result:
110
+ return result
111
+ except json.JSONDecodeError:
112
+ pass
113
+
114
+ # If no valid JSON found, return the raw output
115
+ return {"error": "Could not parse result", "output": output}
116
+ except Exception as e:
117
+ return {"error": f"Error parsing result: {str(e)}", "output": output}
118
+
119
+ def mark_seams(self, object_name, blend_file=None, seam_angle=80):
120
+ """
121
+ Mark seams on an object based on angle
122
+
123
+ Args:
124
+ object_name (str): Name of the object to mark seams on
125
+ blend_file (str): Path to the blend file
126
+ seam_angle (float): Angle in degrees to use for marking seams
127
+
128
+ Returns:
129
+ dict: Result of the operation
130
+ """
131
+ if blend_file is None or not os.path.exists(blend_file):
132
+ return {"error": f"Blend file not found: {blend_file}"}
133
+
134
+ script = f"""
135
+ import bpy
136
+ import json
137
+ import math
138
+ import bmesh
139
+
140
+ result = {{"success": False, "message": ""}}
141
+
142
+ # Check if the object exists
143
+ if "{object_name}" not in bpy.data.objects:
144
+ result["message"] = f"Object '{object_name}' not found in the blend file"
145
+ else:
146
+ obj = bpy.data.objects["{object_name}"]
147
+
148
+ # Check if it's a mesh
149
+ if obj.type != 'MESH':
150
+ result["message"] = f"Object '{object_name}' is not a mesh (type: {{obj.type}})"
151
+ else:
152
+ # Select only this object and make it active
153
+ bpy.ops.object.select_all(action='DESELECT')
154
+ obj.select_set(True)
155
+ bpy.context.view_layer.objects.active = obj
156
+
157
+ # Go to edit mode
158
+ bpy.ops.object.mode_set(mode='EDIT')
159
+
160
+ # Get the bmesh
161
+ me = obj.data
162
+ bm = bmesh.from_edit_mesh(me)
163
+
164
+ # Ensure we have custom data layers for seams
165
+ if not bm.edges.layers.seam:
166
+ bm.edges.layers.seam.new()
167
+
168
+ seam_layer = bm.edges.layers.seam.active
169
+
170
+ # Mark seams by angle
171
+ seam_angle_rad = {seam_angle} * (math.pi / 180.0)
172
+ seam_count = 0
173
+
174
+ for edge in bm.edges:
175
+ if edge.is_boundary:
176
+ # Mark boundary edges as seams
177
+ edge[seam_layer] = True
178
+ seam_count += 1
179
+ continue
180
+
181
+ # Get connected faces
182
+ if len(edge.link_faces) == 2:
183
+ f1, f2 = edge.link_faces
184
+ angle = f1.normal.angle(f2.normal)
185
+
186
+ # If angle between faces exceeds threshold, mark as seam
187
+ if angle > seam_angle_rad:
188
+ edge[seam_layer] = True
189
+ seam_count += 1
190
+
191
+ # Update the mesh and unwrap
192
+ bmesh.update_edit_mesh(me)
193
+
194
+ # Select all faces
195
+ bpy.ops.mesh.select_all(action='SELECT')
196
+
197
+ # Unwrap using the seams
198
+ bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=0.02)
199
+
200
+ result["success"] = True
201
+ result["message"] = f"Marked {{seam_count}} seams on {{obj.name}} using angle threshold of {seam_angle}° and unwrapped"
202
+ result["seam_count"] = seam_count
203
+
204
+ # Return to object mode
205
+ bpy.ops.object.mode_set(mode='OBJECT')
206
+
207
+ # Save the file
208
+ bpy.ops.wm.save_mainfile()
209
+
210
+ # Print the result as JSON for parsing
211
+ print(json.dumps(result))
212
+ """
213
+
214
+ # Execute the script
215
+ output = self.executor.execute("mark_seams", script, blend_file)
216
+
217
+ # Try to parse the JSON result
218
+ try:
219
+ # Extract JSON from the output
220
+ lines = output.strip().split('\n')
221
+ for line in lines:
222
+ try:
223
+ result = json.loads(line)
224
+ if isinstance(result, dict) and "success" in result:
225
+ return result
226
+ except json.JSONDecodeError:
227
+ pass
228
+
229
+ # If no valid JSON found, return the raw output
230
+ return {"error": "Could not parse result", "output": output}
231
+ except Exception as e:
232
+ return {"error": f"Error parsing result: {str(e)}", "output": output}
233
+
234
+ def texture_object(self, object_name, blend_file=None, texture_type="checker", color1=None, color2=None):
235
+ """
236
+ Apply a textured material to an object using its UV maps
237
+
238
+ Args:
239
+ object_name (str): Name of the object to texture
240
+ blend_file (str): Path to the blend file
241
+ texture_type (str): Type of texture (checker, gradient, grid)
242
+ color1 (list): First color as [r, g, b] (0.0-1.0 range)
243
+ color2 (list): Second color as [r, g, b] (0.0-1.0 range)
244
+
245
+ Returns:
246
+ dict: Result of the operation
247
+ """
248
+ if blend_file is None or not os.path.exists(blend_file):
249
+ return {"error": f"Blend file not found: {blend_file}"}
250
+
251
+ # Set default colors if none provided
252
+ if color1 is None:
253
+ color1 = [0.8, 0.1, 0.1] # Red
254
+ if color2 is None:
255
+ color2 = [0.1, 0.1, 0.8] # Blue
256
+
257
+ script = f"""
258
+ import bpy
259
+ import json
260
+
261
+ result = {{"success": False, "message": ""}}
262
+
263
+ # Check if the object exists
264
+ if "{object_name}" not in bpy.data.objects:
265
+ result["message"] = f"Object '{object_name}' not found in the blend file"
266
+ else:
267
+ obj = bpy.data.objects["{object_name}"]
268
+
269
+ # Check if it's a mesh
270
+ if obj.type != 'MESH':
271
+ result["message"] = f"Object '{object_name}' is not a mesh (type: {{obj.type}})"
272
+ else:
273
+ # Check if the object has UVs
274
+ if not obj.data.uv_layers:
275
+ result["message"] = f"Object '{object_name}' has no UV layers. Unwrap it first."
276
+ else:
277
+ # Create a new material
278
+ mat_name = f"{{obj.name}}_{{'{texture_type}'}}_Material"
279
+ mat = bpy.data.materials.new(name=mat_name)
280
+ mat.use_nodes = True
281
+
282
+ # Clear default nodes
283
+ node_tree = mat.node_tree
284
+ for node in node_tree.nodes:
285
+ node_tree.nodes.remove(node)
286
+
287
+ # Create texture coordinate node
288
+ tex_coord = node_tree.nodes.new(type='ShaderNodeTexCoord')
289
+ tex_coord.location = (-600, 0)
290
+
291
+ # Create BSDF node
292
+ bsdf = node_tree.nodes.new(type='ShaderNodeBsdfPrincipled')
293
+ bsdf.location = (0, 0)
294
+
295
+ # Create output node
296
+ output = node_tree.nodes.new(type='ShaderNodeOutputMaterial')
297
+ output.location = (200, 0)
298
+
299
+ # Different texture setup based on type
300
+ texture_type = "{texture_type}".lower()
301
+
302
+ if texture_type == "checker":
303
+ # Create checker texture node
304
+ checker = node_tree.nodes.new(type='ShaderNodeTexChecker')
305
+ checker.location = (-400, 0)
306
+ checker.inputs['Scale'].default_value = 4.0
307
+ checker.inputs['Color1'].default_value = {color1 + [1.0]}
308
+ checker.inputs['Color2'].default_value = {color2 + [1.0]}
309
+
310
+ # Link nodes
311
+ node_tree.links.new(tex_coord.outputs['UV'], checker.inputs['Vector'])
312
+ node_tree.links.new(checker.outputs['Color'], bsdf.inputs['Base Color'])
313
+
314
+ elif texture_type == "gradient":
315
+ # Create gradient texture node
316
+ gradient = node_tree.nodes.new(type='ShaderNodeTexGradient')
317
+ gradient.location = (-400, 0)
318
+
319
+ # Create color ramp for the gradient
320
+ color_ramp = node_tree.nodes.new(type='ShaderNodeValToRGB')
321
+ color_ramp.location = (-200, 0)
322
+ # Customize the color ramp
323
+ color_ramp
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: iflow-mcp_cwahlfeldt_blender-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for executing Blender scripts
5
+ Author: Blender MCP Team
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: mcp>=0.1.0
8
+ Provides-Extra: dev
9
+ Requires-Dist: black>=23.0.0; extra == 'dev'
10
+ Requires-Dist: isort>=5.12.0; extra == 'dev'
11
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
12
+ Description-Content-Type: text/markdown
13
+
14
+ # Blender MCP Server
15
+
16
+ A Model Context Protocol (MCP) server for managing and executing Blender scripts.
17
+
18
+ ## Features
19
+
20
+ - Add, edit, execute, and remove Blender Python scripts
21
+ - Execute scripts in a headless Blender environment
22
+ - View execution results and errors
23
+ - Track script metadata (creation date, last modified, execution count)
24
+
25
+ ## Requirements
26
+
27
+ - Python 3.7+
28
+ - Blender installed and accessible
29
+ - MCP library (`pip install mcp`)
30
+
31
+ ## Usage
32
+
33
+ 1. Start the server:
34
+ ```
35
+ python server.py
36
+ ```
37
+
38
+ 2. Connect to the server using an MCP client (like Claude Desktop)
39
+
40
+ 3. Use the provided tools to manage scripts:
41
+ - `add_script(name, content)` - Add a new script
42
+ - `edit_script(name, content)` - Edit an existing script
43
+ - `execute_script(name, blend_file=None)` - Execute a script in Blender, optionally specifying a .blend file
44
+ - `remove_script(name)` - Remove a script
45
+
46
+ 4. Access resources to get information:
47
+ - `scripts://list` - Get list of available scripts
48
+ - `script://{name}` - Get content of a specific script
49
+ - `result://{name}` - Get execution result of a script
50
+
51
+ ## Examples
52
+
53
+ ### Basic Example
54
+
55
+ ```python
56
+ # Add a simple script
57
+ add_script("hello_cube", '''
58
+ import bpy
59
+
60
+ # Clear existing objects
61
+ bpy.ops.object.select_all(action='SELECT')
62
+ bpy.ops.object.delete()
63
+
64
+ # Create a cube
65
+ bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 0))
66
+ print("Cube created!")
67
+ ''')
68
+
69
+ # Execute the script
70
+ execute_script("hello_cube")
71
+
72
+ # Get the result
73
+ # Access using: result://hello_cube
74
+ ```
75
+
76
+ ### Working with Blend Files
77
+
78
+ ```python
79
+ # Add a script that works with a blend file
80
+ add_script("analyze_scene", '''
81
+ import bpy
82
+
83
+ # Print information about the current scene
84
+ print(f"Current Blender version: {bpy.app.version_string}")
85
+ print(f"Current file: {bpy.data.filepath}")
86
+
87
+ # List all objects in the scene
88
+ print("\\nObjects in the scene:")
89
+ for obj in bpy.data.objects:
90
+ print(f" - {obj.name} ({obj.type})")
91
+ ''')
92
+
93
+ # Execute with a specific blend file
94
+ execute_script("analyze_scene", blend_file="/path/to/your/project.blend")
95
+
96
+ # Get the result
97
+ # Access using: result://analyze_scene
98
+ ```
99
+
100
+ ## How It Works
101
+
102
+ 1. When a script is added, it's stored in the `script_files/scripts` directory
103
+ 2. When executed, the script is run in a headless Blender instance
104
+ - If a blend file is specified, Blender will open that file before running the script
105
+ - Otherwise, a default empty Blender scene is used
106
+ 3. Output and errors are captured and stored in the `script_files/results` directory
107
+ 4. Metadata about scripts is tracked in `script_files/metadata.json`
108
+
109
+ ## Installation
110
+
111
+ 1. Clone this repository
112
+ 2. Install the MCP library: `pip install mcp`
113
+ 3. Ensure Blender is installed and accessible from your PATH
114
+
115
+ ## License
116
+
117
+ MIT
@@ -0,0 +1,13 @@
1
+ blender/__init__.py,sha256=-nr6dwd6f_6xfJEy_nRV7D5unQMFxXvEEvYdk_gHjyo,32
2
+ blender/executor.py,sha256=UbrmYIXjNloWEkUMtsgOoeVqvrtnLtztcbWtvUxD9tU,5464
3
+ blender/uv_tools.py,sha256=anfOL3hW1fZdPTi1LDA2nMC123hCx95rvQfKer_nBrY,11658
4
+ scripts/__init__.py,sha256=x78bZpSWEjokJ24WqB-bvGAx3PnvixRCZ4xzWgmORBo,32
5
+ scripts/manager.py,sha256=06f5O8rCex-jDBMdxuEHEaq5SHFwPojOYnrIPBItVT0,6122
6
+ utils/__init__.py,sha256=XkLKwxjypuJhs2A-RPst7CZ1G_tWfvdHwFFFtZTjAQI,30
7
+ utils/helpers.py,sha256=EZkvfxRBFdr5Jm8Niwzikqv5QmR3WkIE2_MFvusRNqk,5452
8
+ utils/uv_integration.py,sha256=FQuhOTPPRxeUPZWYwGwLEQbkj10gKx-sg0ushKfRPt4,8630
9
+ utils/uv_manager.py,sha256=OCj8r3g6SPj-jHh-zti6H6EKubRkF8OLiCBAlzuARuY,1964
10
+ iflow_mcp_cwahlfeldt_blender_mcp-0.1.0.dist-info/METADATA,sha256=VHchiyQQOtDbh2d2O_H1e6BIrXQO_-kpG_MC23VPJnk,3124
11
+ iflow_mcp_cwahlfeldt_blender_mcp-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ iflow_mcp_cwahlfeldt_blender_mcp-0.1.0.dist-info/entry_points.txt,sha256=QnyWp9XwL2dm3qqSZH1WdCgLZQ0nkmTBgsRJ1wRM3Xc,43
13
+ iflow_mcp_cwahlfeldt_blender_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ blender-mcp = server:mcp
scripts/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # scripts module initialization
scripts/manager.py ADDED
@@ -0,0 +1,200 @@
1
+ import os
2
+ import json
3
+ from pathlib import Path
4
+ import datetime
5
+ import re
6
+
7
+ class ScriptManager:
8
+ """Class to manage Blender scripts"""
9
+
10
+ def __init__(self, scripts_dir=None):
11
+ """
12
+ Initialize the ScriptManager
13
+
14
+ Args:
15
+ scripts_dir (str, optional): Directory to store scripts. If None, will use ./scripts
16
+ """
17
+ self.scripts_dir = Path(scripts_dir or Path.cwd() / "script_files")
18
+ self.scripts_dir.mkdir(exist_ok=True, parents=True)
19
+
20
+ # Create subdirectories
21
+ self.scripts_path = self.scripts_dir / "scripts"
22
+ self.results_path = self.scripts_dir / "results"
23
+ self.metadata_path = self.scripts_dir / "metadata.json"
24
+
25
+ self.scripts_path.mkdir(exist_ok=True)
26
+ self.results_path.mkdir(exist_ok=True)
27
+
28
+ # Initialize metadata
29
+ self._init_metadata()
30
+
31
+ def _init_metadata(self):
32
+ """Initialize or load metadata file"""
33
+ if not self.metadata_path.exists():
34
+ self.metadata = {}
35
+ self._save_metadata()
36
+ else:
37
+ try:
38
+ with open(self.metadata_path, "r") as f:
39
+ self.metadata = json.load(f)
40
+ except json.JSONDecodeError:
41
+ # If the file is corrupted, initialize new metadata
42
+ self.metadata = {}
43
+ self._save_metadata()
44
+
45
+ def _save_metadata(self):
46
+ """Save metadata to file"""
47
+ with open(self.metadata_path, "w") as f:
48
+ json.dump(self.metadata, f, indent=2)
49
+
50
+ def _validate_script_name(self, name):
51
+ """Validate script name"""
52
+ if not re.match(r'^[a-zA-Z0-9_-]+$', name):
53
+ raise ValueError("Script name can only contain letters, numbers, underscores, and hyphens")
54
+ return name
55
+
56
+ def list_scripts(self):
57
+ """List all available scripts with metadata"""
58
+ scripts = []
59
+ for script_name, metadata in self.metadata.items():
60
+ script_info = metadata.copy()
61
+ script_info["name"] = script_name
62
+ scripts.append(script_info)
63
+ return scripts
64
+
65
+ def add_script(self, name, content):
66
+ """
67
+ Add a new script
68
+
69
+ Args:
70
+ name (str): Script name
71
+ content (str): Script content
72
+ """
73
+ name = self._validate_script_name(name)
74
+ script_path = self.scripts_path / f"{name}.py"
75
+
76
+ if script_path.exists():
77
+ raise ValueError(f"Script '{name}' already exists")
78
+
79
+ # Save script content
80
+ with open(script_path, "w") as f:
81
+ f.write(content)
82
+
83
+ # Update metadata
84
+ now = datetime.datetime.now().isoformat()
85
+ self.metadata[name] = {
86
+ "created": now,
87
+ "last_modified": now,
88
+ "last_executed": None,
89
+ "execution_count": 0
90
+ }
91
+ self._save_metadata()
92
+
93
+ def edit_script(self, name, content):
94
+ """
95
+ Edit an existing script
96
+
97
+ Args:
98
+ name (str): Script name
99
+ content (str): New script content
100
+ """
101
+ name = self._validate_script_name(name)
102
+ script_path = self.scripts_path / f"{name}.py"
103
+
104
+ if not script_path.exists():
105
+ raise ValueError(f"Script '{name}' not found")
106
+
107
+ # Save script content
108
+ with open(script_path, "w") as f:
109
+ f.write(content)
110
+
111
+ # Update metadata
112
+ now = datetime.datetime.now().isoformat()
113
+ self.metadata[name]["last_modified"] = now
114
+ self._save_metadata()
115
+
116
+ def get_script(self, name):
117
+ """
118
+ Get script content
119
+
120
+ Args:
121
+ name (str): Script name
122
+
123
+ Returns:
124
+ str: Script content
125
+ """
126
+ name = self._validate_script_name(name)
127
+ script_path = self.scripts_path / f"{name}.py"
128
+
129
+ if not script_path.exists():
130
+ raise ValueError(f"Script '{name}' not found")
131
+
132
+ with open(script_path, "r") as f:
133
+ return f.read()
134
+
135
+ def remove_script(self, name):
136
+ """
137
+ Remove a script
138
+
139
+ Args:
140
+ name (str): Script name
141
+ """
142
+ name = self._validate_script_name(name)
143
+ script_path = self.scripts_path / f"{name}.py"
144
+ result_path = self.results_path / f"{name}.txt"
145
+
146
+ if not script_path.exists():
147
+ raise ValueError(f"Script '{name}' not found")
148
+
149
+ # Remove script file
150
+ script_path.unlink()
151
+
152
+ # Remove result file if it exists
153
+ if result_path.exists():
154
+ result_path.unlink()
155
+
156
+ # Update metadata
157
+ if name in self.metadata:
158
+ del self.metadata[name]
159
+ self._save_metadata()
160
+
161
+ def save_result(self, name, result):
162
+ """
163
+ Save script execution result
164
+
165
+ Args:
166
+ name (str): Script name
167
+ result (str): Execution result
168
+ """
169
+ name = self._validate_script_name(name)
170
+ result_path = self.results_path / f"{name}.txt"
171
+
172
+ # Save result
173
+ with open(result_path, "w") as f:
174
+ f.write(result)
175
+
176
+ # Update metadata
177
+ if name in self.metadata:
178
+ now = datetime.datetime.now().isoformat()
179
+ self.metadata[name]["last_executed"] = now
180
+ self.metadata[name]["execution_count"] = self.metadata[name].get("execution_count", 0) + 1
181
+ self._save_metadata()
182
+
183
+ def get_result(self, name):
184
+ """
185
+ Get script execution result
186
+
187
+ Args:
188
+ name (str): Script name
189
+
190
+ Returns:
191
+ str: Execution result
192
+ """
193
+ name = self._validate_script_name(name)
194
+ result_path = self.results_path / f"{name}.txt"
195
+
196
+ if not result_path.exists():
197
+ return f"No execution results found for script '{name}'"
198
+
199
+ with open(result_path, "r") as f:
200
+ return f.read()
utils/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # utils module initialization
utils/helpers.py ADDED
@@ -0,0 +1,206 @@
1
+ import os
2
+ import sys
3
+ import subprocess
4
+ from pathlib import Path
5
+ import logging
6
+
7
+ def setup_logger(name, level=logging.INFO):
8
+ """Set up and return a logger"""
9
+ logger = logging.getLogger(name)
10
+ logger.setLevel(level)
11
+
12
+ # Create console handler
13
+ ch = logging.StreamHandler()
14
+ ch.setLevel(level)
15
+
16
+ # Create formatter
17
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
18
+ ch.setFormatter(formatter)
19
+
20
+ # Add handler to logger
21
+ logger.addHandler(ch)
22
+
23
+ return logger
24
+
25
+ def verify_blender_installation():
26
+ """
27
+ Verify Blender is installed and return its version
28
+
29
+ Returns:
30
+ tuple: (bool, str) - (is_installed, version_or_error_message)
31
+ """
32
+ common_paths = [
33
+ "blender", # If it's in PATH
34
+ "/usr/bin/blender",
35
+ "/usr/local/bin/blender",
36
+ "/Applications/Blender.app/Contents/MacOS/Blender",
37
+ "C:\\Program Files\\Blender Foundation\\Blender\\blender.exe",
38
+ ]
39
+
40
+ for path in common_paths:
41
+ try:
42
+ result = subprocess.run(
43
+ [path, "--version"],
44
+ stdout=subprocess.PIPE,
45
+ stderr=subprocess.PIPE,
46
+ timeout=5,
47
+ text=True
48
+ )
49
+
50
+ # Extract version from output
51
+ version_output = result.stdout
52
+ return True, version_output.strip()
53
+ except (subprocess.SubprocessError, FileNotFoundError):
54
+ continue
55
+
56
+ return False, "Blender not found. Please install Blender or set the correct path."
57
+
58
+ def generate_example_script():
59
+ """Generate an example Blender script"""
60
+ return """
61
+ import bpy
62
+
63
+ # Clear existing objects
64
+ bpy.ops.object.select_all(action='SELECT')
65
+ bpy.ops.object.delete()
66
+
67
+ # Create a cube
68
+ bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 0))
69
+
70
+ # Create a material
71
+ mat = bpy.data.materials.new(name="Red Material")
72
+ mat.diffuse_color = (1, 0, 0, 1) # Red
73
+
74
+ # Assign material to the cube
75
+ cube = bpy.context.active_object
76
+ if cube.data.materials:
77
+ cube.data.materials[0] = mat
78
+ else:
79
+ cube.data.materials.append(mat)
80
+
81
+ # Print confirmation
82
+ print("Created a red cube at the origin!")
83
+ """
84
+
85
+ def generate_blend_file_example_script():
86
+ """Generate an example script that works with a blend file"""
87
+ return """
88
+ import bpy
89
+ import os
90
+
91
+ # Print information about the current scene
92
+ print(f"Current Blender version: {bpy.app.version_string}")
93
+ print(f"Current file: {bpy.data.filepath}")
94
+
95
+ # List all objects in the scene
96
+ print("\\nObjects in the scene:")
97
+ for obj in bpy.data.objects:
98
+ print(f" - {obj.name} ({obj.type})")
99
+
100
+ # List all materials
101
+ print("\\nMaterials in the scene:")
102
+ for mat in bpy.data.materials:
103
+ print(f" - {mat.name}")
104
+
105
+ # List all scenes
106
+ print("\\nScenes in the file:")
107
+ for scene in bpy.data.scenes:
108
+ print(f" - {scene.name}")
109
+
110
+ # Make a small change to demonstrate saving
111
+ if bpy.data.objects:
112
+ # Move the first object up by 1 unit
113
+ obj = bpy.data.objects[0]
114
+ original_loc = obj.location.copy()
115
+ obj.location.z += 1
116
+ print(f"\\nMoved {obj.name} up by 1 unit (from {original_loc.z} to {obj.location.z})")
117
+
118
+ # Note about saving
119
+ print("\\nNote: To save changes to the blend file, uncomment the save line below.")
120
+ # bpy.ops.wm.save_mainfile()
121
+ """
122
+
123
+ def generate_readme():
124
+ """Generate README content for the project"""
125
+ return """# Blender MCP Server
126
+
127
+ A Model Context Protocol (MCP) server for managing and executing Blender scripts.
128
+
129
+ ## Features
130
+
131
+ - Add, edit, execute, and remove Blender Python scripts
132
+ - Execute scripts in a headless Blender environment
133
+ - View execution results and errors
134
+ - Track script metadata (creation date, last modified, execution count)
135
+
136
+ ## Requirements
137
+
138
+ - Python 3.7+
139
+ - Blender installed and accessible
140
+ - MCP library (`pip install mcp`)
141
+
142
+ ## Usage
143
+
144
+ 1. Start the server:
145
+ ```
146
+ python server.py
147
+ ```
148
+
149
+ 2. Connect to the server using an MCP client (like Claude Desktop)
150
+
151
+ 3. Use the provided tools to manage scripts:
152
+ - `add_script(name, content)` - Add a new script
153
+ - `edit_script(name, content)` - Edit an existing script
154
+ - `execute_script(name)` - Execute a script in Blender
155
+ - `remove_script(name)` - Remove a script
156
+
157
+ 4. Access resources to get information:
158
+ - `scripts://list` - Get list of available scripts
159
+ - `script://{name}` - Get content of a specific script
160
+ - `result://{name}` - Get execution result of a script
161
+
162
+ ## Example
163
+
164
+ ```python
165
+ # Add a simple script
166
+ add_script("hello_cube", '''
167
+ import bpy
168
+
169
+ # Clear existing objects
170
+ bpy.ops.object.select_all(action='SELECT')
171
+ bpy.ops.object.delete()
172
+
173
+ # Create a cube
174
+ bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 0))
175
+ print("Cube created!")
176
+ ''')
177
+
178
+ # Execute the script
179
+ execute_script("hello_cube")
180
+
181
+ # Get the result
182
+ result = get_resource("result://hello_cube")
183
+ print(result)
184
+ ```
185
+
186
+ ## License
187
+
188
+ MIT
189
+ """
190
+
191
+ def create_empty_init_files(root_dir):
192
+ """
193
+ Create empty __init__.py files in all subdirectories
194
+
195
+ Args:
196
+ root_dir (str): Root directory path
197
+ """
198
+ for dirpath, dirnames, filenames in os.walk(root_dir):
199
+ # Skip hidden directories
200
+ dirnames[:] = [d for d in dirnames if not d.startswith('.')]
201
+
202
+ # Create __init__.py if it doesn't exist
203
+ init_file = os.path.join(dirpath, "__init__.py")
204
+ if not os.path.exists(init_file):
205
+ with open(init_file, 'w') as f:
206
+ pass # Create empty file
@@ -0,0 +1,264 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+ import json
5
+ from pathlib import Path
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class UVIntegration:
11
+ """Integration with UV Python for package management"""
12
+
13
+ def __init__(self):
14
+ """Initialize UV integration utils"""
15
+ self.uv_path = self._find_uv_executable()
16
+ self.has_uv = self.uv_path is not None
17
+
18
+ def _find_uv_executable(self):
19
+ """Find the UV executable in the system"""
20
+ # Try common locations
21
+ common_names = ["uv", "uv.exe"]
22
+
23
+ for name in common_names:
24
+ try:
25
+ # Just check if we can run it with --version
26
+ subprocess.run([name, "--version"],
27
+ stdout=subprocess.PIPE,
28
+ stderr=subprocess.PIPE,
29
+ timeout=5)
30
+ return name
31
+ except (subprocess.SubprocessError, FileNotFoundError):
32
+ continue
33
+
34
+ return None
35
+
36
+ def is_available(self):
37
+ """Check if UV is available on the system"""
38
+ return self.has_uv
39
+
40
+ def install_dependencies(self, dependencies, venv_path=None):
41
+ """
42
+ Install dependencies using UV
43
+
44
+ Args:
45
+ dependencies (list): List of dependencies to install
46
+ venv_path (str, optional): Path to virtual environment
47
+
48
+ Returns:
49
+ dict: Result of the operation
50
+ """
51
+ if not self.has_uv:
52
+ return {
53
+ "success": False,
54
+ "message": "UV Python is not available. Please install it first."
55
+ }
56
+
57
+ cmd = [self.uv_path, "pip", "install"]
58
+ cmd.extend(dependencies)
59
+
60
+ env = os.environ.copy()
61
+ if venv_path:
62
+ # Set virtual environment path
63
+ if sys.platform == "win32":
64
+ env["VIRTUAL_ENV"] = venv_path
65
+ env["PATH"] = os.path.join(venv_path, "Scripts") + os.pathsep + env["PATH"]
66
+ else:
67
+ env["VIRTUAL_ENV"] = venv_path
68
+ env["PATH"] = os.path.join(venv_path, "bin") + os.pathsep + env["PATH"]
69
+
70
+ try:
71
+ result = subprocess.run(
72
+ cmd,
73
+ stdout=subprocess.PIPE,
74
+ stderr=subprocess.PIPE,
75
+ env=env,
76
+ text=True
77
+ )
78
+
79
+ if result.returncode == 0:
80
+ return {
81
+ "success": True,
82
+ "message": f"Successfully installed dependencies: {', '.join(dependencies)}",
83
+ "output": result.stdout
84
+ }
85
+ else:
86
+ return {
87
+ "success": False,
88
+ "message": "Failed to install dependencies",
89
+ "error": result.stderr,
90
+ "output": result.stdout
91
+ }
92
+ except Exception as e:
93
+ return {
94
+ "success": False,
95
+ "message": f"Error during installation: {str(e)}"
96
+ }
97
+
98
+ def create_venv(self, path):
99
+ """
100
+ Create a virtual environment using UV
101
+
102
+ Args:
103
+ path (str): Path for the virtual environment
104
+
105
+ Returns:
106
+ dict: Result of the operation
107
+ """
108
+ if not self.has_uv:
109
+ return {
110
+ "success": False,
111
+ "message": "UV Python is not available. Please install it first."
112
+ }
113
+
114
+ path = os.path.abspath(path)
115
+ os.makedirs(os.path.dirname(path), exist_ok=True)
116
+
117
+ cmd = [self.uv_path, "venv", path]
118
+
119
+ try:
120
+ result = subprocess.run(
121
+ cmd,
122
+ stdout=subprocess.PIPE,
123
+ stderr=subprocess.PIPE,
124
+ text=True
125
+ )
126
+
127
+ if result.returncode == 0:
128
+ return {
129
+ "success": True,
130
+ "message": f"Successfully created virtual environment at {path}",
131
+ "path": path,
132
+ "output": result.stdout
133
+ }
134
+ else:
135
+ return {
136
+ "success": False,
137
+ "message": "Failed to create virtual environment",
138
+ "error": result.stderr,
139
+ "output": result.stdout
140
+ }
141
+ except Exception as e:
142
+ return {
143
+ "success": False,
144
+ "message": f"Error creating virtual environment: {str(e)}"
145
+ }
146
+
147
+ def get_installed_packages(self, venv_path=None):
148
+ """
149
+ Get list of installed packages
150
+
151
+ Args:
152
+ venv_path (str, optional): Path to virtual environment
153
+
154
+ Returns:
155
+ dict: Result of the operation with list of packages
156
+ """
157
+ if not self.has_uv:
158
+ return {
159
+ "success": False,
160
+ "message": "UV Python is not available. Please install it first."
161
+ }
162
+
163
+ cmd = [self.uv_path, "pip", "list", "--format=json"]
164
+
165
+ env = os.environ.copy()
166
+ if venv_path:
167
+ # Set virtual environment path
168
+ if sys.platform == "win32":
169
+ env["VIRTUAL_ENV"] = venv_path
170
+ env["PATH"] = os.path.join(venv_path, "Scripts") + os.pathsep + env["PATH"]
171
+ else:
172
+ env["VIRTUAL_ENV"] = venv_path
173
+ env["PATH"] = os.path.join(venv_path, "bin") + os.pathsep + env["PATH"]
174
+
175
+ try:
176
+ result = subprocess.run(
177
+ cmd,
178
+ stdout=subprocess.PIPE,
179
+ stderr=subprocess.PIPE,
180
+ env=env,
181
+ text=True
182
+ )
183
+
184
+ if result.returncode == 0:
185
+ try:
186
+ packages = json.loads(result.stdout)
187
+ return {
188
+ "success": True,
189
+ "packages": packages,
190
+ "count": len(packages)
191
+ }
192
+ except json.JSONDecodeError:
193
+ return {
194
+ "success": False,
195
+ "message": "Failed to parse package list",
196
+ "output": result.stdout
197
+ }
198
+ else:
199
+ return {
200
+ "success": False,
201
+ "message": "Failed to get list of packages",
202
+ "error": result.stderr,
203
+ "output": result.stdout
204
+ }
205
+ except Exception as e:
206
+ return {
207
+ "success": False,
208
+ "message": f"Error getting packages: {str(e)}"
209
+ }
210
+
211
+ def run_script_in_venv(self, script_path, venv_path):
212
+ """
213
+ Run a Python script in a virtual environment
214
+
215
+ Args:
216
+ script_path (str): Path to the script
217
+ venv_path (str): Path to virtual environment
218
+
219
+ Returns:
220
+ dict: Result of the operation
221
+ """
222
+ if not os.path.exists(script_path):
223
+ return {
224
+ "success": False,
225
+ "message": f"Script not found: {script_path}"
226
+ }
227
+
228
+ if not os.path.exists(venv_path):
229
+ return {
230
+ "success": False,
231
+ "message": f"Virtual environment not found: {venv_path}"
232
+ }
233
+
234
+ # Get path to Python in the virtual environment
235
+ if sys.platform == "win32":
236
+ python_path = os.path.join(venv_path, "Scripts", "python.exe")
237
+ else:
238
+ python_path = os.path.join(venv_path, "bin", "python")
239
+
240
+ if not os.path.exists(python_path):
241
+ return {
242
+ "success": False,
243
+ "message": f"Python not found in virtual environment: {python_path}"
244
+ }
245
+
246
+ try:
247
+ result = subprocess.run(
248
+ [python_path, script_path],
249
+ stdout=subprocess.PIPE,
250
+ stderr=subprocess.PIPE,
251
+ text=True
252
+ )
253
+
254
+ return {
255
+ "success": result.returncode == 0,
256
+ "exit_code": result.returncode,
257
+ "stdout": result.stdout,
258
+ "stderr": result.stderr
259
+ }
260
+ except Exception as e:
261
+ return {
262
+ "success": False,
263
+ "message": f"Error running script: {str(e)}"
264
+ }
utils/uv_manager.py ADDED
@@ -0,0 +1,64 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+ import json
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class UVManager:
11
+ """Manager for UV Python package installer and environment manager"""
12
+
13
+ def __init__(self, env_dir=None):
14
+ """
15
+ Initialize UV manager
16
+
17
+ Args:
18
+ env_dir (str, optional): Directory to store environments
19
+ """
20
+ self.uv_bin = self._find_uv_executable()
21
+ self.env_dir = Path(env_dir or Path.cwd() / "python_envs")
22
+ self.env_dir.mkdir(exist_ok=True, parents=True)
23
+
24
+ def _find_uv_executable(self):
25
+ """Find the UV executable in the system"""
26
+ # Check common names and locations
27
+ common_names = ["uv", "uv.exe"]
28
+
29
+ for name in common_names:
30
+ try:
31
+ # Check if we can run it with --version
32
+ result = subprocess.run(
33
+ [name, "--version"],
34
+ stdout=subprocess.PIPE,
35
+ stderr=subprocess.PIPE,
36
+ timeout=5,
37
+ text=True
38
+ )
39
+ if result.returncode == 0:
40
+ return name
41
+ except (subprocess.SubprocessError, FileNotFoundError):
42
+ continue
43
+
44
+ # If not found, try to install it using pip
45
+ try:
46
+ subprocess.run(
47
+ [sys.executable, "-m", "pip", "install", "uv"],
48
+ stdout=subprocess.PIPE,
49
+ stderr=subprocess.PIPE,
50
+ check=True
51
+ )
52
+ return "uv" # Try the command directly after installation
53
+ except Exception as e:
54
+ logger.warning(f"Failed to install UV: {e}")
55
+
56
+ return None
57
+
58
+ def is_available(self):
59
+ """Check if UV is available"""
60
+ return self.uv_bin is not None
61
+
62
+ def create_environment(self, name, python_version=None):
63
+ """
64
+