Skip to content

Instantly share code, notes, and snippets.

@AndrewAltimit
Last active July 23, 2025 02:48
Show Gist options
  • Save AndrewAltimit/c437c9fbc9a72271969127fcbf935561 to your computer and use it in GitHub Desktop.
Save AndrewAltimit/c437c9fbc9a72271969127fcbf935561 to your computer and use it in GitHub Desktop.
Manim MCP Integration

MCP Server Manim Integration

A complete implementation guide for integrating Manim (Mathematical Animation Engine) with MCP (Model Context Protocol) servers, enabling AI assistants to create programmatic animations and visualizations. See the template repository for a complete example.

🖼️ Example Output

Nanite System Git Ignore
Merge Conflict Gitflow

📚 Documentation

🎯 Features

  • Containerized Execution: No local Manim installation required
  • Multiple Output Formats: MP4, GIF, and PNG support
  • Quality Settings: From low (360p) to 4K resolution
  • Automatic File Management: Organized output with timestamps
  • MCP Protocol Integration: Clean AI-assistant interface

📋 Tool Parameters

Parameter Type Required Default Description
code string Yes - Python code with Manim Scene class
output_name string No "animation" Base filename for output
quality string No "medium" Rendering quality: low/medium/high/fourk
format string No "mp4" Output format: mp4/gif/png

🎬 Example Usage

Note: MCP servers communicate via stdio, not HTTP. The container runs a wrapper script (run_server.py) to keep it alive. For actual MCP usage, you'll need to connect your MCP client to the container's stdio.

For testing the tool directly:

# Access the container
docker exec -it mcp-manim-server python3

# In Python, test the tool
from mcp_manim_tool import ManimTool
from pathlib import Path
import asyncio

tool = ManimTool(Path("/workspace/animations"))
result = asyncio.run(tool.create_animation(
    code='''from manim import *
class Test(Scene):
    def construct(self):
        self.play(Write(Text("Hello World!")))
''',
    output_name="test",
    format="gif"
))
print(result)

🔧 Requirements

  • Docker and Docker Compose

Setup Guide

This guide walks through setting up the MCP Manim integration step by step.

Prerequisites

Before starting, ensure you have:

  • Docker Desktop or Docker Engine installed
  • Docker Compose v2.0+
  • Git (for cloning the gist)
  • At least 4GB of available RAM for rendering

Installation Steps

1. Clone the Repository

git clone https://gist.github.com/AndrewAltimit/c437c9fbc9a72271969127fcbf935561 mcp-manim
cd mcp-manim

2. Build the Docker Image

docker-compose build

This process will:

  • Download the Python 3.13 slim base image
  • Install system dependencies (Cairo, Pango, FFmpeg)
  • Install Python packages (Manim, MCP, etc.)
  • Configure the MCP server

Expected build time: 5-10 minutes depending on internet speed.

3. Start the Container

docker-compose up -d

Verify the container is running:

docker-compose ps

You should see:

NAME                STATUS              PORTS
mcp-manim-server    Up 30 seconds       (healthy)

Note: MCP servers use stdio (standard input/output) for communication, not HTTP. The container runs a wrapper script to stay alive. To actually use the MCP server, you need to connect an MCP client to the container's stdio.

4. Test the Installation

To test the MCP server directly:

# Access the container
docker exec -it mcp-manim-server bash

# Run Python and test the tool
python3
>>> from mcp_manim_tool import ManimTool
>>> from pathlib import Path
>>> tool = ManimTool(Path("/workspace/animations"))
>>> # Create a simple test animation
>>> import asyncio
>>> result = asyncio.run(tool.create_animation(
...     code='''from manim import *
... class Test(Scene):
...     def construct(self):
...         self.play(Write(Text("It works!")))
... ''',
...     output_name="test"
... ))
>>> print(result)

5. Using with MCP Clients

To use this server with an MCP client (like Claude Desktop), you'll need to configure the client to connect to the container's stdio. The exact configuration depends on your MCP client.

5. Test the Installation

Create a simple test animation:

from manim import *

class TestScene(Scene):
    def construct(self):
        text = Text("MCP Manim Works!", font_size=48)
        self.play(Write(text))
        self.wait()

Environment Variables

You can customize behavior with these environment variables:

# .env
MCP_PORT=8000
MCP_LOG_LEVEL=INFO
MANIM_QUALITY_DEFAULT=medium
ANIMATION_OUTPUT_DIR=animations

Network Configuration

If you encounter DNS issues during build:

  1. Add to docker-compose.yml:
build:
  network: host
  1. Or use Google's DNS:
dns:
  - 8.8.8.8
  - 8.8.4.4

Verification

Run the verification script:

docker-compose exec mcp-server python3 -c "
import manim
import mcp
print('✅ Manim version:', manim.__version__)
print('✅ MCP imported successfully')
print('✅ Server ready for animations!')
"

Next Steps

Updating

To update the installation:

# Pull latest changes
git pull

# Rebuild the image
docker-compose build --no-cache

# Restart the server
docker-compose restart

Implementation

This document provides technical details about the MCP server integration with Manim.

Architecture Overview

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│                 │     │                  │     │                 │
│  AI Assistant   ├────►│   MCP Server     ├────►│     Manim       │
│  (Claude, etc)  │     │  (Python async)  │     │   (Container)   │
│                 │     │                  │     │                 │
└─────────────────┘     └──────────────────┘     └─────────────────┘
         │                       │                         │
         │  Tool Request         │  Execute Python        │
         └──────────────────────►│  in isolated env       │
                                 └────────────────────────►│

Key Components

1. MCP Server Base (MCPManimServer)

