Skip to main content
Glama

PowerPoint MCP Server

by jenstangen1
mcp_powerpoint_server_win32.py33.7 kB
import win32com.client import pythoncom from typing import List, Optional, Dict, Any, Union from mcp.server.fastmcp import FastMCP import os import pywintypes # Import for specific exception types # Constants from PowerPoint VBA Object Library (usually obtained via makepy) # Using magic numbers for simplicity here, but using makepy is recommended practice # Example: from win32com.client import constants # ppSaveAsDefault = 1 (This might vary, better to use format-specific saves) ppSaveAsOpenXMLPresentation = 24 # .pptx msoShapeRectangle = 1 msoTextOrientationHorizontal = 1 msoPlaceholder = 14 # Shape type for placeholders # Placeholder type constants (might vary slightly by PP version) ppPlaceholderTitle = 1 ppPlaceholderBody = 2 ppPlaceholderCenterTitle = 13 # Often used for Title Only layout ppPlaceholderSubtitle = 14 ppPlaceholderDate = 16 ppPlaceholderSlideNumber = 15 ppPlaceholderFooter = 17 ppPlaceholderHeader = 18 ppPlaceholderObject = 7 # Generic object/content placeholder # Mapping for user-friendly placeholder names to constants PLACEHOLDER_NAME_MAP = { "title": ppPlaceholderTitle, "body": ppPlaceholderBody, "centertitle": ppPlaceholderCenterTitle, "subtitle": ppPlaceholderSubtitle, "date": ppPlaceholderDate, "slidenumber": ppPlaceholderSlideNumber, "footer": ppPlaceholderFooter, "header": ppPlaceholderHeader, "object": ppPlaceholderObject, "content": ppPlaceholderObject, # Common synonym } # Mapping for user-friendly shape type names (reverse of _get_shape_type_name) SHAPE_TYPE_NAME_MAP = { "rectangle": 1, "textbox": 17, # MSO_SHAPE_TYPE.TEXT_BOX (Note: might also be AutoShape with text) "oval": 9, # MSO_SHAPE_TYPE.OVAL (Note: Check MSO AutoShape constants for more specific ovals) "table": 19, # MSO_SHAPE_TYPE.TABLE "chart": 3, # MSO_SHAPE_TYPE.CHART "picture": 13, # MSO_SHAPE_TYPE.PICTURE "line": 20, # MSO_SHAPE_TYPE.LINE "connector": 10, # MSO_SHAPE_TYPE.CONNECTOR (check AutoShape types) "placeholder": msoPlaceholder, # Add more basic types as needed } class PowerPointEditorWin32: def __init__(self): self.app = None self._connect_or_launch_powerpoint() def _connect_or_launch_powerpoint(self): """Connects to a running instance of PowerPoint or launches a new one.""" try: # Use the Pywin32 CoInitializeEx to avoid threading issues with COM pythoncom.CoInitializeEx(pythoncom.COINIT_APARTMENTTHREADED) self.app = win32com.client.GetActiveObject("PowerPoint.Application") print("Connected to running PowerPoint application.") except pywintypes.com_error: try: self.app = win32com.client.Dispatch("PowerPoint.Application") self.app.Visible = True # Make the application visible print("Launched new PowerPoint application.") except Exception as e: print(f"Error launching PowerPoint: {e}") self.app = None except Exception as e: print(f"An unexpected error occurred: {e}") self.app = None def _ensure_connection(self): """Ensures the PowerPoint application object is valid.""" if self.app is None: self._connect_or_launch_powerpoint() if self.app is None: raise ConnectionError("Could not connect to or launch PowerPoint.") # Basic check if the app object seems responsive try: _ = self.app.Version except Exception as e: print(f"PowerPoint connection lost or unresponsive: {e}") self._connect_or_launch_powerpoint() # Try reconnecting if self.app is None: raise ConnectionError("Could not reconnect to PowerPoint.") def list_open_presentations(self) -> List[Dict[str, Any]]: """Lists all currently open presentations.""" self._ensure_connection() presentations_info = [] try: for i in range(1, self.app.Presentations.Count + 1): pres = self.app.Presentations(i) presentations_info.append({ "name": pres.Name, "path": pres.FullName if pres.Path else None, "slides": pres.Slides.Count, "saved": pres.Saved, "index": i # Provide the 1-based index for reference }) except Exception as e: print(f"Error listing presentations: {e}") # Maybe attempt reconnect if it's a COM error if "RPC server is unavailable" in str(e): self._connect_or_launch_powerpoint() # Retry might be needed here depending on strategy raise return presentations_info def get_presentation(self, identifier: str) -> Optional[Any]: """ Gets a presentation object by its name, path, or 1-based index. Args: identifier (str): The name (e.g., "Presentation1.pptx"), full path, or 1-based index (as string or int). """ self._ensure_connection() try: # Try by index first if it's an integer if isinstance(identifier, int) or identifier.isdigit(): idx = int(identifier) if 1 <= idx <= self.app.Presentations.Count: return self.app.Presentations(idx) else: print(f"Index {identifier} out of range.") return None # Try by name or path for i in range(1, self.app.Presentations.Count + 1): pres = self.app.Presentations(i) if pres.Name == identifier or pres.FullName == identifier: return pres print(f"Presentation '{identifier}' not found.") return None except Exception as e: print(f"Error getting presentation '{identifier}': {e}") raise def save_presentation(self, identifier: str, save_path: Optional[str] = None): """ Saves the specified presentation. Args: identifier (str): Name, path, or index of the presentation. save_path (Optional[str]): Path to save to. If None, saves to its current path. If the presentation is new, save_path is required. """ self._ensure_connection() pres = self.get_presentation(identifier) if not pres: raise ValueError(f"Presentation '{identifier}' not found.") try: if save_path: # Ensure the directory exists abs_path = os.path.abspath(save_path) os.makedirs(os.path.dirname(abs_path), exist_ok=True) pres.SaveAs(abs_path, ppSaveAsOpenXMLPresentation) print(f"Presentation saved as '{abs_path}'.") elif pres.Path: # Can only save if it has a path already pres.Save() print(f"Presentation '{pres.Name}' saved.") else: raise ValueError("save_path is required for a new presentation.") except Exception as e: print(f"Error saving presentation '{identifier}': {e}") raise def add_slide(self, identifier: str, layout_index: int = 1) -> int: """ Adds a new slide to the presentation. Args: identifier (str): Name, path, or index of the presentation. layout_index (int): 1-based index of the slide layout to use. Returns: int: The 1-based index of the newly added slide. """ self._ensure_connection() pres = self.get_presentation(identifier) if not pres: raise ValueError(f"Presentation '{identifier}' not found.") try: # Ensure layout_index is valid if not (1 <= layout_index <= pres.SlideMaster.CustomLayouts.Count): print(f"Warning: Layout index {layout_index} invalid. Using layout 1.") layout_index = 1 layout = pres.SlideMaster.CustomLayouts(layout_index) # Add the slide (returns the new Slide object) new_slide = pres.Slides.AddSlide(pres.Slides.Count + 1, layout) print(f"Added slide {new_slide.SlideIndex} to '{pres.Name}'.") return new_slide.SlideIndex except Exception as e: print(f"Error adding slide to '{identifier}': {e}") raise def get_slide(self, identifier: str, slide_index: int) -> Optional[Any]: """ Gets a slide object from a presentation. Args: identifier (str): Name, path, or index of the presentation. slide_index (int): 1-based index of the slide. Returns: Optional[Any]: The slide object or None if not found. """ self._ensure_connection() pres = self.get_presentation(identifier) if not pres: return None try: if 1 <= slide_index <= pres.Slides.Count: return pres.Slides(slide_index) else: print(f"Slide index {slide_index} out of range for '{pres.Name}'.") return None except Exception as e: print(f"Error getting slide {slide_index} from '{identifier}': {e}") raise def add_text_box(self, identifier: str, slide_index: int, text: str, left: float, top: float, width: float, height: float) -> int: """ Adds a text box to a slide. Args: identifier (str): Presentation identifier. slide_index (int): 1-based slide index. text (str): Text content for the box. left, top, width, height (float): Position and size in points. Returns: int: The unique ID of the newly added shape. """ self._ensure_connection() slide = self.get_slide(identifier, slide_index) if not slide: raise ValueError(f"Slide {slide_index} not found in presentation '{identifier}'.") try: shape = slide.Shapes.AddTextbox(msoTextOrientationHorizontal, left, top, width, height) shape.TextFrame.TextRange.Text = text shape.Name = f"TextBox_{shape.Id}" # Assign a default name print(f"Added text box (ID: {shape.Id}) to slide {slide_index}.") return shape.Id except Exception as e: print(f"Error adding text box to slide {slide_index}: {e}") raise def add_shape(self, identifier: str, slide_index: int, shape_type: int, left: float, top: float, width: float, height: float) -> int: """ Adds a basic auto shape to a slide. Args: identifier (str): Presentation identifier. slide_index (int): 1-based slide index. shape_type (int): MSO AutoShapeType constant (e.g., msoShapeRectangle). left, top, width, height (float): Position and size in points. Returns: int: The unique ID of the newly added shape. """ self._ensure_connection() slide = self.get_slide(identifier, slide_index) if not slide: raise ValueError(f"Slide {slide_index} not found in presentation '{identifier}'.") try: # Use AddShape for AutoShapes shape = slide.Shapes.AddShape(shape_type, left, top, width, height) shape.Name = f"Shape_{shape.Id}" # Assign a default name print(f"Added shape (ID: {shape.Id}, Type: {shape_type}) to slide {slide_index}.") return shape.Id except Exception as e: print(f"Error adding shape to slide {slide_index}: {e}") raise def get_shape_by_id(self, identifier: str, slide_index: int, shape_id: int) -> Optional[Any]: """Gets a shape object by its unique ID.""" self._ensure_connection() slide = self.get_slide(identifier, slide_index) if not slide: return None try: # Iterate through shapes to find by ID for i in range(1, slide.Shapes.Count + 1): shape = slide.Shapes(i) if shape.Id == shape_id: return shape print(f"Shape with ID {shape_id} not found on slide {slide_index}.") return None except Exception as e: # Handle potential errors if shape is deleted during iteration etc. print(f"Error finding shape ID {shape_id} on slide {slide_index}: {e}") return None def get_shape_by_name(self, identifier: str, slide_index: int, shape_name: str) -> Optional[Any]: """Gets a shape object by its name.""" self._ensure_connection() slide = self.get_slide(identifier, slide_index) if not slide: return None try: # Accessing by name directly might fail if name is not unique or contains odd chars return slide.Shapes(shape_name) except pywintypes.com_error as e: # Handle common error for item not found if e.hresult == -2147024809: # 0x80070057 (E_INVALIDARG often means not found by name) print(f"Shape with name '{shape_name}' not found on slide {slide_index}.") else: print(f"Error finding shape name '{shape_name}' on slide {slide_index}: {e}") return None except Exception as e: print(f"Error finding shape name '{shape_name}' on slide {slide_index}: {e}") return None def find_shape_by_text(self, identifier: Union[str, int], slide_index: int, search_text: str, partial_match: bool = True) -> List[Dict[str, Any]]: """ Finds shapes on a slide containing specific text. Args: identifier (Union[str, int]): Presentation identifier. slide_index (int): 1-based slide index. search_text (str): The text to search for (case-insensitive). partial_match (bool): If True, finds shapes where search_text is a substring. If False, requires an exact match (ignoring case). Returns: List[Dict[str, Any]]: List of matching shapes with their info. """ self._ensure_connection() matches = [] slide = self.get_slide(identifier, slide_index) if not slide: print(f"Cannot find shapes by text: Slide {slide_index} not found in presentation '{identifier}'.") return [] search_lower = search_text.lower() try: for i in range(1, slide.Shapes.Count + 1): shape = slide.Shapes(i) shape_text = "" has_text_frame = False try: # Check text frame exists and has text if shape.HasTextFrame and shape.TextFrame.HasText: has_text_frame = True shape_text = shape.TextFrame.TextRange.Text except Exception: continue # Skip shapes that error on text access if has_text_frame: shape_text_lower = shape_text.lower() found = False if partial_match: if search_lower in shape_text_lower: found = True else: # Exact match (case-insensitive) if search_lower == shape_text_lower: found = True if found: shape_data = self._get_shape_basic_info(shape) shape_data["text_preview"] = shape_text[:100] + "..." if len(shape_text) > 100 else shape_text matches.append(shape_data) except Exception as e: print(f"Error searching for text '{search_text}' on slide {slide_index}: {e}") return matches def find_shapes_by_type(self, identifier: Union[str, int], slide_index: int, shape_type_name: str) -> List[Dict[str, Any]]: """ Finds shapes on a slide matching a specific type name. Args: identifier (Union[str, int]): Presentation identifier. slide_index (int): 1-based slide index. shape_type_name (str): The user-friendly name of the shape type (e.g., "rectangle", "textbox"). Case-insensitive. Returns: List[Dict[str, Any]]: List of matching shapes with their info. """ self._ensure_connection() matches = [] slide = self.get_slide(identifier, slide_index) if not slide: print(f"Cannot find shapes by type: Slide {slide_index} not found in presentation '{identifier}'.") return [] type_name_lower = shape_type_name.lower() target_type_id = SHAPE_TYPE_NAME_MAP.get(type_name_lower) if target_type_id is None: print(f"Warning: Unknown shape type name '{shape_type_name}'. Supported types: {list(SHAPE_TYPE_NAME_MAP.keys())}") return [] try: for i in range(1, slide.Shapes.Count + 1): shape = slide.Shapes(i) # Special handling for textbox which might be an AutoShape is_match = False if target_type_id == SHAPE_TYPE_NAME_MAP["textbox"]: # Check MSO_SHAPE_TYPE.TEXT_BOX or if it's an AutoShape with text if shape.Type == SHAPE_TYPE_NAME_MAP["textbox"]: is_match = True elif shape.Type == 1: # msoAutoShape try: if shape.HasTextFrame and shape.TextFrame.HasText: # Could refine this - maybe check shape.AutoShapeType? # For now, consider any autoshape with text as potential textbox match is_match = True except: pass # Ignore errors accessing text frame elif shape.Type == target_type_id: is_match = True if is_match: matches.append(self._get_shape_basic_info(shape)) except Exception as e: print(f"Error searching for shape type '{shape_type_name}' on slide {slide_index}: {e}") return matches def get_placeholder_shape(self, identifier: Union[str, int], slide_index: int, placeholder_name: str) -> Optional[Dict[str, Any]]: """ Finds a specific placeholder shape on a slide by its common name. Args: identifier (Union[str, int]): Presentation identifier. slide_index (int): 1-based slide index. placeholder_name (str): Common name of the placeholder (e.g., "title", "body", "footer"). Case-insensitive. Returns: Optional[Dict[str, Any]]: Info of the first matching placeholder shape, or None if not found. """ self._ensure_connection() slide = self.get_slide(identifier, slide_index) if not slide: print(f"Cannot find placeholder: Slide {slide_index} not found in presentation '{identifier}'.") return None name_lower = placeholder_name.lower() target_ph_type = PLACEHOLDER_NAME_MAP.get(name_lower) if target_ph_type is None: print(f"Warning: Unknown placeholder name '{placeholder_name}'. Supported names: {list(PLACEHOLDER_NAME_MAP.keys())}") return None try: for i in range(1, slide.Shapes.Count + 1): shape = slide.Shapes(i) try: # Check if it's a placeholder and its type matches if shape.Type == msoPlaceholder and shape.PlaceholderFormat.Type == target_ph_type: print(f"Found placeholder '{placeholder_name}' (ID: {shape.Id}) on slide {slide_index}.") return self._get_shape_basic_info(shape) except Exception: # Some shapes might error on PlaceholderFormat access if not placeholders continue except Exception as e: print(f"Error searching for placeholder '{placeholder_name}' on slide {slide_index}: {e}") print(f"Placeholder '{placeholder_name}' not found on slide {slide_index}.") return None def edit_element(self, identifier: str, slide_index: int, shape_identifier: Union[int, str], properties: Dict[str, Any]) -> bool: """ Edits properties of a shape identified by ID or name. Args: identifier (str): Presentation identifier. slide_index (int): 1-based slide index. shape_identifier (Union[int, str]): The shape's ID (int) or Name (str). properties (Dict[str, Any]): Dictionary of properties to change. Supported: 'text', 'left', 'top', 'width', 'height', 'name'. Returns: bool: True if successful, False otherwise. """ self._ensure_connection() if isinstance(shape_identifier, int): shape = self.get_shape_by_id(identifier, slide_index, shape_identifier) elif isinstance(shape_identifier, str): shape = self.get_shape_by_name(identifier, slide_index, shape_identifier) else: print("Invalid shape_identifier type. Use int (ID) or str (Name).") return False if not shape: return False try: if 'text' in properties and shape.HasTextFrame and shape.TextFrame.HasText: shape.TextFrame.TextRange.Text = str(properties['text']) if 'left' in properties: shape.Left = float(properties['left']) if 'top' in properties: shape.Top = float(properties['top']) if 'width' in properties: shape.Width = float(properties['width']) if 'height' in properties: shape.Height = float(properties['height']) if 'name' in properties: shape.Name = str(properties['name']) # Allow renaming print(f"Edited properties for shape '{shape_identifier}' on slide {slide_index}.") return True except Exception as e: print(f"Error editing shape '{shape_identifier}' on slide {slide_index}: {e}") return False def list_shapes(self, identifier: Union[str, int], slide_index: int) -> List[Dict[str, Any]]: """Lists shapes on a slide with their ID, Name, Type, and basic geometry.""" self._ensure_connection() shapes_info = [] slide = self.get_slide(identifier, slide_index) if not slide: print(f"Cannot list shapes: Slide {slide_index} not found in presentation '{identifier}'.") return [] try: for i in range(1, slide.Shapes.Count + 1): shape = slide.Shapes(i) shapes_info.append(self._get_shape_basic_info(shape)) # Use helper except Exception as e: print(f"Error listing shapes on slide {slide_index}: {e}") # Don't raise, just return what we have or empty list return shapes_info def _get_shape_basic_info(self, shape: Any) -> Dict[str, Any]: """Helper to get common info dictionary for a shape.""" info = { "id": -1, "name": "Unknown", "type_id": -1, "type_name": "Unknown", "left": 0, "top": 0, "width": 0, "height": 0, "has_text": False, "is_placeholder": False } try: info["id"] = shape.Id info["name"] = shape.Name info["type_id"] = shape.Type info["type_name"] = self._get_shape_type_name(shape.Type) info["left"] = shape.Left info["top"] = shape.Top info["width"] = shape.Width info["height"] = shape.Height # Check for text try: if shape.HasTextFrame and shape.TextFrame.HasText: info["has_text"] = True except Exception: pass # Check if placeholder try: if shape.Type == msoPlaceholder: info["is_placeholder"] = True info["placeholder_type_id"] = shape.PlaceholderFormat.Type # Add friendly name for placeholder type if possible for name, ph_id in PLACEHOLDER_NAME_MAP.items(): if ph_id == info["placeholder_type_id"]: info["placeholder_type_name"] = name break except Exception: pass except Exception as e: print(f"Error getting basic info for a shape: {e}") # Return partial info if possible return info def _get_shape_type_name(self, type_id: int) -> str: """Returns a readable name for MSO_SHAPE_TYPE IDs (add more as needed).""" # This is a simplified mapping. A full mapping would be extensive. # Consider reversing SHAPE_TYPE_NAME_MAP for consistency mapping = { 1: "Rectangle/AutoShape", # Generic AutoShape 17: "TextBox", 9: "Oval", # Generic Oval (likely AutoShape) 19: "Table", 3: "Chart", 13: "Picture", 20: "Line", 10: "Connector", msoPlaceholder: "Placeholder", # Add more from MSO_SHAPE_TYPE if needed 6: "Group", 7: "EmbeddedObject", 8: "FormControl", 11: "Freeform", 12: "Media", 15: "OLEControlObject", 16: "ScriptAnchor", 18: "Canvas", 21: "Ink", 22: "InkComment", 23: "Diagram", # SmartArt? Check MSO_SHAPE_TYPE constants 24: "WebVideo", 25: "ContentApp", # Office Add-in 26: "GraphicFrame", # Holds Table, Chart, SmartArt, etc. 27: "LinkedOLEObject", 28: "LinkedPicture", 29: "Model3D", 30: "LinkedContentApp", } return mapping.get(type_id, f"Unknown ({type_id})") # --- MCP Server Setup --- # Create the PowerPoint editor instance editor = PowerPointEditorWin32() # Create MCP server mcp = FastMCP("PowerPoint MCP (Win32)") @mcp.tool() def list_open_presentations(): """Lists currently open PowerPoint presentations.""" try: return {"presentations": editor.list_open_presentations()} except Exception as e: return {"error": f"Failed to list presentations: {str(e)}"} @mcp.tool() def save_presentation(identifier: str, save_path: str = None): """Saves the specified presentation. Use index, name, or full path as identifier.""" try: editor.save_presentation(identifier, save_path) return {"message": f"Save command issued for presentation '{identifier}'."} except Exception as e: return {"error": f"Failed to save presentation '{identifier}': {str(e)}"} @mcp.tool() def add_slide(identifier: str, layout_index: int = 1): """Adds a slide to the specified presentation. Use index, name, or full path as identifier.""" try: new_slide_index = editor.add_slide(identifier, layout_index) return {"message": "Slide added successfully.", "slide_index": new_slide_index} except Exception as e: return {"error": f"Failed to add slide to '{identifier}': {str(e)}"} @mcp.tool() def add_text_box(identifier: str, slide_index: int, text: str, left: float = 72, top: float = 72, width: float = 288, height: float = 72): """Adds a text box to a specific slide (dimensions in points).""" try: shape_id = editor.add_text_box(identifier, slide_index, text, left, top, width, height) return {"message": "Text box added successfully.", "shape_id": shape_id} except Exception as e: return {"error": f"Failed to add text box to slide {slide_index} in '{identifier}': {str(e)}"} @mcp.tool() def add_rectangle(identifier: str, slide_index: int, left: float = 72, top: float = 150, width: float = 144, height: float = 72): """Adds a rectangle shape to a specific slide (dimensions in points).""" try: # msoShapeRectangle = 1 shape_id = editor.add_shape(identifier, slide_index, msoShapeRectangle, left, top, width, height) return {"message": "Rectangle added successfully.", "shape_id": shape_id} except Exception as e: return {"error": f"Failed to add rectangle to slide {slide_index} in '{identifier}': {str(e)}"} @mcp.tool() def edit_element(identifier: str, slide_index: int, shape_identifier: Union[int, str], properties: Dict[str, Any]): """Edits a shape's properties (text, left, top, width, height, name). Identify shape by ID (int) or Name (str).""" try: success = editor.edit_element(identifier, slide_index, shape_identifier, properties) if success: return {"message": f"Element '{shape_identifier}' updated successfully."} else: # Editor method already prints errors, return a generic failure return {"error": f"Failed to update element '{shape_identifier}'. See server logs for details."} except Exception as e: return {"error": f"Failed to edit element '{shape_identifier}' on slide {slide_index}: {str(e)}"} @mcp.tool() def list_shapes(identifier: str, slide_index: int): """Lists all shapes on a given slide with their ID, Name, and Type.""" try: shapes = editor.list_shapes(identifier, slide_index) return {"shapes": shapes} except Exception as e: return {"error": f"Failed to list shapes on slide {slide_index} in '{identifier}': {str(e)}"} @mcp.tool() def find_shape_by_text(identifier: Union[str, int], slide_index: int, search_text: str, partial_match: bool = True): """Finds shapes on a slide containing specific text (case-insensitive). Set partial_match=False for exact match.""" if not editor: return {"error": "PowerPoint editor not initialized."} try: matches = editor.find_shape_by_text(identifier, slide_index, search_text, partial_match) return {"matches": matches} except Exception as e: return _handle_tool_error("find_shape_by_text", e) @mcp.tool() def find_shapes_by_type(identifier: Union[str, int], slide_index: int, shape_type_name: str): """Finds shapes on a slide by type name (e.g., 'rectangle', 'textbox', 'picture', 'placeholder').""" if not editor: return {"error": "PowerPoint editor not initialized."} supported_types = list(SHAPE_TYPE_NAME_MAP.keys()) if shape_type_name.lower() not in supported_types: return {"error": f"Unsupported shape type name '{shape_type_name}'. Try one of: {supported_types}"} try: matches = editor.find_shapes_by_type(identifier, slide_index, shape_type_name) return {"matches": matches} except Exception as e: return _handle_tool_error("find_shapes_by_type", e) @mcp.tool() def get_placeholder_shape(identifier: Union[str, int], slide_index: int, placeholder_name: str): """Gets a specific placeholder shape by name (e.g., 'title', 'body', 'footer', 'slidenumber').""" if not editor: return {"error": "PowerPoint editor not initialized."} supported_placeholders = list(PLACEHOLDER_NAME_MAP.keys()) if placeholder_name.lower() not in supported_placeholders: return {"error": f"Unsupported placeholder name '{placeholder_name}'. Try one of: {supported_placeholders}"} try: match = editor.get_placeholder_shape(identifier, slide_index, placeholder_name) if match: return {"placeholder_found": True, "shape_info": match} else: return {"placeholder_found": False, "message": f"Placeholder '{placeholder_name}' not found on slide {slide_index}."} except Exception as e: return _handle_tool_error("get_placeholder_shape", e) # You might want to add cleanup for COM objects, although Python's garbage collection # combined with pywin32's handling often manages this. Explicitly setting self.app = None # and maybe calling pythoncom.CoUninitialize() on shutdown could be added for robustness. if __name__ == "__main__": print("Starting PowerPoint MCP Server (Win32)...") # The editor instance is created globally, attempting connection immediately. if editor.app is None: print("Warning: Failed to connect to PowerPoint on startup.") # Server will still run, but tools will fail until PowerPoint is available # and a tool call triggers a reconnect attempt. # Run the MCP server mcp.run() # Make sure pywin32 is installed: pip install pywin32 # You might need to run `python Scripts/pywin32_postinstall.py -install` # from your Python environment's directory if COM doesn't work initially. # To get constants like ppSaveAsOpenXMLPresentation, run makepy: # import win32com.client # win32com.client.gencache.EnsureModule('{91493440-5A91-11CF-8700-00AA0060263B}', 0, 2, 12) # For Office/PPT 16.0 # Then you can use: from win32com.client import constants

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