# 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}"
```