class MCPManimServer:
    """MCP Server with Manim integration."""
    
    def __init__(self, port: int = 8000):
        self.server = Server("mcp-manim-server")
        self.port = port
        self.project_root = Path(os.getenv('MCP_PROJECT_ROOT', '/workspace'))
        self.animation_tool = ManimTool(
            self.project_root / os.getenv('ANIMATION_OUTPUT_DIR', 'animations')
        )
  • Initializes MCP server instance
  • Sets up project paths from environment variables
  • Creates ManimTool instance for animation generation

2. Manim Tool (ManimTool)

The core animation creation logic:

async def create_animation(
    self,
    code: str,
    output_name: str = "animation",
    quality: str = "medium",
    format: str = "mp4"
) -> Dict[str, Any]:

Process Flow:

  1. Validation: Check input code and extract Scene class name
  2. Temporary Environment: Create isolated temp directory
  3. Code Execution: Write Python code to file and execute Manim
  4. Output Management: Find generated file and copy to output directory
  5. Response Formatting: Return structured result with metadata

3. Scene Detection

def _extract_scene_name(self, code: str) -> Optional[str]:
    """Extract the Scene class name from code."""
    for line in code.split('\n'):
        if 'class ' in line and '(Scene)' in line:
            parts = line.split('class ')[1].split('(')[0]
            return parts.strip()
    return None

Simple but effective regex-based detection of Manim Scene classes.

4. Command Building

def _build_manim_command(
    self,
    script_path: Path,
    scene_name: str,
    quality: str,
    format: str
) -> List[str]:

Quality mappings:

  • low: 480p15 (-ql)
  • medium: 720p30 (-qm)
  • high: 1080p60 (-qh)
  • fourk: 4K60 (-qk)

Format-specific flags:

  • GIF: --format=gif
  • PNG: -s (save last frame)
  • MP4: Default, no extra flags

5. Output File Discovery

def _find_output_file(self, temp_path: Path, format: str) -> Optional[Path]:
    """Find the output file created by Manim."""
    media_dir = temp_path / 'media'
    pattern = extensions.get(format, '*.mp4')
    
    for file_path in media_dir.rglob(pattern):
        if 'partial_movie_files' not in str(file_path):
            return file_path

Searches Manim's media directory structure, skipping intermediate files.

Error Handling

The implementation includes comprehensive error handling:

  1. Input Validation: Empty code, missing Scene class
  2. Execution Errors: Manim process failures with stderr capture
  3. Output Errors: Missing output files
  4. Async Exceptions: Proper exception logging and user-friendly messages

Async Execution

Uses Python's asyncio for non-blocking execution:

