#
tokens: 5695/50000 3/3 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── README.md
└── server.py
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
__pycache__

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# MCP PPTX Server
This is a FastMCP server that can be used to create and manipulate PowerPoints.

## How to install to Claude Desktop
```bash
fastmcp install .\server.py
```
```

--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------

```python
"""
FastMCP PowerPoint Manager
Give Claude tools to create and manipulate PowerPoint presentations.
"""
from fastmcp import FastMCP, Image
import io
from typing import List, Optional, Dict, Any, Union
from pydantic import BaseModel, Field

# Create server with dependencies
mcp = FastMCP(
    "PowerPoint Manager", 
    dependencies=["python-pptx", "Pillow"]
)

# === Models for structured inputs ===

class TextFormatting(BaseModel):
    """Text formatting options"""
    font_name: Optional[str] = Field(None, description="Font name (e.g., 'Calibri', 'Arial')")
    font_size: Optional[int] = Field(None, description="Font size in points (e.g., 12, 18, 24)")
    bold: Optional[bool] = Field(None, description="Bold text")
    italic: Optional[bool] = Field(None, description="Italic text")
    color: Optional[str] = Field(None, description="Text color in hex (e.g., '#000000' for black)")

class BulletPoint(BaseModel):
    """Model for a bullet point with text and level"""
    text: str = Field(..., description="Text content of the bullet point")
    level: int = Field(0, description="Indentation level (0 = main, 1 = sub-bullet, etc.)")
    formatting: Optional[TextFormatting] = Field(None, description="Optional formatting for this bullet point")

class Slide(BaseModel):
    """Model for slide content with professional formatting"""
    title: Optional[str] = Field(None, description="Slide title")
    subtitle: Optional[str] = Field(None, description="Slide subtitle (for title slides)")
    bullets: Optional[List[BulletPoint]] = Field(None, description="List of bullet points with levels")
    content: Optional[str] = Field(None, description="Plain text content (as alternative to bullets)")
    layout: str = Field("Title and Content", description="Slide layout name")
    title_formatting: Optional[TextFormatting] = Field(None, description="Formatting for the title")
    content_formatting: Optional[TextFormatting] = Field(None, description="Base formatting for content")
    notes: Optional[str] = Field(None, description="Speaker notes for this slide")

class TextPosition(BaseModel):
    """Model for text position and formatting"""
    left: float = Field(..., description="Left position in inches")
    top: float = Field(..., description="Top position in inches")
    width: Optional[float] = Field(None, description="Width in inches")
    height: Optional[float] = Field(None, description="Height in inches")
    formatting: Optional[TextFormatting] = Field(None, description="Text formatting options")

class PresentationTheme(BaseModel):
    """Model for presentation-wide theme settings"""
    title_font: Optional[TextFormatting] = Field(None, description="Formatting for slide titles")
    body_font: Optional[TextFormatting] = Field(None, description="Formatting for body text")
    background_color: Optional[str] = Field(None, description="Background color in hex")

# === Helper Functions ===

def apply_text_formatting(text_frame, formatting):
    """Apply text formatting to a text frame"""
    from pptx.dml.color import RGBColor
    from pptx.util import Pt
    
    if not formatting:
        return
        
    for paragraph in text_frame.paragraphs:
        for run in paragraph.runs:
            if formatting.font_name:
                run.font.name = formatting.font_name
                
            if formatting.font_size:
                run.font.size = Pt(formatting.font_size)
                
            if formatting.bold is not None:
                run.font.bold = formatting.bold
                
            if formatting.italic is not None:
                run.font.italic = formatting.italic
                
            if formatting.color:
                # Convert hex color to RGB
                color = formatting.color.lstrip('#')
                r, g, b = tuple(int(color[i:i+2], 16) for i in (0, 2, 4))
                run.font.color.rgb = RGBColor(r, g, b)

