We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/ShirshovDIM/retopoflow_blender_mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""
Topology Analysis Tools
Advanced topology analysis tools for retopology quality assessment.
Provides semantic metrics beyond simple polygon counting.
"""
from mcp.server.fastmcp import Context
import logging
logger = logging.getLogger("BlenderMCPServer")
def get_topology_quality(ctx: Context, blender_connection, object_name: str = None) -> str:
"""
Get detailed topology quality metrics for retopology evaluation.
Returns semantic metrics like quad percentage, pole distribution,
aspect ratios, and an overall quality score (0-100).
Parameters:
- object_name: Name of object to analyze (default: active object)
Returns a detailed quality report with actionable metrics.
"""
try:
params = {}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("get_topology_quality", params)
if "error" in result:
return f"Error: {result['error']}"
# Format the quality report
output = f"Topology Quality Report: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
# Face composition
output += "FACE COMPOSITION:\n"
output += f" Total Faces: {result.get('total_faces', 0)}\n"
output += f" Quads: {result.get('quad_count', 0)} ({result.get('quad_percentage', 0):.1f}%)\n"
output += f" Triangles: {result.get('tri_count', 0)} ({result.get('tri_percentage', 0):.1f}%)\n"
output += f" N-gons: {result.get('ngon_count', 0)} ({result.get('ngon_percentage', 0):.1f}%)\n\n"
# Vertex valence
output += "VERTEX VALENCE:\n"
output += f" Total Vertices: {result.get('total_vertices', 0)}\n"
valence = result.get('valence_distribution', {})
output += f" Valence 3 (E-poles): {valence.get(3, 0)}\n"
output += f" Valence 4 (regular): {valence.get(4, 0)} ({result.get('valence_4_ratio', 0):.1f}%)\n"
output += f" Valence 5 (N-poles): {valence.get(5, 0)}\n"
output += f" Valence 6+: {valence.get('6+', 0)}\n"
output += f" Total Poles: {result.get('pole_count', 0)}\n\n"
# Aspect ratio
output += "FACE QUALITY:\n"
output += f" Avg Aspect Ratio: {result.get('avg_aspect_ratio', 1.0):.2f}\n"
output += f" Elongated Faces (>4:1): {result.get('bad_aspect_count', 0)}\n\n"
# Overall score
score = result.get('quality_score', 0)
if score >= 85:
grade = "A - Excellent"
elif score >= 70:
grade = "B - Good"
elif score >= 55:
grade = "C - Acceptable"
elif score >= 40:
grade = "D - Needs Work"
else:
grade = "F - Poor"
output += "OVERALL QUALITY:\n"
output += f" Score: {score:.1f}/100\n"
output += f" Grade: {grade}\n\n"
# Recommendations
output += "RECOMMENDATIONS:\n"
if result.get('ngon_percentage', 0) > 0:
output += " - Convert n-gons to quads (use tris_to_quads or manual)\n"
if result.get('quad_percentage', 0) < 85:
output += " - Increase quad ratio (target: 85%+)\n"
if result.get('pole_count', 0) > result.get('total_vertices', 1) * 0.2:
output += " - Reduce pole count by improving edge flow\n"
if result.get('avg_aspect_ratio', 1) > 3:
output += " - Improve face proportions with relax_vertices\n"
if score >= 85:
output += " - Mesh is ready for production!\n"
return output
except Exception as e:
logger.error(f"Error getting topology quality: {str(e)}")
return f"Error getting topology quality: {str(e)}"
def analyze_mesh_regions(ctx: Context, blender_connection, object_name: str = None) -> str:
"""
Analyze mesh by dividing into spatial regions for localized quality assessment.
Identifies problem areas in the mesh that need attention.
Parameters:
- object_name: Name of object to analyze (default: active object)
Returns a regional breakdown with quality metrics per area.
"""
try:
params = {}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("analyze_mesh_regions", params)
if "error" in result:
return f"Error: {result['error']}"
output = f"Regional Analysis: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
regions = result.get('regions', [])
problem_zones = result.get('problem_zones', [])
output += f"Total Regions: {len(regions)}\n"
output += f"Problem Zones: {len(problem_zones)}\n\n"
for region in regions:
region_id = region.get('id', 0)
is_problem = region_id in problem_zones
marker = " [PROBLEM]" if is_problem else ""
output += f"Region {region_id}{marker}:\n"
output += f" Center: ({region['center'][0]:.2f}, {region['center'][1]:.2f}, {region['center'][2]:.2f})\n"
output += f" Faces: {region.get('face_count', 0)}\n"
output += f" Type: {region.get('dominant_type', 'unknown')}\n"
output += f" Quad %: {region.get('quad_percentage', 0):.1f}%\n"
output += f" Quality: {region.get('quality_score', 0):.1f}/100\n"
issues = region.get('issues', [])
if issues:
output += f" Issues: {', '.join(issues)}\n"
output += "\n"
if problem_zones:
output += "FOCUS AREAS:\n"
output += f" Regions needing attention: {problem_zones}\n"
output += " Use inspect_region tool for detailed view\n"
return output
except Exception as e:
logger.error(f"Error analyzing mesh regions: {str(e)}")
return f"Error analyzing mesh regions: {str(e)}"
def get_vertex_valence(
ctx: Context,
blender_connection,
object_name: str = None,
min_valence: int = 5
) -> str:
"""
Get vertices with valence (edge count) >= threshold.
Identifies potential topology problems like high-valence poles.
Parameters:
- object_name: Name of object to analyze (default: active object)
- min_valence: Minimum valence to include (default: 5)
Returns list of vertices with index, location, and valence.
"""
try:
params = {"min_valence": min_valence}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("get_vertex_valence", params)
if "error" in result:
return f"Error: {result['error']}"
vertices = result.get('vertices', [])
count = result.get('count', 0)
output = f"Vertex Valence Report: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
output += f"Filter: valence >= {result.get('min_valence', 5)}\n"
output += f"Found: {count} vertices\n\n"
if count == 0:
output += "No vertices match the criteria. Mesh has clean topology!\n"
else:
output += "HIGH-VALENCE VERTICES:\n"
for vert in vertices[:20]: # Limit output
loc = vert.get('location', {})
output += f" Vertex {vert['index']}: valence {vert['valence']}"
output += f" at ({loc.get('x', 0):.3f}, {loc.get('y', 0):.3f}, {loc.get('z', 0):.3f})\n"
if count > 20:
output += f"\n ... and {count - 20} more vertices\n"
output += "\nUse focus_view_on_point to navigate to problem vertices.\n"
return output
except Exception as e:
logger.error(f"Error getting vertex valence: {str(e)}")
return f"Error getting vertex valence: {str(e)}"
def get_triangle_faces(
ctx: Context,
blender_connection,
object_name: str = None
) -> str:
"""
Get all triangle faces (3 vertices) in the mesh.
Identifies triangles that may need conversion to quads.
Parameters:
- object_name: Name of object to analyze (default: active object)
Returns list of triangles with index, center, and vertex indices.
"""
try:
params = {}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("get_triangle_faces", params)
if "error" in result:
return f"Error: {result['error']}"
triangles = result.get('triangles', [])
count = result.get('count', 0)
output = f"Triangle Faces Report: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
output += f"Found: {count} triangle faces\n\n"
if count == 0:
output += "No triangles found. Mesh is quad-only or has only n-gons.\n"
else:
output += "TRIANGLE FACES:\n"
for tri in triangles[:15]: # Limit output
center = tri.get('center', {})
output += f" Face {tri['index']}: vertices {tri['vertices']}"
output += f" center ({center.get('x', 0):.3f}, {center.get('y', 0):.3f}, {center.get('z', 0):.3f})\n"
if count > 15:
output += f"\n ... and {count - 15} more triangles\n"
output += "\nUse tris_to_quads to convert compatible triangle pairs.\n"
output += "Use select_triangles to select all triangles for manual editing.\n"
return output
except Exception as e:
logger.error(f"Error getting triangle faces: {str(e)}")
return f"Error getting triangle faces: {str(e)}"
def get_ngon_faces(
ctx: Context,
blender_connection,
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.
Parameters:
- object_name: Name of object to analyze (default: active object)
Returns list of n-gons with index, center, and vertex count.
"""
try:
params = {}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("get_ngon_faces", params)
if "error" in result:
return f"Error: {result['error']}"
ngons = result.get('ngons', [])
count = result.get('count', 0)
output = f"N-gon Faces Report: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
output += f"Found: {count} n-gon faces (5+ vertices)\n\n"
if count == 0:
output += "No n-gons found. Mesh has clean quad/tri topology!\n"
else:
output += "N-GON FACES:\n"
for ngon in ngons[:15]: # Limit output
center = ngon.get('center', {})
output += f" Face {ngon['index']}: {ngon['vertex_count']} vertices"
output += f" center ({center.get('x', 0):.3f}, {center.get('y', 0):.3f}, {center.get('z', 0):.3f})\n"
if count > 15:
output += f"\n ... and {count - 15} more n-gons\n"
output += "\nN-gons should be manually dissolved or triangulated.\n"
output += "Use select_ngons to select all n-gons for editing.\n"
return output
except Exception as e:
logger.error(f"Error getting n-gon faces: {str(e)}")
return f"Error getting n-gon faces: {str(e)}"
def analyze_cylindrical_structure(
ctx: Context,
blender_connection,
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.
"""
try:
params = {"analyze_spacing": analyze_spacing}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("analyze_cylindrical_structure", params)
if "error" in result:
return f"Error: {result['error']}"
output = f"Cylindrical Structure Analysis: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
if result.get('is_cylindrical'):
output += "STRUCTURE: Cylindrical detected\n\n"
else:
output += "STRUCTURE: Not clearly cylindrical (irregular radius)\n\n"
output += "LOOP PROPERTIES:\n"
output += f" Segment count: {result.get('segment_count', 0)}\n"
output += f" Loop vertices: {result.get('loop_vertex_count', 0)}\n"
output += f" Primary axis: {result.get('primary_axis', 'Unknown')}\n"
output += f" Average radius: {result.get('avg_radius', 0):.4f}\n"
center = result.get('center', [0, 0, 0])
output += f" Center: ({center[0]:.3f}, {center[1]:.3f}, {center[2]:.3f})\n\n"
output += "OPTIMIZATION:\n"
output += f" Suggested segments: {result.get('suggested_segments', 8)}\n"
current = result.get('segment_count', 0)
suggested = result.get('suggested_segments', 8)
if current != suggested:
output += f" Note: Consider adjusting from {current} to {suggested} segments\n"
else:
output += " Segment count is optimal!\n"
if analyze_spacing and 'edge_loop_count' in result:
output += f"\nSPACING ANALYSIS:\n"
output += f" Edge loops found: {result.get('edge_loop_count', 1)}\n"
output += f" Average spacing: {result.get('average_spacing', 0):.4f}\n"
return output
except Exception as e:
logger.error(f"Error analyzing cylindrical structure: {str(e)}")
return f"Error analyzing cylindrical structure: {str(e)}"
def find_support_loops(
ctx: Context,
blender_connection,
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.
"""
try:
params = {
"min_angle": min_angle,
"near_sharp": near_sharp,
"suggest": suggest
}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("find_support_loops", params)
if "error" in result:
return f"Error: {result['error']}"
output = f"Support Loop Analysis: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
output += f"Search criteria:\n"
output += f" Min angle: {result.get('min_angle', 30)}°\n"
output += f" Near sharp edges: {result.get('near_sharp', True)}\n\n"
# Sharp edges found
sharp_count = result.get('sharp_edge_count', 0)
output += f"Sharp edges found: {sharp_count}\n"
# Candidate edges
candidates = result.get('candidate_edges', [])
output += f"Candidate support edges: {len(candidates)}\n\n"
if candidates:
output += "CANDIDATE EDGES (by angle):\n"
for i, edge in enumerate(candidates[:20]):
output += f" Edge {edge.get('index', i)}: angle {edge.get('angle', 0):.1f}°\n"
if len(candidates) > 20:
output += f" ... and {len(candidates) - 20} more edges\n"
output += "\n"
# Support loop suggestions
suggestions = result.get('support_suggestions', [])
if suggest and suggestions:
output += "SUGGESTED SUPPORT LOOP LOCATIONS:\n"
for i, sug in enumerate(suggestions[:10]):
loc = sug.get('location', [0, 0, 0])
output += f" {i+1}. Near ({loc[0]:.3f}, {loc[1]:.3f}, {loc[2]:.3f})"
if 'reason' in sug:
output += f" - {sug['reason']}"
output += "\n"
output += "\n"
# Summary
if sharp_count == 0 and len(candidates) == 0:
output += "CONCLUSION: No support loops required.\n"
output += "Mesh appears smooth without hard edges.\n"
elif len(candidates) > 0:
output += "RECOMMENDATION:\n"
output += " Consider adding support loops near candidate edges\n"
output += " to maintain edge sharpness after subdivision.\n"
return output
except Exception as e:
logger.error(f"Error finding support loops: {str(e)}")
return f"Error finding support loops: {str(e)}"