Skip to main content
Glama
server.py13.9 kB
#!/usr/bin/env python3 """ Manim MCP Server This server provides tools for creating mathematical animations using Manim. It exposes MCP tools that allow Claude Desktop to generate video artifacts of mathematical explanations. """ import asyncio import json import logging import os import sys import tempfile import traceback from pathlib import Path from typing import Any import mcp.server.stdio import mcp.types as types from mcp.server import NotificationOptions, Server from mcp.server.models import InitializationOptions # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger("manim-mcp") # Create MCP server instance server = Server("manim-mcp") # Default output directory for rendered videos OUTPUT_DIR = Path(tempfile.gettempdir()) / "manim_mcp_output" OUTPUT_DIR.mkdir(exist_ok=True) logger.info(f"Manim MCP Server initialized. Output directory: {OUTPUT_DIR}") @server.list_tools() async def handle_list_tools() -> list[types.Tool]: """ List available tools. Each tool allows Claude to create mathematical animations. """ return [ types.Tool( name="create_math_animation", description=( "Create a mathematical animation using Manim. " "Provide Python code that defines a Manim Scene class with mathematical content. " "The scene will be rendered to a video file. " "Returns the path to the generated video file." ), inputSchema={ "type": "object", "properties": { "scene_code": { "type": "string", "description": ( "Python code defining a Manim Scene. Must include a class that inherits " "from Scene and implements a construct() method. " "Example:\n" "from manim import *\n\n" "class MyScene(Scene):\n" " def construct(self):\n" " text = Text('Hello, Manim!')\n" " self.play(Write(text))\n" " self.wait()" ), }, "scene_name": { "type": "string", "description": "Name of the Scene class to render (e.g., 'MyScene')", }, "quality": { "type": "string", "enum": ["low", "medium", "high"], "description": "Render quality: low (480p15), medium (720p30), high (1080p60)", "default": "medium", }, "output_filename": { "type": "string", "description": "Optional custom filename for the output video (without extension)", }, }, "required": ["scene_code", "scene_name"], }, ), types.Tool( name="get_manim_example", description=( "Get example Manim code for common mathematical animations. " "Useful for learning Manim syntax and creating starting templates." ), inputSchema={ "type": "object", "properties": { "example_type": { "type": "string", "enum": [ "basic_text", "equation", "graph", "geometry", "transformation", "calculus", ], "description": "Type of example to retrieve", }, }, "required": ["example_type"], }, ), ] @server.call_tool() async def handle_call_tool( name: str, arguments: dict[str, Any] | None ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: """ Handle tool execution requests. """ try: if name == "create_math_animation": return await create_math_animation(arguments or {}) elif name == "get_manim_example": return await get_manim_example(arguments or {}) else: raise ValueError(f"Unknown tool: {name}") except Exception as e: logger.error(f"Error executing tool {name}: {e}") logger.error(traceback.format_exc()) return [ types.TextContent( type="text", text=f"Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}", ) ] async def create_math_animation(args: dict[str, Any]) -> list[types.TextContent]: """ Create and render a Manim animation. """ scene_code = args.get("scene_code", "") scene_name = args.get("scene_name", "") quality = args.get("quality", "medium") output_filename = args.get("output_filename") if not scene_code or not scene_name: return [ types.TextContent( type="text", text="Error: Both 'scene_code' and 'scene_name' are required.", ) ] # Quality mapping quality_flags = { "low": ["-ql"], # 480p15 "medium": ["-qm"], # 720p30 "high": ["-qh"], # 1080p60 } quality_args = quality_flags.get(quality, ["-qm"]) # Create a temporary directory for this render with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) scene_file = temp_path / "scene.py" # Write the scene code to a file try: scene_file.write_text(scene_code) except Exception as e: return [ types.TextContent( type="text", text=f"Error writing scene code to file: {e}", ) ] # Run Manim to render the scene try: # Build the manim command cmd = [ sys.executable, "-m", "manim", *quality_args, str(scene_file), scene_name, "-o", output_filename or scene_name, ] logger.info(f"Executing: {' '.join(cmd)}") # Execute the command process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, cwd=temp_path, ) stdout, stderr = await process.communicate() stdout_text = stdout.decode() stderr_text = stderr.decode() logger.info(f"Manim stdout: {stdout_text}") if stderr_text: logger.warning(f"Manim stderr: {stderr_text}") if process.returncode != 0: return [ types.TextContent( type="text", text=f"Manim rendering failed with return code {process.returncode}\n\n" f"STDOUT:\n{stdout_text}\n\nSTDERR:\n{stderr_text}", ) ] # Find the generated video file media_dir = temp_path / "media" video_files = list(media_dir.rglob("*.mp4")) if not video_files: return [ types.TextContent( type="text", text=f"No video file generated.\n\nSTDOUT:\n{stdout_text}\n\nSTDERR:\n{stderr_text}", ) ] # Use the most recently created video file video_file = max(video_files, key=lambda p: p.stat().st_mtime) # Copy to output directory with a unique name final_filename = output_filename or scene_name final_path = OUTPUT_DIR / f"{final_filename}.mp4" # Ensure unique filename counter = 1 while final_path.exists(): final_path = OUTPUT_DIR / f"{final_filename}_{counter}.mp4" counter += 1 # Copy the file import shutil shutil.copy2(video_file, final_path) return [ types.TextContent( type="text", text=f"Animation rendered successfully!\n\n" f"Video file: {final_path}\n" f"Quality: {quality}\n" f"Scene: {scene_name}\n\n" f"You can view the video at: {final_path}", ) ] except Exception as e: logger.error(f"Error rendering animation: {e}") logger.error(traceback.format_exc()) return [ types.TextContent( type="text", text=f"Error rendering animation: {e}\n\n{traceback.format_exc()}", ) ] async def get_manim_example(args: dict[str, Any]) -> list[types.TextContent]: """ Get example Manim code for common use cases. """ example_type = args.get("example_type", "basic_text") examples = { "basic_text": """from manim import * class BasicText(Scene): def construct(self): # Create text title = Text("Mathematical Animation", font_size=48) subtitle = Text("Created with Manim", font_size=32) subtitle.next_to(title, DOWN) # Animate self.play(Write(title)) self.play(FadeIn(subtitle)) self.wait(2) self.play(FadeOut(title), FadeOut(subtitle)) """, "equation": """from manim import * class EquationExample(Scene): def construct(self): # Create equation equation = MathTex(r"e^{i\\pi} + 1 = 0") # Animate writing the equation self.play(Write(equation)) self.wait(2) # Transform to another equation equation2 = MathTex(r"\\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}") self.play(Transform(equation, equation2)) self.wait(2) """, "graph": """from manim import * class GraphExample(Scene): def construct(self): # Create axes axes = Axes( x_range=[-3, 3, 1], y_range=[-3, 3, 1], x_length=6, y_length=6, ) # Create graph graph = axes.plot(lambda x: x**2, color=BLUE) graph_label = axes.get_graph_label(graph, label='f(x) = x^2') # Animate self.play(Create(axes)) self.play(Create(graph), Write(graph_label)) self.wait(2) """, "geometry": """from manim import * class GeometryExample(Scene): def construct(self): # Create shapes circle = Circle(radius=1, color=BLUE) square = Square(side_length=2, color=RED) triangle = Triangle(color=GREEN) # Position shapes circle.shift(LEFT * 2.5) triangle.shift(RIGHT * 2.5) # Animate self.play(Create(circle), Create(square), Create(triangle)) self.wait(1) self.play(Rotate(square, PI/4), Rotate(triangle, PI/3)) self.wait(2) """, "transformation": """from manim import * class TransformationExample(Scene): def construct(self): # Create circle circle = Circle(color=BLUE) # Animate creation self.play(Create(circle)) self.wait(1) # Transform to square square = Square(color=RED) self.play(Transform(circle, square)) self.wait(1) # Transform to text text = Text("Manim!") self.play(Transform(circle, text)) self.wait(2) """, "calculus": """from manim import * class CalculusExample(Scene): def construct(self): # Create axes axes = Axes( x_range=[0, 4, 1], y_range=[0, 10, 2], x_length=7, y_length=5, ) # Create curve curve = axes.plot(lambda x: x**2, color=BLUE) area = axes.get_area(curve, x_range=[0, 2], color=BLUE, opacity=0.3) # Labels title = MathTex(r"\\int_0^2 x^2 dx = \\frac{8}{3}") title.to_edge(UP) # Animate self.play(Write(title)) self.play(Create(axes), Create(curve)) self.play(FadeIn(area)) self.wait(2) """, } example_code = examples.get(example_type, examples["basic_text"]) return [ types.TextContent( type="text", text=f"Example Manim code for '{example_type}':\n\n{example_code}\n\n" f"To use this example, call create_math_animation with:\n" f"- scene_code: (the code above)\n" f"- scene_name: (the class name, e.g., 'BasicText')", ) ] async def main(): """Run the MCP server.""" logger.info("Starting Manim MCP Server...") async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, InitializationOptions( server_name="manim-mcp", server_version="0.1.0", capabilities=server.get_capabilities( notification_options=NotificationOptions(), experimental_capabilities={}, ), ), ) def main_sync(): """Synchronous entry point for the MCP server (used by console scripts).""" asyncio.run(main()) if __name__ == "__main__": main_sync()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Stelath/manim-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server