def apply_theme_to_presentation(prs, theme):
    """Apply a theme to all slides in the presentation"""
    if not theme:
        return
        
    # Apply to each slide
    for slide in prs.slides:
        # Apply title formatting
        if theme.title_font and slide.shapes.title:
            apply_text_formatting(slide.shapes.title.text_frame, theme.title_font)
        
        # Apply body formatting to all text placeholders
        if theme.body_font:
            for shape in slide.placeholders:
                if hasattr(shape, 'text_frame'):
                    apply_text_formatting(shape.text_frame, theme.body_font)
        
        # Apply background color
        if theme.background_color:
            color = theme.background_color.lstrip('#')
            r, g, b = tuple(int(color[i:i+2], 16) for i in (0, 2, 4))
            from pptx.dml.color import RGBColor
            slide.background.fill.solid()
            slide.background.fill.fore_color.rgb = RGBColor(r, g, b)

# === Core Presentation Tools ===

@mcp.tool()
def create_presentation(title: str, slides: List[Slide], theme: Optional[PresentationTheme] = None) -> str:
    """
    Create a new PowerPoint presentation with the given title and slides.
    Applies professional formatting and layout.
    Returns the filename of the saved presentation.
    """
    from pptx import Presentation
    from pptx.util import Pt
    import os
    
    # Create a new presentation
    prs = Presentation()
    
    # Create a user-accessible directory
    user_docs = os.path.expanduser("~/Documents/ClaudePresentations")
    os.makedirs(user_docs, exist_ok=True)
    
    # Process each slide
    for slide_data in slides:
        # Get the appropriate layout
        layout_name = slide_data.layout
        layout = None
        
        # Find the right layout by name
        for layout_idx, layout_obj in enumerate(prs.slide_layouts):
            if layout_obj.name.lower() == layout_name.lower():
                layout = layout_obj
                break
        
        # If layout not found, default to Title and Content
        if not layout:
            layout = prs.slide_layouts[1]  # Title and Content
        
        # Add a slide
        slide = prs.slides.add_slide(layout)
        
        # Set title if applicable and if the slide has a title placeholder
        if slide_data.title and hasattr(slide, 'shapes') and hasattr(slide.shapes, 'title') and slide.shapes.title:
            slide.shapes.title.text = slide_data.title
            
            # Apply title formatting
            if slide_data.title_formatting:
                apply_text_formatting(slide.shapes.title.text_frame, slide_data.title_formatting)
        
        # Set subtitle if applicable (usually for title slides)
        if slide_data.subtitle:
            for shape in slide.placeholders:
                if shape.placeholder_format.type == 2:  # Subtitle placeholder
                    shape.text = slide_data.subtitle
                    break
        
        # Add content - either as bullets or plain text
        if slide_data.bullets or slide_data.content:
            content_shape = None
            
            # Find the content placeholder
            for shape in slide.placeholders:
                if shape.placeholder_format.type not in [1, 2]:  # Not title or subtitle
                    content_shape = shape
                    break
            
            if content_shape:
                text_frame = content_shape.text_frame
                text_frame.clear()  # Clear any default text
                
                # If using structured bullets
                if slide_data.bullets:
                    for i, bullet in enumerate(slide_data.bullets):
                        if i == 0:
                            p = text_frame.paragraphs[0]
                        else:
                            p = text_frame.add_paragraph()
                        
                        p.text = bullet.text
                        p.level = bullet.level
                        
                        # Apply individual bullet formatting if specified
                        if bullet.formatting:
                            for run in p.runs:
                                if bullet.formatting.font_name:
                                    run.font.name = bullet.formatting.font_name
                                if bullet.formatting.font_size:
                                    run.font.size = Pt(bullet.formatting.font_size)
                                if bullet.formatting.bold is not None:
                                    run.font.bold = bullet.formatting.bold
                                if bullet.formatting.italic is not None:
                                    run.font.italic = bullet.formatting.italic
                
                # If using simple content
                elif slide_data.content:
                    lines = slide_data.content.strip().split('\n')
                    for i, line in enumerate(lines):
                        if i == 0:
                            p = text_frame.paragraphs[0]
                        else:
                            p = text_frame.add_paragraph()
                        p.text = line
                
                # Apply overall content formatting
                if slide_data.content_formatting:
                    apply_text_formatting(text_frame, slide_data.content_formatting)
        
        # Add speaker notes if provided
        if slide_data.notes:
            notes_slide = slide.notes_slide
            notes_slide.notes_text_frame.text = slide_data.notes
    
    # Apply theme if provided
    if theme:
        apply_theme_to_presentation(prs, theme)
    
    # Save the presentation in the user-accessible directory
    filename = f"{title.replace(' ', '_')}.pptx"
    full_path = os.path.join(user_docs, filename)
    prs.save(full_path)
    
    return f"Created presentation at: {full_path}"


