Skip to main content
Glama

PowerPoint MCP Server

by jenstangen1
mcp_powerpoint_server.py61.1 kB
import json import sys import traceback from typing import List, Optional, Dict, Any, Tuple, Union from pptx import Presentation from pptx.util import Inches, Pt from pptx.dml.color import RGBColor from pptx.enum.shapes import MSO_SHAPE_TYPE, MSO_AUTO_SHAPE_TYPE from pptx.enum.dml import MSO_FILL_TYPE, MSO_LINE from pptx.chart.data import CategoryChartData from pptx.enum.chart import XL_CHART_TYPE import os import base64 from PIL import Image import io import numpy as np import uuid from pathlib import Path import tempfile import shutil import datetime from mcp.server.fastmcp import FastMCP class PowerPointContext: def __init__(self, workspace_dir: str = "presentations"): """ Initialize the PowerPoint context with a workspace directory. Args: workspace_dir (str): Directory containing PowerPoint files """ self.workspace_dir = Path(workspace_dir) self.workspace_dir.mkdir(exist_ok=True) self.presentations: Dict[str, Presentation] = {} self.current_presentation: Optional[str] = None # Create a template directory self.template_dir = self.workspace_dir / "templates" self.template_dir.mkdir(exist_ok=True) # Element ID mapping self.element_ids = {} # Maps presentation path -> slide index -> shape id -> internal id def get_presentation(self, path: str) -> Presentation: """ Get a presentation from the workspace. Args: path (str): Path to the presentation (relative to workspace or absolute) """ # Convert to Path object path_obj = Path(path) # If it's not an absolute path, make it relative to workspace if not path_obj.is_absolute(): path_obj = self.workspace_dir / path_obj path_str = str(path_obj) if path_str not in self.presentations: if path_obj.exists(): self.presentations[path_str] = Presentation(path_str) else: self.presentations[path_str] = Presentation() self.current_presentation = path_str return self.presentations[path_str] def save_presentation(self, path: Optional[str] = None) -> None: """ Save the current presentation. Args: path (Optional[str]): Path to save to (if None, uses current path) """ if path: path_obj = Path(path) if not path_obj.is_absolute(): path_obj = self.workspace_dir / path_obj save_path = str(path_obj) else: save_path = self.current_presentation if save_path and save_path in self.presentations: self.presentations[save_path].save(save_path) def list_presentations(self) -> List[str]: """List all PowerPoint files in the workspace.""" return [str(f.relative_to(self.workspace_dir)) for f in self.workspace_dir.glob("*.pptx")] def upload_presentation(self, file_path: str) -> str: """ Upload a new presentation to the workspace. Args: file_path (str): Path to the file to upload Returns: str: Path to the saved file """ if not file_path.endswith('.pptx'): raise ValueError("Only .pptx files are supported") source_path = Path(file_path) if not source_path.exists(): raise FileNotFoundError(f"File {file_path} not found") dest_path = self.workspace_dir / source_path.name # Copy the file to the workspace shutil.copy2(source_path, dest_path) return str(dest_path.relative_to(self.workspace_dir)) # Element Management def generate_element_id(self) -> str: """Generate a unique ID for an element.""" return str(uuid.uuid4()) def register_element(self, presentation_path: str, slide_index: int, shape) -> str: """Register a shape and get its unique ID.""" if presentation_path not in self.element_ids: self.element_ids[presentation_path] = {} if slide_index not in self.element_ids[presentation_path]: self.element_ids[presentation_path][slide_index] = {} # Use shape's internal ID if available, otherwise create one shape_id = getattr(shape, "shape_id", id(shape)) if shape_id not in self.element_ids[presentation_path][slide_index]: element_id = self.generate_element_id() self.element_ids[presentation_path][slide_index][shape_id] = element_id return self.element_ids[presentation_path][slide_index][shape_id] def get_shape_by_id(self, presentation: Presentation, slide_index: int, element_id: str): """Get a shape by its unique ID.""" try: # Verify slide index is valid if slide_index >= len(presentation.slides): return None slide = presentation.slides[slide_index] # Get the presentation path presentation_path = self.current_presentation if not presentation_path: return None # Check if we have element mappings for this presentation and slide if (presentation_path not in self.element_ids or slide_index not in self.element_ids[presentation_path]): return None # Iterate through shapes and find matching element ID for shape in slide.shapes: shape_id = getattr(shape, "shape_id", id(shape)) if (shape_id in self.element_ids[presentation_path][slide_index] and self.element_ids[presentation_path][slide_index][shape_id] == element_id): return shape # If we get here, no matching shape was found return None except Exception as e: print(f"Error in get_shape_by_id: {str(e)}") return None def analyze_slide_content(self, presentation_path: str, slide) -> Dict[str, Any]: """Analyze the content of a slide and return structured information.""" content = { "text_boxes": [], "images": [], "shapes": [], "charts": [], "tables": [], "layout": None } # Get slide layout content["layout"] = slide.slide_layout.name if slide.slide_layout else None # Analyze shapes for shape in slide.shapes: # Register this shape to get a unique ID element_id = self.register_element(presentation_path, slide.slides.index(slide), shape) shape_info = { "id": element_id, "type": str(shape.shape_type), "position": {"x": shape.left / Inches(1), "y": shape.top / Inches(1)}, "size": {"width": shape.width / Inches(1), "height": shape.height / Inches(1)} } if shape.shape_type == MSO_SHAPE_TYPE.PICTURE: content["images"].append(shape_info) elif shape.shape_type == MSO_SHAPE_TYPE.TABLE: content["tables"].append(shape_info) elif shape.shape_type == MSO_SHAPE_TYPE.CHART: content["charts"].append(shape_info) elif hasattr(shape, "text") and shape.text.strip(): shape_info["text"] = shape.text.strip() content["text_boxes"].append(shape_info) else: content["shapes"].append(shape_info) return content def find_element(self, presentation_path: str, slide_index: int, element_type: str = "any", search_text: Optional[str] = None, position: Optional[Dict[str, float]] = None) -> List[Dict[str, Any]]: """ Find elements on a slide based on type, text content, or position. Args: presentation_path: Path to the presentation slide_index: Index of the slide to search element_type: Type of element to find (text, shape, image, chart, table, any) search_text: Text to search for in element content position: Position criteria for finding elements Returns: List of matching elements with confidence scores """ presentation = self.get_presentation(presentation_path) slide = presentation.slides[slide_index] results = [] for shape in slide.shapes: # Skip if filtering by type and this doesn't match if element_type != "any": if element_type == "text" and (not hasattr(shape, "text") or not shape.text.strip()): continue if element_type == "image" and shape.shape_type != MSO_SHAPE_TYPE.PICTURE: continue if element_type == "chart" and shape.shape_type != MSO_SHAPE_TYPE.CHART: continue if element_type == "table" and shape.shape_type != MSO_SHAPE_TYPE.TABLE: continue if element_type == "shape" and (shape.shape_type == MSO_SHAPE_TYPE.PICTURE or shape.shape_type == MSO_SHAPE_TYPE.CHART or shape.shape_type == MSO_SHAPE_TYPE.TABLE or (hasattr(shape, "text") and shape.text.strip())): continue # Check text content if specified text_match = False confidence = 1.0 if search_text and hasattr(shape, "text"): shape_text = shape.text.strip().lower() search_lower = search_text.lower() if shape_text == search_lower: text_match = True confidence = 1.0 elif search_lower in shape_text: text_match = True # Calculate confidence based on how much of the text matches confidence = len(search_lower) / len(shape_text) else: continue # Text doesn't match at all # Check position if specified position_match = True if position: shape_x = shape.left / Inches(1) shape_y = shape.top / Inches(1) # Calculate distance from target position target_x = position.get("x") target_y = position.get("y") proximity = position.get("proximity", 1.0) if target_x is not None and target_y is not None: distance = ((shape_x - target_x) ** 2 + (shape_y - target_y) ** 2) ** 0.5 if distance > proximity: position_match = False else: # Adjust confidence based on proximity position_confidence = 1.0 - (distance / proximity) confidence *= position_confidence # If all criteria match, add to results if (search_text is None or text_match) and position_match: element_id = self.register_element(presentation_path, slide_index, shape) element = { "id": element_id, "type": self._get_shape_type_name(shape), "text": shape.text.strip() if hasattr(shape, "text") else None, "position": {"x": shape.left / Inches(1), "y": shape.top / Inches(1)}, "size": {"width": shape.width / Inches(1), "height": shape.height / Inches(1)}, "confidence": confidence } results.append(element) # Sort by confidence results.sort(key=lambda x: x["confidence"], reverse=True) return results def _get_shape_type_name(self, shape) -> str: """Convert shape type to a user-friendly name.""" if shape.shape_type == MSO_SHAPE_TYPE.AUTO_SHAPE: for name, value in vars(MSO_AUTO_SHAPE_TYPE).items(): if not name.startswith("__") and value == shape.auto_shape_type: return name.lower() return "auto_shape" elif shape.shape_type == MSO_SHAPE_TYPE.PICTURE: return "image" elif shape.shape_type == MSO_SHAPE_TYPE.CHART: return "chart" elif shape.shape_type == MSO_SHAPE_TYPE.TABLE: return "table" else: for name, value in vars(MSO_SHAPE_TYPE).items(): if not name.startswith("__") and value == shape.shape_type: return name.lower() return "unknown" def edit_element(self, presentation_path: str, slide_index: int, element_id: str, properties: Dict[str, Any]) -> Dict[str, Any]: """ Edit properties of a specific element on a slide. Args: presentation_path: Path to the presentation slide_index: Index of the slide element_id: Unique ID of the element to edit properties: Properties to modify (text, position, size, rotation, etc.) Returns: Updated properties of the element """ try: # First verify the presentation exists and can be loaded presentation = self.get_presentation(presentation_path) if not presentation: return {"error": f"Could not load presentation: {presentation_path}"} # Verify the slide index is valid if slide_index >= len(presentation.slides): return {"error": f"Invalid slide index {slide_index}. Presentation has {len(presentation.slides)} slides."} # Get the shape and handle None case explicitly shape = self.get_shape_by_id(presentation, slide_index, element_id) if not shape: return {"error": f"Could not find element with ID {element_id} on slide {slide_index}"} # Apply changes based on properties if "text" in properties and hasattr(shape, "text_frame"): shape.text_frame.text = properties["text"] if "position" in properties: position = properties["position"] if "x" in position: shape.left = Inches(position["x"]) if "y" in position: shape.top = Inches(position["y"]) if "size" in properties: size = properties["size"] if "width" in size: shape.width = Inches(size["width"]) if "height" in size: shape.height = Inches(size["height"]) if "rotation" in properties: shape.rotation = properties["rotation"] if "transparency" in properties and hasattr(shape, "fill"): alpha = int(255 * (100 - properties["transparency"]) / 100) if hasattr(shape.fill.fore_color, "transparency"): shape.fill.fore_color.transparency = (255 - alpha) / 255 # Return updated properties updated_props = { "text": shape.text if hasattr(shape, "text") else None, "position": {"x": shape.left / Inches(1), "y": shape.top / Inches(1)}, "size": {"width": shape.width / Inches(1), "height": shape.height / Inches(1)}, "rotation": shape.rotation if hasattr(shape, "rotation") else None } # Save the changes self.save_presentation(presentation_path) return { "message": "Element updated successfully", "properties": updated_props } except Exception as e: # Add more context to the error message error_msg = f"Error editing element: {str(e)}\n" error_msg += f"Presentation: {presentation_path}\n" error_msg += f"Slide: {slide_index}\n" error_msg += f"Element ID: {element_id}\n" error_msg += f"Properties: {properties}" return {"error": error_msg} def style_element(self, presentation_path: str, slide_index: int, element_id: str, style_properties: Dict[str, Any]) -> bool: """ Apply styling to a specific element on a slide. Args: presentation_path: Path to the presentation slide_index: Index of the slide element_id: Unique ID of the element to style style_properties: Style properties to apply Returns: True if styling was applied successfully """ presentation = self.get_presentation(presentation_path) shape = self.get_shape_by_id(presentation, slide_index, element_id) # Apply font styling if "font" in style_properties and hasattr(shape, "text_frame"): font_props = style_properties["font"] for paragraph in shape.text_frame.paragraphs: for run in paragraph.runs: if "family" in font_props: run.font.name = font_props["family"] if "size" in font_props: run.font.size = Pt(font_props["size"]) if "bold" in font_props: run.font.bold = font_props["bold"] if "italic" in font_props: run.font.italic = font_props["italic"] if "underline" in font_props: run.font.underline = font_props["underline"] if "color" in font_props: color = font_props["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) # Apply fill styling if "fill" in style_properties and hasattr(shape, "fill"): fill_props = style_properties["fill"] if fill_props.get("type") == "solid" and "color" in fill_props: shape.fill.solid() color = fill_props["color"].lstrip('#') r, g, b = tuple(int(color[i:i+2], 16) for i in (0, 2, 4)) shape.fill.fore_color.rgb = RGBColor(r, g, b) elif fill_props.get("type") == "gradient" and "gradient" in fill_props: # PowerPoint API doesn't support setting gradient directly # This would require more complex implementation shape.fill.gradient() # Set start color start_color = fill_props["gradient"]["start_color"].lstrip('#') r, g, b = tuple(int(start_color[i:i+2], 16) for i in (0, 2, 4)) shape.fill.gradient_stops[0].color.rgb = RGBColor(r, g, b) # Set end color end_color = fill_props["gradient"]["end_color"].lstrip('#') r, g, b = tuple(int(end_color[i:i+2], 16) for i in (0, 2, 4)) shape.fill.gradient_stops[-1].color.rgb = RGBColor(r, g, b) elif fill_props.get("type") == "none": shape.fill.background() # Apply line styling if "line" in style_properties and hasattr(shape, "line"): line_props = style_properties["line"] if "color" in line_props: color = line_props["color"].lstrip('#') r, g, b = tuple(int(color[i:i+2], 16) for i in (0, 2, 4)) shape.line.color.rgb = RGBColor(r, g, b) if "width" in line_props: shape.line.width = Pt(line_props["width"]) if "style" in line_props: # Map string style names to PowerPoint constants style_map = { "solid": MSO_LINE.SOLID, "dash": MSO_LINE.DASH, "dot": MSO_LINE.ROUND_DOT, "dash-dot": MSO_LINE.DASH_DOT, "none": None } if line_props["style"] in style_map: if style_map[line_props["style"]] is None: shape.line.fill.background() # No line else: shape.line.dash_style = style_map[line_props["style"]] return True def find_slide_by_content(self, presentation_path: str, search_text: str) -> Optional[int]: """Find a slide index by searching through its content.""" presentation = self.get_presentation(presentation_path) for idx, slide in enumerate(presentation.slides): for shape in slide.shapes: if hasattr(shape, "text") and search_text.lower() in shape.text.lower(): return idx return None def get_slide_preview(self, presentation_path: str, slide_index: int) -> str: """Generate a preview of the slide as a base64 encoded image.""" presentation = self.get_presentation(presentation_path) slide = presentation.slides[slide_index] # This is a placeholder - in a real implementation, you would need to # use a proper PowerPoint rendering library or service # For now, we'll create a simple visualization using PIL width, height = 1920, 1080 # Standard slide dimensions img = Image.new('RGB', (width, height), 'white') # Draw shapes and text (simplified) from PIL import ImageDraw, ImageFont draw = ImageDraw.Draw(img) for shape in slide.shapes: if hasattr(shape, "text") and shape.text.strip(): # Convert PowerPoint coordinates to image coordinates x = int(shape.left * width / 914400) # Convert EMU to pixels y = int(shape.top * height / 685800) draw.text((x, y), shape.text, fill='black') # Convert to base64 buffered = io.BytesIO() img.save(buffered, format="PNG") return base64.b64encode(buffered.getvalue()).decode() def add_shape(self, presentation_path: str, slide_index: int, shape_type: str, position: Dict[str, float], size: Dict[str, float], style_properties: Optional[Dict[str, Any]] = None) -> str: """ Add a new shape to a slide. Args: presentation_path: Path to the presentation slide_index: Index of the slide shape_type: Type of shape to add position: Position of the shape (x, y) size: Size of the shape (width, height) style_properties: Style properties for the shape Returns: ID of the created shape """ presentation = self.get_presentation(presentation_path) slide = presentation.slides[slide_index] # Map shape type names to PowerPoint constants shape_map = { # Basic shapes "rectangle": MSO_AUTO_SHAPE_TYPE.RECTANGLE, "rounded_rectangle": MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, "oval": MSO_AUTO_SHAPE_TYPE.OVAL, "triangle": MSO_AUTO_SHAPE_TYPE.TRIANGLE, "right_triangle": MSO_AUTO_SHAPE_TYPE.RIGHT_TRIANGLE, "diamond": MSO_AUTO_SHAPE_TYPE.DIAMOND, "pentagon": MSO_AUTO_SHAPE_TYPE.PENTAGON, "hexagon": MSO_AUTO_SHAPE_TYPE.HEXAGON, "heptagon": MSO_AUTO_SHAPE_TYPE.HEPTAGON, "octagon": MSO_AUTO_SHAPE_TYPE.OCTAGON, "decagon": MSO_AUTO_SHAPE_TYPE.DECAGON, "dodecagon": MSO_AUTO_SHAPE_TYPE.DODECAGON, # Stars "star4": MSO_AUTO_SHAPE_TYPE.STAR_4_POINT, "star5": MSO_AUTO_SHAPE_TYPE.STAR_5_POINT, "star6": MSO_AUTO_SHAPE_TYPE.STAR_6_POINT, "star7": MSO_AUTO_SHAPE_TYPE.STAR_7_POINT, "star8": MSO_AUTO_SHAPE_TYPE.STAR_8_POINT, "star10": MSO_AUTO_SHAPE_TYPE.STAR_10_POINT, "star12": MSO_AUTO_SHAPE_TYPE.STAR_12_POINT, "star16": MSO_AUTO_SHAPE_TYPE.STAR_16_POINT, "star24": MSO_AUTO_SHAPE_TYPE.STAR_24_POINT, "star32": MSO_AUTO_SHAPE_TYPE.STAR_32_POINT, # Arrows "arrow": MSO_AUTO_SHAPE_TYPE.ARROW, "left_arrow": MSO_AUTO_SHAPE_TYPE.LEFT_ARROW, "right_arrow": MSO_AUTO_SHAPE_TYPE.RIGHT_ARROW, "up_arrow": MSO_AUTO_SHAPE_TYPE.UP_ARROW, "down_arrow": MSO_AUTO_SHAPE_TYPE.DOWN_ARROW, "left_right_arrow": MSO_AUTO_SHAPE_TYPE.LEFT_RIGHT_ARROW, "up_down_arrow": MSO_AUTO_SHAPE_TYPE.UP_DOWN_ARROW, # Special shapes "heart": MSO_AUTO_SHAPE_TYPE.HEART, "lightning_bolt": MSO_AUTO_SHAPE_TYPE.LIGHTNING_BOLT, "sun": MSO_AUTO_SHAPE_TYPE.SUN, "moon": MSO_AUTO_SHAPE_TYPE.MOON, "smiley_face": MSO_AUTO_SHAPE_TYPE.SMILEY_FACE, "cloud": MSO_AUTO_SHAPE_TYPE.CLOUD, # Process shapes "flow_chart_process": MSO_AUTO_SHAPE_TYPE.FLOW_CHART_PROCESS, "flow_chart_decision": MSO_AUTO_SHAPE_TYPE.FLOW_CHART_DECISION, "flow_chart_connector": MSO_AUTO_SHAPE_TYPE.FLOW_CHART_CONNECTOR, } if shape_type.lower() not in shape_map: raise ValueError(f"Unsupported shape type: {shape_type}") shape_type_enum = shape_map[shape_type.lower()] left = Inches(position.get("x", 0)) top = Inches(position.get("y", 0)) width = Inches(size.get("width", 1)) height = Inches(size.get("height", 1)) shape = slide.shapes.add_shape(shape_type_enum, left, top, width, height) # Register the shape to get a unique ID element_id = self.register_element(presentation_path, slide_index, shape) # Apply style properties if provided if style_properties: self.style_element(presentation_path, slide_index, element_id, style_properties) return element_id def connect_shapes(self, presentation_path: str, slide_index: int, from_element_id: str, to_element_id: str, connector_type: str = "straight", style_properties: Optional[Dict[str, Any]] = None) -> str: """ Create a connector between two shapes on a slide. Args: presentation_path: Path to the presentation slide_index: Index of the slide from_element_id: ID of the starting element to_element_id: ID of the ending element connector_type: Type of connector to create (straight, elbow, curved) style_properties: Style properties for the connector Returns: ID of the created connector """ presentation = self.get_presentation(presentation_path) slide = presentation.slides[slide_index] # Get the shapes from_shape = self.get_shape_by_id(presentation, slide_index, from_element_id) to_shape = self.get_shape_by_id(presentation, slide_index, to_element_id) # Map connector types to PowerPoint constants connector_map = { "straight": MSO_AUTO_SHAPE_TYPE.LINE_CONNECTOR_1, "elbow": MSO_AUTO_SHAPE_TYPE.LINE_CONNECTOR_3, "curved": MSO_AUTO_SHAPE_TYPE.CURVED_CONNECTOR_3 } if connector_type.lower() not in connector_map: raise ValueError(f"Unsupported connector type: {connector_type}") connector_type_enum = connector_map[connector_type.lower()] # Add connector # Note: python-pptx doesn't have direct support for connecting shapes # We create a line in between as an approximation start_x = from_shape.left + from_shape.width / 2 start_y = from_shape.top + from_shape.height / 2 end_x = to_shape.left + to_shape.width / 2 end_y = to_shape.top + to_shape.height / 2 # Create connector connector = slide.shapes.add_connector( connector_type_enum, start_x, start_y, end_x - start_x, end_y - start_y ) # Register the connector to get a unique ID element_id = self.register_element(presentation_path, slide_index, connector) # Apply style properties if provided if style_properties: self.style_element(presentation_path, slide_index, element_id, style_properties) return element_id def get_company_financials(self, company_id: str, metrics: List[str] = ["revenue", "ebitda", "profit"], years: Optional[List[int]] = None) -> Dict[str, Any]: """ Fetch financial data for a Norwegian company from the Proff API. Args: company_id: Company name or organization number metrics: Financial metrics to retrieve years: Years to retrieve data for (empty means all available years) Returns: Company information and financial data """ # This is a placeholder - in a real implementation, you would connect to Proff API # For now, we'll generate sample data # Sample company info company_info = { "name": company_id, "org_number": "123456789", "industry": "Technology", "founded": "2010" } # Sample financial data financials = {} current_year = 2023 # Generate data for the requested years or last 5 years if not specified if not years: years = list(range(current_year - 4, current_year + 1)) for year in years: financials[str(year)] = {} if "revenue" in metrics: financials[str(year)]["revenue"] = 1000000 * (1 + (year - 2018) * 0.1) * (0.9 + 0.2 * np.random.random()) if "ebitda" in metrics: financials[str(year)]["ebitda"] = financials[str(year)].get("revenue", 1000000) * (0.15 + 0.05 * np.random.random()) if "ebit" in metrics: financials[str(year)]["ebit"] = financials[str(year)].get("ebitda", 150000) * 0.8 if "profit" in metrics: financials[str(year)]["profit"] = financials[str(year)].get("ebit", 120000) * 0.75 if "assets" in metrics: financials[str(year)]["assets"] = financials[str(year)].get("revenue", 1000000) * (0.8 + 0.4 * np.random.random()) if "equity" in metrics: financials[str(year)]["equity"] = financials[str(year)].get("assets", 800000) * (0.4 + 0.2 * np.random.random()) if "debt" in metrics: financials[str(year)]["debt"] = financials[str(year)].get("assets", 800000) - financials[str(year)].get("equity", 320000) if "employees" in metrics: financials[str(year)]["employees"] = int(10 * (1 + (year - 2018) * 0.15)) if "growth" in metrics: if year > min(years): prev_revenue = financials[str(year-1)].get("revenue", 0) if prev_revenue > 0: financials[str(year)]["growth"] = (financials[str(year)].get("revenue", 0) - prev_revenue) / prev_revenue else: financials[str(year)]["growth"] = 0 else: financials[str(year)]["growth"] = 0 if "margin" in metrics: if financials[str(year)].get("revenue", 0) > 0: financials[str(year)]["margin"] = financials[str(year)].get("ebitda", 0) / financials[str(year)].get("revenue", 1) else: financials[str(year)]["margin"] = 0 return { "company_info": company_info, "financials": financials } def create_financial_chart(self, presentation_path: str, slide_index: int, chart_type: str, data: Dict[str, Any], position: Dict[str, float], size: Dict[str, float], title: Optional[str] = None) -> str: """ Create a financial chart on a slide. Args: presentation_path: Path to the presentation slide_index: Index of the slide chart_type: Type of chart to create data: Chart data structure position: Position of the chart size: Size of the chart title: Chart title Returns: ID of the created chart """ presentation = self.get_presentation(presentation_path) slide = presentation.slides[slide_index] # Map chart types to PowerPoint constants chart_map = { "line": XL_CHART_TYPE.LINE, "bar": XL_CHART_TYPE.BAR_CLUSTERED, "column": XL_CHART_TYPE.COLUMN_CLUSTERED, "pie": XL_CHART_TYPE.PIE, "doughnut": XL_CHART_TYPE.DOUGHNUT, "area": XL_CHART_TYPE.AREA, "scatter": XL_CHART_TYPE.XY_SCATTER, "bubble": XL_CHART_TYPE.BUBBLE, "radar": XL_CHART_TYPE.RADAR, "waterfall": XL_CHART_TYPE.WATERFALL } if chart_type.lower() not in chart_map: raise ValueError(f"Unsupported chart type: {chart_type}") chart_type_enum = chart_map[chart_type.lower()] # Prepare chart data chart_data = CategoryChartData() chart_data.categories = data["categories"] for series in data["series"]: chart_data.add_series(series["name"], series["values"]) # Add chart to slide left = Inches(position.get("x", 0)) top = Inches(position.get("y", 0)) width = Inches(size.get("width", 5)) height = Inches(size.get("height", 3)) chart_shape = slide.shapes.add_chart( chart_type_enum, left, top, width, height, chart_data ) chart = chart_shape.chart # Set chart title if provided if title: chart.has_title = True chart.chart_title.text_frame.text = title # Register the chart to get a unique ID element_id = self.register_element(presentation_path, slide_index, chart_shape) return element_id def create_comparison_table(self, presentation_path: str, slide_index: int, companies: List[str], metrics: List[str], position: Dict[str, float], title: Optional[str] = None) -> str: """ Create a comparison table on a slide. Args: presentation_path: Path to the presentation slide_index: Index of the slide companies: List of companies to compare metrics: List of metrics to compare position: Position of the table title: Table title Returns: ID of the created table """ presentation = self.get_presentation(presentation_path) slide = presentation.slides[slide_index] # Get company data companies_data = [] for company in companies: company_data = self.get_company_financials(company, metrics) companies_data.append(company_data) # Determine rows and columns rows = len(metrics) + 1 # +1 for header cols = len(companies) + 1 # +1 for metrics names # Add table to slide left = Inches(position.get("x", 0)) top = Inches(position.get("y", 0)) width = Inches(6) # Default width height = Inches(rows * 0.5) # Height based on number of rows table = slide.shapes.add_table(rows, cols, left, top, width, height).table # Fill header row table.cell(0, 0).text = "Metrics" for i, company in enumerate(companies): table.cell(0, i + 1).text = companies_data[i]["company_info"]["name"] # Fill metrics rows for i, metric in enumerate(metrics): table.cell(i + 1, 0).text = metric.capitalize() for j, company in enumerate(companies): # Get the latest year data for this metric years = sorted(companies_data[j]["financials"].keys()) if years: latest_year = years[-1] value = companies_data[j]["financials"][latest_year].get(metric, "N/A") # Format value based on metric type if metric in ["revenue", "ebitda", "ebit", "profit", "assets", "equity", "debt"]: # Format as currency if isinstance(value, (int, float)): table.cell(i + 1, j + 1).text = f"NOK {value:,.0f}" else: table.cell(i + 1, j + 1).text = str(value) elif metric in ["growth", "margin", "roi"]: # Format as percentage if isinstance(value, (int, float)): table.cell(i + 1, j + 1).text = f"{value:.1%}" else: table.cell(i + 1, j + 1).text = str(value) else: # Format as is table.cell(i + 1, j + 1).text = str(value) else: table.cell(i + 1, j + 1).text = "N/A" # Register the table to get a unique ID element_id = self.register_element(presentation_path, slide_index, table) # Add title if provided if title: title_left = left title_top = top - Inches(0.5) title_shape = slide.shapes.add_textbox(title_left, title_top, Inches(6), Inches(0.5)) title_shape.text_frame.text = title title_shape.text_frame.paragraphs[0].font.bold = True title_shape.text_frame.paragraphs[0].font.size = Pt(14) return element_id def apply_template(self, presentation_path: str, template_name: str, options: Optional[Dict[str, bool]] = None) -> Dict[str, Any]: """ Apply a template to a presentation. Args: presentation_path: Path to the presentation template_name: Name of the template to apply options: Template application options Returns: Elements applied from the template """ presentation = self.get_presentation(presentation_path) # Get template path template_path = self.template_dir / f"{template_name}.pptx" if not template_path.exists(): raise ValueError(f"Template '{template_name}' not found") # Open template template = Presentation(template_path) # Set default options if options is None: options = { "apply_master": True, "apply_theme": True, "apply_layouts": True } # Apply master slides if requested applied_elements = { "master_slides": False, "theme": False, "layouts": [] } # Note: python-pptx doesn't provide direct methods to apply templates # This would require a more complex implementation # For now, we'll simulate it by copying elements # Apply theme (colors, fonts) if options.get("apply_theme", True): # In a real implementation, this would apply the template's theme applied_elements["theme"] = True # Apply layouts if options.get("apply_layouts", True): # In a real implementation, this would copy the template's layouts for layout in template.slide_layouts: applied_elements["layouts"].append(layout.name) return applied_elements def create_slide_from_template(self, presentation_path: str, template_name: str, content: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """ Create a new slide based on a template. Args: presentation_path: Path to the presentation template_name: Name of the slide template to use content: Content to populate the template with Returns: Information about the created slide """ presentation = self.get_presentation(presentation_path) # Get template path template_path = self.template_dir / f"{template_name}.pptx" if not template_path.exists(): raise ValueError(f"Template '{template_name}' not found") # Open template template = Presentation(template_path) # Use first slide from template as a model if not template.slides: raise ValueError(f"Template '{template_name}' has no slides") template_slide = template.slides[0] # Find matching layout in target presentation # In a real implementation, you would find the best matching layout # For now, we'll use a default layout layout = presentation.slide_layouts[0] # Title Slide layout # Create new slide with the layout slide = presentation.slides.add_slide(layout) # Populate placeholders if content is provided populated_placeholders = [] if content: for shape in slide.shapes: if hasattr(shape, "is_placeholder") and shape.is_placeholder: ph_name = shape.placeholder_format.type if hasattr(shape.placeholder_format, "type") else "unknown" # Check if content has a matching key for key, value in content.items(): if key.lower() in ph_name.lower() or ph_name.lower() in key.lower(): if hasattr(shape, "text_frame"): shape.text_frame.text = str(value) populated_placeholders.append(key) return { "slide_index": len(presentation.slides) - 1, "populated_placeholders": populated_placeholders } def list_templates(self) -> List[Dict[str, Any]]: """ List all available templates. Returns: List of templates with metadata """ templates = [] for template_path in self.template_dir.glob("*.pptx"): template_name = template_path.stem # Load template metadata if available metadata_path = self.template_dir / f"{template_name}.json" metadata = {} if metadata_path.exists(): try: with open(metadata_path, 'r') as f: metadata = json.load(f) except Exception: pass templates.append({ "name": template_name, "path": str(template_path.relative_to(self.workspace_dir)), "metadata": metadata }) return templates def save_as_template(self, presentation_path: str, slide_index: int, template_name: str, template_description: str = "") -> Dict[str, Any]: """ Save a slide as a template. Args: presentation_path: Path to the presentation slide_index: Index of the slide to save as template template_name: Name for the new template template_description: Description of the template Returns: Information about the saved template """ presentation = self.get_presentation(presentation_path) # Create a new presentation with just this slide template = Presentation() # Create a temporary file with tempfile.NamedTemporaryFile(suffix=".pptx", delete=False) as temp_file: temp_path = temp_file.name try: # Copy the slide to the template # In a real implementation, this would properly copy the slide # For now, we'll just create a new presentation with a blank slide layout = template.slide_layouts[0] template.slides.add_slide(layout) # Save the template template_path = self.template_dir / f"{template_name}.pptx" template.save(template_path) # Save metadata metadata = { "description": template_description, "created_from": presentation_path, "slide_index": slide_index, "created_date": str(datetime.datetime.now()) } metadata_path = self.template_dir / f"{template_name}.json" with open(metadata_path, 'w') as f: json.dump(metadata, f, indent=2) return { "name": template_name, "path": str(template_path.relative_to(self.workspace_dir)), "metadata": metadata } finally: # Clean up try: os.unlink(temp_path) except Exception: pass def add_slide(self, presentation_path: str, layout_name: str = "Title and Content") -> int: """Add a new slide to the presentation and return its index.""" presentation = self.get_presentation(presentation_path) # Try to get the layout by name layout = None for slide_layout in presentation.slide_layouts: if hasattr(slide_layout, "name") and slide_layout.name == layout_name: layout = slide_layout break # If layout not found, use the first layout if layout is None: layout = presentation.slide_layouts[0] slide = presentation.slides.add_slide(layout) return len(presentation.slides) - 1 # Return the index of the new slide def add_text(self, presentation_path: str, slide_index: int, text: str, position: List[float] = [0.5, 0.5], font_size: float = 18) -> str: """Add text to a slide and return the element ID.""" presentation = self.get_presentation(presentation_path) slide = presentation.slides[slide_index] # Create a text box left, top = Inches(position[0]), Inches(position[1]) width, height = Inches(6), Inches(1) textbox = slide.shapes.add_textbox(left, top, width, height) textbox.text_frame.text = text # Set font size if textbox.text_frame.paragraphs: p = textbox.text_frame.paragraphs[0] p.font.size = Pt(font_size) # Register and return the element ID return self.register_element(presentation_path, slide_index, textbox) def get_slide_count(self, presentation_path: str) -> int: """Get the total number of slides in the presentation.""" presentation = self.get_presentation(presentation_path) return len(presentation.slides) def set_background_color(self, presentation_path: str, slide_index: int, color: Union[List[int], str]) -> bool: """Set the background color of a specific slide.""" presentation = self.get_presentation(presentation_path) slide = presentation.slides[slide_index] background = slide.background fill = background.fill fill.solid() # Convert hex color to RGB if it's a string if isinstance(color, str) and color.startswith('#'): color = color.lstrip('#') r, g, b = tuple(int(color[i:i+2], 16) for i in (0, 2, 4)) fill.fore_color.rgb = RGBColor(r, g, b) else: fill.fore_color.rgb = RGBColor(*color) return True def delete_slide(self, presentation_path: str, slide_index: int) -> bool: """Delete a specific slide from the presentation.""" presentation = self.get_presentation(presentation_path) xml_slides = presentation.slides._sldIdLst slides = list(xml_slides) if 0 <= slide_index < len(slides): xml_slides.remove(slides[slide_index]) return True return False # Create the PowerPoint context context = PowerPointContext() # Create MCP server mcp = FastMCP("PowerPoint MCP") # Basic Presentation Operations @mcp.tool() def list_presentations(): """List all PowerPoint files in the workspace.""" try: presentations = context.list_presentations() return {"presentations": presentations} except Exception as e: return {"error": str(e)} @mcp.tool() def upload_presentation(file_path: str): """Upload a new presentation to the workspace.""" try: path = context.upload_presentation(file_path) return { "message": "Presentation uploaded successfully", "path": path } except Exception as e: return {"error": str(e)} @mcp.tool() def save_presentation(presentation_path: str = None): """Save the current presentation.""" try: context.save_presentation(presentation_path) return {"message": "Presentation saved successfully"} except Exception as e: return {"error": str(e)} # Slide Operations @mcp.tool() def add_slide(presentation_path: str, layout_name: str = "Title and Content"): """Add a new slide to the presentation.""" try: slide_index = context.add_slide(presentation_path, layout_name) return { "message": "Slide added successfully", "slide_index": slide_index } except Exception as e: return {"error": str(e)} @mcp.tool() def delete_slide(presentation_path: str, slide_index: int): """Delete a slide from the presentation.""" try: success = context.delete_slide(presentation_path, slide_index) return { "success": success, "message": "Slide deleted successfully" if success else "Failed to delete slide" } except Exception as e: return {"error": str(e)} @mcp.tool() def get_slide_count(presentation_path: str): """Get the total number of slides in the presentation.""" try: count = context.get_slide_count(presentation_path) return {"count": count} except Exception as e: return {"error": str(e)} @mcp.tool() def analyze_slide(presentation_path: str, slide_index: int): """Analyze the content of a slide.""" try: presentation = context.get_presentation(presentation_path) slide = presentation.slides[slide_index] content = context.analyze_slide_content(presentation_path, slide) preview = context.get_slide_preview(presentation_path, slide_index) return { "content": content, "preview": preview } except Exception as e: return {"error": str(e)} @mcp.tool() def set_background_color(presentation_path: str, slide_index: int, color: Union[List[int], str]): """Set the background color of a slide.""" try: success = context.set_background_color(presentation_path, slide_index, color) return { "success": success, "message": "Background color set successfully" } except Exception as e: return {"error": str(e)} # Element Operations @mcp.tool() def add_text(presentation_path: str, slide_index: int, text: str, position: List[float] = [0.5, 0.5], font_size: float = 18): """Add text to a slide.""" try: element_id = context.add_text(presentation_path, slide_index, text, position, font_size) return { "message": "Text added successfully", "element_id": element_id } except Exception as e: return {"error": str(e)} @mcp.tool() def add_shape(presentation_path: str, slide_index: int, shape_type: str, position: Dict[str, float], size: Dict[str, float], style_properties: Dict[str, Any] = None): """Add a shape to a slide.""" try: element_id = context.add_shape(presentation_path, slide_index, shape_type, position, size, style_properties) return { "message": "Shape added successfully", "element_id": element_id } except Exception as e: return {"error": str(e)} @mcp.tool() def edit_element(presentation_path: str, slide_index: int, element_id: str, properties: Dict[str, Any]): """Edit an element's properties.""" try: updated_props = context.edit_element(presentation_path, slide_index, element_id, properties) return { "message": "Element updated successfully", "properties": updated_props } except Exception as e: return {"error": str(e)} @mcp.tool() def style_element(presentation_path: str, slide_index: int, element_id: str, style_properties: Dict[str, Any]): """Apply styling to an element.""" try: success = context.style_element(presentation_path, slide_index, element_id, style_properties) return { "success": success, "message": "Style applied successfully" } except Exception as e: return {"error": str(e)} @mcp.tool() def connect_shapes(presentation_path: str, slide_index: int, from_element_id: str, to_element_id: str, connector_type: str = "straight", style_properties: Dict[str, Any] = None): """Connect two shapes with a connector.""" try: connector_id = context.connect_shapes(presentation_path, slide_index, from_element_id, to_element_id, connector_type, style_properties) return { "message": "Shapes connected successfully", "connector_id": connector_id } except Exception as e: return {"error": str(e)} @mcp.tool() def find_element(presentation_path: str, slide_index: int, element_type: str = "any", search_text: str = None, position: Dict[str, float] = None): """Find elements on a slide based on criteria.""" try: elements = context.find_element(presentation_path, slide_index, element_type, search_text, position) return { "elements": elements } except Exception as e: return {"error": str(e)} # Financial Tools @mcp.tool() def get_company_financials(company_id: str, metrics: List[str] = ["revenue", "ebitda", "profit"], years: List[int] = None): """Get financial data for a company.""" try: data = context.get_company_financials(company_id, metrics, years) return data except Exception as e: return {"error": str(e)} @mcp.tool() def create_financial_chart(presentation_path: str, slide_index: int, chart_type: str, data: Dict[str, Any], position: Dict[str, float], size: Dict[str, float], title: str = None): """Create a financial chart on a slide.""" try: chart_id = context.create_financial_chart(presentation_path, slide_index, chart_type, data, position, size, title) return { "message": "Chart created successfully", "chart_id": chart_id } except Exception as e: return {"error": str(e)} @mcp.tool() def create_comparison_table(presentation_path: str, slide_index: int, companies: List[str], metrics: List[str], position: Dict[str, float], title: str = None): """Create a comparison table for companies.""" try: table_id = context.create_comparison_table(presentation_path, slide_index, companies, metrics, position, title) return { "message": "Comparison table created successfully", "table_id": table_id } except Exception as e: return {"error": str(e)} # Template Operations @mcp.tool() def list_templates(): """List all available templates.""" try: templates = context.list_templates() return {"templates": templates} except Exception as e: return {"error": str(e)} @mcp.tool() def apply_template(presentation_path: str, template_name: str, options: Dict[str, bool] = None): """Apply a template to a presentation.""" try: applied_elements = context.apply_template(presentation_path, template_name, options) return { "message": "Template applied successfully", "applied_elements": applied_elements } except Exception as e: return {"error": str(e)} @mcp.tool() def create_slide_from_template(presentation_path: str, template_name: str, content: Dict[str, Any] = None): """Create a new slide from a template.""" try: result = context.create_slide_from_template(presentation_path, template_name, content) return { "message": "Slide created from template successfully", "slide_index": result["slide_index"], "populated_placeholders": result["populated_placeholders"] } except Exception as e: return {"error": str(e)} @mcp.tool() def save_as_template(presentation_path: str, slide_index: int, template_name: str, template_description: str = ""): """Save a slide as a template.""" try: template_info = context.save_as_template(presentation_path, slide_index, template_name, template_description) return { "message": "Template saved successfully", "template_info": template_info } except Exception as e: return {"error": str(e)} @mcp.tool() def debug_element_mappings(presentation_path: str, slide_index: int): """Debug tool to inspect element mappings for a slide.""" try: if presentation_path not in context.element_ids: return {"error": f"No elements registered for presentation: {presentation_path}"} if slide_index not in context.element_ids[presentation_path]: return {"error": f"No elements registered for slide {slide_index}"} mappings = context.element_ids[presentation_path][slide_index] return { "presentation": presentation_path, "slide": slide_index, "mappings": mappings } except Exception as e: return {"error": str(e)} if __name__ == "__main__": print("Starting PowerPoint MCP Server...") print("Initializing workspace...") # Initialize workspace if not os.path.exists(context.workspace_dir): os.makedirs(context.workspace_dir) print(f"Created workspace directory: {context.workspace_dir}") if not os.path.exists(os.path.join(context.workspace_dir, "templates")): os.makedirs(os.path.join(context.workspace_dir, "templates")) print("Created templates directory") # Run the server mcp.run()

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jenstangen1/pptx-xlsx-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server