# Directory Structure
```
├── .github
│   └── FUNDING.yml
├── .idea
│   ├── inspectionProfiles
│   │   ├── profiles_settings.xml
│   │   └── Project_Default.xml
│   ├── misc.xml
│   ├── modules.xml
│   ├── powerpoint.iml
│   └── vcs.xml
├── .python-version
├── LICENSE
├── pyproject.toml
├── README.md
├── src
│   └── powerpoint
│       ├── __init__.py
│       ├── chart_manager.py
│       ├── presentation_manager.py
│       ├── server.py
│       └── vision_manager.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
3.12
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# powerpoint MCP server
A MCP server project that creates powerpoint presentations
<a href="https://glama.ai/mcp/servers/h1wl85c8gs">
  <img width="380" height="200" src="https://glama.ai/mcp/servers/h1wl85c8gs/badge" alt="Powerpoint Server MCP server" />
</a>
## Components
### Tools
The server implements multiple tools:
- ```create-presentation```: Starts a presentation
  - Takes "name"  as required string arguments
  - Creates a presentation object
- ```add-slide-title-only```: Adds a title slide to the presentation
  - Takes "presentation_name" and "title" as required string arguments
  - Creates a title slide with "title" and adds it to presentation
- ```add-slide-section-header```: Adds a section header slide to the presentation
  - Takes "presentation_name" and "header" as required string arguments
  - Creates a section header slide with "header" (and optionally "subtitle") and adds it to the presentation
- ```add-slide-title-content```: Adds a title with content slide to the presentation
  - Takes "presentation_name", "title", "content" as required string arguments
  - Creates a title with content slide with "title" and "content" and adds it to presentation
- ```add-slide-title-with-table```: Adds a title slide with a table
  - Takes "presentation_name", "title", "data" as required string and array arguments
  - Creates a title slide with "title" and adds a table dynamically built from data
- ```add-slide-title-with-chart```: Adds a title slide with a chart
  - Takes "presentation_name", "title", "data" as required string and object arguments
  - Creates a title slide with "title" and adds a chart dynamically built from data. Attempts to figure out the best type of chart from the data source.
- ```add-slide-picture-with-caption```: Adds a picture with caption slide
  - Takes "presentation_name", "title", "caption", "image_path" as required string arguments
  - Creates a picture with caption slide using the supplied "title", "caption", and "image_path". Can either use images created via the "generate-and-save-image" tool or use an "image_path" supplied by the user (image must exist in folder_path)
- ```open-presentation```: Opens a presentation for editing
  - Takes "presentation_name" as required arguments
  - Opens the given presentation and automatically saves a backup of it as "backup.pptx"
  - This tool allows the client to work with existing pptx files and add slides to them. Just make sure the client calls "save-presentation" tool at the end.
- ```save-presentation```: Saves the presentation to a file.
  - Takes "presentation_name" as required arguments.
  - Saves the presentation to the folder_path. The client must call this tool to finalize the process.
- ```generate-and-save-image```: Generates an image for the presentation using a FLUX model
  - Takes "prompt" and "file_name" as required string arguments
  - Creates an image using the free FLUX model on TogetherAI (requires an API key)
## Configuration
An environment variable is required for image generation via TogetherAI
Register for an account: https://api.together.xyz/settings/api-keys
```
"env": {
        "TOGETHER_API_KEY": "api_key"
      }
```
A folder_path is required. All presentations and images will be saved to this folder.
```
"--folder-path",
        "/path/to/decks_folder"
```
## Quickstart
### Install
#### Make sure you have UV installed
MacOS/Linux
```
curl -LsSf https://astral.sh/uv/install.sh | sh
```
Windows
```
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```
#### Clone the repo
```
git clone https://github.com/supercurses/powerpoint.git
```
#### Claude Desktop
On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
- ```--directory```: the path where you cloned the repo above
- ```--folder-path```: the path where powerpoint decks and images will be saved to. Also the path where you should place any images you want the MCP server to use.
```
  # Add the server to your claude_desktop_config.json
  "mcpServers": {
    "powerpoint": {
      "command": "uv",
      "env": {
        "TOGETHER_API_KEY": "api_key"
      },
      "args": [
        "--directory",
        "/path/to/powerpoint",
        "run",
        "powerpoint",
        "--folder-path",
        "/path/to/decks_folder"
      ]
    }
```
### Usage Examples
```
Create a presentation about fish, create some images and include tables and charts
```
```
Create a presentation about the attached paper. Please use the following images in the presentation:
author.jpeg
```
Assuming you have SQLite MCP Server installed.
```
Review 2024 Sales Data table. Create a presentation showing current trends, use tables and charts as appropriate
```
# License
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
```
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
```yaml
ko_fi: russellashby
```
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="VcsDirectoryMappings">
    <mapping directory="" vcs="Git" />
  </component>
</project>
```
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
```
<component name="InspectionProjectProfileManager">
  <settings>
    <option name="USE_PROJECT_PROFILE" value="false" />
    <version value="1.0" />
  </settings>
</component>
```
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ProjectModuleManager">
    <modules>
      <module fileurl="file://$PROJECT_DIR$/.idea/powerpoint.iml" filepath="$PROJECT_DIR$/.idea/powerpoint.iml" />
    </modules>
  </component>
</project>
```
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="Black">
    <option name="sdkName" value="uv (powerpoint)" />
  </component>
  <component name="ProjectRootManager" version="2" project-jdk-name="uv (powerpoint)" project-jdk-type="Python SDK" />
</project>
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[project]
name = "powerpoint"
version = "0.1.0"
description = "A MCP server project that creates powerpoint presentations"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
 "mcp>=1.3.0",
 "pillow>=11.1.0",
 "python-pptx>=1.0.2",
 "requests>=2.32.3",
 "together>=1.4.1",
]
[[project.authors]]
name = "Russell Ashby"
email = "[email protected]"
[build-system]
requires = [ "hatchling",]
build-backend = "hatchling.build"
[project.scripts]
powerpoint = "powerpoint:main"
```
--------------------------------------------------------------------------------
/src/powerpoint/__init__.py:
--------------------------------------------------------------------------------
```python
from . import server
import asyncio
import argparse
def main():
    """Main entry point for the package."""
    parser = argparse.ArgumentParser(description='Powerpoint MCP Server')
    parser.add_argument('--folder-path',
                       default="/users/russellashby/decks/",
                       help="Folder to store completed decks in.")
    args = parser.parse_args()
    asyncio.run(server.main(args.folder_path))