@mcp.tool()
def add_slide(filename: str, new_slide: Slide) -> str:
    """
    Add a professionally formatted slide to an existing presentation.
    Returns the filename of the updated presentation.
    """
    from pptx import Presentation
    import os
    
    # If filename doesn't include path, assume it's in the user directory
    if not os.path.dirname(filename):
        user_docs = os.path.expanduser("~/Documents/ClaudePresentations")
        filename = os.path.join(user_docs, filename)
    
    # Check if file exists
    if not os.path.exists(filename):
        return f"Error: File not found: {filename}"
    
    # Open the existing presentation
    prs = Presentation(filename)
    
    # Get the appropriate layout
    layout_name = new_slide.layout
    layout = None
    for layout_idx, layout_obj in enumerate(prs.slide_layouts):
        if layout_obj.name.lower() == layout_name.lower():
            layout = layout_obj
            break
    
    # If layout not found, default to Title and Content
    if not layout:
        layout = prs.slide_layouts[1]  # Title and Content
    
    # Add a slide
    slide = prs.slides.add_slide(layout)
    
    # Set title if applicable
    if new_slide.title and hasattr(slide, 'shapes') and hasattr(slide.shapes, 'title') and slide.shapes.title:
        slide.shapes.title.text = new_slide.title
        
        # Apply title formatting
        if new_slide.title_formatting:
            apply_text_formatting(slide.shapes.title.text_frame, new_slide.title_formatting)
    
    # Set subtitle if applicable (usually for title slides)
    if new_slide.subtitle:
        for shape in slide.placeholders:
            if shape.placeholder_format.type == 2:  # Subtitle placeholder
                shape.text = new_slide.subtitle
                break
    
    # Add content - either as bullets or plain text
    if new_slide.bullets or new_slide.content:
        content_shape = None
        
        # Find the content placeholder
        for shape in slide.placeholders:
            if shape.placeholder_format.type not in [1, 2]:  # Not title or subtitle
                content_shape = shape
                break
        
        if content_shape:
            text_frame = content_shape.text_frame
            text_frame.clear()  # Clear any default text
            
            # If using structured bullets
            if new_slide.bullets:
                for i, bullet in enumerate(new_slide.bullets):
                    if i == 0:
                        p = text_frame.paragraphs[0]
                    else:
                        p = text_frame.add_paragraph()
                    
                    p.text = bullet.text
                    p.level = bullet.level
                    
                    # Apply individual bullet formatting if specified
                    if bullet.formatting:
                        for run in p.runs:
                            if bullet.formatting.font_name:
                                run.font.name = bullet.formatting.font_name
                            if bullet.formatting.font_size:
                                run.font.size = Pt(bullet.formatting.font_size)
                            if bullet.formatting.bold is not None:
                                run.font.bold = bullet.formatting.bold
                            if bullet.formatting.italic is not None:
                                run.font.italic = bullet.formatting.italic
            
            # If using simple content
            elif new_slide.content:
                lines = new_slide.content.strip().split('\n')
                for i, line in enumerate(lines):
                    if i == 0:
                        p = text_frame.paragraphs[0]
                    else:
                        p = text_frame.add_paragraph()
                    p.text = line
            
            # Apply overall content formatting
            if new_slide.content_formatting:
                apply_text_formatting(text_frame, new_slide.content_formatting)
    
    # Add speaker notes if provided
    if new_slide.notes:
        notes_slide = slide.notes_slide
        notes_slide.notes_text_frame.text = new_slide.notes
    
    # Save the presentation
    prs.save(filename)
    return f"Updated presentation at: {filename}"


