# Code created by Siddharth Ahuja: www.github.com/ahujasid © 2025
import bpy
import json
import threading
import socket
import traceback
import sys
import os
from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty
# Add the blender-mcp repo directory to sys.path so 'src' module can be found
# This is required when the addon is loaded by Blender
BLENDER_MCP_REPO = r"D:\repos\blender-mcp"
if BLENDER_MCP_REPO not in sys.path:
sys.path.insert(0, BLENDER_MCP_REPO)
# Import all handler functions
from src.blender_handlers import (
# Scene
get_scene_info as handler_get_scene_info,
get_object_info as handler_get_object_info,
get_viewport_screenshot as handler_get_viewport_screenshot,
execute_code as handler_execute_code,
# Mesh Analysis
mesh_stats as handler_mesh_stats,
detect_topology_issues as handler_detect_topology_issues,
# Topology Analysis
get_vertex_valence as handler_get_vertex_valence,
get_triangle_faces as handler_get_triangle_faces,
get_ngon_faces as handler_get_ngon_faces,
analyze_cylindrical_structure as handler_analyze_cylindrical_structure,
find_support_loops as handler_find_support_loops,
# Viewport Advanced
set_shading_mode as handler_set_shading_mode,
# Remeshing
decimate as handler_decimate,
shrinkwrap_reproject as handler_shrinkwrap_reproject,
remesh_voxel as handler_remesh_voxel,
quadriflow_remesh as handler_quadriflow_remesh,
# Viewport
align_view_to_axis as handler_align_view_to_axis,
frame_selected as handler_frame_selected,
focus_view_on_point as handler_focus_view_on_point,
set_view_axis as handler_set_view_axis,
orbit_view as handler_orbit_view,
# Shading
mark_seams_by_angle as handler_mark_seams_by_angle,
mark_sharp_by_angle as handler_mark_sharp_by_angle,
# Modifiers
add_mirror_modifier as handler_add_mirror_modifier,
add_shrinkwrap_modifier as handler_add_shrinkwrap_modifier,
add_weighted_normal_modifier as handler_add_weighted_normal_modifier,
add_data_transfer_modifier as handler_add_data_transfer_modifier,
add_decimate_modifier as handler_add_decimate_modifier,
add_laplacian_smooth_modifier as handler_add_laplacian_smooth_modifier,
# Mesh Operations
symmetrize_mesh as handler_symmetrize_mesh,
smooth_vertices as handler_smooth_vertices,
recalculate_normals as handler_recalculate_normals,
merge_by_distance as handler_merge_by_distance,
delete_loose as handler_delete_loose,
dissolve_edge_loops as handler_dissolve_edge_loops,
dissolve_selected as handler_dissolve_selected,
# Selection
select_non_manifold as handler_select_non_manifold,
select_by_valence as handler_select_by_valence,
select_triangles as handler_select_triangles,
select_ngons as handler_select_ngons,
select_edge_loop as handler_select_edge_loop,
select_by_criteria as handler_select_by_criteria,
get_selected_elements as handler_get_selected_elements,
# Topology Cleanup
tris_to_quads as handler_tris_to_quads,
align_vertex_to_loop as handler_align_vertex_to_loop,
dissolve_edge_loop_by_selection as handler_dissolve_edge_loop_by_selection,
reduce_vertex_valence as handler_reduce_vertex_valence,
fix_ngons as handler_fix_ngons,
# Shading Validation
set_matcap_shading as handler_set_matcap_shading,
toggle_wireframe_overlay as handler_toggle_wireframe_overlay,
toggle_smooth_shading as handler_toggle_smooth_shading,
# Snapping
set_snapping as handler_set_snapping,
# Camera
align_active_camera_to_view as handler_align_active_camera_to_view,
set_camera_projection as handler_set_camera_projection,
set_active_camera as handler_set_active_camera,
# Baking
bake_normals as handler_bake_normals,
bake_ambient_occlusion as handler_bake_ambient_occlusion,
)
bl_info = {
"name": "Blender MCP",
"author": "BlenderMCP",
"version": (1, 2),
"blender": (3, 0, 0),
"location": "View3D > Sidebar > BlenderMCP",
"description": "Connect Blender to Claude via MCP",
"category": "Interface",
}
class BlenderMCPServer:
def __init__(self, host='localhost', port=9876):
self.host = host
self.port = port
self.running = False
self.socket = None
self.server_thread = None
def start(self):
if self.running:
print("Server is already running")
return
self.running = True
try:
# Create socket
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind((self.host, self.port))
self.socket.listen(1)
# Start server thread
self.server_thread = threading.Thread(target=self._server_loop)
self.server_thread.daemon = True
self.server_thread.start()
print(f"BlenderMCP server started on {self.host}:{self.port}")
except Exception as e:
print(f"Failed to start server: {str(e)}")
self.stop()
def stop(self):
self.running = False
# Close socket
if self.socket:
try:
self.socket.close()
except:
pass
self.socket = None
# Wait for thread to finish
if self.server_thread:
try:
if self.server_thread.is_alive():
self.server_thread.join(timeout=1.0)
except:
pass
self.server_thread = None
print("BlenderMCP server stopped")
def _server_loop(self):
"""Main server loop in a separate thread"""
print("Server thread started")
self.socket.settimeout(1.0) # Timeout to allow for stopping
while self.running:
try:
# Accept new connection
try:
client, address = self.socket.accept()
print(f"Client connected from {address}")
client.settimeout(None) # No timeout for client socket
# Handle client in a new thread
client_thread = threading.Thread(
target=self._handle_client,
args=(client,)
)
client_thread.daemon = True
client_thread.start()
except socket.timeout:
continue # Check if still running
except Exception as e:
if self.running:
print(f"Error accepting connection: {str(e)}")
print("Server thread stopped")
def _handle_client(self, client):
"""Handle a client connection"""
print("Client handler started")
try:
buffer = ""
while self.running:
try:
data = client.recv(4096)
if not data:
break
buffer += data.decode('utf-8')
# Try to parse complete JSON messages
try:
command = json.loads(buffer)
buffer = "" # Clear buffer after successful parse
# Execute command in Blender's main thread
def execute_wrapper():
try:
response = self.execute_command(command)
response_json = json.dumps(response)
try:
client.sendall(response_json.encode('utf-8'))
except:
print("Failed to send response - client disconnected")
except Exception as e:
print(f"Error executing command: {str(e)}")
traceback.print_exc()
try:
error_response = {
"status": "error",
"message": str(e)
}
client.sendall(json.dumps(error_response).encode('utf-8'))
except:
pass
return None
# Schedule execution in main thread
bpy.app.timers.register(execute_wrapper, first_interval=0.0)
except json.JSONDecodeError:
# Incomplete data, wait for more
pass
except Exception as e:
print(f"Error receiving data: {str(e)}")
break
except Exception as e:
print(f"Error in client handler: {str(e)}")
finally:
try:
client.close()
except:
pass
print("Client handler stopped")
def execute_command(self, command):
"""Execute a command in the main Blender thread"""
try:
return self._execute_command_internal(command)
except Exception as e:
print(f"Error executing command: {str(e)}")
traceback.print_exc()
return {"status": "error", "message": str(e)}
def _execute_command_internal(self, command):
"""Internal command execution with proper context"""
cmd_type = command.get("type")
params = command.get("params", {})
# Base handlers that are always available
handlers = {
"get_scene_info": self.get_scene_info,
"get_object_info": self.get_object_info,
"get_viewport_screenshot": self.get_viewport_screenshot,
"execute_code": self.execute_code,
"mesh_stats": self.mesh_stats,
"detect_topology_issues": self.detect_topology_issues,
# Topology Analysis
"get_vertex_valence": self.get_vertex_valence,
"get_triangle_faces": self.get_triangle_faces,
"get_ngon_faces": self.get_ngon_faces,
"analyze_cylindrical_structure": self.analyze_cylindrical_structure,
"find_support_loops": self.find_support_loops,
# Viewport Advanced
"set_shading_mode": self.set_shading_mode,
"decimate": self.decimate,
"shrinkwrap_reproject": self.shrinkwrap_reproject,
"mark_seams_by_angle": self.mark_seams_by_angle,
"mark_sharp_by_angle": self.mark_sharp_by_angle,
"align_view_to_axis": self.align_view_to_axis,
"frame_selected": self.frame_selected,
"focus_view_on_point": self.focus_view_on_point,
"set_view_axis": self.set_view_axis,
"orbit_view": self.orbit_view,
# Retopo tools - Modifiers
"add_mirror_modifier": self.add_mirror_modifier,
"add_shrinkwrap_modifier": self.add_shrinkwrap_modifier,
"add_weighted_normal_modifier": self.add_weighted_normal_modifier,
"add_data_transfer_modifier": self.add_data_transfer_modifier,
"add_decimate_modifier": self.add_decimate_modifier,
"add_laplacian_smooth_modifier": self.add_laplacian_smooth_modifier,
# Retopo tools - Mesh Operations
"symmetrize_mesh": self.symmetrize_mesh,
"smooth_vertices": self.smooth_vertices,
"recalculate_normals": self.recalculate_normals,
"merge_by_distance": self.merge_by_distance,
"delete_loose": self.delete_loose,
"dissolve_edge_loops": self.dissolve_edge_loops,
"dissolve_selected": self.dissolve_selected,
# Retopo tools - Selection
"select_non_manifold": self.select_non_manifold,
"select_by_valence": self.select_by_valence,
"select_triangles": self.select_triangles,
"select_ngons": self.select_ngons,
"select_edge_loop": self.select_edge_loop,
"select_by_criteria": self.select_by_criteria,
"get_selected_elements": self.get_selected_elements,
# Topology Cleanup
"tris_to_quads": self.tris_to_quads,
"align_vertex_to_loop": self.align_vertex_to_loop,
"dissolve_edge_loop_by_selection": self.dissolve_edge_loop_by_selection,
"reduce_vertex_valence": self.reduce_vertex_valence,
"fix_ngons": self.fix_ngons,
# Shading Validation
"set_matcap_shading": self.set_matcap_shading,
"toggle_wireframe_overlay": self.toggle_wireframe_overlay,
"toggle_smooth_shading": self.toggle_smooth_shading,
# Retopo tools - Snapping
"set_snapping": self.set_snapping,
# Retopo tools - Remeshing (Operators)
"remesh_voxel": self.remesh_voxel,
"quadriflow_remesh": self.quadriflow_remesh,
# Retopo tools - Camera
"align_active_camera_to_view": self.align_active_camera_to_view,
"set_camera_projection": self.set_camera_projection,
"set_active_camera": self.set_active_camera,
# Retopo tools - Baking
"bake_normals": self.bake_normals,
"bake_ambient_occlusion": self.bake_ambient_occlusion,
}
handler = handlers.get(cmd_type)
if handler:
try:
print(f"Executing handler for {cmd_type}")
result = handler(**params)
print(f"Handler execution complete")
return {"status": "success", "result": result}
except Exception as e:
print(f"Error in handler: {str(e)}")
traceback.print_exc()
return {"status": "error", "message": str(e)}
else:
return {"status": "error", "message": f"Unknown command type: {cmd_type}"}
# Scene handlers
def get_scene_info(self):
"""Get information about the current Blender scene"""
return handler_get_scene_info()
def get_object_info(self, name):
"""Get detailed information about a specific object"""
return handler_get_object_info(name)
def get_viewport_screenshot(self, max_size=800, filepath=None, format="png"):
"""Capture a screenshot of the current viewport"""
return handler_get_viewport_screenshot(max_size, filepath, format)
def execute_code(self, code, confirm=False):
"""Execute Python code in Blender's context"""
return handler_execute_code(code)
# Mesh analysis handlers
def mesh_stats(self, active_only=True):
"""Get mesh statistics"""
return handler_mesh_stats(active_only)
def detect_topology_issues(self, angle_sharp=30, distance_doubles=0.0001):
"""Detect topology issues in mesh"""
return handler_detect_topology_issues(angle_sharp, distance_doubles)
# Topology Analysis handlers
def get_topology_quality(self, object_name=None):
"""Get detailed topology quality metrics"""
return handler_get_topology_quality(object_name)
def analyze_mesh_regions(self, object_name=None, num_regions=8):
"""Analyze mesh by spatial regions"""
return handler_analyze_mesh_regions(object_name, num_regions)
def get_vertex_valence(self, object_name=None, min_valence=5):
"""Get vertices with valence >= threshold"""
return handler_get_vertex_valence(object_name, min_valence)
def get_triangle_faces(self, object_name=None):
"""Get all triangle faces in the mesh"""
return handler_get_triangle_faces(object_name)
def get_ngon_faces(self, object_name=None):
"""Get all n-gon faces (5+ vertices) in the mesh"""
return handler_get_ngon_faces(object_name)
def analyze_cylindrical_structure(self, object_name=None, analyze_spacing=False):
"""Analyze cylindrical mesh regions from selected edge loop"""
return handler_analyze_cylindrical_structure(object_name, analyze_spacing)
def find_support_loops(self, object_name=None, min_angle=30.0, near_sharp=True, suggest=False):
"""Find edge loops suitable for subdivision support"""
return handler_find_support_loops(object_name, min_angle, near_sharp, suggest)
# Viewport Advanced handlers
def set_shading_mode(self, mode="SOLID", show_wireframe=False, show_xray=False, xray_alpha=0.5):
"""Set viewport shading mode"""
return handler_set_shading_mode(mode, show_wireframe, show_xray, xray_alpha)
# Remeshing handlers
def decimate(self, ratio=0.5, mode="COLLAPSE", angle_limit=5.0):
"""Decimate mesh"""
return handler_decimate(ratio, mode, angle_limit)
def shrinkwrap_reproject(self, high, low, method="NEAREST_SURFACEPOINT", offset=0.0):
"""Shrinkwrap reproject from high to low poly"""
return handler_shrinkwrap_reproject(high, low, method, offset)
def remesh_voxel(self, voxel_size, adaptivity, preserve_volume):
"""Remesh using voxel method"""
return handler_remesh_voxel(voxel_size, adaptivity, preserve_volume)
def quadriflow_remesh(self, target_faces, preserve_sharp, use_symmetry):
"""Remesh using QuadriFlow"""
return handler_quadriflow_remesh(target_faces, preserve_sharp, use_symmetry)
# Shading handlers
def mark_seams_by_angle(self, angle=60.0, clear_existing=False):
"""Mark seams by angle"""
return handler_mark_seams_by_angle(angle, clear_existing)
def mark_sharp_by_angle(self, angle=45.0, clear_existing=False):
"""Mark sharp edges by angle"""
return handler_mark_sharp_by_angle(angle, clear_existing)
# Viewport handlers
def align_view_to_axis(self, axis, side):
"""Align view to axis"""
return handler_align_view_to_axis(axis, side)
def frame_selected(self):
"""Frame selected objects in view"""
return handler_frame_selected()
def focus_view_on_point(self, x, y, z, distance=None, view_axis=None):
"""Focus view on a specific 3D point"""
return handler_focus_view_on_point(x, y, z, distance, view_axis)
def set_view_axis(self, axis):
"""Set viewport to a standard axis view"""
return handler_set_view_axis(axis)
def orbit_view(self, yaw_delta=0.0, pitch_delta=0.0, distance_delta=0.0, target=None, point=None):
"""Orbit the viewport by specified angles"""
return handler_orbit_view(yaw_delta, pitch_delta, distance_delta, target, point)
# Modifier handlers
def add_mirror_modifier(self, axis, use_clip, use_bisect, mirror_object):
"""Add mirror modifier"""
return handler_add_mirror_modifier(axis, use_clip, use_bisect, mirror_object)
def add_shrinkwrap_modifier(self, target, method, offset, on_cage, cull_backfaces):
"""Add shrinkwrap modifier"""
return handler_add_shrinkwrap_modifier(target, method, offset, on_cage, cull_backfaces)
def add_weighted_normal_modifier(self, mode, weight, keep_sharp, face_influence):
"""Add weighted normal modifier"""
return handler_add_weighted_normal_modifier(mode, weight, keep_sharp, face_influence)
def add_data_transfer_modifier(self, source, data_types, mapping, mix_factor):
"""Add data transfer modifier"""
return handler_add_data_transfer_modifier(source, data_types, mapping, mix_factor)
def add_decimate_modifier(self, mode, ratio, iterations):
"""Add decimate modifier"""
return handler_add_decimate_modifier(mode, ratio, iterations)
def add_laplacian_smooth_modifier(self, factor, repeat, preserve_volume):
"""Add laplacian smooth modifier"""
return handler_add_laplacian_smooth_modifier(factor, repeat, preserve_volume)
# Mesh operation handlers
def symmetrize_mesh(self, direction, threshold):
"""Symmetrize mesh"""
return handler_symmetrize_mesh(direction, threshold)
def smooth_vertices(self, iterations, factor, axes):
"""Smooth vertices"""
return handler_smooth_vertices(iterations, factor, axes)
def recalculate_normals(self, inside):
"""Recalculate normals"""
return handler_recalculate_normals(inside)
def merge_by_distance(self, distance, unselected):
"""Merge vertices by distance"""
return handler_merge_by_distance(distance, unselected)
def delete_loose(self, delete_faces):
"""Delete loose geometry"""
return handler_delete_loose(delete_faces)
def dissolve_edge_loops(self):
"""Dissolve edge loops"""
return handler_dissolve_edge_loops()
def dissolve_selected(self, object_name=None, mode='VERT', use_verts=True, use_face_split=False):
"""Dissolve selected mesh elements"""
return handler_dissolve_selected(object_name, mode, use_verts, use_face_split)
# Selection handlers
def select_non_manifold(self, wire, boundaries, multiple_faces, non_contiguous):
"""Select non-manifold geometry"""
return handler_select_non_manifold(wire, boundaries, multiple_faces, non_contiguous)
def select_by_valence(self, object_name=None, min_valence=5):
"""Select vertices by valence threshold"""
return handler_select_by_valence(object_name, min_valence)
def select_triangles(self, object_name=None):
"""Select all triangle faces"""
return handler_select_triangles(object_name)
def select_ngons(self, object_name=None):
"""Select all n-gon faces"""
return handler_select_ngons(object_name)
def select_edge_loop(self, object_name=None, edge_index=0):
"""Select edge loop from edge index"""
return handler_select_edge_loop(object_name, edge_index)
def select_by_criteria(self, object_name=None, criteria='valence', min_value=None, max_value=None):
"""Select mesh elements by flexible criteria"""
return handler_select_by_criteria(object_name, criteria, min_value, max_value)
def get_selected_elements(self, object_name=None, mode='VERT'):
"""Get list of selected mesh elements with detailed data"""
return handler_get_selected_elements(object_name, mode)
# Topology Cleanup handlers
def tris_to_quads(self, object_name=None, angle_threshold=40.0):
"""Convert triangle pairs to quads"""
return handler_tris_to_quads(object_name, angle_threshold)
def align_vertex_to_loop(self, object_name=None, vertex_index=0, axis="Y", target_coord=0.0):
"""Align vertex to edge loop position"""
return handler_align_vertex_to_loop(object_name, vertex_index, axis, target_coord)
def dissolve_edge_loop_by_selection(self, object_name=None):
"""Dissolve selected edge loop"""
return handler_dissolve_edge_loop_by_selection(object_name)
def reduce_vertex_valence(self, object_name=None, vertex_index=0, target_valence=4):
"""Reduce vertex valence by collapsing edges"""
return handler_reduce_vertex_valence(object_name, vertex_index, target_valence)
def fix_ngons(self, object_name=None, method='triangulate', select_all=True):
"""Convert n-gon faces to triangles or quads"""
return handler_fix_ngons(object_name, method, select_all)
# Shading Validation handlers
def set_matcap_shading(self, matcap_name=None):
"""Set matcap shading for surface validation"""
return handler_set_matcap_shading(matcap_name)
def toggle_wireframe_overlay(self, enabled=None):
"""Toggle wireframe overlay"""
return handler_toggle_wireframe_overlay(enabled)
def toggle_smooth_shading(self, object_name=None, smooth=True):
"""Set smooth or flat shading"""
return handler_toggle_smooth_shading(object_name, smooth)
# Snapping handlers
def set_snapping(self, enable, mode, align_rotation_to_target, project_individual_elements, backface_culling):
"""Configure snapping settings"""
return handler_set_snapping(enable, mode, align_rotation_to_target, project_individual_elements, backface_culling)
# Camera handlers
def align_active_camera_to_view(self):
"""Align active camera to current view"""
return handler_align_active_camera_to_view()
def set_camera_projection(self, projection_type, ortho_scale):
"""Set camera projection type"""
return handler_set_camera_projection(projection_type, ortho_scale)
def set_active_camera(self, camera_name):
"""Set active camera"""
return handler_set_active_camera(camera_name)
# Baking handlers
def bake_normals(self, high_poly, low_poly, map_size, space, cage_object, max_ray_distance):
"""Bake normal map"""
return handler_bake_normals(high_poly, low_poly, map_size, space, cage_object, max_ray_distance)
def bake_ambient_occlusion(self, target, map_size, samples):
"""Bake ambient occlusion"""
return handler_bake_ambient_occlusion(target, map_size, samples)
# UI Panel
class BLENDERMCP_PT_Panel(bpy.types.Panel):
bl_label = "Blender MCP"
bl_idname = "BLENDERMCP_PT_Panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'BlenderMCP'
def draw(self, context):
layout = self.layout
scene = context.scene
layout.prop(scene, "blendermcp_port")
if not scene.blendermcp_server_running:
layout.operator("blendermcp.start_server", text="Connect to MCP server")
else:
layout.operator("blendermcp.stop_server", text="Disconnect from MCP server")
layout.label(text=f"Running on port {scene.blendermcp_port}")
# Operator to start the server
class BLENDERMCP_OT_StartServer(bpy.types.Operator):
bl_idname = "blendermcp.start_server"
bl_label = "Connect to Claude"
bl_description = "Start the BlenderMCP server to connect with Claude"
def execute(self, context):
scene = context.scene
# Create a new server instance
if not hasattr(bpy.types, "blendermcp_server") or not bpy.types.blendermcp_server:
bpy.types.blendermcp_server = BlenderMCPServer(port=scene.blendermcp_port)
# Start the server
bpy.types.blendermcp_server.start()
scene.blendermcp_server_running = True
return {'FINISHED'}
# Operator to stop the server
class BLENDERMCP_OT_StopServer(bpy.types.Operator):
bl_idname = "blendermcp.stop_server"
bl_label = "Stop the connection to Claude"
bl_description = "Stop the connection to Claude"
def execute(self, context):
scene = context.scene
# Stop the server if it exists
if hasattr(bpy.types, "blendermcp_server") and bpy.types.blendermcp_server:
bpy.types.blendermcp_server.stop()
del bpy.types.blendermcp_server
scene.blendermcp_server_running = False
return {'FINISHED'}
# Registration functions
def register():
bpy.types.Scene.blendermcp_port = IntProperty(
name="Port",
description="Port for the BlenderMCP server",
default=9876,
min=1024,
max=65535
)
bpy.types.Scene.blendermcp_server_running = bpy.props.BoolProperty(
name="Server Running",
default=False
)
bpy.utils.register_class(BLENDERMCP_PT_Panel)
bpy.utils.register_class(BLENDERMCP_OT_StartServer)
bpy.utils.register_class(BLENDERMCP_OT_StopServer)
print("BlenderMCP addon registered")
def unregister():
# Stop the server if it's running
if hasattr(bpy.types, "blendermcp_server") and bpy.types.blendermcp_server:
bpy.types.blendermcp_server.stop()
del bpy.types.blendermcp_server
bpy.utils.unregister_class(BLENDERMCP_PT_Panel)
bpy.utils.unregister_class(BLENDERMCP_OT_StartServer)
bpy.utils.unregister_class(BLENDERMCP_OT_StopServer)
del bpy.types.Scene.blendermcp_port
del bpy.types.Scene.blendermcp_server_running
print("BlenderMCP addon unregistered")
if __name__ == "__main__":
register()