# Optionally expose other important items at package level
__all__ = ['main', 'server']
```
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
```
<component name="InspectionProjectProfileManager">
  <profile version="1.0">
    <option name="myName" value="Project Default" />
    <inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
      <option name="ignoredPackages">
        <value>
          <list size="2">
            <item index="0" class="java.lang.String" itemvalue="wavmark" />
            <item index="1" class="java.lang.String" itemvalue="numpy" />
          </list>
        </value>
      </option>
    </inspection_tool>
  </profile>
</component>
```
--------------------------------------------------------------------------------
/src/powerpoint/vision_manager.py:
--------------------------------------------------------------------------------
```python
import os
import requests
from PIL import Image
from io import BytesIO
from together import Together
class VisionManager:
    async def generate_and_save_image(self, prompt: str, output_path: str) -> str:
        """Generate an image using Together AI/Flux Model and save it to the specified path."""
        api_key = os.environ.get('TOGETHER_API_KEY')
        if not api_key:
            raise ValueError("TOGETHER_API_KEY environment variable not set.")
        client = Together(api_key=api_key)
        try:
            # Generate the image
            response = client.images.generate(
                prompt=prompt,
                width=1024,
                height=1024,
                steps=4,
                model="black-forest-labs/FLUX.1-schnell-Free",
                n=1,
            )
        except Exception as e:
            raise ValueError(f"Failed to generate image: {str(e)}")
        image_url = response.data[0].url
        # Download the image
        try:
            response = requests.get(image_url)
            if response.status_code != 200:
                raise ValueError(f"Failed to download generated image: HTTP {response.status_code}")
        except requests.RequestException as e:
            raise ValueError(f"Network error downloading image: {str(e)}")
        # Save the image
        try:
            image = Image.open(BytesIO(response.content))
            # Ensure the save directory exists
            try:
                os.makedirs(os.path.dirname(output_path), exist_ok=True)
            except OSError as e:
                raise ValueError(f"Failed to create a directory for image: str({e})")
            # Save the image
            image.save(output_path)
        except (IOError, OSError) as e:
            raise ValueError(f"Failed to save image to {output_path}: {str(e)}")
        return output_path
```
--------------------------------------------------------------------------------
/src/powerpoint/chart_manager.py:
--------------------------------------------------------------------------------
```python
from pptx.chart import chart
from pptx.chart.data import CategoryChartData, XyChartData
from pptx.enum.chart import XL_CHART_TYPE
from pptx.util import Inches
from pptx.enum.chart import XL_LEGEND_POSITION
from typing import Literal, Union, List, Dict, Any
class ChartManager:
    def __init__(self):
        self.name = "Chart Manager"
    def determine_chart_type(self, data: Dict[str, Any]) -> tuple[XL_CHART_TYPE, str]:
        """
        Analyze the data structure and determine the most appropriate chart type.
        Returns tuple of (PowerPoint chart type enum, chart_format)
        """
        # evaluate the data
        series_count = len(data["series"])
        categories = data.get("categories", [])
        # Check for XY data more safely by checking the first value of each series
        is_xy_data = False
        for series in data["series"]:
            values = series.get("values", [])
            if values:
                first_value = values[0]
                is_xy_data = isinstance(first_value, (list, tuple)) and len(first_value) == 2
                break
        if is_xy_data:
            return XL_CHART_TYPE.XY_SCATTER, "xy"
        # If we have percentage data that adds up to ~100, suggest pie chart
        if series_count == 1 and categories:
            values = data["series"][0].get("values", [])
            if len(values) <= 8:
                try:
                    total = sum(float(v) for v in values)
                    if 95 <= total <= 105:
                        return XL_CHART_TYPE.PIE, "category"
                except (TypeError, ValueError):
                    pass
        # For time series or trending data, suggest line chart
        if categories and any(
                isinstance(cat, (str, int)) and
                any(term in str(cat).lower() for term in
                    ["date", "time", "year", "month", "quarter", "q1", "q2", "q3", "q4"])
                for cat in categories
        ):
            return XL_CHART_TYPE.LINE, "category"
        # For multiple series comparing values, suggest bar chart
        if series_count > 1 and categories:
            return XL_CHART_TYPE.BAR_CLUSTERED, "category"
        # Default to column chart for single series
        return XL_CHART_TYPE.COLUMN_CLUSTERED, "category"
    def add_chart_to_slide(self, slide, chart_type: XL_CHART_TYPE, data: Dict[str, Any],
                           chart_format: str = "category") -> chart:
        """Add a chart to the slide with the specified data."""
        # Position chart in the middle of the slide with margins
        left = Inches(1)
        top = Inches(2)
        width = Inches(8)
        height = Inches(5)
        if chart_format == "category":
            chart_data = CategoryChartData()
            chart_data.categories = data.get("categories", [])
            # Add each series
            for series in data["series"]:
                chart_data.add_series(series["name"], series["values"])
        elif chart_format == "xy":
            chart_data = XyChartData()
            # Add each series
            for series in data["series"]:
                series_data = chart_data.add_series(series["name"])
                for x, y in series["values"]:
                    series_data.add_data_point(x, y)
        # Add and configure the chart
        graphic_frame = slide.shapes.add_chart(
            chart_type, left, top, width, height, chart_data
        )
        chart = graphic_frame.chart
        # Basic formatting
        chart.has_legend = True
        if len(data["series"]) > 1:
            chart.legend.position = XL_LEGEND_POSITION.BOTTOM
        # Add axis titles if provided
        if "x_axis" in data:
            chart.category_axis.axis_title.text_frame.text = data["x_axis"]
        if "y_axis" in data:
            chart.value_axis.axis_title.text_frame.text = data["y_axis"]
        return chart
```
--------------------------------------------------------------------------------
/src/powerpoint/presentation_manager.py:
--------------------------------------------------------------------------------
```python
import os
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.util import Inches
from pptx.slide import Slide
from PIL import Image, UnidentifiedImageError
import logging
from typing import Literal, Union, List, Dict, Any
ChartTypes = Literal["bar", "line", "pie", "scatter", "area"]
class PresentationManager:
    # Slide layout constants
    SLIDE_LAYOUT_TITLE = 0
    SLIDE_LAYOUT_TITLE_AND_CONTENT = 1
    SLIDE_LAYOUT_SECTION_HEADER = 2
    SLIDE_LAYOUT_TWO_CONTENT = 3
    SLIDE_LAYOUT_COMPARISON = 4
    SLIDE_LAYOUT_TITLE_ONLY = 5
    SLIDE_LAYOUT_BLANK = 6
    SLIDE_LAYOUT_CONTENT_WITH_CAPTION = 7
    SLIDE_LAYOUT_PICTURE_WITH_CAPTION = 8
    def __init__(self):
        self.presentations: Dict[str, Any] = {}
    def _add_formatted_bullets(self, text_frame, text_block):
        """
        Process a text block and add paragraphs with proper bullet indentation
        using ASCII code detection:
        - ASCII 10 (LF) or ASCII 13 (CR) or combination for new lines (main bullets)
        - ASCII 9 (HT) for tab indentation (sub-bullets)
        Args:
            text_frame: The PowerPoint text frame to add text to
            text_block: String of text to process
        """
        # First, normalize all line endings to a single format
        # Replace CR+LF (Windows) with a single marker
        normalized_text = text_block.replace('\r\n', '\n')
        # Replace any remaining CR (old Mac) with LF
        normalized_text = normalized_text.replace('\r', '\n')
        # Split the text block into lines using ASCII 10 (LF)
        lines = normalized_text.split('\n')
        # Clear any existing text
        if text_frame.paragraphs:
            p = text_frame.paragraphs[0]
            p.text = ""
        else:
            p = text_frame.add_paragraph()
        # Process the first line separately (if it exists)
        if lines and lines[0].strip():
            first_line = lines[0]
            # Count leading tabs (ASCII 9) to determine indentation level
            level = 0
            while first_line and ord(first_line[0]) == 9:  # ASCII 9 is HT (tab)
                level += 1
                first_line = first_line[1:]
            p.text = first_line.strip()
            p.level = level
        # Process remaining lines
        for line in lines[1:]:
            if not line.strip():
                continue  # Skip empty lines
            # Count leading tabs (ASCII 9) to determine indentation level
            level = 0
            while line and ord(line[0]) == 9:  # ASCII 9 is HT (tab)
                level += 1
                line = line[1:]
            # Add the paragraph with proper indentation
            p = text_frame.add_paragraph()
            p.text = line.strip()
            p.level = level
    def add_section_header_slide(self, presentation_name: str, header: str, subtitle: str):
        """
        Create a section header slide for the given presentation
        Args:
            presentation_name: The presentation to add the slide to
            header: The section header to use
            subtitle: The subtitle of the section header to use
        """
        try:
            prs = self.presentations[presentation_name]
        except KeyError as e:
            raise ValueError(f"Presentation '{presentation_name}' not found")
        slide_master = prs.slide_master
        # Add a new slide with layout
        slide_layout = prs.slide_layouts[self.SLIDE_LAYOUT_SECTION_HEADER]
        slide = prs.slides.add_slide(slide_layout)
        # Set the subtitle
        if subtitle:
            subtitle_shape = slide.placeholders[1]
            text_frame = subtitle_shape.text_frame
            text_frame.text = subtitle
        # Set the section header
        if header:
            header_shape = slide.shapes.title
            header_shape.text = header
        return slide
    def add_comparison_slide(self, presentation_name: str, title: str, left_side_title: str, left_side_content: str,
                             right_side_title: str, right_side_content: str ):
        """
        Create a section header slide for the given presentation
        Args:
            presentation_name: The presentation to add the slide to
            title: The title of the slide
            left_side_title: The title of the left hand side content
            left_side_content: The body content for the left hand side
            right_side_title: The title of the right hand side content
            right_side_content: The body content for the right hand side
        """
        try:
            prs = self.presentations[presentation_name]
        except KeyError as e:
            raise ValueError(f"Presentation '{presentation_name}' not found")
        slide_master = prs.slide_master
        # Add a new slide with layout
        slide_layout = prs.slide_layouts[self.SLIDE_LAYOUT_COMPARISON]
        slide = prs.slides.add_slide(slide_layout)
        # Set the title
        title_shape = slide.shapes.title
        title_shape.text = title
        # Build the left hand content
        content_shape = slide.placeholders[1]
        text_frame = content_shape.text_frame
        text_frame.text = left_side_title
        content_shape = slide.placeholders[2]
        text_frame = content_shape.text_frame
        text_frame.text = left_side_content
        # Build the right hand content
        content_shape = slide.placeholders[3]
        text_frame = content_shape.text_frame
        text_frame.text = right_side_title
        content_shape = slide.placeholders[4]
        text_frame = content_shape.text_frame
        text_frame.text = right_side_content
        return slide
    def add_picture_with_caption_slide(self, presentation_name: str, title: str,
                                       image_path: str, caption_text: str) -> Slide:
        """
        For the given presentation builds a slide with the picture with caption template.
        Maintains the image's aspect ratio by adjusting the picture object after insertion.
        Args:
            presentation_name: The presentation to add the slide to
            title: The title of the slide
            image_path: The path to the image to insert
            caption_text: The caption content
        """
        try:
            prs = self.presentations[presentation_name]
        except KeyError as e:
            raise ValueError(f"Presentation '{presentation_name}' not found")
        # Add a new slide with layout 8 (Picture with Caption)
        try:
            slide_layout = prs.slide_layouts[self.SLIDE_LAYOUT_PICTURE_WITH_CAPTION]
            slide = prs.slides.add_slide(slide_layout)
        except IndexError as e:
            error_message = f"Slide Index does not exist. Error: {str(e)}"
            raise ValueError(error_message)
        # Set the title
        title_shape = slide.shapes.title
        title_shape.text = title
        # Get the image placeholder
        try:
            placeholder = slide.placeholders[1]
        except IndexError as e:
            error_message = f"Placeholder index does not exist. Error {str(e)}"
            raise ValueError(error_message)
        # Insert the picture into the placeholder
        if not os.path.exists(image_path):
            raise FileNotFoundError(f"Image not found: {image_path}")
        try:
            picture = placeholder.insert_picture(image_path)
        except FileNotFoundError as e:
            error_message = f"Image not found during insertion: {str(e)}"
            raise
        except UnidentifiedImageError as e:
            error_message = f"Image file {image_path} is not a valid image: {str(e)}"
            raise ValueError(error_message)
        except Exception as e:
            error_message = f"An unexpected error occured during picture insertion: {str(e)}"
            raise
        # Get placeholder dimensions after picture insertion
        available_width = picture.width
        available_height = picture.height
        # Get original image dimensions directly from the picture object
        image_width, image_height = picture.image.size
        # Calculate aspect ratios
        placeholder_aspect_ratio = float(available_width) / float(available_height)
        image_aspect_ratio = float(image_width) / float(image_height)
        # Store initial position
        pos_left, pos_top = picture.left, picture.top
        # Remove any cropping
        picture.crop_top = 0
        picture.crop_left = 0
        picture.crop_bottom = 0
        picture.crop_right = 0
        # Adjust picture dimensions based on aspect ratio comparison
        if placeholder_aspect_ratio > image_aspect_ratio:
            # Placeholder is wider than image - adjust width down while maintaining height
            picture.width = int(image_aspect_ratio * available_height)
            picture.height = available_height
        else:
            # Placeholder is taller than image - adjust height down while maintaining width
            picture.height = int(available_width / image_aspect_ratio)
            picture.width = available_width
        # Center the image within the available space
        picture.left = pos_left + int((available_width - picture.width) / 2)
        picture.top = pos_top + int((available_height - picture.height) / 2)
        # Set the caption
        caption = slide.placeholders[2]
        caption.text = caption_text
        return slide
    def add_title_with_content_slide(self, presentation_name: str, title: str, content: str) -> Slide:
        try:
            prs = self.presentations[presentation_name]
        except KeyError as e:
            raise ValueError(f"Presentation '{presentation_name}' not found")
        slide_master = prs.slide_master
        # Add a slide with title and content
        slide_layout = prs.slide_layouts[self.SLIDE_LAYOUT_TITLE_AND_CONTENT]  # Use layout with title and content
        slide = prs.slides.add_slide(slide_layout)
        # Set the title
        title_shape = slide.shapes.title
        title_shape.text = title
        # Set the content
        content_shape = slide.placeholders[1]
        #content_shape.text = content
        # Get the content placeholder and add our formatted text
        text_frame = content_shape.text_frame
        self._add_formatted_bullets(text_frame, content)
        return slide
    def add_table_slide(self, presentation_name: str, title: str, headers: str, rows: str) -> Slide:
        try:
            prs = self.presentations[presentation_name]
        except KeyError as e:
            raise ValueError(f"Presentation '{presentation_name}' not found")
        slide_layout = prs.slide_layouts[self.SLIDE_LAYOUT_TITLE_ONLY]
        slide = prs.slides.add_slide(slide_layout)
        # Set the title
        title_shape = slide.shapes.title
        title_shape.text = title
        # Calculate table dimensions and position
        num_rows = len(rows) + 1  # +1 for header row
        num_cols = len(headers)
        # Position table in the middle of the slide with some margins
        x = Inches(1)  # Left margin
        y = Inches(2)  # Top margin below title
        # Make table width proportional to the number of columns
        width_per_col = Inches(8 / num_cols)  # Divide available width (8 inches) by number of columns
        height_per_row = Inches(0.4)  # Standard height per row
        # Create table
        shape = slide.shapes.add_table(
            num_rows,
            num_cols,
            x,
            y,
            width_per_col * num_cols,
            height_per_row * num_rows
        )
        table = shape.table
        # Add headers
        for col_idx, header in enumerate(headers):
            cell = table.cell(0, col_idx)
            cell.text = str(header)
            # Style header row
            paragraph = cell.text_frame.paragraphs[0]
            paragraph.font.bold = True
            paragraph.font.size = Pt(11)
        # Add data rows
        for row_idx, row_data in enumerate(rows, start=1):
            for col_idx, cell_value in enumerate(row_data):
                cell = table.cell(row_idx, col_idx)
                cell.text = str(cell_value)
                # Style data cells
                paragraph = cell.text_frame.paragraphs[0]
                paragraph.font.size = Pt(10)
        return slide
    def add_title_slide(self, presentation_name: str, title: str) -> Slide:
        try:
            prs = self.presentations[presentation_name]
        except KeyError as e:
            raise ValueError(f"Presentation '{presentation_name}' not found")
        # Add a slide with title and content
        slide_layout = prs.slide_layouts[self.SLIDE_LAYOUT_TITLE]
        slide = prs.slides.add_slide(slide_layout)
        # Set the title
        title_shape = slide.shapes.title
        title_shape.text = title
        return slide
```
--------------------------------------------------------------------------------
/src/powerpoint/server.py:
--------------------------------------------------------------------------------
```python
import os
from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationOptions
import mcp.server.stdio
import mcp.types as types
import asyncio
from pptx import Presentation
import logging
from .presentation_manager import PresentationManager
from .chart_manager import ChartManager
from .vision_manager import VisionManager
logger = logging.getLogger('mcp_powerpoint_server')
logger.info("Starting MCP Powerpoint Server")
BACKUP_FILE_NAME = 'backup.pptx'
def sanitize_path(base_path: str, file_name: str) -> str:
    """
    Ensure that the resulting path doesn't escape outside the base directory
    Returns a safe, normalized path
    """
    joined_path = os.path.join(base_path, file_name)
    normalized_path = os.path.normpath(joined_path)
    if not normalized_path.startswith(base_path):
        raise ValueError(f"Invalid path. Attempted to access location outside allowed directory.")
    return normalized_path
async def main(folder_path):
    logger.info(f"Starting Powerpoint MCP Server")
    presentation_manager = PresentationManager()
    chart_manager = ChartManager()
    vision_manager = VisionManager()
    server = Server("powerpoint-server")
    logger.debug("Registering Handlers")
    path = folder_path
    @server.list_tools()
    async def handle_list_tools() -> list[types.Tool]:
        """List available PowerPoint tools."""
        return [
            types.Tool(
                name="create-presentation",
                description="This tool starts the process of generating a new powerpoint presentation with the name given "
                            "by the user. Use this tool when the user requests to create or generate a new presentation.",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "name": {
                            "type": "string",
                            "description": "Name of the presentation (without .pptx extension)",
                        },
                    },
                    "required": ["name"],
                },
            ),
            types.Tool(
                name="generate-and-save-image",
                description="Generates an image using a FLUX model and save the image to the specified path. The tool "
                            "will return a PNG file path. It should be used when the user asks to generate or create an "
                            "image or a picture.",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "prompt": {
                            "type": "string",
                            "description": "Description of the image to generate in the form of a prompt.",
                        },
                        "file_name": {
                            "type": "string",
                            "description": "Filename of the image. Include the extension of .png",
                        },
                    },
                    "required": ["prompt", "file_name"],
                },
            ),
            types.Tool(
                name="add-slide-title-only",
                description="This tool adds a new title slide to the presentation you are working on. The tool doesn't "
                            "return anything. It requires the presentation_name to work on.",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "presentation_name": {
                            "type": "string",
                            "description": "Name of the presentation to add the slide to",
                        },
                        "title": {
                            "type": "string",
                            "description": "Title of the slide",
                        }
                    },
                    "required": ["presentation_name", "title"],
                },
            ),
            types.Tool(
                name="add-slide-section-header",
                description="This tool adds a section header (a.k.a segue) slide to the presentation you are working on. The tool doesn't "
                            "return anything. It requires the presentation_name to work on.",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "presentation_name": {
                            "type": "string",
                            "description": "Name of the presentation to add the slide to",
                        },
                        "header": {
                            "type": "string",
                            "description": "Section header title",
                        },
                        "subtitle": {
                            "type": "string",
                            "description": "Section header subtitle",
                        }
                    },
                    "required": ["presentation_name", "header"],
                },
            ),
            types.Tool(
                name="add-slide-title-content",
                description="Add a new slide with a title and content to an existing presentation",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "presentation_name": {
                            "type": "string",
                            "description": "Name of the presentation to add the slide to",
                        },
                        "title": {
                            "type": "string",
                            "description": "Title of the slide",
                        },
                        "content": {
                            "type": "string",
                            "description": "Content/body text of the slide. "
                                           "Separate main points with a single carriage return character."
                                           "Make sub-points with tab character."
                                           "Do not use bullet points, asterisks or dashes for points."
                                           "Max main points is 4"
                        },
                    },
                    "required": ["presentation_name", "title", "content"],
                },
            ),
            types.Tool(
                name="add-slide-comparison",
                description="Add a new a comparison slide with title and comparison content. Use when you wish to "
                            "compare two concepts",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "presentation_name": {
                            "type": "string",
                            "description": "Name of the presentation to add the slide to",
                        },
                        "title": {
                            "type": "string",
                            "description": "Title of the slide",
                        },
                        "left_side_title": {
                            "type": "string",
                            "description": "Title of the left concept",
                        },
                        "left_side_content": {
                            "type": "string",
                            "description": "Content/body text of left concept. "
                                           "Separate main points with a single carriage return character."
                                           "Make sub-points with tab character."
                                           "Do not use bullet points, asterisks or dashes for points."
                                           "Max main points is 4"
                        },
                        "right_side_title": {
                            "type": "string",
                            "description": "Title of the right concept",
                        },
                        "right_side_content": {
                            "type": "string",
                            "description": "Content/body text of right concept. "
                                           "Separate main points with a single carriage return character."
                                           "Make sub-points with tab character."
                                           "Do not use bullet points, asterisks or dashes for points."
                                           "Max main points is 4"
                        },
                    },
                    "required": ["presentation_name", "title", "left_side_title", "left_side_content",
                                 "right_side_title", "right_side_content"],
                },
            ),
            types.Tool(
                name="add-slide-title-with-table",
                description="Add a new slide with a title and table containing the provided data",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "presentation_name": {
                            "type": "string",
                            "description": "Name of the presentation to add the slide to",
                        },
                        "title": {
                            "type": "string",
                            "description": "Title of the slide",
                        },
                        "data": {
                            "type": "object",
                            "description": "Table data object with headers and rows",
                            "properties": {
                                "headers": {
                                    "type": "array",
                                    "items": {"type": "string"},
                                    "description": "Array of column headers"
                                },
                                "rows": {
                                    "type": "array",
                                    "items": {
                                        "type": "array",
                                        "items": {"type": ["string", "number"]},
                                    },
                                    "description": "Array of row data arrays"
                                }
                            },
                            "required": ["headers", "rows"]
                        }
                    },
                    "required": ["presentation_name", "title", "data"],
                },
            ),
            types.Tool(
                name="add-slide-title-with-chart",
                description="Add a new slide with a title and chart. The chart type will be automatically selected based on the data structure.",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "presentation_name": {
                            "type": "string",
                            "description": "Name of the presentation to add the slide to",
                        },
                        "title": {
                            "type": "string",
                            "description": "Title of the slide",
                        },
                        "data": {
                            "type": "object",
                            "description": "Chart data structure",
                            "properties": {
                                "categories": {
                                    "type": "array",
                                    "items": {"type": ["string", "number"]},
                                    "description": "X-axis categories or labels (optional)"
                                },
                                "series": {
                                    "type": "array",
                                    "items": {
                                        "type": "object",
                                        "properties": {
                                            "name": {
                                                "type": "string",
                                                "description": "Name of the data series"
                                            },
                                            "values": {
                                                "type": "array",
                                                "items": {
                                                    "oneOf": [
                                                        {"type": "number"},
                                                        {
                                                            "type": "array",
                                                            "items": {"type": "number"},
                                                            "minItems": 2,
                                                            "maxItems": 2
                                                        }
                                                    ]
                                                },
                                                "description": "Values for the series. Can be simple numbers or [x,y] pairs for scatter plots"
                                            }
                                        },
                                        "required": ["name", "values"]
                                    }
                                },
                                "x_axis": {
                                    "type": "string",
                                    "description": "X-axis title (optional)"
                                },
                                "y_axis": {
                                    "type": "string",
                                    "description": "Y-axis title (optional)"
                                }
                            },
                            "required": ["series"]
                        }
                    },
                    "required": ["presentation_name", "title", "data"],
                },
            ),
            types.Tool(
                name="add-slide-picture-with-caption",
                description="Add a new slide with a picture and caption to an existing presentation",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "presentation_name": {
                            "type": "string",
                            "description": "Name of the presentation to add the slide to",
                        },
                        "title": {
                            "type": "string",
                            "description": "Title of the slide",
                        },
                        "caption": {
                            "type": "string",
                            "description": "Caption text to appear below the picture"
                        },
                        "image_path": {
                            "type": "string",
                            "description": "Path to the image file to insert"
                        }
                    },
                    "required": ["presentation_name", "title", "caption", "image_path"],
                },
            ),
            types.Tool(
                name="open-presentation",
                description="Opens an existing presentation and saves a copy to a new file for backup. Use this tool when "
                            "the user requests to open a presentation that has already been created.",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "presentation_name": {
                            "type": "string",
                            "description": "Name of the presentation to open",
                        },
                        "output_path": {
                            "type": "string",
                            "description": "Path where to save the presentation (optional)",
                        },
                    },
                    "required": ["presentation_name"],
                },
            ),
            types.Tool(
                name="save-presentation",
                description="Save the presentation to a file. Always use this tool at the end of any process that has "
                            "added slides to a presentation.",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "presentation_name": {
                            "type": "string",
                            "description": "Name of the presentation to save",
                        },
                        "output_path": {
                            "type": "string",
                            "description": "Path where to save the presentation (optional)",
                        },
                    },
                    "required": ["presentation_name"],
                },
            ),
        ]
    @server.call_tool()
    async def handle_call_tool(
            name: str, arguments: dict | None
    ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
        """Handle PowerPoint tool execution requests."""
        if not arguments:
            raise ValueError("Missing arguments")
        if name == "open-presentation":
            presentation_name = arguments.get("presentation_name")
            if not presentation_name:
                raise ValueError("Missing presentation name")
            file_name = f"{presentation_name}.pptx"
            try:
                safe_file_path = sanitize_path(folder_path, file_name)
            except ValueError as e:
                raise ValueError(f"Invalid file path: {str(e)}")
            # attempt to load presentation
            try:
                prs = Presentation(safe_file_path)
            except Exception as e:
                raise ValueError(f"Unable to load {safe_file_path}. Error: {str(e)}")
            # Create a backup of the original file
            file_name = BACKUP_FILE_NAME
            try:
                safe_file_path = sanitize_path(folder_path, file_name)
            except ValueError as e:
                raise ValueError(f"Invalid file path: {str(e)}")
            # attempt to save a backup of presentation
            try:
                prs.save(safe_file_path)
            except Exception as e:
                raise ValueError(f"Unable to save {safe_file_path}. Error: {str(e)}")
            presentation_manager.presentations[presentation_name] = prs
            return [
                types.TextContent(
                    type="text",
                    text=f"Opened presentation: {presentation_name}"
                )
            ]
        elif name == "generate-and-save-image":
            prompt = arguments.get("prompt")
            file_name = arguments.get("file_name")
            try:
                safe_file_path = sanitize_path(folder_path, file_name)
            except ValueError as e:
                raise ValueError(f"Invalid file path: {str(e)}")
            if not all([prompt, file_name]):
                raise ValueError("Missing required arguments")
            try:
                saved_path = await vision_manager.generate_and_save_image(prompt, str(safe_file_path))
                return [
                    types.TextContent(
                        type="text",
                        text=f"Successfully generated and saved image to: {saved_path}"
                    )
                ]
            except Exception as e:
                return [
                    types.TextContent(
                        type="text",
                        text=f"Failed to generate image: {str(e)}"
                    )
                ]
        elif name == "add-slide-comparison":
            # Get arguments
            presentation_name = arguments["presentation_name"]
            title = arguments["title"]
            left_side_title = arguments["left_side_title"]
            left_side_content = arguments["left_side_content"]
            right_side_title = arguments["right_side_title"]
            right_side_content = arguments["right_side_content"]
            if not all([presentation_name, title, left_side_title, left_side_content,
                        right_side_title, right_side_content]):
                raise ValueError("Missing required arguments")
            if presentation_name not in presentation_manager.presentations:
                raise ValueError(f"Presentation not found: {presentation_name}")
            try:
                slide = presentation_manager.add_comparison_slide(presentation_name, title, left_side_title,
                                                                  left_side_content, right_side_title, right_side_content)
            except Exception as e:
                raise ValueError(f"Unable to add comparison slide to {presentation_name}.pptx")
            return [types.TextContent(
                type="text",
                text=f"Successfully added comparison slide {title} to {presentation_name}.pptx"
            )]
        elif name == "add-slide-picture-with-caption":
            # Get arguments
            presentation_name = arguments["presentation_name"]
            title = arguments["title"]
            caption = arguments["caption"]
            file_name = arguments["image_path"]
            if not all([presentation_name, title, caption, file_name]):
                raise ValueError("Missing required arguments")
            if presentation_name not in presentation_manager.presentations:
                raise ValueError(f"Presentation not found: {presentation_name}")
            try:
                safe_file_path = sanitize_path(folder_path, file_name)
            except ValueError as e:
                raise ValueError(f"Invalid file path: {str(e)}")
            try:
                slide = presentation_manager.add_picture_with_caption_slide(presentation_name, title, str(safe_file_path), caption)
            except Exception as e:
                raise ValueError(f"Unable to add slide with caption and picture layout to {presentation_name}.pptx. Error: {str(e)}")
            return [types.TextContent(
                type="text",
                text=f"Successfully added slide with caption and picture layout to {presentation_name}.pptx"
            )]
        elif name == "create-presentation":
            presentation_name = arguments.get("name")
            if not presentation_name:
                raise ValueError("Missing presentation name")
            # Create new presentation
            prs = Presentation()
            try:
                presentation_manager.presentations[presentation_name] = prs
            except KeyError as e:
                raise ValueError(f"Unable to add {presentation_name} to presentation. Error: {str(e)}")
            return [
                types.TextContent(
                    type="text",
                    text=f"Created new presentation: {presentation_name}"
                )
            ]
        elif name == "add-slide-title-content":
            presentation_name = arguments.get("presentation_name")
            title = arguments.get("title")
            content = arguments.get("content")
            if not all([presentation_name, title, content]):
                raise ValueError("Missing required arguments")
            if presentation_name not in presentation_manager.presentations:
                raise ValueError(f"Presentation not found: {presentation_name}")
            try:
                slide = presentation_manager.add_title_with_content_slide(presentation_name, title, content)
            except Exception as e:
                raise ValueError(f"Unable to add slide '{title}' to presentation: {presentation_name}")
            return [
                types.TextContent(
                    type="text",
                    text=f"Added slide '{title}' to presentation: {presentation_name}"
                )
            ]
        elif name == "add-slide-section-header":
            presentation_name = arguments.get("presentation_name")
            header = arguments.get("header")
            subtitle = arguments.get("subtitle")
            if not all([presentation_name, header]):
                raise ValueError("Missing required arguments")
            if presentation_name not in presentation_manager.presentations:
                raise ValueError(f"Presentation not found: {presentation_name}")
            try:
                slide = presentation_manager.add_section_header_slide(presentation_name, header, subtitle)
            except Exception as e:
                raise ValueError(f"Unable to add slide '{header}' to presentation: {presentation_name}")
            return [
                types.TextContent(
                    type="text",
                    text=f"Added slide '{header}' to presentation: {presentation_name}"
                )
            ]
        elif name == "add-slide-title-with-table":
            presentation_name = arguments.get("presentation_name")
            title = arguments.get("title")
            table_data = arguments.get("data")
            if not all([presentation_name, title, table_data]):
                raise ValueError("Missing required arguments")
            if presentation_name not in presentation_manager.presentations:
                raise ValueError(f"Presentation not found: {presentation_name}")
            # Validate table data structure
            headers = table_data.get("headers", [])
            rows = table_data.get("rows", [])
            if not headers:
                raise ValueError("Table headers are required")
            if not rows:
                raise ValueError("Table rows are required")
            # Validate that all rows match header length
            if not all(len(row) == len(headers) for row in rows):
                raise ValueError("All rows must have the same number of columns as headers")
            try:
                slide = presentation_manager.add_table_slide(presentation_name, title, headers, rows)
            except Exception as e:
                raise ValueError(f"Unable to add slide '{title}' with a table to presentation: {presentation_name}")
            return [
                types.TextContent(
                    type="text",
                    text=f"Added slide '{title}' with a table to presentation: {presentation_name}"
                )
            ]
        elif name == "add-slide-title-with-chart":
            presentation_name = arguments.get("presentation_name")
            title = arguments.get("title")
            chart_data = arguments.get("data")
            if not all([presentation_name, title, chart_data]):
                raise ValueError("Missing required arguments")
            if presentation_name not in presentation_manager.presentations:
                raise ValueError(f"Presentation not found: {presentation_name}")
            # Get the presentation and create a new slide
            prs = presentation_manager.presentations[presentation_name]
            slide_layout = prs.slide_layouts[5]  # Title and blank content
            slide = prs.slides.add_slide(slide_layout)
            # Set the title
            title_shape = slide.shapes.title
            title_shape.text = title
            # Determine the best chart type for the data
            try:
                chart_type, chart_format = chart_manager.determine_chart_type(chart_data)
            except Exception as e:
                raise ValueError(f"Unable to determine chart type.")
            # Add the chart to the slide
            try:
                chart = chart_manager.add_chart_to_slide(slide, chart_type, chart_data, chart_format)
                chart_type_name = chart_type.name.lower().replace('xl_chart_type.', '')
                return [
                    types.TextContent(
                        type="text",
                        text=f"Added slide '{title}' with a {chart_type_name} chart to presentation: {presentation_name}"
                    )
                ]
            except Exception as e:
                raise ValueError(f"Failed to create slide with chart: {str(e)}")
        elif name == "add-slide-title-only":
            presentation_name = arguments.get("presentation_name")
            title = arguments.get("title")
            if not all([presentation_name, title]):
                raise ValueError("Missing required arguments")
            if presentation_name not in presentation_manager.presentations:
                raise ValueError(f"Presentation not found: {presentation_name}")
            try:
                slide = presentation_manager.add_title_slide(presentation_name, title)
            except Exception as e:
                 raise ValueError(f"Unable to add '{title} to presentation: {presentation_name}. Error: {e}")
            return [
                types.TextContent(
                    type="text",
                    text=f"Added slide '{title}' to presentation: {presentation_name}"
                )
            ]
        elif name == "save-presentation":
            presentation_name = arguments.get("presentation_name")
            output_path = arguments.get("output_path")
            if not presentation_name:
                raise ValueError("Missing presentation name")
            if presentation_name not in presentation_manager.presentations:
                raise ValueError(f"Presentation not found: {presentation_name}")
            prs = presentation_manager.presentations[presentation_name]
            # Default output path if none provided
            if not output_path:
                output_path = f"{presentation_name}.pptx"
            file_path = os.path.join(path,output_path)
            # Save the presentation
            try:
                prs.save(file_path)
            except Exception as e:
                raise ValueError(f"Unable to save the {presentation_name}. Error: {e}")
            return [
                types.TextContent(
                    type="text",
                    text=f"Saved presentation to: {file_path}"
                )
            ]
        else:
            raise ValueError(f"Unknown tool: {name}")
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        logger.info("Server running with stdio transport")
        await server.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="powerpoint",
                server_version="0.1.0",
                capabilities=server.get_capabilities(
                    notification_options=NotificationOptions(),
                    experimental_capabilities={},
                ),
            ),
        )
if __name__ == "__main__":
    asyncio.run(main())
```