@mcp.tool()
def list_available_layouts() -> str:
    """
    List all available slide layouts with descriptions to help create professional slides.
    """
    from pptx import Presentation
    
    # Create a temporary presentation to inspect layouts
    prs = Presentation()
    
    layouts = []
    layouts.append("# Available Slide Layouts\n")
    layouts.append("Use these layouts when creating slides for professional presentations:\n")
    
    layout_descriptions = {
        "Title Slide": "Main title slide with title and subtitle",
        "Title and Content": "Standard slide with title and content area (bullets, text, etc.)",
        "Section Header": "Section divider slide with large title",
        "Two Content": "Side-by-side content areas with title",
        "Comparison": "Side-by-side content with title and headings for comparison",
        "Title Only": "Just a title, rest of slide is blank for custom content",
        "Blank": "Completely blank slide with no placeholders",
        "Content with Caption": "Content with side caption text",
        "Picture with Caption": "Picture with caption text below"
    }
    
    for i, layout in enumerate(prs.slide_layouts):
        layout_name = layout.name
        description = layout_descriptions.get(layout_name, "Standard slide layout")
        layouts.append(f"## {layout_name}")
        layouts.append(f"{description}")
        
        # Count placeholders to help understand what's available
        placeholder_counts = {}
        for shape in layout.placeholders:
            ph_type = shape.placeholder_format.type
            name = {1: "Title", 2: "Subtitle/Body", 3: "Date", 4: "Slide Number", 
                   5: "Footer", 7: "Content", 18: "Picture"}.get(ph_type, f"Type {ph_type}")
            
            placeholder_counts[name] = placeholder_counts.get(name, 0) + 1
        
        if placeholder_counts:
            layouts.append("Contains: " + ", ".join(f"{count} {name}" for name, count in placeholder_counts.items()))
        
        layouts.append("")  # Empty line between layouts
    
    return "\n".join(layouts)


@mcp.tool()
def get_slide_preview(filename: str, slide_index: int = 0) -> Image:
    """
    Generate a preview image of a specific slide from the presentation.
    Returns the slide as an image.
    """
    from pptx import Presentation
    from PIL import Image as PILImage, ImageDraw, ImageFont
    import io
    import os
    
    # If filename doesn't include path, assume it's in the user directory
    if not os.path.dirname(filename):
        user_docs = os.path.expanduser("~/Documents/ClaudePresentations")
        filename = os.path.join(user_docs, filename)
    
    try:
        # Open the presentation
        prs = Presentation(filename)
        
        # Make sure slide index is valid
        if slide_index < 0 or slide_index >= len(prs.slides):
            slide_index = 0
            
        # Create a representation of the slide
        img = PILImage.new('RGB', (960, 540), color='white')  # 16:9 aspect ratio
        draw = ImageDraw.Draw(img)
        
        # Try to get a system font
        try:
            font = ImageFont.truetype("Arial", 24)
            small_font = ImageFont.truetype("Arial", 16)
        except:
            font = ImageFont.load_default()
            small_font = ImageFont.load_default()
        
        # Draw slide frame
        draw.rectangle([20, 20, 940, 520], outline=(200, 200, 200))
        
        # Get slide info
        slide = prs.slides[slide_index]
        
        # Draw title if present
        if slide.shapes.title:
            title_text = slide.shapes.title.text
            draw.text((60, 40), title_text, fill=(0, 0, 0), font=font)
        
        # Draw content placeholder representation
        y_pos = 100
        for shape in slide.placeholders:
            if hasattr(shape, 'text') and shape.text and shape.placeholder_format.type != 1:  # Not title
                lines = shape.text.split('\n')
                for line in lines:
                    draw.text((60, y_pos), line, fill=(0, 0, 0), font=small_font)
                    y_pos += 30
        
        # Draw slide number
        draw.text((880, 500), f"Slide {slide_index + 1}", fill=(100, 100, 100), font=small_font)
        
        # Save to buffer with high quality
        buffer = io.BytesIO()
        img.save(buffer, format="PNG")
        
        return Image(data=buffer.getvalue(), format="png")
    
    except Exception as e:
        # Create an error image
        img = PILImage.new('RGB', (960, 540), color=(245, 245, 245))
        draw = ImageDraw.Draw(img)
        
        try:
            font = ImageFont.truetype("Arial", 24)
            small_font = ImageFont.truetype("Arial", 16)
        except:
            font = ImageFont.load_default()
            small_font = ImageFont.load_default()
            
        draw.text((100, 100), f"Error loading slide preview:", fill=(200, 0, 0), font=font)
        draw.text((100, 150), str(e), fill=(100, 0, 0), font=small_font)
        draw.text((100, 230), f"File: {filename}", fill=(0, 0, 0), font=small_font)
        draw.text((100, 260), f"Slide index: {slide_index}", fill=(0, 0, 0), font=small_font)
        
        buffer = io.BytesIO()
        img.save(buffer, format="PNG")
        
        return Image(data=buffer.getvalue(), format="png")


