#!/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()