process = await asyncio.create_subprocess_exec(
    *cmd,
    cwd=temp_dir,
    stdout=asyncio.subprocess.PIPE,
    stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()

This allows the MCP server to handle multiple animation requests concurrently.

File Management

  • Temporary Files: Uses Python's tempfile.TemporaryDirectory() for automatic cleanup
  • Output Organization: Timestamps in filenames prevent collisions
  • Path Safety: All paths use pathlib.Path for cross-platform compatibility

Performance Considerations

  1. Container Caching: Manim cache volume persists between runs
  2. Parallel Execution: Async design allows concurrent animations
  3. Resource Limits: Docker container limits CPU/memory usage
  4. Output Size: File size tracking helps monitor disk usage

Security

  • Code Isolation: Each animation runs in isolated temp directory
  • Container Sandbox: Docker provides additional security layer
  • No File System Access: Animation code can't access host filesystem
  • Resource Limits: Container prevents resource exhaustion

Extensibility

The design allows easy extension:

  1. New Parameters: Add to tool registration and ManimTool.create_animation
  2. Custom Scenes: Support additional Manim features through code parameter
  3. Output Processing: Post-process animations before returning
  4. Multiple Engines: Could support other animation libraries with similar interface

Examples

This document provides complete examples of animations you can create with the MCP Manim integration.

Basic Examples

1. Simple Shapes and Transformations

from manim import *

class SimpleDemo(Scene):
    def construct(self):
        # Create and animate basic shapes
        circle = Circle(radius=1.5, color=BLUE, fill_opacity=0.5)
        square = Square(side_length=3, color=GREEN, fill_opacity=0.5)
        
        self.play(Create(circle))
        self.play(Transform(circle, square))
        self.wait()

2. Text and Mathematical Equations

from manim import *

class MathDemo(Scene):
    def construct(self):
        # Display text
        title = Text("Euler's Identity", font_size=48)
        self.play(Write(title))
        self.play(title.animate.to_edge(UP))
        
        # Show famous equation
        equation = MathTex(r"e^{i\pi} + 1 = 0")
        self.play(Write(equation))
        self.wait(2)

3. Function Graphs

from manim import *
import numpy as np

class GraphDemo(Scene):
    def construct(self):
        # Create axes
        axes = Axes(
            x_range=[-3, 3, 1],
            y_range=[-2, 2, 1],
            axis_config={"color": BLUE},
        )
        
        # Plot functions
        sin_graph = axes.plot(lambda x: np.sin(x), color=GREEN)
        cos_graph = axes.plot(lambda x: np.cos(x), color=RED)
        
        # Animate
        self.play(Create(axes))
        self.play(Create(sin_graph), Create(cos_graph))
        self.wait()

Advanced Examples

4. 3D Animations

from manim import *

class ThreeDDemo(ThreeDScene):
    def construct(self):
        # Set up 3D axes
        axes = ThreeDAxes()
        
        # Create 3D objects
        sphere = Sphere(radius=1, color=BLUE)
        cube = Cube(side_length=2, color=RED)
        
        # Position objects
        sphere.shift(LEFT * 3)
        cube.shift(RIGHT * 3)
        
        # Animate with camera movement
        self.set_camera_orientation(phi=60 * DEGREES, theta=45 * DEGREES)
        self.play(Create(axes))
        self.play(Create(sphere), Create(cube))
        self.play(
            Rotate(sphere, angle=PI, axis=UP),
            Rotate(cube, angle=PI/2, axis=RIGHT)
        )
        self.wait()

5. Complex Technical Visualization (UE5 Nanite)

See nanite_system.py

Animation Techniques

6. Custom Animations with UpdaterFunctions

from manim import *

class UpdaterDemo(Scene):
    def construct(self):
        # Create a value tracker
        tracker = ValueTracker(0)
        
        # Create a number that updates based on tracker
        number = DecimalNumber(0, num_decimal_places=2)
        number.add_updater(lambda m: m.set_value(tracker.get_value()))
        
        # Create a circle that changes size
        circle = Circle(radius=1)
        circle.add_updater(
            lambda m: m.set_width(2 + tracker.get_value())
        )
        
        self.add(number, circle)
        self.play(tracker.animate.set_value(3), run_time=3)
        self.wait()

7. Particle Systems

from manim import *
import random

class ParticleDemo(Scene):
    def construct(self):
        particles = VGroup()
        
        # Create particles
        for _ in range(50):
            particle = Dot(
                point=ORIGIN,
                radius=0.05,
                color=random.choice([BLUE, GREEN, YELLOW, RED])
            )
            particles.add(particle)
        
        self.add(particles)
        
        # Animate particles spreading out
        animations = []
        for particle in particles:
            direction = np.array([
                random.uniform(-1, 1),
                random.uniform(-1, 1),
                0
            ])
            animations.append(
                particle.animate.shift(direction * 3)
            )
        
        self.play(*animations, run_time=2)
        self.wait()

MCP Tool Usage Examples

Basic Animation Request

{
  "tool": "create_manim_animation",
  "arguments": {
    "code": "from manim import *\n\nclass HelloWorld(Scene):\n    def construct(self):\n        text = Text('Hello, World!')\n        self.play(Write(text))\n        self.wait()",
    "output_name": "hello_world",
    "quality": "medium",
    "format": "mp4"
  }
}

High-Quality GIF Export

{
  "tool": "create_manim_animation",
  "arguments": {
    "code": "<your_manim_code_here>",
    "output_name": "my_animation",
    "quality": "high",
    "format": "gif"
  }
}

4K Video Render

{
  "tool": "create_manim_animation",
  "arguments": {
    "code": "<your_manim_code_here>",
    "output_name": "ultra_hd_animation",
    "quality": "fourk",
    "format": "mp4"
  }
}

Best Practices

  1. Scene Structure

    • Always inherit from Scene (or ThreeDScene for 3D)
    • Use the construct method for your animation logic
    • Keep scenes focused on a single concept
  2. Performance

    • Use VGroup to batch similar objects
    • Minimize the number of objects for GIF exports
    • Use lower quality for testing, high/4K for final renders
  3. Visual Design

    • Use consistent color schemes
    • Add appropriate wait times between animations
    • Label important elements with text
  4. Code Organization

    • Define helper methods for complex object creation
    • Use descriptive variable names
    • Comment complex animation sequences

Output Examples

The examples above can generate various outputs:

  • MP4 Videos: Smooth, high-quality animations
  • GIFs: Great for documentation and sharing
  • PNG Frames: Single frames for static visualization

Example outputs are available in the assets/ directory of this gist.

Troubleshooting

Common issues and solutions when using the MCP Manim integration.

Installation Issues

Docker Build Failures

Problem: Docker build fails with network errors

Temporary failure in name resolution

Solution: Add DNS configuration to docker-compose.yml

build:
  network: host  # Use host network for DNS resolution
volumes:
  - /etc/resolv.conf:/etc/resolv.conf:ro  # Share host DNS

Missing Dependencies

Problem: Build fails with missing system packages

error: Microsoft Visual C++ 14.0 is required (Windows)
No such file or directory: 'gcc' (Linux)

Solution: Ensure Dockerfile includes build dependencies

RUN apt-get update && apt-get install -y \
    build-essential \
    libcairo2-dev \
    libpango1.0-dev \
    ffmpeg

MCP Package Not Found

Problem: ModuleNotFoundError: No module named 'mcp'

Solution: Use the correct package name

# requirements.txt
mcp-python>=0.1.0  # NOT just "mcp"

Runtime Errors

Scene Not Found

Problem: "No Scene class found in code"

Solution: Ensure your code has a proper Scene class

from manim import *

class MyAnimation(Scene):  # Must inherit from Scene
    def construct(self):   # Must have construct method
        pass

Animation Creation Failed

Problem: Manim process returns non-zero exit code

Common Causes:

  1. Syntax errors in Python code
  2. Import errors - missing Manim imports
  3. Invalid Manim operations

Debugging Steps:

  1. Check the error details in the response
  2. Test the code locally first
  3. Ensure all imports are included

Output File Not Found

Problem: Animation completes but no output file

Possible Causes:

  1. Scene has no animations (empty construct method)
  2. Wrong output format specified
  3. Manim skipped rendering

Solution: Ensure your scene actually creates content

def construct(self):
    text = Text("Hello")
    self.play(Write(text))  # Must have at least one animation
    self.wait()             # Or a wait command

Performance Issues

Slow Rendering

Problem: Animations take too long to generate

Solutions:

  1. Use lower quality for testing

    {"quality": "low"}  # 480p15 instead of 1080p60
  2. Reduce scene complexity

    • Fewer objects
    • Simpler animations
    • Shorter duration
  3. Enable Manim caching (already configured in docker-compose)

Large Output Files

Problem: GIF files are too large

Solutions:

  1. Use MP4 format instead when possible
  2. Reduce animation duration
  3. Lower the quality setting
  4. Simplify the scene content

Container Issues

MCP Server Not Starting

Problem: Container starts but MCP server doesn't respond

Debugging:

# Check container logs
docker-compose logs mcp-manim-server

# Verify server is running
docker-compose ps

# Test health check
docker-compose exec mcp-manim-server python3 -c "print('Server is responsive')"

Permission Errors

Problem: Cannot write to output directory

Solution: Ensure proper volume permissions

# On host, check permissions
ls -la animations/

# Fix if needed
chmod 755 animations/

Out of Disk Space

Problem: Animations fail with disk space errors

Solution: Clean up old files and Docker resources

# Remove old animations
rm -rf animations/*.mp4

# Clean Docker resources
docker system prune -f
docker volume prune -f

Code-Specific Issues

3D Animations Not Working

Problem: 3D scenes appear flat or don't render

Solution: Use ThreeDScene and set camera

class My3DScene(ThreeDScene):  # Note: ThreeDScene, not Scene
    def construct(self):
        self.set_camera_orientation(phi=60*DEGREES, theta=45*DEGREES)
        # Your 3D animations here

Text Rendering Issues

Problem: Text appears garbled or doesn't render

Solutions:

  1. Use basic fonts
  2. Avoid special characters in some cases
  3. For math, use MathTex instead of Text
# Good
equation = MathTex(r"\int_0^1 x^2 dx")

# Instead of
equation = Text("∫₀¹ x² dx")  # May not render correctly

Color Issues

Problem: Colors don't appear as expected

Solution: Use Manim color constants

# Good
circle = Circle(color=BLUE)

# Avoid
circle = Circle(color="#0000FF")  # May not work as expected

Debugging Tips

1. Enable Debug Logging

Set environment variable:

environment:
  - LOG_LEVEL=DEBUG

2. Test Locally First

Before using through MCP, test your Manim code:

# Inside container
docker-compose exec mcp-manim-server bash
cd /tmp
echo "your_code_here" > test.py
manim -ql test.py YourSceneName

3. Check Intermediate Files

Look in the media directory for clues:

ls -la media/videos/*/
ls -la media/images/*/

4. Validate JSON Requests

Ensure your MCP tool requests are properly formatted:

# Valid JSON
{
  "tool": "create_manim_animation",
  "arguments": {
    "code": "...",
    "output_name": "test"
  }
}

Getting Help

If you encounter issues not covered here:

  1. Check Manim documentation: https://docs.manim.community/
  2. Review MCP documentation: https://modelcontextprotocol.io/
  3. Check container logs for detailed error messages
  4. Ensure you're using compatible versions of all dependencies
services:
mcp-manim-server:
build:
context: .
dockerfile: Dockerfile
network: host # Helps with DNS resolution during build
container_name: mcp-manim-server
restart: unless-stopped
stdin_open: true
tty: true
# Note: MCP servers use stdio, not HTTP, so port mapping isn't used
# ports:
# - "8000:8000"
environment:
- MCP_PROJECT_ROOT=/workspace
- PYTHONPATH=/workspace
- MANIM_QUALITY_DEFAULT=${MANIM_QUALITY_DEFAULT:-medium}
- ANIMATION_OUTPUT_DIR=/workspace/animations
- LOG_LEVEL=${LOG_LEVEL:-INFO}
volumes:
# Project files (assuming you are adding new files not in the container already)
- ./examples:/workspace/examples:ro
# Output directories
- ./animations:/workspace/animations
- ./logs:/workspace/logs
# Cache directories for faster rendering
- manim-cache:/home/mcp/.cache/manim
# Optional: Share host DNS for better reliability
- /etc/resolv.conf:/etc/resolv.conf:ro
healthcheck:
test: ["CMD", "python3", "-c", "import mcp, manim; print('OK')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- mcp-network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
volumes:
manim-cache:
driver: local
networks:
mcp-network:
driver: bridge
# MCP Server with Manim Integration
FROM python:3.13-slim
# Metadata
LABEL maintainer="MCP Manim Integration"
LABEL description="MCP server with Manim animation capabilities"
LABEL version="1.0.0"
# Environment variables
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
# Manim settings
MANIM_QUALITY_DEFAULT=medium \
ANIMATION_OUTPUT_DIR=/workspace/animations
# Install system dependencies
RUN apt-get update && apt-get install -y \
# Version control
git \
# Network tools
curl \
wget \
# Build essentials for Python packages
build-essential \
pkg-config \
# Manim system dependencies
libcairo2-dev \
libpango1.0-dev \
libglib2.0-dev \
libffi-dev \
# Media libraries
ffmpeg \
# Additional tools
nano \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# Create workspace
WORKDIR /workspace
# Install Python dependencies
COPY requirements.txt /tmp/
RUN pip install --upgrade pip setuptools wheel && \
pip install --no-cache-dir -r /tmp/requirements.txt && \
# Verify installations
python -c "import manim; print(f'Manim {manim.__version__} installed')" && \
python -c "import mcp; print('MCP SDK installed')" && \
rm /tmp/requirements.txt
# Create non-root user
RUN useradd --create-home --shell /bin/bash mcp && \
mkdir -p /workspace/animations /workspace/logs && \
chown -R mcp:mcp /workspace
# Copy application files
COPY --chown=mcp:mcp *.py /workspace/
RUN chmod +x /workspace/run_server.py
# Switch to non-root user
USER mcp
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python3 -c "import mcp, manim; print('OK')" || exit 1
# Expose MCP server port
EXPOSE 8000
# Start wrapper to keep container running
# For actual MCP usage, connect your client to stdio
CMD ["python3", "run_server.py"]
#!/usr/bin/env python3
"""
MCP Server with Manim Integration
Provides animation creation capabilities through the Model Context Protocol.
"""
import asyncio
import json
import logging
import os
import shutil
import sys
import tempfile
import time
from pathlib import Path
from typing import Any, Dict, List, Optional
import mcp.server.stdio
import mcp.types as types
from mcp.server import NotificationOptions, Server, InitializationOptions
from pydantic import AnyUrl
# Configure logging
logging.basicConfig(
level=os.getenv('LOG_LEVEL', 'INFO'),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class ManimTool:
"""Manim animation creation tool for MCP."""
def __init__(self, output_dir: Path):
self.output_dir = output_dir
self.output_dir.mkdir(exist_ok=True, parents=True)
async def create_animation(
self,
code: str,
output_name: str = "animation",
quality: str = "medium",
format: str = "mp4"
) -> Dict[str, Any]:
"""
Create a Manim animation from Python code.
Args:
code: Python code containing a Scene class
output_name: Base name for the output file
quality: Rendering quality (low, medium, high, fourk)
format: Output format (mp4, gif, png)
Returns:
Dictionary with status and output information
"""
start_time = time.time()
# Validate inputs
if not code:
return {"status": "error", "message": "No code provided"}
# Extract scene name
scene_name = self._extract_scene_name(code)
if not scene_name:
return {"status": "error", "message": "No Scene class found in code"}
# Create temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Write code to temporary file
script_path = temp_path / f"{output_name}.py"
script_path.write_text(code)
# Build Manim command
cmd = self._build_manim_command(
script_path, scene_name, quality, format
)
logger.info(f"Executing Manim: {' '.join(cmd)}")
# Execute Manim
try:
process = await asyncio.create_subprocess_exec(
*cmd,
cwd=temp_dir,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
logger.error(f"Manim failed: {stderr.decode()}")
return {
"status": "error",
"message": "Animation creation failed",
"details": stderr.decode()
}
# Find output file
output_file = self._find_output_file(temp_path, format)
if not output_file:
return {
"status": "error",
"message": "Output file not found"
}
# Copy to output directory
timestamp = time.strftime('%Y%m%d_%H%M%S')
final_filename = f"{output_name}_{timestamp}.{format}"
final_path = self.output_dir / final_filename
shutil.copy2(output_file, final_path)
execution_time = time.time() - start_time
file_size_kb = final_path.stat().st_size / 1024
logger.info(f"Animation created: {final_filename} ({file_size_kb:.1f} KB)")
return {
"status": "success",
"filename": final_filename,
"path": str(final_path),
"size_kb": file_size_kb,
"execution_time": execution_time,
"scene": scene_name,
"quality": quality,
"format": format
}
except Exception as e:
logger.exception("Error during animation creation")
return {
"status": "error",
"message": f"Exception: {str(e)}"
}
def _extract_scene_name(self, code: str) -> Optional[str]:
"""Extract the Scene class name from code."""
for line in code.split('\n'):
if 'class ' in line and '(Scene)' in line:
# Extract class name
parts = line.split('class ')[1].split('(')[0]
return parts.strip()
return None
def _build_manim_command(
self,
script_path: Path,
scene_name: str,
quality: str,
format: str
) -> List[str]:
"""Build the Manim command line."""
quality_flags = {
'low': '-ql',
'medium': '-qm',
'high': '-qh',
'fourk': '-qk'
}
cmd = ['manim', quality_flags.get(quality, '-qm')]
# Add format-specific flags
if format == 'gif':
cmd.append('--format=gif')
elif format == 'png':
cmd.append('-s') # Save last frame
cmd.extend([str(script_path), scene_name])
return cmd
def _find_output_file(self, temp_path: Path, format: str) -> Optional[Path]:
"""Find the output file created by Manim."""
media_dir = temp_path / 'media'
extensions = {
'mp4': '*.mp4',
'gif': '*.gif',
'png': '*.png'
}
pattern = extensions.get(format, '*.mp4')
# Search for output file
for file_path in media_dir.rglob(pattern):
# Skip partial files
if 'partial_movie_files' not in str(file_path):
return file_path
return None
class MCPManimServer:
"""MCP Server with Manim integration."""
def __init__(self, port: int = 8000):
self.server = Server("mcp-manim-server")
self.port = port
self.project_root = Path(os.getenv('MCP_PROJECT_ROOT', '/workspace'))
self.animation_tool = ManimTool(
self.project_root / os.getenv('ANIMATION_OUTPUT_DIR', 'animations')
)
self._setup_tools()
def _setup_tools(self):
"""Register MCP tools."""
@self.server.call_tool()
async def create_manim_animation(arguments: Dict[str, Any]) -> List[types.TextContent]:
"""
Create a Manim animation from Python code.
Parameters:
- code: Python code containing a Manim Scene class
- output_name: Base filename (default: "animation")
- quality: "low", "medium", "high", or "fourk" (default: "medium")
- format: "mp4", "gif", or "png" (default: "mp4")
"""
result = await self.animation_tool.create_animation(
code=arguments.get('code', ''),
output_name=arguments.get('output_name', 'animation'),
quality=arguments.get('quality', 'medium'),
format=arguments.get('format', 'mp4')
)
# Format response
if result['status'] == 'success':
response = f"""🎬 Animation Created Successfully!
📄 File: {result['filename']}
📁 Location: {result['path']}
📏 Size: {result['size_kb']:.1f} KB
⏱️ Time: {result['execution_time']:.2f}s
🎯 Quality: {result['quality']}
📼 Format: {result['format']}
🎭 Scene: {result['scene']}"""
else:
response = f"""❌ Animation Creation Failed
Error: {result['message']}
{result.get('details', '')}"""
return [types.TextContent(type="text", text=response)]
def run(self):
"""Run the MCP server."""
logger.info(f"Starting MCP Manim server on port {self.port}")
async def main():
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await self.server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="mcp-manim-server",
server_version="1.0.0",
capabilities=self.server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
asyncio.run(main())
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="MCP Server with Manim Integration")
parser.add_argument(
"--port",
type=int,
default=8000,
help="Port to run the server on"
)
args = parser.parse_args()
server = MCPManimServer(port=args.port)
server.run()
"""
Unreal Engine 5 Nanite Virtualized Geometry System Visualization
This animation demonstrates how Nanite dynamically adjusts mesh complexity based on screen space.
"""
from manim import *
import numpy as np
class NaniteSystem(Scene):
"""Visualization of UE5's Nanite virtualized geometry technology."""
def construct(self):
# Title sequence
self.show_title()
# Demonstrate LOD concept
self.show_lod_concept()
# Show Nanite's virtualized geometry
self.show_nanite_virtualization()
# Demonstrate cluster hierarchy
self.show_cluster_hierarchy()
# Performance comparison
self.show_performance_comparison()
# Conclusion
self.show_conclusion()
def show_title(self):
"""Display title and introduction."""
# Main title
title = Text("Unreal Engine 5", font_size=48, weight=BOLD)
subtitle = Text("Nanite Virtualized Geometry", font_size=36, color=BLUE)
subtitle.next_to(title, DOWN, buff=0.5)
# Epic Games credit
credit = Text("Revolutionary Technology by Epic Games", font_size=20, color=GRAY)
credit.next_to(subtitle, DOWN, buff=1)
self.play(Write(title))
self.play(FadeIn(subtitle))
self.play(FadeIn(credit))
self.wait(2)
self.play(FadeOut(title), FadeOut(subtitle), FadeOut(credit))
def show_lod_concept(self):
"""Demonstrate traditional LOD vs Nanite approach."""
section_title = Text("Traditional LOD vs Nanite", font_size=36, weight=BOLD)
section_title.to_edge(UP)
self.play(Write(section_title))
# Traditional LOD representation
lod_label = Text("Traditional LOD System", font_size=20)
lod_label.shift(LEFT * 4 + UP * 2)
# Create LOD meshes
lod0 = self.create_mesh(resolution=20, color=BLUE)
lod1 = self.create_mesh(resolution=10, color=GREEN)
lod2 = self.create_mesh(resolution=5, color=YELLOW)
lod3 = self.create_mesh(resolution=3, color=RED)
lods = VGroup(lod0, lod1, lod2, lod3).arrange(RIGHT, buff=0.5)
lods.shift(LEFT * 3)
# Labels
lod_labels = VGroup(
Text("LOD 0\n(High)", font_size=14),
Text("LOD 1\n(Medium)", font_size=14),
Text("LOD 2\n(Low)", font_size=14),
Text("LOD 3\n(Lowest)", font_size=14)
)
for i, label in enumerate(lod_labels):
label.next_to(lods[i], DOWN, buff=0.2)
self.play(Write(lod_label))
self.play(*[Create(lod) for lod in lods])
self.play(*[Write(label) for label in lod_labels])
self.wait(2)
# Show LOD popping
pop_text = Text("❌ LOD Popping", font_size=18, color=RED)
pop_text.next_to(lods, DOWN, buff=1)
self.play(
Transform(lod0, lod1),
Write(pop_text)
)
self.wait(1)
# Nanite approach
nanite_label = Text("Nanite Continuous LOD", font_size=20)
nanite_label.shift(RIGHT * 4 + UP * 2)
nanite_mesh = self.create_continuous_lod_mesh()
nanite_mesh.shift(RIGHT * 4)
self.play(
FadeOut(pop_text),
Write(nanite_label),
Create(nanite_mesh)
)
# Show smooth transition
smooth_text = Text("✓ Smooth Transitions", font_size=18, color=GREEN)
smooth_text.next_to(nanite_mesh, DOWN, buff=1)
self.play(
nanite_mesh.animate.scale(0.5),
Write(smooth_text)
)
self.wait(2)
# Clear scene
self.play(*[FadeOut(mob) for mob in self.mobjects if mob != section_title])
self.play(FadeOut(section_title))
def show_nanite_virtualization(self):
"""Show how Nanite virtualizes geometry."""
title = Text("Virtualized Geometry Pipeline", font_size=36, weight=BOLD)
title.to_edge(UP)
self.play(Write(title))
# Source mesh
source_label = Text("Source Mesh\n(Billions of triangles)", font_size=16)
source_mesh = self.create_detailed_mesh()
source_mesh.shift(LEFT * 5)
source_label.next_to(source_mesh, DOWN, buff=0.5)
self.play(Create(source_mesh), Write(source_label))
# Cluster hierarchy
clusters = self.create_cluster_representation()
clusters.move_to(ORIGIN)
cluster_label = Text("Hierarchical Clusters", font_size=16)
cluster_label.next_to(clusters, DOWN, buff=0.5)
# Arrows showing process
arrow1 = Arrow(source_mesh.get_right(), clusters.get_left(), color=YELLOW)
arrow2 = Arrow(clusters.get_right(), RIGHT * 4, color=YELLOW)
self.play(Create(arrow1))
self.play(Create(clusters), Write(cluster_label))
# Rendered output
output_mesh = self.create_mesh(resolution=15, color=GREEN)
output_mesh.shift(RIGHT * 5)
output_label = Text("Rendered Output\n(Pixel-perfect detail)", font_size=16)
output_label.next_to(output_mesh, DOWN, buff=0.5)
self.play(Create(arrow2))
self.play(Create(output_mesh), Write(output_label))
# Streaming indicator
stream_text = Text("Streaming only visible clusters", font_size=20, color=BLUE)
stream_text.to_edge(DOWN)
self.play(Write(stream_text))
self.wait(3)
self.play(*[FadeOut(mob) for mob in self.mobjects])
def show_cluster_hierarchy(self):
"""Demonstrate the cluster hierarchy system."""
title = Text("Cluster Hierarchy System", font_size=36, weight=BOLD)
title.to_edge(UP)
self.play(Write(title))
# Create hierarchical structure
root = Circle(radius=0.3, color=GOLD, fill_opacity=0.8)
root_text = Text("Root", font_size=12, color=BLACK)
root_text.move_to(root.get_center())
root_group = VGroup(root, root_text)
root_group.to_edge(UP).shift(DOWN * 1.5)
# Level 1 clusters
level1_clusters = VGroup()
for i in range(3):
cluster = Circle(radius=0.25, color=BLUE, fill_opacity=0.8)
text = Text(f"C{i}", font_size=10, color=WHITE)
text.move_to(cluster.get_center())
cluster_group = VGroup(cluster, text)
level1_clusters.add(cluster_group)
level1_clusters.arrange(RIGHT, buff=1.5)
level1_clusters.next_to(root_group, DOWN, buff=1.5)
# Level 2 clusters (leaf nodes)
level2_clusters = VGroup()
for i in range(9):
cluster = Circle(radius=0.2, color=GREEN, fill_opacity=0.8)
text = Text(f"L{i}", font_size=8, color=WHITE)
text.move_to(cluster.get_center())
cluster_group = VGroup(cluster, text)
level2_clusters.add(cluster_group)
# Arrange level 2 clusters under level 1
for i in range(3):
sub_clusters = VGroup(*[level2_clusters[j] for j in range(i*3, (i+1)*3)])
sub_clusters.arrange(RIGHT, buff=0.3)
sub_clusters.next_to(level1_clusters[i], DOWN, buff=0.8)
# Draw connections
connections = VGroup()
# Root to level 1
for cluster in level1_clusters:
line = Line(root_group.get_bottom(), cluster[0].get_top(), color=GRAY)
connections.add(line)
# Level 1 to level 2
for i in range(3):
for j in range(3):
line = Line(
level1_clusters[i].get_bottom(),
level2_clusters[i*3 + j][0].get_top(),
color=GRAY
)
connections.add(line)
# Animate construction
self.play(FadeIn(root_group))
self.play(
*[Create(line) for line in connections[:3]],
*[FadeIn(cluster) for cluster in level1_clusters]
)
self.play(
*[Create(line) for line in connections[3:]],
*[FadeIn(cluster) for cluster in level2_clusters]
)
# Show culling
culling_text = Text("Only visible clusters are loaded", font_size=20, color=YELLOW)
culling_text.to_edge(DOWN)
self.play(Write(culling_text))
# Highlight visible clusters
visible_clusters = [level2_clusters[2], level2_clusters[3], level2_clusters[7]]
self.play(
*[cluster.animate.set_color(YELLOW) for cluster in visible_clusters],
*[cluster.animate.set_opacity(0.3) for cluster in level2_clusters if cluster not in visible_clusters]
)
self.wait(3)
self.play(*[FadeOut(mob) for mob in self.mobjects])
def show_performance_comparison(self):
"""Show performance benefits of Nanite."""
title = Text("Performance Impact", font_size=36, weight=BOLD)
title.to_edge(UP)
self.play(Write(title))
# Create comparison chart
chart_title = Text("Triangle Count Performance", font_size=24)
chart_title.next_to(title, DOWN, buff=0.5)
# Axes
axes = Axes(
x_range=[0, 10, 1],
y_range=[0, 100, 20],
x_length=8,
y_length=5,
axis_config={"color": WHITE},
x_axis_config={"numbers_to_include": np.arange(0, 11, 2)},
y_axis_config={"numbers_to_include": np.arange(0, 101, 20)}
)
# Labels
x_label = Text("Billions of Triangles", font_size=16)
x_label.next_to(axes.x_axis, DOWN)
y_label = Text("FPS", font_size=16)
y_label.next_to(axes.y_axis, LEFT).rotate(PI/2)
self.play(Write(chart_title))
self.play(Create(axes), Write(x_label), Write(y_label))
# Traditional rendering curve (drops quickly)
traditional_curve = axes.plot(
lambda x: 90 * np.exp(-x/2),
x_range=[0, 10],
color=RED
)
traditional_label = Text("Traditional", font_size=14, color=RED)
traditional_label.next_to(traditional_curve.point_from_proportion(0.2), UP)
# Nanite curve (stays relatively flat)
nanite_curve = axes.plot(
lambda x: 85 - 2*x,
x_range=[0, 10],
color=GREEN
)
nanite_label = Text("Nanite", font_size=14, color=GREEN)
nanite_label.next_to(nanite_curve.point_from_proportion(0.8), UP)
self.play(
Create(traditional_curve),
Write(traditional_label)
)
self.play(
Create(nanite_curve),
Write(nanite_label)
)
# Highlight the difference
highlight = Text(
"Nanite maintains performance with massive geometry",
font_size=20,
color=YELLOW
)
highlight.to_edge(DOWN)
self.play(Write(highlight))
self.wait(3)
self.play(*[FadeOut(mob) for mob in self.mobjects])
def show_conclusion(self):
"""Show conclusion and key benefits."""
# Title
title = Text("Nanite Benefits", font_size=48, weight=BOLD)
self.play(Write(title))
self.wait(1)
self.play(title.animate.to_edge(UP))
# Benefits list
benefits = VGroup(
Text("• Film-quality assets in real-time", font_size=24),
Text("• No more LOD authoring", font_size=24),
Text("• Pixel-scale geometric detail", font_size=24),
Text("• Automatic streaming", font_size=24),
Text("• Massive open worlds", font_size=24)
).arrange(DOWN, aligned_edge=LEFT, buff=0.5)
for benefit in benefits:
self.play(Write(benefit))
self.wait(0.5)
# Final message
final = VGroup(
Text("Nanite", font_size=60, weight=BOLD, color=GOLD),
Text("The Future of Real-Time Rendering", font_size=30, color=BLUE)
).arrange(DOWN, buff=0.5)
self.wait(2)
self.play(
FadeOut(title),
FadeOut(benefits),
Write(final[0]),
FadeIn(final[1])
)
self.wait(3)
def create_mesh(self, resolution=10, color=BLUE):
"""Create a simple mesh representation."""
vertices = []
for i in range(resolution):
for j in range(resolution):
x = (i - resolution/2) * 0.1
y = (j - resolution/2) * 0.1
z = 0.1 * np.sin(x*2) * np.cos(y*2)
vertices.append([x, y, z])
# Create dots for vertices
dots = VGroup(*[
Dot(point=v, radius=0.02, color=color)
for v in vertices
])
return dots
def create_continuous_lod_mesh(self):
"""Create a mesh that appears to have continuous LOD."""
# Create gradient mesh
mesh = VGroup()
for i in range(20):
for j in range(20):
radius = 0.02 * (1 - (i + j) / 40) # Decrease size gradually
opacity = 0.8 * (1 - (i + j) / 40) # Decrease opacity
x = (i - 10) * 0.1
y = (j - 10) * 0.1
dot = Dot(
point=[x, y, 0],
radius=radius,
color=BLUE,
fill_opacity=opacity
)
mesh.add(dot)
return mesh
def create_detailed_mesh(self):
"""Create a highly detailed mesh representation."""
# Simulate high detail with many small dots
mesh = VGroup()
for i in range(30):
for j in range(30):
x = (i - 15) * 0.05
y = (j - 15) * 0.05
z = 0.1 * np.sin(x*3) * np.cos(y*3)
dot = Dot(
point=[x, y, z],
radius=0.01,
color=PURPLE,
fill_opacity=0.8
)
mesh.add(dot)
return mesh
def create_cluster_representation(self):
"""Create a visual representation of geometry clusters."""
clusters = VGroup()
# Create multiple clusters of different sizes
cluster_configs = [
{"center": [-1, 1, 0], "size": 0.8, "color": RED},
{"center": [1, 1, 0], "size": 0.6, "color": BLUE},
{"center": [-1, -1, 0], "size": 0.7, "color": GREEN},
{"center": [1, -1, 0], "size": 0.5, "color": YELLOW},
{"center": [0, 0, 0], "size": 0.9, "color": PURPLE}
]
for config in cluster_configs:
cluster = Circle(
radius=config["size"],
color=config["color"],
fill_opacity=0.3,
stroke_width=2
)
cluster.move_to(config["center"])
# Add some dots inside each cluster
for _ in range(10):
angle = np.random.uniform(0, 2*PI)
r = np.random.uniform(0, config["size"] * 0.8)
x = config["center"][0] + r * np.cos(angle)
y = config["center"][1] + r * np.sin(angle)
dot = Dot(
point=[x, y, 0],
radius=0.03,
color=config["color"]
)
clusters.add(dot)
clusters.add(cluster)
return clusters
# MCP SDK
mcp-python>=0.1.0
# Core dependencies
pydantic>=2.0.0
pyyaml>=6.0
aiofiles>=23.0.0
# Manim and its dependencies
manim>=0.18.0
numpy>=1.26.0
scipy>=1.11.0
pillow>=10.0.0
# Optional performance improvements
watchdog>=3.0.0 # File watching for development
python-dotenv>=1.0.0 # Environment variable management
#!/usr/bin/env python3
"""
Wrapper script to keep MCP server container running.
For production use, integrate with your MCP client instead.
"""
import subprocess
import sys
import time
print("=== MCP Manim Server Container ===")
print("This container is running an MCP server via stdio.")
print("To use it, connect an MCP client to this container's stdio.")
print("")
print("For testing animations directly, you can:")
print("1. docker exec -it mcp-manim-server python3")
print("2. Import and use the ManimTool class directly")
print("")
print("Container will stay running. Press Ctrl+C to stop.")
try:
# Keep the container alive
while True:
time.sleep(60)
print(".", end="", flush=True)
except KeyboardInterrupt:
print("\nShutting down...")
sys.exit(0)
@Samar-Bons
Copy link

This is awesome. How do I set up the MCP server with Claude Code?

@AndrewAltimit
Copy link
Author

AndrewAltimit commented Jul 20, 2025

This is awesome. How do I set up the MCP server with Claude Code?

I have a template repository with it setup for Claude Code, this particular section is the relevant server you will want in your .mcp.json file for manim and this is the script it uses

That being said, I'm updating the template repo all the time, as I'm going to keep adding MCP tools and AI agents to it, so I may reorganize things to keep it tidy in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment