Powerpoint MCP Server

by supercurses
Verified
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())