@mcp.tool()
def apply_professional_theme(filename: str, theme_name: str = "professional") -> str:
    """
    Apply a professional theme to an existing presentation.
    Available themes: professional, minimal, bold, corporate, creative
    Returns the filename of the updated presentation.
    """
    from pptx import Presentation
    import os
    
    # If filename doesn't include path, assume it's in the user directory
    if not os.path.dirname(filename):
        user_docs = os.path.expanduser("~/Documents/ClaudePresentations")
        filename = os.path.join(user_docs, filename)
    
    # Define professional themes
    themes = {
        "professional": PresentationTheme(
            title_font=TextFormatting(font_name="Calibri", font_size=32, bold=True, color="#1F497D"),
            body_font=TextFormatting(font_name="Calibri", font_size=18, color="#333333"),
            background_color="#FFFFFF"
        ),
        "minimal": PresentationTheme(
            title_font=TextFormatting(font_name="Arial", font_size=36, bold=True, color="#333333"),
            body_font=TextFormatting(font_name="Arial", font_size=20, color="#505050"),
            background_color="#F7F7F7"
        ),
        "bold": PresentationTheme(
            title_font=TextFormatting(font_name="Verdana", font_size=40, bold=True, color="#FFFFFF"),
            body_font=TextFormatting(font_name="Verdana", font_size=20, color="#FFFFFF"),
            background_color="#2D2D2D"
        ),
        "corporate": PresentationTheme(
            title_font=TextFormatting(font_name="Segoe UI", font_size=36, bold=True, color="#003366"),
            body_font=TextFormatting(font_name="Segoe UI", font_size=18, color="#333333"),
            background_color="#F2F2F2"
        ),
        "creative": PresentationTheme(
            title_font=TextFormatting(font_name="Georgia", font_size=38, bold=True, color="#663399"),
            body_font=TextFormatting(font_name="Georgia", font_size=20, color="#333333"),
            background_color="#FFF8E1"
        )
    }
    
    # Get the appropriate theme
    selected_theme = themes.get(theme_name.lower(), themes["professional"])
    
    # Open the existing presentation
    prs = Presentation(filename)
    
    # Apply the theme
    apply_theme_to_presentation(prs, selected_theme)
    
    # Save the presentation
    prs.save(filename)
    return f"Applied '{theme_name}' theme to presentation at: {filename}"


@mcp.tool()
def get_save_location() -> str:
    """
    Returns the directory where presentations are saved.
    """
    import os
    user_docs = os.path.expanduser("~/Documents/ClaudePresentations")
    return f"Presentations are saved to: {user_docs}"


@mcp.tool()
def create_professional_presentation(title: str, subtitle: str, content_slides: List[Dict[str, Any]]) -> str:
    """
    Create a professionally formatted presentation with standard slides.
    This is a simplified interface for creating complete presentations.
    
    Parameters:
    - title: Presentation title
    - subtitle: Presentation subtitle
    - content_slides: List of dictionaries with 'title' and 'points' keys (points should be a list of strings)
    
    Returns the path to the created presentation.
    """
    from pptx import Presentation
    import os
    
    # Create user directory
    user_docs = os.path.expanduser("~/Documents/ClaudePresentations")
    os.makedirs(user_docs, exist_ok=True)
    
    # Create a list of properly formatted slides
    slides = []
    
    # Add title slide
    title_slide = Slide(
        title=title,
        subtitle=subtitle,
        layout="Title Slide",
        title_formatting=TextFormatting(font_size=40, bold=True, color="#1F497D"),
    )
    slides.append(title_slide)
    
    # Add content slides
    for slide_data in content_slides:
        # Convert string points to proper BulletPoint objects
        bullet_points = []
        if "points" in slide_data:
            for point in slide_data["points"]:
                # Check if it's a string or already has level info
                if isinstance(point, str):
                    bullet_points.append(BulletPoint(text=point, level=0))
                elif isinstance(point, dict) and "text" in point:
                    level = point.get("level", 0)
                    bullet_points.append(BulletPoint(text=point["text"], level=level))
        
        # Create the slide
        content_slide = Slide(
            title=slide_data.get("title", ""),
            bullets=bullet_points,
            layout="Title and Content",
            title_formatting=TextFormatting(font_size=32, bold=True, color="#1F497D"),
            content_formatting=TextFormatting(font_size=20, color="#333333")
        )
        slides.append(content_slide)
    
    # Define a professional theme
    theme = PresentationTheme(
        title_font=TextFormatting(font_name="Calibri", font_size=32, bold=True, color="#1F497D"),
        body_font=TextFormatting(font_name="Calibri", font_size=20, color="#333333"),
        background_color="#FFFFFF"
    )
    
    # Create the presentation
    prs = Presentation()
    
    # Process each slide
    for slide_data in slides:
        # Get layout
        layout_name = slide_data.layout
        layout = None
        for lo in prs.slide_layouts:
            if lo.name.lower() == layout_name.lower():
                layout = lo
                break
        
        if not layout:
            layout = prs.slide_layouts[1]  # Default to Title and Content
        
        # Add slide
        slide = prs.slides.add_slide(layout)
        
        # Set title
        if slide_data.title and slide.shapes.title:
            slide.shapes.title.text = slide_data.title
            apply_text_formatting(slide.shapes.title.text_frame, slide_data.title_formatting)
        
        # Set subtitle (for title slide)
        if slide_data.subtitle:
            for shape in slide.placeholders:
                if shape.placeholder_format.type == 2:  # Subtitle
                    shape.text = slide_data.subtitle
                    break
        
        # Add bullets
        if slide_data.bullets:
            for shape in slide.placeholders:
                if shape.placeholder_format.type not in [1, 2]:  # Not title or subtitle
                    text_frame = shape.text_frame
                    text_frame.clear()
                    
                    for i, bullet in enumerate(slide_data.bullets):
                        if i == 0:
                            p = text_frame.paragraphs[0]
                        else:
                            p = text_frame.add_paragraph()
                        
                        p.text = bullet.text
                        p.level = bullet.level
                    
                    # Apply formatting
                    if slide_data.content_formatting:
                        apply_text_formatting(text_frame, slide_data.content_formatting)
                    
                    break
    
    # Apply theme
    apply_theme_to_presentation(prs, theme)
    
    # Save file
    filename = f"{title.replace(' ', '_')}.pptx"
    full_path = os.path.join(user_docs, filename)
    prs.save(full_path)
    
    return f"Created professional presentation at: {full_path}"
```