# blender_mcp_server.py
from mcp.server.fastmcp import FastMCP, Context, Image
import socket
import json
import asyncio
import logging
import tempfile
from dataclasses import dataclass
from contextlib import asynccontextmanager
from typing import AsyncIterator, Dict, Any, List
import os
from pathlib import Path
import base64
from urllib.parse import urlparse
# Configure logging
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger("BlenderMCPServer")
# Default configuration
DEFAULT_HOST = "localhost"
DEFAULT_PORT = 9876
@dataclass
class BlenderConnection:
host: str
port: int
sock: socket.socket = None # Changed from 'socket' to 'sock' to avoid naming conflict
def connect(self) -> bool:
"""Connect to the Blender addon socket server"""
if self.sock:
return True
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((self.host, self.port))
logger.info(f"Connected to Blender at {self.host}:{self.port}")
return True
except Exception as e:
logger.error(f"Failed to connect to Blender: {str(e)}")
self.sock = None
return False
def disconnect(self):
"""Disconnect from the Blender addon"""
if self.sock:
try:
self.sock.close()
except Exception as e:
logger.error(f"Error disconnecting from Blender: {str(e)}")
finally:
self.sock = None
def receive_full_response(self, sock, buffer_size=8192):
"""Receive the complete response, potentially in multiple chunks"""
chunks = []
# Use a consistent timeout value that matches the addon's timeout
sock.settimeout(15.0) # Match the addon's timeout
try:
while True:
try:
chunk = sock.recv(buffer_size)
if not chunk:
# If we get an empty chunk, the connection might be closed
if not chunks: # If we haven't received anything yet, this is an error
raise Exception("Connection closed before receiving any data")
break
chunks.append(chunk)
# Check if we've received a complete JSON object
try:
data = b''.join(chunks)
json.loads(data.decode('utf-8'))
# If we get here, it parsed successfully
logger.info(f"Received complete response ({len(data)} bytes)")
return data
except json.JSONDecodeError:
# Incomplete JSON, continue receiving
continue
except socket.timeout:
# If we hit a timeout during receiving, break the loop and try to use what we have
logger.warning("Socket timeout during chunked receive")
break
except (ConnectionError, BrokenPipeError, ConnectionResetError) as e:
logger.error(f"Socket connection error during receive: {str(e)}")
raise # Re-raise to be handled by the caller
except socket.timeout:
logger.warning("Socket timeout during chunked receive")
except Exception as e:
logger.error(f"Error during receive: {str(e)}")
raise
# If we get here, we either timed out or broke out of the loop
# Try to use what we have
if chunks:
data = b''.join(chunks)
logger.info(f"Returning data after receive completion ({len(data)} bytes)")
try:
# Try to parse what we have
json.loads(data.decode('utf-8'))
return data
except json.JSONDecodeError:
# If we can't parse it, it's incomplete
raise Exception("Incomplete JSON response received")
else:
raise Exception("No data received")
def send_command(self, command_type: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
"""Send a command to Blender and return the response"""
if not self.sock and not self.connect():
raise ConnectionError("Not connected to Blender")
command = {
"type": command_type,
"params": params or {}
}
try:
# Log the command being sent
logger.info(f"Sending command: {command_type} with params: {params}")
# Send the command
self.sock.sendall(json.dumps(command).encode('utf-8'))
logger.info(f"Command sent, waiting for response...")
# Set a timeout for receiving - use the same timeout as in receive_full_response
self.sock.settimeout(15.0) # Match the addon's timeout
# Receive the response using the improved receive_full_response method
response_data = self.receive_full_response(self.sock)
logger.info(f"Received {len(response_data)} bytes of data")
response = json.loads(response_data.decode('utf-8'))
logger.info(f"Response parsed, status: {response.get('status', 'unknown')}")
if response.get("status") == "error":
logger.error(f"Blender error: {response.get('message')}")
raise Exception(response.get("message", "Unknown error from Blender"))
return response.get("result", {})
except socket.timeout:
logger.error("Socket timeout while waiting for response from Blender")
# Don't try to reconnect here - let the get_blender_connection handle reconnection
# Just invalidate the current socket so it will be recreated next time
self.sock = None
raise Exception("Timeout waiting for Blender response - try simplifying your request")
except (ConnectionError, BrokenPipeError, ConnectionResetError) as e:
logger.error(f"Socket connection error: {str(e)}")
self.sock = None
raise Exception(f"Connection to Blender lost: {str(e)}")
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON response from Blender: {str(e)}")
# Try to log what was received
if 'response_data' in locals() and response_data:
logger.error(f"Raw response (first 200 bytes): {response_data[:200]}")
raise Exception(f"Invalid response from Blender: {str(e)}")
except Exception as e:
logger.error(f"Error communicating with Blender: {str(e)}")
# Don't try to reconnect here - let the get_blender_connection handle reconnection
self.sock = None
raise Exception(f"Communication error with Blender: {str(e)}")
@asynccontextmanager
async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
"""Manage server startup and shutdown lifecycle"""
# We don't need to create a connection here since we're using the global connection
# for resources and tools
try:
# Just log that we're starting up
logger.info("BlenderMCP server starting up")
# Try to connect to Blender on startup to verify it's available
try:
# This will initialize the global connection if needed
blender = get_blender_connection()
logger.info("Successfully connected to Blender on startup")
except Exception as e:
logger.warning(f"Could not connect to Blender on startup: {str(e)}")
logger.warning("Make sure the Blender addon is running before using Blender resources or tools")
# Return an empty context - we're using the global connection
yield {}
finally:
# Clean up the global connection on shutdown
global _blender_connection
if _blender_connection:
logger.info("Disconnecting from Blender on shutdown")
_blender_connection.disconnect()
_blender_connection = None
logger.info("BlenderMCP server shut down")
# Create the MCP server with lifespan support
mcp = FastMCP(
"BlenderMCP",
lifespan=server_lifespan
)
# Resource endpoints
# Global connection for resources (since resources can't access context)
_blender_connection = None
def get_blender_connection():
"""Get or create a persistent Blender connection"""
global _blender_connection
# If we have an existing connection, check if it's still valid
if _blender_connection is not None:
try:
# Ping to check connection validity
result = _blender_connection.send_command("get_scene_info")
return _blender_connection
except Exception as e:
# Connection is dead, close it and create a new one
logger.warning(f"Existing connection is no longer valid: {str(e)}")
try:
_blender_connection.disconnect()
except:
pass
_blender_connection = None
# Create a new connection if needed
if _blender_connection is None:
host = os.getenv("BLENDER_HOST", DEFAULT_HOST)
port = int(os.getenv("BLENDER_PORT", DEFAULT_PORT))
_blender_connection = BlenderConnection(host=host, port=port)
if not _blender_connection.connect():
logger.error("Failed to connect to Blender")
_blender_connection = None
raise Exception("Could not connect to Blender. Make sure the Blender addon is running.")
logger.info("Created new persistent connection to Blender")
return _blender_connection
# ============================================================================
# TOOL AND PROMPT IMPORTS
# ============================================================================
#
# DO NOT DEFINE TOOLS DIRECTLY IN THIS FILE!
#
# All MCP tools are organized in separate modules under src/blender_mcp/tools/
# All MCP prompts are organized in separate modules under src/blender_mcp/prompts/
#
# Structure:
# - tools/mesh_analysis.py: Topology analysis and issue detection
# - tools/remeshing.py: Remeshing operations (voxel, quadriflow, decimate, shrinkwrap)
# - tools/viewport.py: Viewport control and navigation
# - tools/shading.py: Seams and sharp edge marking
# - tools/scene.py: Scene and object information
# - tools/integrations/polyhaven.py: PolyHaven asset integration
# - tools/integrations/sketchfab.py: Sketchfab model integration
# - tools/integrations/hyper3d.py: Hyper3D Rodin AI generation
# - prompts/retopo.py: Retopology pipeline guidance
# - prompts/asset_creation.py: Asset creation strategy
#
# When adding new tools, create them in the appropriate module and import here.
# ============================================================================
from .tools.mesh_analysis import mesh_stats as mesh_stats_impl, detect_topology_issues as detect_topology_issues_impl
from .tools.topology_analysis import (
get_topology_quality as get_topology_quality_impl,
analyze_mesh_regions as analyze_mesh_regions_impl,
get_vertex_valence as get_vertex_valence_impl,
get_triangle_faces as get_triangle_faces_impl,
get_ngon_faces as get_ngon_faces_impl,
analyze_cylindrical_structure as analyze_cylindrical_structure_impl,
find_support_loops as find_support_loops_impl
)
from .tools.topology_evaluation import evaluate_retopology as evaluate_retopology_impl
from .tools.retopo_workflow import create_retopo_setup as create_retopo_setup_impl, project_vertices as project_vertices_impl
from .tools.viewport_advanced import (
set_shading_mode as set_shading_mode_impl,
set_overlay_options as set_overlay_options_impl,
get_topology_screenshot as get_topology_screenshot_impl
)
from .tools.remeshing import decimate as decimate_impl, shrinkwrap_reproject as shrinkwrap_reproject_impl
from .tools.viewport import set_view_projection as set_view_projection_impl, align_view_to_axis as align_view_to_axis_impl, frame_selected as frame_selected_impl, focus_view_on_point as focus_view_on_point_impl
from .tools.shading import mark_seams_by_angle as mark_seams_by_angle_impl, mark_sharp_by_angle as mark_sharp_by_angle_impl
from .tools.scene import get_scene_info as get_scene_info_impl, get_object_info as get_object_info_impl, get_viewport_screenshot as get_viewport_screenshot_impl, execute_blender_code as execute_blender_code_impl
from .prompts.retopo import retopo_pipeline as retopo_pipeline_impl
from .prompts.asset_creation import asset_creation_strategy as asset_creation_strategy_impl
# Retopo tools imports
from .retopo_tools.modifiers import (
add_mirror_modifier as add_mirror_modifier_impl,
add_shrinkwrap_modifier as add_shrinkwrap_modifier_impl,
add_weighted_normal_modifier as add_weighted_normal_modifier_impl,
add_data_transfer_modifier as add_data_transfer_modifier_impl,
add_decimate_modifier as add_decimate_modifier_impl,
add_laplacian_smooth_modifier as add_laplacian_smooth_modifier_impl
)
from .retopo_tools.mesh_ops import (
symmetrize_mesh as symmetrize_mesh_impl,
smooth_vertices as smooth_vertices_impl,
recalculate_normals as recalculate_normals_impl,
merge_by_distance as merge_by_distance_impl,
delete_loose as delete_loose_impl,
dissolve_edge_loops as dissolve_edge_loops_impl,
dissolve_selected as dissolve_selected_impl,
knife_project as knife_project_impl
)
from .retopo_tools.selection import select_non_manifold as select_non_manifold_impl
from .tools.selection_tools import (
select_by_valence as select_by_valence_impl,
select_triangles as select_triangles_impl,
select_ngons as select_ngons_impl,
select_edge_loop as select_edge_loop_impl,
select_by_criteria as select_by_criteria_impl,
get_selected_elements as get_selected_elements_impl
)
from .tools.topology_cleanup import (
tris_to_quads as tris_to_quads_impl,
align_vertex_to_loop as align_vertex_to_loop_impl,
dissolve_edge_loop_by_selection as dissolve_edge_loop_by_selection_impl,
reduce_vertex_valence as reduce_vertex_valence_impl,
fix_ngons as fix_ngons_impl
)
from .retopo_tools.snapping import set_snapping as set_snapping_impl
from .retopo_tools.remesh import remesh_voxel as remesh_voxel_impl, quadriflow_remesh as quadriflow_remesh_retopo_impl
from .retopo_tools.camera import (
align_active_camera_to_view as align_active_camera_to_view_impl,
set_camera_projection as set_camera_projection_impl,
set_active_camera as set_active_camera_impl
)
from .retopo_tools.baking import (
bake_normals as bake_normals_impl,
bake_ambient_occlusion as bake_ambient_occlusion_impl
)
from .retopo_tools.io import (
import_reference as import_reference_impl,
export_asset as export_asset_impl
)
from .retopo_tools.draw_xray import (
draw_xray_enable as draw_xray_enable_impl
)
from .tools.shading_validation import (
set_matcap_shading as set_matcap_shading_impl,
toggle_wireframe_overlay as toggle_wireframe_overlay_impl,
toggle_smooth_shading as toggle_smooth_shading_impl
)
from .tools.view_control import (
set_view_axis as set_view_axis_impl,
orbit_view as orbit_view_impl
)
# ============================================================================
# TOOL WRAPPERS
# ============================================================================
# These wrappers connect the MCP decorator to the tool implementations
@mcp.tool()
def get_scene_info(ctx: Context) -> str:
"""Get detailed information about the current Blender scene"""
return get_scene_info_impl(ctx, get_blender_connection())
@mcp.tool()
def get_object_info(ctx: Context, object_name: str) -> str:
"""
Get detailed information about a specific object in the Blender scene.
Parameters:
- object_name: The name of the object to get information about
"""
return get_object_info_impl(ctx, get_blender_connection(), object_name)
@mcp.tool()
def get_viewport_screenshot(ctx: Context, max_size: int = 800) -> Image:
"""
Capture a screenshot of the current Blender 3D viewport.
Parameters:
- max_size: Maximum size in pixels for the largest dimension (default: 800)
Returns the screenshot as an Image.
"""
return get_viewport_screenshot_impl(ctx, get_blender_connection(), max_size)
@mcp.tool()
def execute_blender_code(ctx: Context, code: str) -> str:
"""
Execute arbitrary Python code in Blender. Make sure to do it step-by-step by breaking it into smaller chunks.
Parameters:
- code: The Python code to execute
"""
return execute_blender_code_impl(ctx, get_blender_connection(), code)
# Retopology Tools
@mcp.tool()
def mesh_stats(ctx: Context, active_only: bool = True) -> str:
"""
Get detailed topology statistics for mesh objects.
Returns counts of vertices/edges/faces, tri/quad/ngon breakdown,
non-manifold counts, sharp edges, and surface area/volume metrics.
Parameters:
- active_only: Analyze only the active object (true) or all mesh objects (false)
Returns detailed mesh statistics including topology metrics.
"""
return mesh_stats_impl(ctx, get_blender_connection(), active_only)
@mcp.tool()
def detect_topology_issues(
ctx: Context,
angle_sharp: float = 30.0,
distance_doubles: float = 0.0001
) -> str:
"""
Detect topology issues in the active mesh object.
Reports non-manifold edges, loose geometry, duplicate vertices,
inverted normals, and provides suggested fixes.
Parameters:
- angle_sharp: Threshold angle in degrees for detecting sharp edges (default: 30)
- distance_doubles: Merge distance for detecting duplicate vertices (default: 0.0001)
Returns a detailed report of topology issues with suggestions for fixes.
"""
return detect_topology_issues_impl(ctx, get_blender_connection(), angle_sharp, distance_doubles)
@mcp.tool()
def get_vertex_valence(
ctx: Context,
object_name: str = None,
min_valence: int = 5
) -> str:
"""
Get vertices with valence (edge count) >= threshold.
Identifies high-valence poles that may cause topology problems.
Returns vertex indices with locations for navigation.
Parameters:
- object_name: Name of object to analyze (default: active object)
- min_valence: Minimum valence to include (default: 5)
Use with focus_view_on_point to navigate to problem vertices.
"""
return get_vertex_valence_impl(ctx, get_blender_connection(), object_name, min_valence)
@mcp.tool()
def get_triangle_faces(
ctx: Context,
object_name: str = None
) -> str:
"""
Get all triangle faces (3 vertices) in the mesh.
Identifies triangles that may need conversion to quads for
clean topology. Returns face indices with center locations.
Parameters:
- object_name: Name of object to analyze (default: active object)
Use with tris_to_quads to convert compatible triangle pairs.
"""
return get_triangle_faces_impl(ctx, get_blender_connection(), object_name)
@mcp.tool()
def get_ngon_faces(
ctx: Context,
object_name: str = None
) -> str:
"""
Get all n-gon faces (5+ vertices) in the mesh.
N-gons should be avoided in retopology for clean edge flow.
Returns face indices with center locations and vertex counts.
Parameters:
- object_name: Name of object to analyze (default: active object)
N-gons should be manually dissolved or triangulated.
"""
return get_ngon_faces_impl(ctx, get_blender_connection(), object_name)
@mcp.tool()
def analyze_cylindrical_structure(
ctx: Context,
object_name: str = None,
analyze_spacing: bool = False
) -> str:
"""
Analyze cylindrical mesh regions from selected edge loop.
Detects segment count, loop structure, and axis orientation
for cylindrical topology like limbs, pipes, or tubular forms.
Parameters:
- object_name: Name of object (default: active object)
- analyze_spacing: Include edge loop spacing analysis (default: false)
Returns segment count, axis info, and optimization suggestions.
Requires an edge loop to be selected in Edit Mode.
"""
return analyze_cylindrical_structure_impl(ctx, get_blender_connection(), object_name, analyze_spacing)
@mcp.tool()
def find_support_loops(
ctx: Context,
object_name: str = None,
min_angle: float = 30.0,
near_sharp: bool = True,
suggest: bool = False
) -> str:
"""
Find edge loops suitable for subdivision support.
Identifies edge loops near sharp edges or at significant angle
transitions that could serve as support loops for subdivision surfaces.
Parameters:
- object_name: Name of object (default: active object)
- min_angle: Minimum angle (degrees) to consider an edge sharp (default: 30)
- near_sharp: Find loops near marked sharp edges (default: true)
- suggest: Suggest where to add support loops (default: false)
Returns candidate loops with their locations and edge indices.
"""
return find_support_loops_impl(ctx, get_blender_connection(), object_name, min_angle, near_sharp, suggest)
# ============================================================================
# VIEWPORT ADVANCED - Shading and Topology Visualization
# ============================================================================
@mcp.tool()
def set_shading_mode(
ctx: Context,
mode: str = "SOLID",
show_wireframe: bool = False,
show_xray: bool = False,
xray_alpha: float = 0.5
) -> str:
"""
Set the viewport shading mode and options.
Controls how objects are displayed in the 3D viewport.
Essential for retopology visualization and topology inspection.
Parameters:
- mode: Shading mode - "WIREFRAME", "SOLID", "MATERIAL", "RENDERED" (default: "SOLID")
- show_wireframe: Show wireframe overlay on solid/material modes (default: False)
- show_xray: Enable X-Ray mode for seeing through objects (default: False)
- xray_alpha: X-Ray transparency 0.0-1.0 (default: 0.5)
Use with show_xray=True for retopology to see through the lowpoly mesh.
"""
return set_shading_mode_impl(ctx, get_blender_connection(), mode, show_wireframe, show_xray, xray_alpha)
@mcp.tool()
def decimate(
ctx: Context,
ratio: float = 0.5,
mode: str = "COLLAPSE",
angle_limit: float = 5.0
) -> str:
"""
Reduce polygon count while preserving shape using decimation.
Parameters:
- ratio: Target ratio of faces to keep, 0-1 (default: 0.5 = 50%)
- mode: Decimation mode (default: "COLLAPSE")
* "COLLAPSE" - Edge collapse (best for general use)
* "DISSOLVE" - Planar decimation (good for flat surfaces)
* "UNSUBDIVIDE" - Reverse subdivision (for subdivided meshes)
- angle_limit: Angle limit in degrees for DISSOLVE mode (default: 5.0)
Returns success status and face count reduction.
"""
return decimate_impl(ctx, get_blender_connection(), ratio, mode, angle_limit)
@mcp.tool()
def shrinkwrap_reproject(
ctx: Context,
high: str,
low: str,
method: str = "NEAREST_SURFACEPOINT",
offset: float = 0.0
) -> str:
"""
Project low-poly mesh vertices onto high-poly surface to preserve silhouette.
Parameters:
- high: Name of the high-poly reference mesh
- low: Name of the low-poly mesh to project
- method: Projection method (default: "NEAREST_SURFACEPOINT")
* "NEAREST_SURFACEPOINT" - Project to nearest point on surface
* "PROJECT" - Project along axis
* "NEAREST_VERTEX" - Snap to nearest vertex
* "TARGET_PROJECT" - Project to target
- offset: Distance offset from surface (default: 0.0)
Returns success status.
"""
return shrinkwrap_reproject_impl(ctx, get_blender_connection(), high, low, method, offset)
@mcp.tool()
def mark_seams_by_angle(
ctx: Context,
angle: float = 60.0,
clear_existing: bool = False
) -> str:
"""
Automatically mark UV seams on edges based on angle threshold.
Useful for preparing meshes for UV unwrapping.
Parameters:
- angle: Angle threshold in degrees (default: 60). Edges with angle ≥ this become seams.
- clear_existing: Clear existing seams before marking new ones (default: False)
Returns count of edges marked as seams.
"""
return mark_seams_by_angle_impl(ctx, get_blender_connection(), angle, clear_existing)
@mcp.tool()
def mark_sharp_by_angle(
ctx: Context,
angle: float = 45.0,
clear_existing: bool = False
) -> str:
"""
Automatically mark sharp edges based on angle threshold for proper shading.
Sharp edges will show crisp transitions in smooth shading.
Parameters:
- angle: Angle threshold in degrees (default: 45). Edges with angle ≥ this become sharp.
- clear_existing: Clear existing sharp edges before marking new ones (default: False)
Returns count of edges marked as sharp.
"""
return mark_sharp_by_angle_impl(ctx, get_blender_connection(), angle, clear_existing)
@mcp.tool()
def align_view_to_axis(
ctx: Context,
axis: str,
side: str
) -> str:
"""
Align viewport to a specific global axis and side.
Parameters:
- axis: "X", "Y", or "Z"
- side: "POS" (positive direction) or "NEG" (negative direction)
Mappings:
- Y/POS = FRONT view
- Y/NEG = BACK view
- X/POS = RIGHT view
- X/NEG = LEFT view
- Z/POS = TOP view
- Z/NEG = BOTTOM view
"""
return align_view_to_axis_impl(ctx, get_blender_connection(), axis, side)
@mcp.tool()
def frame_selected(ctx: Context) -> str:
"""
Frame the currently selected objects in the viewport.
The viewport will center on and zoom to fit the selected objects.
No parameters required. Operates on current selection.
"""
return frame_selected_impl(ctx, get_blender_connection())
@mcp.tool()
def focus_view_on_point(
ctx: Context,
x: float,
y: float,
z: float,
distance: float = None,
view_axis: str = None
) -> str:
"""
Focus viewport on a specific 3D point.
Centers the viewport on the specified coordinates with optional
distance and axis alignment. Essential for navigating to problem
areas identified by analyze_mesh_regions.
Parameters:
- x: X coordinate of the target point
- y: Y coordinate of the target point
- z: Z coordinate of the target point
- distance: Optional viewing distance from target (default: maintain current)
- view_axis: Optional axis alignment - "FRONT", "BACK", "LEFT", "RIGHT", "TOP", "BOTTOM"
Use with analyze_mesh_regions to navigate to problem zones.
"""
return focus_view_on_point_impl(ctx, get_blender_connection(), x, y, z, distance, view_axis)
# ============================================================================
# RETOPO TOOLS - MODIFIERS
# ============================================================================
@mcp.tool()
def add_mirror_modifier(
ctx: Context,
axis: str = "X",
use_clip: bool = True,
use_bisect: bool = True,
mirror_object: str = None
) -> str:
"""
Add a Mirror modifier to the active mesh object.
Creates a Mirror modifier that mirrors the mesh along the specified axis
with optional clipping and bisecting.
Parameters:
- axis: Mirror axis - 'X', 'Y', or 'Z' (default: 'X')
- use_clip: Prevent vertices from crossing the mirror plane (default: true)
- use_bisect: Cut the mesh at the mirror plane (default: true)
- mirror_object: Name of object to use as mirror plane (optional)
"""
return add_mirror_modifier_impl(ctx, get_blender_connection(), axis, use_clip, use_bisect, mirror_object)
@mcp.tool()
def add_shrinkwrap_modifier(
ctx: Context,
target: str,
method: str = "NEAREST_SURFACEPOINT",
offset: float = 0.0,
on_cage: bool = True,
cull_backfaces: bool = False
) -> str:
"""
Add a Shrinkwrap modifier to project low-poly mesh onto high-poly surface.
Projects vertices of the active object onto the target surface using the
specified method. Essential for retopology workflows.
Parameters:
- target: Name of the target object to shrinkwrap to
- method: Wrap method - 'NEAREST_SURFACEPOINT', 'PROJECT', 'NEAREST_VERTEX', 'TARGET_PROJECT' (default: 'NEAREST_SURFACEPOINT')
- offset: Distance to offset from target surface (default: 0.0)
- on_cage: Project in screen space for retopology (default: true)
- cull_backfaces: Don't project to backfaces (default: false)
"""
return add_shrinkwrap_modifier_impl(ctx, get_blender_connection(), target, method, offset, on_cage, cull_backfaces)
@mcp.tool()
def add_weighted_normal_modifier(
ctx: Context,
mode: str = "FACE_AREA",
weight: int = 50,
keep_sharp: bool = True,
face_influence: bool = True
) -> str:
"""
Add a Weighted Normal modifier to improve hard-surface shading.
Recalculates custom normals using face area or corner angle weighting
to improve shading on hard-surface models without adding geometry.
Parameters:
- mode: Weighting mode - 'FACE_AREA', 'CORNER_ANGLE', 'FACE_AREA_WITH_ANGLE' (default: 'FACE_AREA')
- weight: Weight influence 0-100 (default: 50)
- keep_sharp: Keep sharp edges (default: true)
- face_influence: Use face influence (default: true)
"""
return add_weighted_normal_modifier_impl(ctx, get_blender_connection(), mode, weight, keep_sharp, face_influence)
@mcp.tool()
def add_data_transfer_modifier(
ctx: Context,
source: str,
data_types: List[str],
mapping: str = "NEAREST_FACE",
mix_factor: float = 1.0
) -> str:
"""
Add a Data Transfer modifier to copy data from high-poly to low-poly.
Transfers various data types (normals, UVs, vertex colors, etc.) from
a source object to the active object using specified mapping method.
Parameters:
- source: Name of the source object to transfer data from
- data_types: List of data types to transfer - ['CUSTOM_NORMAL', 'UV', 'VCOL', 'VGROUP_WEIGHTS']
- mapping: Mapping method - 'NEAREST_FACE', 'NEAREST_VERTEX', 'POLYINTERP_NEAREST', 'POLYINTERP_LNORPROJ' (default: 'NEAREST_FACE')
- mix_factor: Blend factor 0-1 (default: 1.0)
"""
return add_data_transfer_modifier_impl(ctx, get_blender_connection(), source, data_types, mapping, mix_factor)
@mcp.tool()
def add_decimate_modifier(
ctx: Context,
mode: str = "COLLAPSE",
ratio: float = 0.5,
iterations: int = 1
) -> str:
"""
Add a Decimate modifier to reduce polygon count predictably.
Reduces mesh complexity using collapse, dissolve, or un-subdivide modes
while attempting to preserve overall shape.
Parameters:
- mode: Decimation mode - 'COLLAPSE', 'DISSOLVE', 'UNSUBDIV' (default: 'COLLAPSE')
- ratio: For COLLAPSE: reduction ratio 0-1 (default: 0.5). For others: 1.0
- iterations: For UNSUBDIV: number of iterations (default: 1)
"""
return add_decimate_modifier_impl(ctx, get_blender_connection(), mode, ratio, iterations)
@mcp.tool()
def add_laplacian_smooth_modifier(
ctx: Context,
factor: float = 1.0,
repeat: int = 2,
preserve_volume: bool = True
) -> str:
"""
Add a Laplacian Smooth modifier to reduce surface noise.
Applies Laplacian smoothing to reduce noise and high-frequency detail
while optionally preserving overall volume.
Parameters:
- factor: Smoothing factor 0-100 (default: 1.0)
- repeat: Number of iterations (default: 2)
- preserve_volume: Maintain mesh volume (default: true)
"""
return add_laplacian_smooth_modifier_impl(ctx, get_blender_connection(), factor, repeat, preserve_volume)
# ============================================================================
# RETOPO TOOLS - MESH OPERATIONS
# ============================================================================
@mcp.tool()
def symmetrize_mesh(
ctx: Context,
direction: str = "NEGATIVE_X_TO_POSITIVE_X",
threshold: float = 0.0001
) -> str:
"""
Enforce mesh symmetry by mirroring one side to the other.
Cuts the mesh along an axis and mirrors one side to the other, copying
UVs, vertex colors, and weights. Requires Edit Mode.
Parameters:
- direction: Mirror direction - 'NEGATIVE_X_TO_POSITIVE_X', 'POSITIVE_X_TO_NEGATIVE_X',
'NEGATIVE_Y_TO_POSITIVE_Y', 'POSITIVE_Y_TO_NEGATIVE_Y',
'NEGATIVE_Z_TO_POSITIVE_Z', 'POSITIVE_Z_TO_NEGATIVE_Z' (default: 'NEGATIVE_X_TO_POSITIVE_X')
- threshold: Merge distance for symmetry plane (default: 0.0001)
"""
return symmetrize_mesh_impl(ctx, get_blender_connection(), direction, threshold)
@mcp.tool()
def smooth_vertices(
ctx: Context,
iterations: int = 5,
factor: float = 0.5,
axes: str = "XYZ"
) -> str:
"""
Smooth selected vertices by averaging neighbor positions.
Relaxes topology without subdividing by averaging vertex positions across
adjacent faces. Works on selected vertices in Edit Mode.
Parameters:
- iterations: Number of smoothing iterations (default: 5)
- factor: Smoothing strength 0-1 (default: 0.5)
- axes: Axes to constrain smoothing - 'X', 'Y', 'Z', 'XY', 'XZ', 'YZ', 'XYZ' (default: 'XYZ')
"""
return smooth_vertices_impl(ctx, get_blender_connection(), iterations, factor, axes)
@mcp.tool()
def recalculate_normals(
ctx: Context,
inside: bool = False
) -> str:
"""
Recalculate face normals for consistent orientation.
Makes all face normals point consistently outward (or inward if specified).
Essential for fixing shading issues and preparing for remeshing. Requires Edit Mode.
Parameters:
- inside: Point normals inward instead of outward (default: false)
"""
return recalculate_normals_impl(ctx, get_blender_connection(), inside)
@mcp.tool()
def merge_by_distance(
ctx: Context,
distance: float = 0.0001,
unselected: bool = False
) -> str:
"""
Merge vertices that are closer than the specified distance.
Removes duplicate and near-duplicate vertices to clean topology.
Works in Edit Mode on selected vertices (or all if unselected is true).
Parameters:
- distance: Maximum merge distance (default: 0.0001)
- unselected: Also merge unselected vertices (default: false)
"""
return merge_by_distance_impl(ctx, get_blender_connection(), distance, unselected)
@mcp.tool()
def delete_loose(
ctx: Context,
delete_faces: bool = False
) -> str:
"""
Delete loose vertices and edges not connected to faces.
Removes stray geometry that isn't part of the main mesh structure.
Requires Edit Mode with all geometry selected.
Parameters:
- delete_faces: Also delete loose faces (default: false)
"""
return delete_loose_impl(ctx, get_blender_connection(), delete_faces)
@mcp.tool()
def dissolve_edge_loops(ctx: Context) -> str:
"""
Dissolve selected edge loops without leaving holes.
Removes redundant edge loops by merging surrounding faces. The edge loop
must be selected in Edit Mode. Useful for reducing edge flow complexity.
"""
return dissolve_edge_loops_impl(ctx, get_blender_connection())
@mcp.tool()
def dissolve_selected(
ctx: Context,
object_name: str = None,
mode: str = "VERT",
use_verts: bool = True,
use_face_split: bool = False
) -> str:
"""
Dissolve selected mesh elements without creating holes.
Removes selected vertices, edges, or faces while merging surrounding
geometry. Works in Edit Mode on currently selected elements.
Parameters:
- object_name: Name of object (default: active object)
- mode: Element type to dissolve - 'VERT' | 'EDGE' | 'FACE' (default: 'VERT')
- use_verts: For edge dissolve, also dissolve verts (default: true)
- use_face_split: Split non-planar faces during dissolve (default: false)
"""
return dissolve_selected_impl(ctx, get_blender_connection(), object_name, mode, use_verts, use_face_split)
# ============================================================================
# RETOPO TOOLS - SELECTION
# ============================================================================
@mcp.tool()
def select_non_manifold(
ctx: Context,
wire: bool = True,
boundaries: bool = True,
multiple_faces: bool = True,
non_contiguous: bool = True
) -> str:
"""
Select non-manifold mesh elements for topology validation.
Selects problematic geometry that may cause issues in remeshing operations.
Non-manifold geometry includes edges with more than 2 faces, edges with
only 1 face (boundaries), wire edges, and non-contiguous vertices.
Requires Edit Mode.
Parameters:
- wire: Select wire edges (edges with no faces) (default: true)
- boundaries: Select boundary edges (edges with 1 face) (default: true)
- multiple_faces: Select edges with 3+ faces (default: true)
- non_contiguous: Select vertices connecting disjoint geometry (default: true)
"""
return select_non_manifold_impl(ctx, get_blender_connection(), wire, boundaries, multiple_faces, non_contiguous)
@mcp.tool()
def select_by_valence(
ctx: Context,
object_name: str = None,
min_valence: int = 5
) -> str:
"""
Select vertices with valence (edge count) >= threshold.
Useful for identifying high-valence poles that need topology fixes.
Parameters:
- object_name: Name of object to select in (default: active object)
- min_valence: Minimum valence to select (default: 5)
"""
return select_by_valence_impl(ctx, get_blender_connection(), object_name, min_valence)
@mcp.tool()
def select_triangles(
ctx: Context,
object_name: str = None
) -> str:
"""
Select all triangle faces (3 vertices) in the mesh.
Useful for identifying triangles that should be converted to quads.
Parameters:
- object_name: Name of object to select in (default: active object)
"""
return select_triangles_impl(ctx, get_blender_connection(), object_name)
@mcp.tool()
def select_ngons(
ctx: Context,
object_name: str = None
) -> str:
"""
Select all n-gon faces (5+ vertices) in the mesh.
N-gons should be eliminated for clean retopology.
Parameters:
- object_name: Name of object to select in (default: active object)
"""
return select_ngons_impl(ctx, get_blender_connection(), object_name)
@mcp.tool()
def select_edge_loop(
ctx: Context,
object_name: str = None,
edge_index: int = 0
) -> str:
"""
Select the edge loop containing the specified edge.
Useful for selecting and manipulating complete edge loops.
Parameters:
- object_name: Name of object to select in (default: active object)
- edge_index: Index of edge to start loop selection from
"""
return select_edge_loop_impl(ctx, get_blender_connection(), object_name, edge_index)
@mcp.tool()
def select_by_criteria(
ctx: Context,
object_name: str = None,
criteria: str = "valence",
min_value: float = None,
max_value: float = None
) -> str:
"""
Select mesh elements by flexible criteria.
A versatile selection tool supporting multiple criteria types for
identifying specific topology patterns.
Parameters:
- object_name: Name of object (default: active object)
- criteria: Selection type:
- 'valence': Select vertices by edge count
- 'face_sides': Select faces by vertex count (3=tris, 4=quads, 5+=ngons)
- 'boundary': Select boundary edges
- 'area': Select faces by surface area
- 'non_manifold': Select non-manifold edges
- min_value: Minimum value for range criteria (optional)
- max_value: Maximum value for range criteria (optional)
"""
return select_by_criteria_impl(ctx, get_blender_connection(), object_name, criteria, min_value, max_value)
@mcp.tool()
def get_selected_elements(
ctx: Context,
object_name: str = None,
mode: str = "VERT"
) -> str:
"""
Get list of selected mesh elements with detailed data.
Returns information about currently selected vertices, edges, or faces
including coordinates, indices, and geometry properties.
Parameters:
- object_name: Name of object (default: active object)
- mode: Element type to query - 'VERT' | 'EDGE' | 'FACE' (default: 'VERT')
Returns detailed element data including:
- VERT: index, coordinates, valence, normal
- EDGE: index, vertex pair, length, boundary status
- FACE: index, vertices, area, center, normal
"""
return get_selected_elements_impl(ctx, get_blender_connection(), object_name, mode)
# ============================================================================
# TOPOLOGY CLEANUP
# ============================================================================
@mcp.tool()
def tris_to_quads(
ctx: Context,
object_name: str = None,
angle_threshold: float = 40.0
) -> str:
"""
Convert triangle pairs to quads.
Merges adjacent triangles into quads when their shared edge angle
is below the threshold. Essential for cleaning up triangulated meshes.
Parameters:
- object_name: Name of object (default: active object)
- angle_threshold: Maximum angle between face normals for merging (degrees, default: 40)
"""
return tris_to_quads_impl(ctx, get_blender_connection(), object_name, angle_threshold)
@mcp.tool()
def align_vertex_to_loop(
ctx: Context,
object_name: str = None,
vertex_index: int = 0,
axis: str = "Y",
target_coord: float = 0.0
) -> str:
"""
Align a vertex coordinate to match edge loop position.
Useful for straightening edge loops by aligning vertices to a common coordinate.
Parameters:
- object_name: Name of object (default: active object)
- vertex_index: Index of vertex to align
- axis: Axis to align ("X", "Y", or "Z")
- target_coord: Target coordinate value
"""
return align_vertex_to_loop_impl(ctx, get_blender_connection(), object_name, vertex_index, axis, target_coord)
@mcp.tool()
def dissolve_edge_loop_by_selection(
ctx: Context,
object_name: str = None
) -> str:
"""
Dissolve currently selected edge loop.
The edge loop must be pre-selected in Edit Mode.
Use select_edge_loop first to select the loop to dissolve.
Parameters:
- object_name: Name of object (default: active object)
"""
return dissolve_edge_loop_by_selection_impl(ctx, get_blender_connection(), object_name)
@mcp.tool()
def reduce_vertex_valence(
ctx: Context,
object_name: str = None,
vertex_index: int = 0,
target_valence: int = 4
) -> str:
"""
Attempt to reduce vertex valence by collapsing nearby edges.
High-valence vertices (poles with 5+ edges) can cause shading artifacts.
This tool attempts to simplify them by collapsing shortest edges.
Note: Complex operation that may not always succeed. Manual fixes often preferred.
Parameters:
- object_name: Name of object (default: active object)
- vertex_index: Index of vertex to reduce
- target_valence: Target valence (minimum 3, default: 4)
"""
return reduce_vertex_valence_impl(ctx, get_blender_connection(), object_name, vertex_index, target_valence)
@mcp.tool()
def fix_ngons(
ctx: Context,
object_name: str = None,
method: str = "triangulate",
select_all: bool = True
) -> str:
"""
Convert n-gon faces (5+ vertices) to quads or triangles.
N-gons can cause issues with subdivision, animation, and rendering.
This tool converts them using various triangulation methods.
Parameters:
- object_name: Name of object (default: active object)
- method: Conversion method:
- 'triangulate': Beauty-fill triangulation (best quality)
- 'poke': Create triangles radiating from center point
- 'fan': Fan triangulation from first vertex (fastest)
- select_all: Process all n-gons in mesh (default: true)
"""
return fix_ngons_impl(ctx, get_blender_connection(), object_name, method, select_all)
# ============================================================================
# RETOPO TOOLS - SNAPPING
# ============================================================================
@mcp.tool()
def set_snapping(
ctx: Context,
enable: bool = True,
mode: str = "FACE_PROJECT",
align_rotation_to_target: bool = False,
project_individual_elements: bool = True,
backface_culling: bool = True
) -> str:
"""
Configure snapping settings for retopology workflow.
Sets up snapping to project new geometry onto reference surfaces.
Essential for manual retopology. Affects the active 3D viewport.
Parameters:
- enable: Enable snapping (default: true)
- mode: Snap mode - 'INCREMENT', 'VERTEX', 'EDGE', 'FACE', 'FACE_PROJECT', 'VOLUME' (default: 'FACE_PROJECT')
- align_rotation_to_target: Align rotation to target normal (default: false)
- project_individual_elements: Project each element individually (default: true)
- backface_culling: Don't snap to backfaces (default: true)
"""
return set_snapping_impl(ctx, get_blender_connection(), enable, mode, align_rotation_to_target, project_individual_elements, backface_culling)
# ============================================================================
# RETOPO TOOLS - REMESHING
# ============================================================================
@mcp.tool()
def remesh_voxel(
ctx: Context,
voxel_size: float = 0.01,
adaptivity: float = 0.0,
preserve_volume: bool = True
) -> str:
"""
Apply voxel-based remeshing to create uniform topology.
Rebuilds the mesh using a voxel grid. Good for cleanup and creating
uniform base topology. Generates geometry from scratch, ignoring modifiers.
Works on the active object in Object Mode.
Parameters:
- voxel_size: Size of voxels - smaller = more detail (default: 0.01)
- adaptivity: Adaptivity 0-1 for reducing detail in flat areas (default: 0.0)
- preserve_volume: Try to preserve mesh volume (default: true)
"""
return remesh_voxel_impl(ctx, get_blender_connection(), voxel_size, adaptivity, preserve_volume)
@mcp.tool()
def quadriflow_remesh(
ctx: Context,
target_faces: int = 5000,
preserve_sharp: bool = True,
use_symmetry: bool = False
) -> str:
"""
Apply QuadriFlow remeshing to generate quad-dominant topology.
Creates high-quality quad-dominant meshes suitable for animation and
subdivision. REQUIRES manifold input with consistent normals.
Works on the active object in Object Mode.
Parameters:
- target_faces: Desired face count (default: 5000)
- preserve_sharp: Preserve sharp edges (default: true)
- use_symmetry: Detect and preserve mesh symmetry (default: false)
"""
return quadriflow_remesh_retopo_impl(ctx, get_blender_connection(), target_faces, preserve_sharp, use_symmetry)
# ============================================================================
# RETOPO TOOLS - CAMERA
# ============================================================================
@mcp.tool()
def align_active_camera_to_view(ctx: Context) -> str:
"""
Align the active camera to match the current viewport view.
Positions and rotates the active camera to exactly match the current
3D viewport angle. Useful for setting up orthographic reference cameras.
Requires an active camera in the scene.
"""
return align_active_camera_to_view_impl(ctx, get_blender_connection())
@mcp.tool()
def set_camera_projection(
ctx: Context,
projection_type: str = "ORTHO",
ortho_scale: float = 10.0
) -> str:
"""
Set the active camera's projection type and scale.
Changes the camera between perspective and orthographic projection.
Orthographic cameras have no perspective distortion, making them ideal
for technical references and orthographic views.
Parameters:
- projection_type: Camera type - 'PERSP' or 'ORTHO' (default: 'ORTHO')
- ortho_scale: For ORTHO cameras, the orthographic scale (default: 10.0)
"""
return set_camera_projection_impl(ctx, get_blender_connection(), projection_type, ortho_scale)
@mcp.tool()
def set_active_camera(
ctx: Context,
camera_name: str
) -> str:
"""
Set the specified camera as the active scene camera.
Makes the named camera the active render camera for the scene.
Useful when working with multiple reference cameras.
Parameters:
- camera_name: Name of the camera object to make active
"""
return set_active_camera_impl(ctx, get_blender_connection(), camera_name)
# ============================================================================
# RETOPO TOOLS - BAKING
# ============================================================================
@mcp.tool()
def bake_normals(
ctx: Context,
high_poly: str,
low_poly: str,
map_size: int = 2048,
space: str = "TANGENT",
cage_object: str = None,
max_ray_distance: float = 0.1
) -> str:
"""
Bake a normal map from high-poly mesh to low-poly mesh.
Transfers surface detail from a high-resolution source mesh to a
low-resolution target mesh by baking a normal map. The low-poly mesh
must have UV coordinates. The result is saved to //textures/ directory
and automatically assigned to the low-poly material.
Parameters:
- high_poly: Name of the high-poly source mesh
- low_poly: Name of the low-poly target mesh
- map_size: Texture resolution (512, 1024, 2048, 4096) (default: 2048)
- space: Normal space - 'TANGENT' or 'OBJECT' (default: 'TANGENT')
- cage_object: Optional cage object for ray-casting (default: None)
- max_ray_distance: Maximum distance for ray-casting (default: 0.1)
"""
return bake_normals_impl(ctx, get_blender_connection(), high_poly, low_poly, map_size, space, cage_object, max_ray_distance)
@mcp.tool()
def bake_ambient_occlusion(
ctx: Context,
target: str,
map_size: int = 2048,
samples: int = 128
) -> str:
"""
Bake an ambient occlusion map for the target mesh.
Creates an ambient occlusion map that captures how exposed each point
is to ambient lighting. Useful for adding depth to textures.
The target mesh must have UV coordinates.
Parameters:
- target: Name of the target mesh
- map_size: Texture resolution (512, 1024, 2048, 4096) (default: 2048)
- samples: Number of AO samples for quality (default: 128)
"""
return bake_ambient_occlusion_impl(ctx, get_blender_connection(), target, map_size, samples)
# ============================================================================
# SHADING VALIDATION
# ============================================================================
@mcp.tool()
def set_matcap_shading(
ctx: Context,
matcap_name: str = None
) -> str:
"""
Set matcap shading for surface validation.
Matcap shading reveals shading artifacts, harsh lines, and surface issues.
Optimal for checking retopology quality before final approval.
Parameters:
- matcap_name: Optional specific matcap name (default: use current)
"""
return set_matcap_shading_impl(ctx, get_blender_connection(), matcap_name)
@mcp.tool()
def toggle_wireframe_overlay(
ctx: Context,
enabled: bool = None
) -> str:
"""
Toggle or set wireframe overlay visibility.
Controls whether topology wireframe is visible on mesh surface.
Useful for quickly switching between clean shading and topology view.
Parameters:
- enabled: True to show, False to hide, None to toggle current state
"""
return toggle_wireframe_overlay_impl(ctx, get_blender_connection(), enabled)
@mcp.tool()
def toggle_smooth_shading(
ctx: Context,
object_name: str = None,
smooth: bool = True
) -> str:
"""
Set smooth or flat shading on mesh object.
Smooth shading reveals surface quality and flow.
Flat shading shows individual face angles for problem identification.
Parameters:
- object_name: Name of object (default: active object)
- smooth: True for smooth shading, False for flat
"""
return toggle_smooth_shading_impl(ctx, get_blender_connection(), object_name, smooth)
# ============================================================================
# VIEW CONTROL
# ============================================================================
@mcp.tool()
def set_view_axis(
ctx: Context,
axis: str
) -> str:
"""
Set viewport to a standard axis view.
Simple and reliable way to align viewport to standard views.
No context errors - uses proper temp_override with region.
Parameters:
- axis: View name - "FRONT", "BACK", "LEFT", "RIGHT", "TOP", "BOTTOM"
Equivalent to pressing numpad keys in Blender.
"""
return set_view_axis_impl(ctx, get_blender_connection(), axis)
@mcp.tool()
def orbit_view(
ctx: Context,
yaw_delta: float = 0.0,
pitch_delta: float = 0.0,
distance_delta: float = 0.0,
target: str = None,
point: list = None
) -> str:
"""
Orbit the viewport by specified angles.
Rotates the camera around the current or specified orbit center.
Useful for inspecting mesh from different angles programmatically.
Parameters:
- yaw_delta: Horizontal rotation in degrees (positive = rotate right)
- pitch_delta: Vertical rotation in degrees (positive = rotate up)
- distance_delta: Change in view distance (positive = zoom out, negative = zoom in)
- target: Object name to orbit around (centers view on object location)
- point: List of [x, y, z] coordinates to orbit around
Use with focus_view_on_point or frame_selected for complete navigation.
"""
return orbit_view_impl(ctx, get_blender_connection(), yaw_delta, pitch_delta, distance_delta, target, point)
# ============================================================================
# PROMPTS
# ============================================================================
@mcp.prompt()
def retopo_pipeline() -> str:
"""Guided workflow for complete retopology pipeline from high-poly to game-ready asset"""
return retopo_pipeline_impl()
@mcp.prompt()
def asset_creation_strategy() -> str:
"""Defines the preferred strategy for creating assets in Blender"""
return asset_creation_strategy_impl()
# Main execution
def main():
"""Run the MCP server"""
mcp.run()
if __name__ == "__main__":
main()