├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── __init__.py ├── mcp-config.json ├── mcp_all_tools_templates_effects_demo.pptx ├── mcp_config_sample.json ├── ppt_mcp_server.py ├── public ├── demo.gif └── demo.mp4 ├── pyproject.toml ├── requirements.txt ├── setup_mcp.py ├── slide_layout_templates.json ├── smithery.yaml ├── tools ├── __init__.py ├── chart_tools.py ├── connector_tools.py ├── content_tools.py ├── hyperlink_tools.py ├── master_tools.py ├── presentation_tools.py ├── professional_tools.py ├── structural_tools.py ├── template_tools.py └── transition_tools.py └── utils ├── __init__.py ├── content_utils.py ├── core_utils.py ├── design_utils.py ├── presentation_utils.py ├── template_utils.py └── validation_utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | *.bak 12 | test/ 13 | .DS_Store 14 | CLAUDE.md -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | FROM python:3.10-alpine 3 | 4 | # Install system dependencies (if any required, e.g., for pillow) 5 | RUN apk add --no-cache gcc musl-dev libffi-dev 6 | 7 | # Set work directory 8 | WORKDIR /app 9 | 10 | # Copy the application code 11 | COPY . . 12 | 13 | # Install Python dependencies 14 | RUN pip install --no-cache-dir -r requirements.txt 15 | 16 | # Expose port if needed (not needed for stdio) 17 | 18 | # Set the entrypoint to run the MCP server 19 | ENTRYPOINT ["python", "ppt_mcp_server.py"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 GongRzhe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # PowerPoint MCP Server -------------------------------------------------------------------------------- /mcp-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "ppt": { 4 | "command": "/Users/gongzhe/GitRepos/Office-PowerPoint-MCP-Server/.venv/bin/python", 5 | "args": [ 6 | "/Users/gongzhe/GitRepos/Office-PowerPoint-MCP-Server/ppt_mcp_server.py" 7 | ], 8 | "env": { 9 | "PYTHONPATH": "/Users/gongzhe/GitRepos/Office-PowerPoint-MCP-Server", 10 | "PPT_TEMPLATE_PATH": "/Users/gongzhe/GitRepos/Office-PowerPoint-MCP-Server/templates" 11 | } 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /mcp_all_tools_templates_effects_demo.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GongRzhe/Office-PowerPoint-MCP-Server/b8d585fc5808244c1ef8a2cffbe8a4996471ec98/mcp_all_tools_templates_effects_demo.pptx -------------------------------------------------------------------------------- /mcp_config_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "word-document-server": { 4 | "command": "D:\\BackDataService\\Office-Word-MCP-Server\\.venv\\Scripts\\python.exe", 5 | "args": [ 6 | "D:\\BackDataService\\Office-Word-MCP-Server\\word_server.py" 7 | ], 8 | "env": { 9 | "PYTHONPATH": "D:\\BackDataService\\Office-Word-MCP-Server" 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /ppt_mcp_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | MCP Server for PowerPoint manipulation using python-pptx. 4 | Consolidated version with 20 tools organized into multiple modules. 5 | """ 6 | import os 7 | import argparse 8 | from typing import Dict, Any 9 | from mcp.server.fastmcp import FastMCP 10 | 11 | # import utils # Currently unused 12 | from tools import ( 13 | register_presentation_tools, 14 | register_content_tools, 15 | register_structural_tools, 16 | register_professional_tools, 17 | register_template_tools, 18 | register_hyperlink_tools, 19 | register_chart_tools, 20 | register_connector_tools, 21 | register_master_tools, 22 | register_transition_tools 23 | ) 24 | 25 | # Initialize the FastMCP server 26 | app = FastMCP( 27 | name="ppt-mcp-server", 28 | description="MCP Server for PowerPoint manipulation using python-pptx - Consolidated Edition", 29 | version="2.0.0", 30 | log_level="INFO" 31 | ) 32 | 33 | # Global state to store presentations in memory 34 | presentations = {} 35 | current_presentation_id = None 36 | 37 | # Template configuration 38 | def get_template_search_directories(): 39 | """ 40 | Get list of directories to search for templates. 41 | Uses environment variable PPT_TEMPLATE_PATH if set, otherwise uses default directories. 42 | 43 | Returns: 44 | List of directories to search for templates 45 | """ 46 | template_env_path = os.environ.get('PPT_TEMPLATE_PATH') 47 | 48 | if template_env_path: 49 | # If environment variable is set, use it as the primary template directory 50 | # Support multiple paths separated by colon (Unix) or semicolon (Windows) 51 | import platform 52 | separator = ';' if platform.system() == "Windows" else ':' 53 | env_dirs = [path.strip() for path in template_env_path.split(separator) if path.strip()] 54 | 55 | # Verify that the directories exist 56 | valid_env_dirs = [] 57 | for dir_path in env_dirs: 58 | expanded_path = os.path.expanduser(dir_path) 59 | if os.path.exists(expanded_path) and os.path.isdir(expanded_path): 60 | valid_env_dirs.append(expanded_path) 61 | 62 | if valid_env_dirs: 63 | # Add default fallback directories 64 | return valid_env_dirs + ['.', './templates', './assets', './resources'] 65 | else: 66 | print(f"Warning: PPT_TEMPLATE_PATH directories not found: {template_env_path}") 67 | 68 | # Default search directories when no environment variable or invalid paths 69 | return ['.', './templates', './assets', './resources'] 70 | 71 | # ---- Helper Functions ---- 72 | 73 | def get_current_presentation(): 74 | """Get the current presentation object or raise an error if none is loaded.""" 75 | if current_presentation_id is None or current_presentation_id not in presentations: 76 | raise ValueError("No presentation is currently loaded. Please create or open a presentation first.") 77 | return presentations[current_presentation_id] 78 | 79 | def get_current_presentation_id(): 80 | """Get the current presentation ID.""" 81 | return current_presentation_id 82 | 83 | def set_current_presentation_id(pres_id): 84 | """Set the current presentation ID.""" 85 | global current_presentation_id 86 | current_presentation_id = pres_id 87 | 88 | def validate_parameters(params): 89 | """ 90 | Validate parameters against constraints. 91 | 92 | Args: 93 | params: Dictionary of parameter name: (value, constraints) pairs 94 | 95 | Returns: 96 | (True, None) if all valid, or (False, error_message) if invalid 97 | """ 98 | for param_name, (value, constraints) in params.items(): 99 | for constraint_func, error_msg in constraints: 100 | if not constraint_func(value): 101 | return False, f"Parameter '{param_name}': {error_msg}" 102 | return True, None 103 | 104 | def is_positive(value): 105 | """Check if a value is positive.""" 106 | return value > 0 107 | 108 | def is_non_negative(value): 109 | """Check if a value is non-negative.""" 110 | return value >= 0 111 | 112 | def is_in_range(min_val, max_val): 113 | """Create a function that checks if a value is in a range.""" 114 | return lambda x: min_val <= x <= max_val 115 | 116 | def is_in_list(valid_list): 117 | """Create a function that checks if a value is in a list.""" 118 | return lambda x: x in valid_list 119 | 120 | def is_valid_rgb(color_list): 121 | """Check if a color list is a valid RGB tuple.""" 122 | if not isinstance(color_list, list) or len(color_list) != 3: 123 | return False 124 | return all(isinstance(c, int) and 0 <= c <= 255 for c in color_list) 125 | 126 | def add_shape_direct(slide, shape_type: str, left: float, top: float, width: float, height: float) -> Any: 127 | """ 128 | Add an auto shape to a slide using direct integer values instead of enum objects. 129 | 130 | This implementation provides a reliable alternative that bypasses potential 131 | enum-related issues in the python-pptx library. 132 | 133 | Args: 134 | slide: The slide object 135 | shape_type: Shape type string (e.g., 'rectangle', 'oval', 'triangle') 136 | left: Left position in inches 137 | top: Top position in inches 138 | width: Width in inches 139 | height: Height in inches 140 | 141 | Returns: 142 | The created shape 143 | """ 144 | from pptx.util import Inches 145 | 146 | # Direct mapping of shape types to their integer values 147 | # These values are directly from the MS Office VBA documentation 148 | shape_type_map = { 149 | 'rectangle': 1, 150 | 'rounded_rectangle': 2, 151 | 'oval': 9, 152 | 'diamond': 4, 153 | 'triangle': 5, # This is ISOSCELES_TRIANGLE 154 | 'right_triangle': 6, 155 | 'pentagon': 56, 156 | 'hexagon': 10, 157 | 'heptagon': 11, 158 | 'octagon': 12, 159 | 'star': 12, # This is STAR_5_POINTS (value 12) 160 | 'arrow': 13, 161 | 'cloud': 35, 162 | 'heart': 21, 163 | 'lightning_bolt': 22, 164 | 'sun': 23, 165 | 'moon': 24, 166 | 'smiley_face': 17, 167 | 'no_symbol': 19, 168 | 'flowchart_process': 112, 169 | 'flowchart_decision': 114, 170 | 'flowchart_data': 115, 171 | 'flowchart_document': 119 172 | } 173 | 174 | # Check if shape type is valid before trying to use it 175 | shape_type_lower = str(shape_type).lower() 176 | if shape_type_lower not in shape_type_map: 177 | available_shapes = ', '.join(sorted(shape_type_map.keys())) 178 | raise ValueError(f"Unsupported shape type: '{shape_type}'. Available shape types: {available_shapes}") 179 | 180 | # Get the integer value for the shape type 181 | shape_value = shape_type_map[shape_type_lower] 182 | 183 | # Create the shape using the direct integer value 184 | try: 185 | # The integer value is passed directly to add_shape 186 | shape = slide.shapes.add_shape( 187 | shape_value, Inches(left), Inches(top), Inches(width), Inches(height) 188 | ) 189 | return shape 190 | except Exception as e: 191 | raise ValueError(f"Failed to create '{shape_type}' shape using direct value {shape_value}: {str(e)}") 192 | 193 | # ---- Custom presentation management wrapper ---- 194 | 195 | class PresentationManager: 196 | """Wrapper to handle presentation state updates.""" 197 | 198 | def __init__(self, presentations_dict): 199 | self.presentations = presentations_dict 200 | 201 | def store_presentation(self, pres, pres_id): 202 | """Store a presentation and set it as current.""" 203 | self.presentations[pres_id] = pres 204 | set_current_presentation_id(pres_id) 205 | return pres_id 206 | 207 | # ---- Register Tools ---- 208 | 209 | # Create presentation manager wrapper 210 | presentation_manager = PresentationManager(presentations) 211 | 212 | # Wrapper functions to handle state management 213 | def create_presentation_wrapper(original_func): 214 | """Wrapper to handle presentation creation with state management.""" 215 | def wrapper(*args, **kwargs): 216 | result = original_func(*args, **kwargs) 217 | if "presentation_id" in result and result["presentation_id"] in presentations: 218 | set_current_presentation_id(result["presentation_id"]) 219 | return result 220 | return wrapper 221 | 222 | def open_presentation_wrapper(original_func): 223 | """Wrapper to handle presentation opening with state management.""" 224 | def wrapper(*args, **kwargs): 225 | result = original_func(*args, **kwargs) 226 | if "presentation_id" in result and result["presentation_id"] in presentations: 227 | set_current_presentation_id(result["presentation_id"]) 228 | return result 229 | return wrapper 230 | 231 | # Register all tool modules 232 | register_presentation_tools( 233 | app, 234 | presentations, 235 | get_current_presentation_id, 236 | get_template_search_directories 237 | ) 238 | 239 | register_content_tools( 240 | app, 241 | presentations, 242 | get_current_presentation_id, 243 | validate_parameters, 244 | is_positive, 245 | is_non_negative, 246 | is_in_range, 247 | is_valid_rgb 248 | ) 249 | 250 | register_structural_tools( 251 | app, 252 | presentations, 253 | get_current_presentation_id, 254 | validate_parameters, 255 | is_positive, 256 | is_non_negative, 257 | is_in_range, 258 | is_valid_rgb, 259 | add_shape_direct 260 | ) 261 | 262 | register_professional_tools( 263 | app, 264 | presentations, 265 | get_current_presentation_id 266 | ) 267 | 268 | register_template_tools( 269 | app, 270 | presentations, 271 | get_current_presentation_id 272 | ) 273 | 274 | register_hyperlink_tools( 275 | app, 276 | presentations, 277 | get_current_presentation_id, 278 | validate_parameters, 279 | is_positive, 280 | is_non_negative, 281 | is_in_range, 282 | is_valid_rgb 283 | ) 284 | 285 | register_chart_tools( 286 | app, 287 | presentations, 288 | get_current_presentation_id, 289 | validate_parameters, 290 | is_positive, 291 | is_non_negative, 292 | is_in_range, 293 | is_valid_rgb 294 | ) 295 | 296 | 297 | register_connector_tools( 298 | app, 299 | presentations, 300 | get_current_presentation_id, 301 | validate_parameters, 302 | is_positive, 303 | is_non_negative, 304 | is_in_range, 305 | is_valid_rgb 306 | ) 307 | 308 | register_master_tools( 309 | app, 310 | presentations, 311 | get_current_presentation_id, 312 | validate_parameters, 313 | is_positive, 314 | is_non_negative, 315 | is_in_range, 316 | is_valid_rgb 317 | ) 318 | 319 | register_transition_tools( 320 | app, 321 | presentations, 322 | get_current_presentation_id, 323 | validate_parameters, 324 | is_positive, 325 | is_non_negative, 326 | is_in_range, 327 | is_valid_rgb 328 | ) 329 | 330 | 331 | # ---- Additional Utility Tools ---- 332 | 333 | @app.tool() 334 | def list_presentations() -> Dict: 335 | """List all loaded presentations.""" 336 | return { 337 | "presentations": [ 338 | { 339 | "id": pres_id, 340 | "slide_count": len(pres.slides), 341 | "is_current": pres_id == current_presentation_id 342 | } 343 | for pres_id, pres in presentations.items() 344 | ], 345 | "current_presentation_id": current_presentation_id, 346 | "total_presentations": len(presentations) 347 | } 348 | 349 | @app.tool() 350 | def switch_presentation(presentation_id: str) -> Dict: 351 | """Switch to a different loaded presentation.""" 352 | if presentation_id not in presentations: 353 | return { 354 | "error": f"Presentation '{presentation_id}' not found. Available presentations: {list(presentations.keys())}" 355 | } 356 | 357 | global current_presentation_id 358 | old_id = current_presentation_id 359 | current_presentation_id = presentation_id 360 | 361 | return { 362 | "message": f"Switched from presentation '{old_id}' to '{presentation_id}'", 363 | "previous_presentation_id": old_id, 364 | "current_presentation_id": current_presentation_id 365 | } 366 | 367 | @app.tool() 368 | def get_server_info() -> Dict: 369 | """Get information about the MCP server.""" 370 | return { 371 | "name": "PowerPoint MCP Server - Enhanced Edition", 372 | "version": "2.1.0", 373 | "total_tools": 32, # Organized into 11 specialized modules 374 | "loaded_presentations": len(presentations), 375 | "current_presentation": current_presentation_id, 376 | "features": [ 377 | "Presentation Management (7 tools)", 378 | "Content Management (6 tools)", 379 | "Template Operations (7 tools)", 380 | "Structural Elements (4 tools)", 381 | "Professional Design (3 tools)", 382 | "Specialized Features (5 tools)" 383 | ], 384 | "improvements": [ 385 | "32 specialized tools organized into 11 focused modules", 386 | "68+ utility functions across 7 organized utility modules", 387 | "Enhanced parameter handling and validation", 388 | "Unified operation interfaces with comprehensive coverage", 389 | "Advanced template system with auto-generation capabilities", 390 | "Professional design tools with multiple effects and styling", 391 | "Specialized features including hyperlinks, connectors, slide masters", 392 | "Dynamic text sizing and intelligent wrapping", 393 | "Advanced visual effects and styling", 394 | "Content-aware optimization and validation", 395 | "Complete PowerPoint lifecycle management", 396 | "Modular architecture for better maintainability" 397 | ], 398 | "new_enhanced_features": [ 399 | "Hyperlink Management - Add, update, remove, and list hyperlinks in text", 400 | "Advanced Chart Data Updates - Replace chart data with new categories and series", 401 | "Advanced Text Run Formatting - Apply formatting to specific text runs", 402 | "Shape Connectors - Add connector lines and arrows between points", 403 | "Slide Master Management - Access and manage slide masters and layouts", 404 | "Slide Transitions - Basic transition management (placeholder for future)" 405 | ] 406 | } 407 | 408 | # ---- Main Function ---- 409 | def main(transport: str = "stdio", port: int = 8000): 410 | if transport == "http": 411 | import asyncio 412 | # Set the port for HTTP transport 413 | app.settings.port = port 414 | # Start the FastMCP server with HTTP transport 415 | try: 416 | app.run(transport='streamable-http') 417 | except asyncio.exceptions.CancelledError: 418 | print("Server stopped by user.") 419 | except KeyboardInterrupt: 420 | print("Server stopped by user.") 421 | except Exception as e: 422 | print(f"Error starting server: {e}") 423 | 424 | else: 425 | # Run the FastMCP server 426 | app.run(transport='stdio') 427 | 428 | if __name__ == "__main__": 429 | # Parse command line arguments 430 | parser = argparse.ArgumentParser(description="MCP Server for PowerPoint manipulation using python-pptx") 431 | 432 | parser.add_argument( 433 | "-t", 434 | "--transport", 435 | type=str, 436 | default="stdio", 437 | choices=["stdio", "http"], 438 | help="Transport method for the MCP server (default: stdio)" 439 | ) 440 | 441 | parser.add_argument( 442 | "-p", 443 | "--port", 444 | type=int, 445 | default=8000, 446 | help="Port to run the MCP server on (default: 8000)" 447 | ) 448 | args = parser.parse_args() 449 | main(args.transport, args.port) -------------------------------------------------------------------------------- /public/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GongRzhe/Office-PowerPoint-MCP-Server/b8d585fc5808244c1ef8a2cffbe8a4996471ec98/public/demo.gif -------------------------------------------------------------------------------- /public/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GongRzhe/Office-PowerPoint-MCP-Server/b8d585fc5808244c1ef8a2cffbe8a4996471ec98/public/demo.mp4 -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "office-powerpoint-mcp-server" 7 | version = "2.0.2" 8 | description = "MCP Server for PowerPoint manipulation using python-pptx - Consolidated Edition" 9 | readme = "README.md" 10 | license = {file = "LICENSE"} 11 | authors = [ 12 | {name = "GongRzhe", email = "gongrzhe@gmail.com"} 13 | ] 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | ] 19 | requires-python = ">=3.6" 20 | dependencies = [ 21 | "python-pptx>=0.6.21", 22 | "mcp[cli]>=1.3.0", 23 | "Pillow>=8.0.0", 24 | "fonttools>=4.0.0", 25 | ] 26 | 27 | [project.urls] 28 | "Homepage" = "https://github.com/GongRzhe/Office-PowerPoint-MCP-Server.git" 29 | "Bug Tracker" = "https://github.com/GongRzhe/Office-PowerPoint-MCP-Server.git/issues" 30 | 31 | [tool.hatch.build.targets.wheel] 32 | only-include = ["ppt_mcp_server.py", "tools/", "utils/", "enhanced_slide_templates.json", "slide_layout_templates.json"] 33 | sources = ["."] 34 | 35 | [tool.hatch.build] 36 | exclude = [ 37 | "public/demo.mp4", 38 | "public/demo.gif", 39 | "*.pptx" 40 | ] 41 | 42 | [project.scripts] 43 | ppt_mcp_server = "ppt_mcp_server:main" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mcp[cli] 2 | python-pptx 3 | Pillow 4 | fonttools -------------------------------------------------------------------------------- /setup_mcp.py: -------------------------------------------------------------------------------- 1 | # Import necessary Python standard libraries 2 | import os # For operating with file system, handling files and directory paths 3 | import json # For processing JSON format data 4 | import subprocess # For creating and managing subprocesses 5 | import sys # For accessing Python interpreter related variables and functions 6 | import platform # For getting current operating system information 7 | import shutil # For checking if executables exist in PATH 8 | 9 | def check_prerequisites(): 10 | """ 11 | Check if necessary prerequisites are installed 12 | 13 | Returns: 14 | tuple: (python_ok, uv_installed, uvx_installed, ppt_server_installed) 15 | """ 16 | # Check Python version 17 | python_version = sys.version_info 18 | python_ok = python_version.major >= 3 and python_version.minor >= 6 19 | 20 | # Check if uv/uvx is installed 21 | uv_installed = shutil.which("uv") is not None 22 | uvx_installed = shutil.which("uvx") is not None 23 | 24 | # Check if office-powerpoint-mcp-server is already installed via pip 25 | try: 26 | result = subprocess.run( 27 | [sys.executable, "-m", "pip", "show", "office-powerpoint-mcp-server"], 28 | capture_output=True, 29 | text=True, 30 | check=False 31 | ) 32 | ppt_server_installed = result.returncode == 0 33 | except Exception: 34 | ppt_server_installed = False 35 | 36 | return (python_ok, uv_installed, uvx_installed, ppt_server_installed) 37 | 38 | def setup_venv(): 39 | """ 40 | Function to set up Python virtual environment 41 | 42 | Features: 43 | - Checks if Python version meets requirements (3.6+) 44 | - Creates Python virtual environment (if it doesn't exist) 45 | - Installs required dependencies in the newly created virtual environment 46 | 47 | No parameters required 48 | 49 | Returns: Path to Python interpreter in the virtual environment 50 | """ 51 | # Check Python version 52 | python_version = sys.version_info 53 | if python_version.major < 3 or (python_version.major == 3 and python_version.minor < 6): 54 | print("Error: Python 3.6 or higher is required.") 55 | sys.exit(1) 56 | 57 | # Get absolute path of the directory containing the current script 58 | base_path = os.path.abspath(os.path.dirname(__file__)) 59 | # Set virtual environment directory path 60 | venv_path = os.path.join(base_path, '.venv') 61 | 62 | # Determine pip and python executable paths based on operating system 63 | is_windows = platform.system() == "Windows" 64 | if is_windows: 65 | pip_path = os.path.join(venv_path, 'Scripts', 'pip.exe') 66 | python_path = os.path.join(venv_path, 'Scripts', 'python.exe') 67 | else: 68 | pip_path = os.path.join(venv_path, 'bin', 'pip') 69 | python_path = os.path.join(venv_path, 'bin', 'python') 70 | 71 | # Check if virtual environment already exists and is valid 72 | venv_exists = os.path.exists(venv_path) 73 | pip_exists = os.path.exists(pip_path) 74 | 75 | if not venv_exists or not pip_exists: 76 | print("Creating new virtual environment...") 77 | # Remove existing venv if it's invalid 78 | if venv_exists and not pip_exists: 79 | print("Existing virtual environment is incomplete, recreating it...") 80 | try: 81 | shutil.rmtree(venv_path) 82 | except Exception as e: 83 | print(f"Warning: Could not remove existing virtual environment: {e}") 84 | print("Please delete the .venv directory manually and try again.") 85 | sys.exit(1) 86 | 87 | # Create virtual environment 88 | try: 89 | subprocess.run([sys.executable, '-m', 'venv', venv_path], check=True) 90 | print("Virtual environment created successfully!") 91 | except subprocess.CalledProcessError as e: 92 | print(f"Error creating virtual environment: {e}") 93 | sys.exit(1) 94 | else: 95 | print("Valid virtual environment already exists.") 96 | 97 | # Double-check that pip exists after creating venv 98 | if not os.path.exists(pip_path): 99 | print(f"Error: pip executable not found at {pip_path}") 100 | print("Try creating the virtual environment manually with: python -m venv .venv") 101 | sys.exit(1) 102 | 103 | # Install or update dependencies 104 | print("\nInstalling requirements...") 105 | try: 106 | # Install mcp package 107 | subprocess.run([pip_path, 'install', 'mcp[cli]'], check=True) 108 | # Install python-pptx package 109 | subprocess.run([pip_path, 'install', 'python-pptx'], check=True) 110 | 111 | # Also install dependencies from requirements.txt if it exists 112 | requirements_path = os.path.join(base_path, 'requirements.txt') 113 | if os.path.exists(requirements_path): 114 | subprocess.run([pip_path, 'install', '-r', requirements_path], check=True) 115 | 116 | 117 | print("Requirements installed successfully!") 118 | except subprocess.CalledProcessError as e: 119 | print(f"Error installing requirements: {e}") 120 | sys.exit(1) 121 | except FileNotFoundError: 122 | print(f"Error: Could not execute {pip_path}") 123 | print("Try activating the virtual environment manually and installing requirements:") 124 | if is_windows: 125 | print(f".venv\\Scripts\\activate") 126 | else: 127 | print("source .venv/bin/activate") 128 | print("pip install mcp[cli] python-pptx") 129 | sys.exit(1) 130 | 131 | return python_path 132 | 133 | def generate_mcp_config_local(python_path): 134 | """ 135 | Generate MCP configuration for locally installed office-powerpoint-mcp-server 136 | 137 | Parameters: 138 | - python_path: Path to Python interpreter in the virtual environment 139 | 140 | Returns: Path to the generated config file 141 | """ 142 | # Get absolute path of the directory containing the current script 143 | base_path = os.path.abspath(os.path.dirname(__file__)) 144 | 145 | # Path to PowerPoint Server script 146 | server_script_path = os.path.join(base_path, 'ppt_mcp_server.py') 147 | 148 | # Path to templates directory 149 | templates_path = os.path.join(base_path, 'templates') 150 | 151 | # Create MCP configuration dictionary 152 | config = { 153 | "mcpServers": { 154 | "ppt": { 155 | "command": python_path, 156 | "args": [server_script_path], 157 | "env": { 158 | "PYTHONPATH": base_path, 159 | "PPT_TEMPLATE_PATH": templates_path 160 | } 161 | } 162 | } 163 | } 164 | 165 | # Save configuration to JSON file 166 | config_path = os.path.join(base_path, 'mcp-config.json') 167 | with open(config_path, 'w') as f: 168 | json.dump(config, f, indent=2) # indent=2 gives the JSON file good formatting 169 | 170 | return config_path 171 | 172 | def generate_mcp_config_uvx(): 173 | """ 174 | Generate MCP configuration for PyPI-installed office-powerpoint-mcp-server using UVX 175 | 176 | Returns: Path to the generated config file 177 | """ 178 | # Get absolute path of the directory containing the current script 179 | base_path = os.path.abspath(os.path.dirname(__file__)) 180 | 181 | # Path to templates directory (optional for UVX installs) 182 | templates_path = os.path.join(base_path, 'templates') 183 | 184 | # Create MCP configuration dictionary 185 | env_config = {} 186 | if os.path.exists(templates_path): 187 | env_config["PPT_TEMPLATE_PATH"] = templates_path 188 | 189 | config = { 190 | "mcpServers": { 191 | "ppt": { 192 | "command": "uvx", 193 | "args": ["--from", "office-powerpoint-mcp-server", "ppt_mcp_server"], 194 | "env": env_config 195 | } 196 | } 197 | } 198 | 199 | # Save configuration to JSON file 200 | config_path = os.path.join(base_path, 'mcp-config.json') 201 | with open(config_path, 'w') as f: 202 | json.dump(config, f, indent=2) # indent=2 gives the JSON file good formatting 203 | 204 | return config_path 205 | 206 | def generate_mcp_config_module(): 207 | """ 208 | Generate MCP configuration for PyPI-installed office-powerpoint-mcp-server using Python module 209 | 210 | Returns: Path to the generated config file 211 | """ 212 | # Get absolute path of the directory containing the current script 213 | base_path = os.path.abspath(os.path.dirname(__file__)) 214 | 215 | # Path to templates directory (optional for module installs) 216 | templates_path = os.path.join(base_path, 'templates') 217 | 218 | # Create MCP configuration dictionary 219 | env_config = {} 220 | if os.path.exists(templates_path): 221 | env_config["PPT_TEMPLATE_PATH"] = templates_path 222 | 223 | config = { 224 | "mcpServers": { 225 | "ppt": { 226 | "command": sys.executable, 227 | "args": ["-m", "office_powerpoint_mcp_server"], 228 | "env": env_config 229 | } 230 | } 231 | } 232 | 233 | # Save configuration to JSON file 234 | config_path = os.path.join(base_path, 'mcp-config.json') 235 | with open(config_path, 'w') as f: 236 | json.dump(config, f, indent=2) # indent=2 gives the JSON file good formatting 237 | 238 | return config_path 239 | 240 | def install_from_pypi(): 241 | """ 242 | Install office-powerpoint-mcp-server from PyPI 243 | 244 | Returns: True if successful, False otherwise 245 | """ 246 | print("\nInstalling office-powerpoint-mcp-server from PyPI...") 247 | try: 248 | subprocess.run([sys.executable, "-m", "pip", "install", "office-powerpoint-mcp-server"], check=True) 249 | print("office-powerpoint-mcp-server successfully installed from PyPI!") 250 | return True 251 | except subprocess.CalledProcessError: 252 | print("Failed to install office-powerpoint-mcp-server from PyPI.") 253 | return False 254 | 255 | def print_config_instructions(config_path): 256 | """ 257 | Print instructions for using the generated config 258 | 259 | Parameters: 260 | - config_path: Path to the generated config file 261 | """ 262 | print(f"\nMCP configuration has been written to: {config_path}") 263 | 264 | with open(config_path, 'r') as f: 265 | config = json.load(f) 266 | 267 | print("\nMCP configuration for Claude Desktop:") 268 | print(json.dumps(config, indent=2)) 269 | 270 | # Provide instructions for adding configuration to Claude Desktop configuration file 271 | if platform.system() == "Windows": 272 | claude_config_path = os.path.expandvars("%APPDATA%\\Claude\\claude_desktop_config.json") 273 | else: # macOS 274 | claude_config_path = os.path.expanduser("~/Library/Application Support/Claude/claude_desktop_config.json") 275 | 276 | print(f"\nTo use with Claude Desktop, merge this configuration into: {claude_config_path}") 277 | 278 | def create_package_structure(): 279 | """ 280 | Create necessary package structure and directories 281 | """ 282 | # Get absolute path of the directory containing the current script 283 | base_path = os.path.abspath(os.path.dirname(__file__)) 284 | 285 | # Create __init__.py file 286 | init_path = os.path.join(base_path, '__init__.py') 287 | if not os.path.exists(init_path): 288 | with open(init_path, 'w') as f: 289 | f.write('# PowerPoint MCP Server') 290 | print(f"Created __init__.py at: {init_path}") 291 | 292 | # Create requirements.txt file 293 | requirements_path = os.path.join(base_path, 'requirements.txt') 294 | if not os.path.exists(requirements_path): 295 | with open(requirements_path, 'w') as f: 296 | f.write('mcp[cli]\npython-pptx\n') 297 | print(f"Created requirements.txt at: {requirements_path}") 298 | 299 | # Create templates directory for PowerPoint templates 300 | templates_dir = os.path.join(base_path, 'templates') 301 | if not os.path.exists(templates_dir): 302 | os.makedirs(templates_dir) 303 | print(f"Created templates directory at: {templates_dir}") 304 | 305 | # Create a README file in templates directory 306 | readme_path = os.path.join(templates_dir, 'README.md') 307 | with open(readme_path, 'w') as f: 308 | f.write("""# PowerPoint Templates 309 | 310 | This directory is for storing PowerPoint template files (.pptx or .potx) that can be used with the MCP server. 311 | 312 | ## Usage 313 | 314 | 1. Place your template files in this directory 315 | 2. Use the `create_presentation_from_template` tool with the template filename 316 | 3. The server will automatically search for templates in this directory 317 | 318 | ## Supported Formats 319 | 320 | - `.pptx` - PowerPoint presentation files 321 | - `.potx` - PowerPoint template files 322 | 323 | ## Example 324 | 325 | ```python 326 | # Create presentation from template 327 | result = create_presentation_from_template("company_template.pptx") 328 | ``` 329 | 330 | The server will search for templates in: 331 | - Current directory 332 | - ./templates/ (this directory) 333 | - ./assets/ 334 | - ./resources/ 335 | """) 336 | print(f"Created templates README at: {readme_path}") 337 | 338 | # Offer to create a sample template 339 | create_sample = input("\nWould you like to create a sample template for testing? (y/n): ").lower().strip() 340 | if create_sample in ['y', 'yes']: 341 | create_sample_template(templates_dir) 342 | 343 | def create_sample_template(templates_dir): 344 | """ 345 | Create a sample PowerPoint template for testing 346 | 347 | Parameters: 348 | - templates_dir: Directory where templates are stored 349 | """ 350 | try: 351 | # Import required modules for creating a sample template 352 | from pptx import Presentation 353 | from pptx.util import Inches, Pt 354 | from pptx.dml.color import RGBColor 355 | from pptx.enum.text import PP_ALIGN 356 | 357 | print("Creating sample template...") 358 | 359 | # Create a new presentation 360 | prs = Presentation() 361 | 362 | # Get the title slide layout 363 | title_slide_layout = prs.slide_layouts[0] 364 | slide = prs.slides.add_slide(title_slide_layout) 365 | 366 | # Set title and subtitle 367 | title = slide.shapes.title 368 | subtitle = slide.placeholders[1] 369 | 370 | title.text = "Sample Company Template" 371 | subtitle.text = "Professional Presentation Template\nCreated by PowerPoint MCP Server" 372 | 373 | # Format title 374 | title_paragraph = title.text_frame.paragraphs[0] 375 | title_paragraph.font.size = Pt(44) 376 | title_paragraph.font.bold = True 377 | title_paragraph.font.color.rgb = RGBColor(31, 73, 125) # Dark blue 378 | 379 | # Format subtitle 380 | for paragraph in subtitle.text_frame.paragraphs: 381 | paragraph.font.size = Pt(18) 382 | paragraph.font.color.rgb = RGBColor(68, 84, 106) # Gray blue 383 | paragraph.alignment = PP_ALIGN.CENTER 384 | 385 | # Add a content slide 386 | content_slide_layout = prs.slide_layouts[1] 387 | content_slide = prs.slides.add_slide(content_slide_layout) 388 | 389 | content_title = content_slide.shapes.title 390 | content_title.text = "Sample Content Slide" 391 | 392 | # Add bullet points to content 393 | content_placeholder = content_slide.placeholders[1] 394 | text_frame = content_placeholder.text_frame 395 | text_frame.text = "Key Features" 396 | 397 | # Add bullet points 398 | bullet_points = [ 399 | "Professional theme and colors", 400 | "Custom layouts and placeholders", 401 | "Ready for content creation", 402 | "Compatible with MCP server tools" 403 | ] 404 | 405 | for point in bullet_points: 406 | p = text_frame.add_paragraph() 407 | p.text = point 408 | p.level = 1 409 | 410 | # Add a section header slide 411 | section_slide_layout = prs.slide_layouts[2] if len(prs.slide_layouts) > 2 else prs.slide_layouts[0] 412 | section_slide = prs.slides.add_slide(section_slide_layout) 413 | 414 | if section_slide.shapes.title: 415 | section_slide.shapes.title.text = "Template Features" 416 | 417 | # Save the sample template 418 | template_path = os.path.join(templates_dir, 'sample_template.pptx') 419 | prs.save(template_path) 420 | 421 | print(f"✅ Sample template created: {template_path}") 422 | print(" You can now test the template feature with:") 423 | print(" • get_template_info('sample_template.pptx')") 424 | print(" • create_presentation_from_template('sample_template.pptx')") 425 | 426 | except ImportError: 427 | print("⚠️ Cannot create sample template: python-pptx not installed yet") 428 | print(" Run the setup first, then manually create templates in the templates/ directory") 429 | except Exception as e: 430 | print(f"❌ Failed to create sample template: {str(e)}") 431 | print(" You can manually add template files to the templates/ directory") 432 | 433 | # Main execution entry point 434 | if __name__ == '__main__': 435 | # Check prerequisites 436 | python_ok, uv_installed, uvx_installed, ppt_server_installed = check_prerequisites() 437 | 438 | if not python_ok: 439 | print("Error: Python 3.6 or higher is required.") 440 | sys.exit(1) 441 | 442 | print("PowerPoint MCP Server Setup") 443 | print("===========================\n") 444 | 445 | # Create necessary files 446 | create_package_structure() 447 | 448 | # If office-powerpoint-mcp-server is already installed, offer config options 449 | if ppt_server_installed: 450 | print("office-powerpoint-mcp-server is already installed via pip.") 451 | 452 | if uvx_installed: 453 | print("\nOptions:") 454 | print("1. Generate MCP config for UVX (recommended)") 455 | print("2. Generate MCP config for Python module") 456 | print("3. Set up local development environment") 457 | 458 | choice = input("\nEnter your choice (1-3): ") 459 | 460 | if choice == "1": 461 | config_path = generate_mcp_config_uvx() 462 | print_config_instructions(config_path) 463 | elif choice == "2": 464 | config_path = generate_mcp_config_module() 465 | print_config_instructions(config_path) 466 | elif choice == "3": 467 | python_path = setup_venv() 468 | config_path = generate_mcp_config_local(python_path) 469 | print_config_instructions(config_path) 470 | else: 471 | print("Invalid choice. Exiting.") 472 | sys.exit(1) 473 | else: 474 | print("\nOptions:") 475 | print("1. Generate MCP config for Python module") 476 | print("2. Set up local development environment") 477 | 478 | choice = input("\nEnter your choice (1-2): ") 479 | 480 | if choice == "1": 481 | config_path = generate_mcp_config_module() 482 | print_config_instructions(config_path) 483 | elif choice == "2": 484 | python_path = setup_venv() 485 | config_path = generate_mcp_config_local(python_path) 486 | print_config_instructions(config_path) 487 | else: 488 | print("Invalid choice. Exiting.") 489 | sys.exit(1) 490 | 491 | # If office-powerpoint-mcp-server is not installed, offer installation options 492 | else: 493 | print("office-powerpoint-mcp-server is not installed.") 494 | 495 | print("\nOptions:") 496 | print("1. Install from PyPI (recommended)") 497 | print("2. Set up local development environment") 498 | 499 | choice = input("\nEnter your choice (1-2): ") 500 | 501 | if choice == "1": 502 | if install_from_pypi(): 503 | if uvx_installed: 504 | print("\nNow generating MCP config for UVX...") 505 | config_path = generate_mcp_config_uvx() 506 | else: 507 | print("\nUVX not found. Generating MCP config for Python module...") 508 | config_path = generate_mcp_config_module() 509 | print_config_instructions(config_path) 510 | elif choice == "2": 511 | python_path = setup_venv() 512 | config_path = generate_mcp_config_local(python_path) 513 | print_config_instructions(config_path) 514 | else: 515 | print("Invalid choice. Exiting.") 516 | sys.exit(1) 517 | 518 | print("\nSetup complete! You can now use the PowerPoint MCP server with compatible clients like Claude Desktop.") 519 | 520 | print("\n" + "="*60) 521 | print("POWERPOINT MCP SERVER - NEW FEATURES") 522 | print("="*60) 523 | print("\n📁 Template Support:") 524 | print(" • Place PowerPoint templates (.pptx/.potx) in the ./templates/ directory") 525 | print(" • Use 'create_presentation_from_template' tool to create presentations from templates") 526 | print(" • Use 'get_template_info' tool to inspect template layouts and properties") 527 | print(" • Templates preserve branding, themes, and custom layouts") 528 | print(" • Template path configured via PPT_TEMPLATE_PATH environment variable") 529 | 530 | print("\n🔧 Available MCP Tools:") 531 | print(" Presentations:") 532 | print(" • create_presentation - Create new blank presentation") 533 | print(" • create_presentation_from_template - Create from template file") 534 | print(" • get_template_info - Inspect template file details") 535 | print(" • open_presentation - Open existing presentation") 536 | print(" • save_presentation - Save presentation to file") 537 | 538 | print("\n Content:") 539 | print(" • add_slide - Add slides with various layouts") 540 | print(" • add_textbox - Add formatted text boxes") 541 | print(" • add_image - Add images from files or base64") 542 | print(" • add_table - Add formatted tables") 543 | print(" • add_shape - Add various auto shapes") 544 | print(" • add_chart - Add column, bar, line, and pie charts") 545 | 546 | print("\n📚 Documentation:") 547 | print(" • Full API documentation available in README.md") 548 | print(" • Template usage examples included") 549 | print(" • Check ./templates/README.md for template guidelines") 550 | 551 | print("\n🚀 Quick Start with Templates:") 552 | print(" 1. Copy your .pptx template to ./templates/") 553 | print(" 2. Use: create_presentation_from_template('your_template.pptx')") 554 | print(" 3. Add slides using template layouts") 555 | print(" 4. Save your presentation") 556 | print("\n💡 Custom Template Paths:") 557 | print(" • Set PPT_TEMPLATE_PATH environment variable for custom locations") 558 | print(" • Supports multiple paths (colon-separated on Unix, semicolon on Windows)") 559 | print(" • Example: PPT_TEMPLATE_PATH='/path/to/templates:/path/to/more/templates'") 560 | 561 | print("\n" + "="*60) -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | {} 8 | commandFunction: 9 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio. 10 | |- 11 | (config) => ({ 12 | command: 'python', 13 | args: ['ppt_mcp_server.py'], 14 | env: {} 15 | }) 16 | exampleConfig: {} 17 | -------------------------------------------------------------------------------- /tools/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tools package for PowerPoint MCP Server. 3 | Organizes tools into logical modules for better maintainability. 4 | """ 5 | 6 | from .presentation_tools import register_presentation_tools 7 | from .content_tools import register_content_tools 8 | from .structural_tools import register_structural_tools 9 | from .professional_tools import register_professional_tools 10 | from .template_tools import register_template_tools 11 | from .hyperlink_tools import register_hyperlink_tools 12 | from .chart_tools import register_chart_tools 13 | from .connector_tools import register_connector_tools 14 | from .master_tools import register_master_tools 15 | from .transition_tools import register_transition_tools 16 | 17 | __all__ = [ 18 | "register_presentation_tools", 19 | "register_content_tools", 20 | "register_structural_tools", 21 | "register_professional_tools", 22 | "register_template_tools", 23 | "register_hyperlink_tools", 24 | "register_chart_tools", 25 | "register_connector_tools", 26 | "register_master_tools", 27 | "register_transition_tools" 28 | ] -------------------------------------------------------------------------------- /tools/chart_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Chart data management tools for PowerPoint MCP Server. 3 | Implements advanced chart data manipulation capabilities. 4 | """ 5 | 6 | from typing import Dict, List, Optional, Any 7 | from pptx.chart.data import ChartData 8 | 9 | def register_chart_tools(app, presentations, get_current_presentation_id, validate_parameters, 10 | is_positive, is_non_negative, is_in_range, is_valid_rgb): 11 | """Register chart data management tools with the FastMCP app.""" 12 | 13 | @app.tool() 14 | def update_chart_data( 15 | slide_index: int, 16 | shape_index: int, 17 | categories: List[str], 18 | series_data: List[Dict], 19 | presentation_id: str = None 20 | ) -> Dict: 21 | """ 22 | Replace existing chart data with new categories and series. 23 | 24 | Args: 25 | slide_index: Index of the slide (0-based) 26 | shape_index: Index of the chart shape (0-based) 27 | categories: List of category names 28 | series_data: List of dictionaries with 'name' and 'values' keys 29 | presentation_id: Optional presentation ID (uses current if not provided) 30 | 31 | Returns: 32 | Dictionary with operation results 33 | """ 34 | try: 35 | # Get presentation 36 | pres_id = presentation_id or get_current_presentation_id() 37 | if pres_id not in presentations: 38 | return {"error": "Presentation not found"} 39 | 40 | pres = presentations[pres_id] 41 | 42 | # Validate slide index 43 | if not (0 <= slide_index < len(pres.slides)): 44 | return {"error": f"Slide index {slide_index} out of range"} 45 | 46 | slide = pres.slides[slide_index] 47 | 48 | # Validate shape index 49 | if not (0 <= shape_index < len(slide.shapes)): 50 | return {"error": f"Shape index {shape_index} out of range"} 51 | 52 | shape = slide.shapes[shape_index] 53 | 54 | # Check if shape is a chart 55 | if not hasattr(shape, 'has_chart') or not shape.has_chart: 56 | return {"error": "Shape is not a chart"} 57 | 58 | chart = shape.chart 59 | 60 | # Create new ChartData 61 | chart_data = ChartData() 62 | chart_data.categories = categories 63 | 64 | # Add series data 65 | for series in series_data: 66 | if 'name' not in series or 'values' not in series: 67 | return {"error": "Each series must have 'name' and 'values' keys"} 68 | 69 | chart_data.add_series(series['name'], series['values']) 70 | 71 | # Replace chart data 72 | chart.replace_data(chart_data) 73 | 74 | return { 75 | "message": f"Updated chart data on slide {slide_index}, shape {shape_index}", 76 | "categories": categories, 77 | "series_count": len(series_data), 78 | "series_names": [s['name'] for s in series_data] 79 | } 80 | 81 | except Exception as e: 82 | return {"error": f"Failed to update chart data: {str(e)}"} -------------------------------------------------------------------------------- /tools/connector_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Connector and line tools for PowerPoint MCP Server. 3 | Implements connector line/arrow drawing capabilities. 4 | """ 5 | 6 | from typing import Dict, List, Optional, Any 7 | from pptx.util import Inches, Pt 8 | from pptx.enum.shapes import MSO_CONNECTOR 9 | from pptx.dml.color import RGBColor 10 | 11 | def register_connector_tools(app, presentations, get_current_presentation_id, validate_parameters, 12 | is_positive, is_non_negative, is_in_range, is_valid_rgb): 13 | """Register connector tools with the FastMCP app.""" 14 | 15 | @app.tool() 16 | def add_connector( 17 | slide_index: int, 18 | connector_type: str, 19 | start_x: float, 20 | start_y: float, 21 | end_x: float, 22 | end_y: float, 23 | line_width: float = 1.0, 24 | color: List[int] = None, 25 | presentation_id: str = None 26 | ) -> Dict: 27 | """ 28 | Add connector lines/arrows between points on a slide. 29 | 30 | Args: 31 | slide_index: Index of the slide (0-based) 32 | connector_type: Type of connector ("straight", "elbow", "curved") 33 | start_x: Starting X coordinate in inches 34 | start_y: Starting Y coordinate in inches 35 | end_x: Ending X coordinate in inches 36 | end_y: Ending Y coordinate in inches 37 | line_width: Width of the connector line in points 38 | color: RGB color as [r, g, b] list 39 | presentation_id: Optional presentation ID (uses current if not provided) 40 | 41 | Returns: 42 | Dictionary with operation results 43 | """ 44 | try: 45 | # Get presentation 46 | pres_id = presentation_id or get_current_presentation_id() 47 | if pres_id not in presentations: 48 | return {"error": "Presentation not found"} 49 | 50 | pres = presentations[pres_id] 51 | 52 | # Validate slide index 53 | if not (0 <= slide_index < len(pres.slides)): 54 | return {"error": f"Slide index {slide_index} out of range"} 55 | 56 | slide = pres.slides[slide_index] 57 | 58 | # Map connector types 59 | connector_map = { 60 | 'straight': MSO_CONNECTOR.STRAIGHT, 61 | 'elbow': MSO_CONNECTOR.ELBOW, 62 | 'curved': MSO_CONNECTOR.CURVED 63 | } 64 | 65 | if connector_type.lower() not in connector_map: 66 | return {"error": f"Invalid connector type. Use: {list(connector_map.keys())}"} 67 | 68 | # Add connector 69 | connector = slide.shapes.add_connector( 70 | connector_map[connector_type.lower()], 71 | Inches(start_x), Inches(start_y), 72 | Inches(end_x), Inches(end_y) 73 | ) 74 | 75 | # Apply formatting 76 | if line_width: 77 | connector.line.width = Pt(line_width) 78 | 79 | if color and is_valid_rgb(color): 80 | connector.line.color.rgb = RGBColor(*color) 81 | 82 | return { 83 | "message": f"Added {connector_type} connector to slide {slide_index}", 84 | "connector_type": connector_type, 85 | "start_point": [start_x, start_y], 86 | "end_point": [end_x, end_y], 87 | "shape_index": len(slide.shapes) - 1 88 | } 89 | 90 | except Exception as e: 91 | return {"error": f"Failed to add connector: {str(e)}"} -------------------------------------------------------------------------------- /tools/content_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Content management tools for PowerPoint MCP Server. 3 | Handles slides, text, images, and content manipulation. 4 | """ 5 | from typing import Dict, List, Optional, Any, Union 6 | from mcp.server.fastmcp import FastMCP 7 | import utils as ppt_utils 8 | import tempfile 9 | import base64 10 | import os 11 | 12 | 13 | def register_content_tools(app: FastMCP, presentations: Dict, get_current_presentation_id, validate_parameters, is_positive, is_non_negative, is_in_range, is_valid_rgb): 14 | """Register content management tools with the FastMCP app""" 15 | 16 | @app.tool() 17 | def add_slide( 18 | layout_index: int = 1, 19 | title: Optional[str] = None, 20 | background_type: Optional[str] = None, # "solid", "gradient", "professional_gradient" 21 | background_colors: Optional[List[List[int]]] = None, # For gradient: [[start_rgb], [end_rgb]] 22 | gradient_direction: str = "horizontal", 23 | color_scheme: str = "modern_blue", 24 | presentation_id: Optional[str] = None 25 | ) -> Dict: 26 | """Add a new slide to the presentation with optional background styling.""" 27 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 28 | 29 | if pres_id is None or pres_id not in presentations: 30 | return { 31 | "error": "No presentation is currently loaded or the specified ID is invalid" 32 | } 33 | 34 | pres = presentations[pres_id] 35 | 36 | # Validate layout index 37 | if layout_index < 0 or layout_index >= len(pres.slide_layouts): 38 | return { 39 | "error": f"Invalid layout index: {layout_index}. Available layouts: 0-{len(pres.slide_layouts) - 1}" 40 | } 41 | 42 | try: 43 | # Add the slide 44 | slide, layout = ppt_utils.add_slide(pres, layout_index) 45 | slide_index = len(pres.slides) - 1 46 | 47 | # Set title if provided 48 | if title: 49 | ppt_utils.set_title(slide, title) 50 | 51 | # Apply background if specified 52 | if background_type == "gradient" and background_colors and len(background_colors) >= 2: 53 | ppt_utils.set_slide_gradient_background( 54 | slide, background_colors[0], background_colors[1], gradient_direction 55 | ) 56 | elif background_type == "professional_gradient": 57 | ppt_utils.create_professional_gradient_background( 58 | slide, color_scheme, "subtle", gradient_direction 59 | ) 60 | 61 | return { 62 | "message": f"Added slide {slide_index} with layout {layout_index}", 63 | "slide_index": slide_index, 64 | "layout_name": layout.name if hasattr(layout, 'name') else f"Layout {layout_index}" 65 | } 66 | except Exception as e: 67 | return { 68 | "error": f"Failed to add slide: {str(e)}" 69 | } 70 | 71 | @app.tool() 72 | def get_slide_info(slide_index: int, presentation_id: Optional[str] = None) -> Dict: 73 | """Get information about a specific slide.""" 74 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 75 | 76 | if pres_id is None or pres_id not in presentations: 77 | return { 78 | "error": "No presentation is currently loaded or the specified ID is invalid" 79 | } 80 | 81 | pres = presentations[pres_id] 82 | 83 | if slide_index < 0 or slide_index >= len(pres.slides): 84 | return { 85 | "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" 86 | } 87 | 88 | slide = pres.slides[slide_index] 89 | 90 | try: 91 | return ppt_utils.get_slide_info(slide, slide_index) 92 | except Exception as e: 93 | return { 94 | "error": f"Failed to get slide info: {str(e)}" 95 | } 96 | 97 | @app.tool() 98 | def populate_placeholder( 99 | slide_index: int, 100 | placeholder_idx: int, 101 | text: str, 102 | presentation_id: Optional[str] = None 103 | ) -> Dict: 104 | """Populate a placeholder with text.""" 105 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 106 | 107 | if pres_id is None or pres_id not in presentations: 108 | return { 109 | "error": "No presentation is currently loaded or the specified ID is invalid" 110 | } 111 | 112 | pres = presentations[pres_id] 113 | 114 | if slide_index < 0 or slide_index >= len(pres.slides): 115 | return { 116 | "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" 117 | } 118 | 119 | slide = pres.slides[slide_index] 120 | 121 | try: 122 | ppt_utils.populate_placeholder(slide, placeholder_idx, text) 123 | return { 124 | "message": f"Populated placeholder {placeholder_idx} on slide {slide_index}" 125 | } 126 | except Exception as e: 127 | return { 128 | "error": f"Failed to populate placeholder: {str(e)}" 129 | } 130 | 131 | @app.tool() 132 | def add_bullet_points( 133 | slide_index: int, 134 | placeholder_idx: int, 135 | bullet_points: List[str], 136 | presentation_id: Optional[str] = None 137 | ) -> Dict: 138 | """Add bullet points to a placeholder.""" 139 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 140 | 141 | if pres_id is None or pres_id not in presentations: 142 | return { 143 | "error": "No presentation is currently loaded or the specified ID is invalid" 144 | } 145 | 146 | pres = presentations[pres_id] 147 | 148 | if slide_index < 0 or slide_index >= len(pres.slides): 149 | return { 150 | "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" 151 | } 152 | 153 | slide = pres.slides[slide_index] 154 | 155 | try: 156 | placeholder = slide.placeholders[placeholder_idx] 157 | ppt_utils.add_bullet_points(placeholder, bullet_points) 158 | return { 159 | "message": f"Added {len(bullet_points)} bullet points to placeholder {placeholder_idx} on slide {slide_index}" 160 | } 161 | except Exception as e: 162 | return { 163 | "error": f"Failed to add bullet points: {str(e)}" 164 | } 165 | 166 | @app.tool() 167 | def manage_text( 168 | slide_index: int, 169 | operation: str, # "add", "format", "validate", "format_runs" 170 | left: float = 1.0, 171 | top: float = 1.0, 172 | width: float = 4.0, 173 | height: float = 2.0, 174 | text: str = "", 175 | shape_index: Optional[int] = None, # For format/validate operations 176 | text_runs: Optional[List[Dict]] = None, # For format_runs operation 177 | # Formatting options 178 | font_size: Optional[int] = None, 179 | font_name: Optional[str] = None, 180 | bold: Optional[bool] = None, 181 | italic: Optional[bool] = None, 182 | underline: Optional[bool] = None, 183 | color: Optional[List[int]] = None, 184 | bg_color: Optional[List[int]] = None, 185 | alignment: Optional[str] = None, 186 | vertical_alignment: Optional[str] = None, 187 | # Advanced options 188 | auto_fit: bool = True, 189 | validation_only: bool = False, 190 | min_font_size: int = 8, 191 | max_font_size: int = 72, 192 | presentation_id: Optional[str] = None 193 | ) -> Dict: 194 | """Unified text management tool for adding, formatting, validating text, and formatting multiple text runs.""" 195 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 196 | 197 | if pres_id is None or pres_id not in presentations: 198 | return { 199 | "error": "No presentation is currently loaded or the specified ID is invalid" 200 | } 201 | 202 | pres = presentations[pres_id] 203 | 204 | if slide_index < 0 or slide_index >= len(pres.slides): 205 | return { 206 | "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" 207 | } 208 | 209 | slide = pres.slides[slide_index] 210 | 211 | # Validate parameters 212 | validations = {} 213 | if font_size is not None: 214 | validations["font_size"] = (font_size, [(is_positive, "must be a positive integer")]) 215 | if color is not None: 216 | validations["color"] = (color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")]) 217 | if bg_color is not None: 218 | validations["bg_color"] = (bg_color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")]) 219 | 220 | if validations: 221 | valid, error = validate_parameters(validations) 222 | if not valid: 223 | return {"error": error} 224 | 225 | try: 226 | if operation == "add": 227 | # Add new textbox 228 | shape = ppt_utils.add_textbox( 229 | slide, left, top, width, height, text, 230 | font_size=font_size, 231 | font_name=font_name, 232 | bold=bold, 233 | italic=italic, 234 | underline=underline, 235 | color=tuple(color) if color else None, 236 | bg_color=tuple(bg_color) if bg_color else None, 237 | alignment=alignment, 238 | vertical_alignment=vertical_alignment, 239 | auto_fit=auto_fit 240 | ) 241 | return { 242 | "message": f"Added text box to slide {slide_index}", 243 | "shape_index": len(slide.shapes) - 1, 244 | "text": text 245 | } 246 | 247 | elif operation == "format": 248 | # Format existing text shape 249 | if shape_index is None or shape_index < 0 or shape_index >= len(slide.shapes): 250 | return { 251 | "error": f"Invalid shape index for formatting: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}" 252 | } 253 | 254 | shape = slide.shapes[shape_index] 255 | ppt_utils.format_text_advanced( 256 | shape, 257 | font_size=font_size, 258 | font_name=font_name, 259 | bold=bold, 260 | italic=italic, 261 | underline=underline, 262 | color=tuple(color) if color else None, 263 | bg_color=tuple(bg_color) if bg_color else None, 264 | alignment=alignment, 265 | vertical_alignment=vertical_alignment 266 | ) 267 | return { 268 | "message": f"Formatted text shape {shape_index} on slide {slide_index}" 269 | } 270 | 271 | elif operation == "validate": 272 | # Validate text fit 273 | if shape_index is None or shape_index < 0 or shape_index >= len(slide.shapes): 274 | return { 275 | "error": f"Invalid shape index for validation: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}" 276 | } 277 | 278 | validation_result = ppt_utils.validate_text_fit( 279 | slide.shapes[shape_index], 280 | text_content=text or None, 281 | font_size=font_size or 12 282 | ) 283 | 284 | if not validation_only and validation_result.get("needs_optimization"): 285 | # Apply automatic fixes 286 | fix_result = ppt_utils.validate_and_fix_slide( 287 | slide, 288 | auto_fix=True, 289 | min_font_size=min_font_size, 290 | max_font_size=max_font_size 291 | ) 292 | validation_result.update(fix_result) 293 | 294 | return validation_result 295 | 296 | elif operation == "format_runs": 297 | # Format multiple text runs with different formatting 298 | if shape_index is None or shape_index < 0 or shape_index >= len(slide.shapes): 299 | return { 300 | "error": f"Invalid shape index for format_runs: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}" 301 | } 302 | 303 | if not text_runs: 304 | return {"error": "text_runs parameter is required for format_runs operation"} 305 | 306 | shape = slide.shapes[shape_index] 307 | 308 | # Check if shape has text 309 | if not hasattr(shape, 'text_frame') or not shape.text_frame: 310 | return {"error": "Shape does not contain text"} 311 | 312 | # Clear existing text and rebuild with formatted runs 313 | text_frame = shape.text_frame 314 | text_frame.clear() 315 | 316 | formatted_runs = [] 317 | 318 | for run_data in text_runs: 319 | if 'text' not in run_data: 320 | continue 321 | 322 | # Add paragraph if needed 323 | if not text_frame.paragraphs: 324 | paragraph = text_frame.paragraphs[0] 325 | else: 326 | paragraph = text_frame.add_paragraph() 327 | 328 | # Add run with text 329 | run = paragraph.add_run() 330 | run.text = run_data['text'] 331 | 332 | # Apply formatting using pptx imports 333 | from pptx.util import Pt 334 | from pptx.dml.color import RGBColor 335 | 336 | if 'bold' in run_data: 337 | run.font.bold = run_data['bold'] 338 | if 'italic' in run_data: 339 | run.font.italic = run_data['italic'] 340 | if 'underline' in run_data: 341 | run.font.underline = run_data['underline'] 342 | if 'font_size' in run_data: 343 | run.font.size = Pt(run_data['font_size']) 344 | if 'font_name' in run_data: 345 | run.font.name = run_data['font_name'] 346 | if 'color' in run_data and is_valid_rgb(run_data['color']): 347 | run.font.color.rgb = RGBColor(*run_data['color']) 348 | if 'hyperlink' in run_data: 349 | run.hyperlink.address = run_data['hyperlink'] 350 | 351 | formatted_runs.append({ 352 | "text": run_data['text'], 353 | "formatting_applied": {k: v for k, v in run_data.items() if k != 'text'} 354 | }) 355 | 356 | return { 357 | "message": f"Applied formatting to {len(formatted_runs)} text runs on shape {shape_index}", 358 | "slide_index": slide_index, 359 | "shape_index": shape_index, 360 | "formatted_runs": formatted_runs 361 | } 362 | 363 | else: 364 | return { 365 | "error": f"Invalid operation: {operation}. Must be 'add', 'format', 'validate', or 'format_runs'" 366 | } 367 | 368 | except Exception as e: 369 | return { 370 | "error": f"Failed to {operation} text: {str(e)}" 371 | } 372 | 373 | @app.tool() 374 | def manage_image( 375 | slide_index: int, 376 | operation: str, # "add", "enhance" 377 | image_source: str, # file path or base64 string 378 | source_type: str = "file", # "file" or "base64" 379 | left: float = 1.0, 380 | top: float = 1.0, 381 | width: Optional[float] = None, 382 | height: Optional[float] = None, 383 | # Enhancement options 384 | enhancement_style: Optional[str] = None, # "presentation", "custom" 385 | brightness: float = 1.0, 386 | contrast: float = 1.0, 387 | saturation: float = 1.0, 388 | sharpness: float = 1.0, 389 | blur_radius: float = 0, 390 | filter_type: Optional[str] = None, 391 | output_path: Optional[str] = None, 392 | presentation_id: Optional[str] = None 393 | ) -> Dict: 394 | """Unified image management tool for adding and enhancing images.""" 395 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 396 | 397 | if pres_id is None or pres_id not in presentations: 398 | return { 399 | "error": "No presentation is currently loaded or the specified ID is invalid" 400 | } 401 | 402 | pres = presentations[pres_id] 403 | 404 | if slide_index < 0 or slide_index >= len(pres.slides): 405 | return { 406 | "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" 407 | } 408 | 409 | slide = pres.slides[slide_index] 410 | 411 | try: 412 | if operation == "add": 413 | if source_type == "base64": 414 | # Handle base64 image 415 | try: 416 | image_data = base64.b64decode(image_source) 417 | with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file: 418 | temp_file.write(image_data) 419 | temp_path = temp_file.name 420 | 421 | # Add image from temporary file 422 | shape = ppt_utils.add_image(slide, temp_path, left, top, width, height) 423 | 424 | # Clean up temporary file 425 | os.unlink(temp_path) 426 | 427 | return { 428 | "message": f"Added image from base64 to slide {slide_index}", 429 | "shape_index": len(slide.shapes) - 1 430 | } 431 | except Exception as e: 432 | return { 433 | "error": f"Failed to process base64 image: {str(e)}" 434 | } 435 | else: 436 | # Handle file path 437 | if not os.path.exists(image_source): 438 | return { 439 | "error": f"Image file not found: {image_source}" 440 | } 441 | 442 | shape = ppt_utils.add_image(slide, image_source, left, top, width, height) 443 | return { 444 | "message": f"Added image to slide {slide_index}", 445 | "shape_index": len(slide.shapes) - 1, 446 | "image_path": image_source 447 | } 448 | 449 | elif operation == "enhance": 450 | # Enhance existing image file 451 | if source_type == "base64": 452 | return { 453 | "error": "Enhancement operation requires file path, not base64 data" 454 | } 455 | 456 | if not os.path.exists(image_source): 457 | return { 458 | "error": f"Image file not found: {image_source}" 459 | } 460 | 461 | if enhancement_style == "presentation": 462 | # Apply professional enhancement 463 | enhanced_path = ppt_utils.apply_professional_image_enhancement( 464 | image_source, style="presentation", output_path=output_path 465 | ) 466 | else: 467 | # Apply custom enhancement 468 | enhanced_path = ppt_utils.enhance_image_with_pillow( 469 | image_source, 470 | brightness=brightness, 471 | contrast=contrast, 472 | saturation=saturation, 473 | sharpness=sharpness, 474 | blur_radius=blur_radius, 475 | filter_type=filter_type, 476 | output_path=output_path 477 | ) 478 | 479 | return { 480 | "message": f"Enhanced image: {image_source}", 481 | "enhanced_path": enhanced_path 482 | } 483 | 484 | else: 485 | return { 486 | "error": f"Invalid operation: {operation}. Must be 'add' or 'enhance'" 487 | } 488 | 489 | except Exception as e: 490 | return { 491 | "error": f"Failed to {operation} image: {str(e)}" 492 | } -------------------------------------------------------------------------------- /tools/hyperlink_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hyperlink management tools for PowerPoint MCP Server. 3 | Implements hyperlink operations for text shapes and runs. 4 | """ 5 | 6 | from typing import Dict, List, Optional, Any 7 | 8 | def register_hyperlink_tools(app, presentations, get_current_presentation_id, validate_parameters, 9 | is_positive, is_non_negative, is_in_range, is_valid_rgb): 10 | """Register hyperlink management tools with the FastMCP app.""" 11 | 12 | @app.tool() 13 | def manage_hyperlinks( 14 | operation: str, 15 | slide_index: int, 16 | shape_index: int = None, 17 | text: str = None, 18 | url: str = None, 19 | run_index: int = 0, 20 | presentation_id: str = None 21 | ) -> Dict: 22 | """ 23 | Manage hyperlinks in text shapes and runs. 24 | 25 | Args: 26 | operation: Operation type ("add", "remove", "list", "update") 27 | slide_index: Index of the slide (0-based) 28 | shape_index: Index of the shape on the slide (0-based) 29 | text: Text to make into hyperlink (for "add" operation) 30 | url: URL for the hyperlink 31 | run_index: Index of text run within the shape (0-based) 32 | presentation_id: Optional presentation ID (uses current if not provided) 33 | 34 | Returns: 35 | Dictionary with operation results 36 | """ 37 | try: 38 | # Get presentation 39 | pres_id = presentation_id or get_current_presentation_id() 40 | if pres_id not in presentations: 41 | return {"error": "Presentation not found"} 42 | 43 | pres = presentations[pres_id] 44 | 45 | # Validate slide index 46 | if not (0 <= slide_index < len(pres.slides)): 47 | return {"error": f"Slide index {slide_index} out of range"} 48 | 49 | slide = pres.slides[slide_index] 50 | 51 | if operation == "list": 52 | # List all hyperlinks in the slide 53 | hyperlinks = [] 54 | for shape_idx, shape in enumerate(slide.shapes): 55 | if hasattr(shape, 'text_frame') and shape.text_frame: 56 | for para_idx, paragraph in enumerate(shape.text_frame.paragraphs): 57 | for run_idx, run in enumerate(paragraph.runs): 58 | if run.hyperlink.address: 59 | hyperlinks.append({ 60 | "shape_index": shape_idx, 61 | "paragraph_index": para_idx, 62 | "run_index": run_idx, 63 | "text": run.text, 64 | "url": run.hyperlink.address 65 | }) 66 | 67 | return { 68 | "message": f"Found {len(hyperlinks)} hyperlinks on slide {slide_index}", 69 | "hyperlinks": hyperlinks 70 | } 71 | 72 | # For other operations, validate shape index 73 | if shape_index is None or not (0 <= shape_index < len(slide.shapes)): 74 | return {"error": f"Shape index {shape_index} out of range"} 75 | 76 | shape = slide.shapes[shape_index] 77 | 78 | # Check if shape has text 79 | if not hasattr(shape, 'text_frame') or not shape.text_frame: 80 | return {"error": "Shape does not contain text"} 81 | 82 | if operation == "add": 83 | if not text or not url: 84 | return {"error": "Both 'text' and 'url' are required for adding hyperlinks"} 85 | 86 | # Add new text run with hyperlink 87 | paragraph = shape.text_frame.paragraphs[0] 88 | run = paragraph.add_run() 89 | run.text = text 90 | run.hyperlink.address = url 91 | 92 | return { 93 | "message": f"Added hyperlink '{text}' -> '{url}' to shape {shape_index}", 94 | "text": text, 95 | "url": url 96 | } 97 | 98 | elif operation == "update": 99 | if not url: 100 | return {"error": "URL is required for updating hyperlinks"} 101 | 102 | # Update existing hyperlink 103 | paragraphs = shape.text_frame.paragraphs 104 | if run_index < len(paragraphs[0].runs): 105 | run = paragraphs[0].runs[run_index] 106 | old_url = run.hyperlink.address 107 | run.hyperlink.address = url 108 | 109 | return { 110 | "message": f"Updated hyperlink from '{old_url}' to '{url}'", 111 | "old_url": old_url, 112 | "new_url": url, 113 | "text": run.text 114 | } 115 | else: 116 | return {"error": f"Run index {run_index} out of range"} 117 | 118 | elif operation == "remove": 119 | # Remove hyperlink from specific run 120 | paragraphs = shape.text_frame.paragraphs 121 | if run_index < len(paragraphs[0].runs): 122 | run = paragraphs[0].runs[run_index] 123 | old_url = run.hyperlink.address 124 | run.hyperlink.address = None 125 | 126 | return { 127 | "message": f"Removed hyperlink '{old_url}' from text '{run.text}'", 128 | "removed_url": old_url, 129 | "text": run.text 130 | } 131 | else: 132 | return {"error": f"Run index {run_index} out of range"} 133 | 134 | else: 135 | return {"error": f"Unsupported operation: {operation}. Use 'add', 'remove', 'list', or 'update'"} 136 | 137 | except Exception as e: 138 | return {"error": f"Failed to manage hyperlinks: {str(e)}"} -------------------------------------------------------------------------------- /tools/master_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Slide master management tools for PowerPoint MCP Server. 3 | Implements slide master and layout access capabilities. 4 | """ 5 | 6 | from typing import Dict, List, Optional, Any 7 | 8 | def register_master_tools(app, presentations, get_current_presentation_id, validate_parameters, 9 | is_positive, is_non_negative, is_in_range, is_valid_rgb): 10 | """Register slide master management tools with the FastMCP app.""" 11 | 12 | @app.tool() 13 | def manage_slide_masters( 14 | operation: str, 15 | master_index: int = 0, 16 | layout_index: int = None, 17 | presentation_id: str = None 18 | ) -> Dict: 19 | """ 20 | Access and manage slide master properties and layouts. 21 | 22 | Args: 23 | operation: Operation type ("list", "get_layouts", "get_info") 24 | master_index: Index of the slide master (0-based) 25 | layout_index: Index of specific layout within master (0-based) 26 | presentation_id: Optional presentation ID (uses current if not provided) 27 | 28 | Returns: 29 | Dictionary with slide master information 30 | """ 31 | try: 32 | # Get presentation 33 | pres_id = presentation_id or get_current_presentation_id() 34 | if pres_id not in presentations: 35 | return {"error": "Presentation not found"} 36 | 37 | pres = presentations[pres_id] 38 | 39 | if operation == "list": 40 | # List all slide masters 41 | masters_info = [] 42 | for idx, master in enumerate(pres.slide_masters): 43 | masters_info.append({ 44 | "index": idx, 45 | "layout_count": len(master.slide_layouts), 46 | "name": getattr(master, 'name', f"Master {idx}") 47 | }) 48 | 49 | return { 50 | "message": f"Found {len(masters_info)} slide masters", 51 | "masters": masters_info, 52 | "total_masters": len(pres.slide_masters) 53 | } 54 | 55 | # Validate master index 56 | if not (0 <= master_index < len(pres.slide_masters)): 57 | return {"error": f"Master index {master_index} out of range"} 58 | 59 | master = pres.slide_masters[master_index] 60 | 61 | if operation == "get_layouts": 62 | # Get all layouts for a specific master 63 | layouts_info = [] 64 | for idx, layout in enumerate(master.slide_layouts): 65 | layouts_info.append({ 66 | "index": idx, 67 | "name": layout.name, 68 | "placeholder_count": len(layout.placeholders) if hasattr(layout, 'placeholders') else 0 69 | }) 70 | 71 | return { 72 | "message": f"Master {master_index} has {len(layouts_info)} layouts", 73 | "master_index": master_index, 74 | "layouts": layouts_info 75 | } 76 | 77 | elif operation == "get_info": 78 | # Get detailed info about master or specific layout 79 | if layout_index is not None: 80 | if not (0 <= layout_index < len(master.slide_layouts)): 81 | return {"error": f"Layout index {layout_index} out of range"} 82 | 83 | layout = master.slide_layouts[layout_index] 84 | placeholders_info = [] 85 | 86 | if hasattr(layout, 'placeholders'): 87 | for placeholder in layout.placeholders: 88 | placeholders_info.append({ 89 | "idx": placeholder.placeholder_format.idx, 90 | "type": str(placeholder.placeholder_format.type), 91 | "name": getattr(placeholder, 'name', 'Unnamed') 92 | }) 93 | 94 | return { 95 | "message": f"Layout info for master {master_index}, layout {layout_index}", 96 | "master_index": master_index, 97 | "layout_index": layout_index, 98 | "layout_name": layout.name, 99 | "placeholders": placeholders_info 100 | } 101 | else: 102 | # Master info 103 | return { 104 | "message": f"Master {master_index} information", 105 | "master_index": master_index, 106 | "layout_count": len(master.slide_layouts), 107 | "name": getattr(master, 'name', f"Master {master_index}") 108 | } 109 | 110 | else: 111 | return {"error": f"Unsupported operation: {operation}. Use 'list', 'get_layouts', or 'get_info'"} 112 | 113 | except Exception as e: 114 | return {"error": f"Failed to manage slide masters: {str(e)}"} -------------------------------------------------------------------------------- /tools/presentation_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Presentation management tools for PowerPoint MCP Server. 3 | Handles presentation creation, opening, saving, and core properties. 4 | """ 5 | from typing import Dict, List, Optional, Any 6 | import os 7 | from mcp.server.fastmcp import FastMCP 8 | import utils as ppt_utils 9 | 10 | 11 | def register_presentation_tools(app: FastMCP, presentations: Dict, get_current_presentation_id, get_template_search_directories): 12 | """Register presentation management tools with the FastMCP app""" 13 | 14 | @app.tool() 15 | def create_presentation(id: Optional[str] = None) -> Dict: 16 | """Create a new PowerPoint presentation.""" 17 | # Create a new presentation 18 | pres = ppt_utils.create_presentation() 19 | 20 | # Generate an ID if not provided 21 | if id is None: 22 | id = f"presentation_{len(presentations) + 1}" 23 | 24 | # Store the presentation 25 | presentations[id] = pres 26 | # Set as current presentation (this would need to be handled by caller) 27 | 28 | return { 29 | "presentation_id": id, 30 | "message": f"Created new presentation with ID: {id}", 31 | "slide_count": len(pres.slides) 32 | } 33 | 34 | @app.tool() 35 | def create_presentation_from_template(template_path: str, id: Optional[str] = None) -> Dict: 36 | """Create a new PowerPoint presentation from a template file.""" 37 | # Check if template file exists 38 | if not os.path.exists(template_path): 39 | # Try to find the template by searching in configured directories 40 | search_dirs = get_template_search_directories() 41 | template_name = os.path.basename(template_path) 42 | 43 | for directory in search_dirs: 44 | potential_path = os.path.join(directory, template_name) 45 | if os.path.exists(potential_path): 46 | template_path = potential_path 47 | break 48 | else: 49 | env_path_info = f" (PPT_TEMPLATE_PATH: {os.environ.get('PPT_TEMPLATE_PATH', 'not set')})" if os.environ.get('PPT_TEMPLATE_PATH') else "" 50 | return { 51 | "error": f"Template file not found: {template_path}. Searched in {', '.join(search_dirs)}{env_path_info}" 52 | } 53 | 54 | # Create presentation from template 55 | try: 56 | pres = ppt_utils.create_presentation_from_template(template_path) 57 | except Exception as e: 58 | return { 59 | "error": f"Failed to create presentation from template: {str(e)}" 60 | } 61 | 62 | # Generate an ID if not provided 63 | if id is None: 64 | id = f"presentation_{len(presentations) + 1}" 65 | 66 | # Store the presentation 67 | presentations[id] = pres 68 | 69 | return { 70 | "presentation_id": id, 71 | "message": f"Created new presentation from template '{template_path}' with ID: {id}", 72 | "template_path": template_path, 73 | "slide_count": len(pres.slides), 74 | "layout_count": len(pres.slide_layouts) 75 | } 76 | 77 | @app.tool() 78 | def open_presentation(file_path: str, id: Optional[str] = None) -> Dict: 79 | """Open an existing PowerPoint presentation from a file.""" 80 | # Check if file exists 81 | if not os.path.exists(file_path): 82 | return { 83 | "error": f"File not found: {file_path}" 84 | } 85 | 86 | # Open the presentation 87 | try: 88 | pres = ppt_utils.open_presentation(file_path) 89 | except Exception as e: 90 | return { 91 | "error": f"Failed to open presentation: {str(e)}" 92 | } 93 | 94 | # Generate an ID if not provided 95 | if id is None: 96 | id = f"presentation_{len(presentations) + 1}" 97 | 98 | # Store the presentation 99 | presentations[id] = pres 100 | 101 | return { 102 | "presentation_id": id, 103 | "message": f"Opened presentation from {file_path} with ID: {id}", 104 | "slide_count": len(pres.slides) 105 | } 106 | 107 | @app.tool() 108 | def save_presentation(file_path: str, presentation_id: Optional[str] = None) -> Dict: 109 | """Save a presentation to a file.""" 110 | # Use the specified presentation or the current one 111 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 112 | 113 | if pres_id is None or pres_id not in presentations: 114 | return { 115 | "error": "No presentation is currently loaded or the specified ID is invalid" 116 | } 117 | 118 | # Save the presentation 119 | try: 120 | saved_path = ppt_utils.save_presentation(presentations[pres_id], file_path) 121 | return { 122 | "message": f"Presentation saved to {saved_path}", 123 | "file_path": saved_path 124 | } 125 | except Exception as e: 126 | return { 127 | "error": f"Failed to save presentation: {str(e)}" 128 | } 129 | 130 | @app.tool() 131 | def get_presentation_info(presentation_id: Optional[str] = None) -> Dict: 132 | """Get information about a presentation.""" 133 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 134 | 135 | if pres_id is None or pres_id not in presentations: 136 | return { 137 | "error": "No presentation is currently loaded or the specified ID is invalid" 138 | } 139 | 140 | pres = presentations[pres_id] 141 | 142 | try: 143 | info = ppt_utils.get_presentation_info(pres) 144 | info["presentation_id"] = pres_id 145 | return info 146 | except Exception as e: 147 | return { 148 | "error": f"Failed to get presentation info: {str(e)}" 149 | } 150 | 151 | @app.tool() 152 | def get_template_file_info(template_path: str) -> Dict: 153 | """Get information about a template file including layouts and properties.""" 154 | # Check if template file exists 155 | if not os.path.exists(template_path): 156 | # Try to find the template by searching in configured directories 157 | search_dirs = get_template_search_directories() 158 | template_name = os.path.basename(template_path) 159 | 160 | for directory in search_dirs: 161 | potential_path = os.path.join(directory, template_name) 162 | if os.path.exists(potential_path): 163 | template_path = potential_path 164 | break 165 | else: 166 | return { 167 | "error": f"Template file not found: {template_path}. Searched in {', '.join(search_dirs)}" 168 | } 169 | 170 | try: 171 | return ppt_utils.get_template_info(template_path) 172 | except Exception as e: 173 | return { 174 | "error": f"Failed to get template info: {str(e)}" 175 | } 176 | 177 | @app.tool() 178 | def set_core_properties( 179 | title: Optional[str] = None, 180 | subject: Optional[str] = None, 181 | author: Optional[str] = None, 182 | keywords: Optional[str] = None, 183 | comments: Optional[str] = None, 184 | presentation_id: Optional[str] = None 185 | ) -> Dict: 186 | """Set core document properties.""" 187 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 188 | 189 | if pres_id is None or pres_id not in presentations: 190 | return { 191 | "error": "No presentation is currently loaded or the specified ID is invalid" 192 | } 193 | 194 | pres = presentations[pres_id] 195 | 196 | try: 197 | ppt_utils.set_core_properties( 198 | pres, 199 | title=title, 200 | subject=subject, 201 | author=author, 202 | keywords=keywords, 203 | comments=comments 204 | ) 205 | 206 | return { 207 | "message": "Core properties updated successfully" 208 | } 209 | except Exception as e: 210 | return { 211 | "error": f"Failed to set core properties: {str(e)}" 212 | } -------------------------------------------------------------------------------- /tools/professional_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Professional design tools for PowerPoint MCP Server. 3 | Handles themes, effects, fonts, and advanced formatting. 4 | """ 5 | from typing import Dict, List, Optional, Any 6 | from mcp.server.fastmcp import FastMCP 7 | import utils as ppt_utils 8 | 9 | 10 | def register_professional_tools(app: FastMCP, presentations: Dict, get_current_presentation_id): 11 | """Register professional design tools with the FastMCP app""" 12 | 13 | @app.tool() 14 | def apply_professional_design( 15 | operation: str, # "professional_slide", "theme", "enhance", "get_schemes" 16 | slide_index: Optional[int] = None, 17 | slide_type: str = "title_content", 18 | color_scheme: str = "modern_blue", 19 | title: Optional[str] = None, 20 | content: Optional[List[str]] = None, 21 | apply_to_existing: bool = True, 22 | enhance_title: bool = True, 23 | enhance_content: bool = True, 24 | enhance_shapes: bool = True, 25 | enhance_charts: bool = True, 26 | presentation_id: Optional[str] = None 27 | ) -> Dict: 28 | """Unified professional design tool for themes, slides, and visual enhancements. 29 | This applies professional styling and themes rather than structural layout changes.""" 30 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 31 | 32 | if operation == "get_schemes": 33 | # Return available color schemes 34 | return ppt_utils.get_color_schemes() 35 | 36 | if pres_id is None or pres_id not in presentations: 37 | return { 38 | "error": "No presentation is currently loaded or the specified ID is invalid" 39 | } 40 | 41 | pres = presentations[pres_id] 42 | 43 | try: 44 | if operation == "professional_slide": 45 | # Add professional slide with advanced styling 46 | if slide_index is not None and (slide_index < 0 or slide_index >= len(pres.slides)): 47 | return { 48 | "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" 49 | } 50 | 51 | result = ppt_utils.add_professional_slide( 52 | pres, 53 | slide_type=slide_type, 54 | color_scheme=color_scheme, 55 | title=title, 56 | content=content 57 | ) 58 | 59 | return { 60 | "message": f"Added professional {slide_type} slide", 61 | "slide_index": len(pres.slides) - 1, 62 | "color_scheme": color_scheme, 63 | "slide_type": slide_type 64 | } 65 | 66 | elif operation == "theme": 67 | # Apply professional theme 68 | ppt_utils.apply_professional_theme( 69 | pres, 70 | color_scheme=color_scheme, 71 | apply_to_existing=apply_to_existing 72 | ) 73 | 74 | return { 75 | "message": f"Applied {color_scheme} theme to presentation", 76 | "color_scheme": color_scheme, 77 | "applied_to_existing": apply_to_existing 78 | } 79 | 80 | elif operation == "enhance": 81 | # Enhance existing slide 82 | if slide_index is None: 83 | return { 84 | "error": "slide_index is required for enhance operation" 85 | } 86 | 87 | if slide_index < 0 or slide_index >= len(pres.slides): 88 | return { 89 | "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" 90 | } 91 | 92 | slide = pres.slides[slide_index] 93 | result = ppt_utils.enhance_existing_slide( 94 | slide, 95 | color_scheme=color_scheme, 96 | enhance_title=enhance_title, 97 | enhance_content=enhance_content, 98 | enhance_shapes=enhance_shapes, 99 | enhance_charts=enhance_charts 100 | ) 101 | 102 | return { 103 | "message": f"Enhanced slide {slide_index} with {color_scheme} scheme", 104 | "slide_index": slide_index, 105 | "color_scheme": color_scheme, 106 | "enhancements_applied": result.get("enhancements_applied", []) 107 | } 108 | 109 | else: 110 | return { 111 | "error": f"Invalid operation: {operation}. Must be 'slide', 'theme', 'enhance', or 'get_schemes'" 112 | } 113 | 114 | except Exception as e: 115 | return { 116 | "error": f"Failed to apply professional design: {str(e)}" 117 | } 118 | 119 | @app.tool() 120 | def apply_picture_effects( 121 | slide_index: int, 122 | shape_index: int, 123 | effects: Dict[str, Dict], # {"shadow": {"blur_radius": 4.0, ...}, "glow": {...}} 124 | presentation_id: Optional[str] = None 125 | ) -> Dict: 126 | """Apply multiple picture effects in combination.""" 127 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 128 | 129 | if pres_id is None or pres_id not in presentations: 130 | return { 131 | "error": "No presentation is currently loaded or the specified ID is invalid" 132 | } 133 | 134 | pres = presentations[pres_id] 135 | 136 | if slide_index < 0 or slide_index >= len(pres.slides): 137 | return { 138 | "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" 139 | } 140 | 141 | slide = pres.slides[slide_index] 142 | 143 | if shape_index < 0 or shape_index >= len(slide.shapes): 144 | return { 145 | "error": f"Invalid shape index: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}" 146 | } 147 | 148 | shape = slide.shapes[shape_index] 149 | 150 | try: 151 | applied_effects = [] 152 | warnings = [] 153 | 154 | # Apply each effect 155 | for effect_type, effect_params in effects.items(): 156 | try: 157 | if effect_type == "shadow": 158 | ppt_utils.apply_picture_shadow( 159 | shape, 160 | shadow_type=effect_params.get("shadow_type", "outer"), 161 | blur_radius=effect_params.get("blur_radius", 4.0), 162 | distance=effect_params.get("distance", 3.0), 163 | direction=effect_params.get("direction", 315.0), 164 | color=effect_params.get("color", [0, 0, 0]), 165 | transparency=effect_params.get("transparency", 0.6) 166 | ) 167 | applied_effects.append("shadow") 168 | 169 | elif effect_type == "reflection": 170 | ppt_utils.apply_picture_reflection( 171 | shape, 172 | size=effect_params.get("size", 0.5), 173 | transparency=effect_params.get("transparency", 0.5), 174 | distance=effect_params.get("distance", 0.0), 175 | blur=effect_params.get("blur", 4.0) 176 | ) 177 | applied_effects.append("reflection") 178 | 179 | elif effect_type == "glow": 180 | ppt_utils.apply_picture_glow( 181 | shape, 182 | size=effect_params.get("size", 5.0), 183 | color=effect_params.get("color", [0, 176, 240]), 184 | transparency=effect_params.get("transparency", 0.4) 185 | ) 186 | applied_effects.append("glow") 187 | 188 | elif effect_type == "soft_edges": 189 | ppt_utils.apply_picture_soft_edges( 190 | shape, 191 | radius=effect_params.get("radius", 2.5) 192 | ) 193 | applied_effects.append("soft_edges") 194 | 195 | elif effect_type == "rotation": 196 | ppt_utils.apply_picture_rotation( 197 | shape, 198 | rotation=effect_params.get("rotation", 0.0) 199 | ) 200 | applied_effects.append("rotation") 201 | 202 | elif effect_type == "transparency": 203 | ppt_utils.apply_picture_transparency( 204 | shape, 205 | transparency=effect_params.get("transparency", 0.0) 206 | ) 207 | applied_effects.append("transparency") 208 | 209 | elif effect_type == "bevel": 210 | ppt_utils.apply_picture_bevel( 211 | shape, 212 | bevel_type=effect_params.get("bevel_type", "circle"), 213 | width=effect_params.get("width", 6.0), 214 | height=effect_params.get("height", 6.0) 215 | ) 216 | applied_effects.append("bevel") 217 | 218 | elif effect_type == "filter": 219 | ppt_utils.apply_picture_filter( 220 | shape, 221 | filter_type=effect_params.get("filter_type", "none"), 222 | intensity=effect_params.get("intensity", 0.5) 223 | ) 224 | applied_effects.append("filter") 225 | 226 | else: 227 | warnings.append(f"Unknown effect type: {effect_type}") 228 | 229 | except Exception as e: 230 | warnings.append(f"Failed to apply {effect_type} effect: {str(e)}") 231 | 232 | result = { 233 | "message": f"Applied {len(applied_effects)} effects to shape {shape_index} on slide {slide_index}", 234 | "applied_effects": applied_effects 235 | } 236 | 237 | if warnings: 238 | result["warnings"] = warnings 239 | 240 | return result 241 | 242 | except Exception as e: 243 | return { 244 | "error": f"Failed to apply picture effects: {str(e)}" 245 | } 246 | 247 | @app.tool() 248 | def manage_fonts( 249 | operation: str, # "analyze", "optimize", "recommend" 250 | font_path: str, 251 | output_path: Optional[str] = None, 252 | presentation_type: str = "business", 253 | text_content: Optional[str] = None 254 | ) -> Dict: 255 | """Unified font management tool for analysis, optimization, and recommendations.""" 256 | try: 257 | if operation == "analyze": 258 | # Analyze font file 259 | return ppt_utils.analyze_font_file(font_path) 260 | 261 | elif operation == "optimize": 262 | # Optimize font file 263 | optimized_path = ppt_utils.optimize_font_for_presentation( 264 | font_path, 265 | output_path=output_path, 266 | text_content=text_content 267 | ) 268 | 269 | return { 270 | "message": f"Optimized font: {font_path}", 271 | "original_path": font_path, 272 | "optimized_path": optimized_path 273 | } 274 | 275 | elif operation == "recommend": 276 | # Get font recommendations 277 | return ppt_utils.get_font_recommendations( 278 | font_path, 279 | presentation_type=presentation_type 280 | ) 281 | 282 | else: 283 | return { 284 | "error": f"Invalid operation: {operation}. Must be 'analyze', 'optimize', or 'recommend'" 285 | } 286 | 287 | except Exception as e: 288 | return { 289 | "error": f"Failed to {operation} font: {str(e)}" 290 | } -------------------------------------------------------------------------------- /tools/structural_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Structural element tools for PowerPoint MCP Server. 3 | Handles tables, shapes, and charts. 4 | """ 5 | from typing import Dict, List, Optional, Any 6 | from mcp.server.fastmcp import FastMCP 7 | import utils as ppt_utils 8 | 9 | 10 | def register_structural_tools(app: FastMCP, presentations: Dict, get_current_presentation_id, validate_parameters, is_positive, is_non_negative, is_in_range, is_valid_rgb, add_shape_direct): 11 | """Register structural element tools with the FastMCP app""" 12 | 13 | @app.tool() 14 | def add_table( 15 | slide_index: int, 16 | rows: int, 17 | cols: int, 18 | left: float, 19 | top: float, 20 | width: float, 21 | height: float, 22 | data: Optional[List[List[str]]] = None, 23 | header_row: bool = True, 24 | header_font_size: int = 12, 25 | body_font_size: int = 10, 26 | header_bg_color: Optional[List[int]] = None, 27 | body_bg_color: Optional[List[int]] = None, 28 | border_color: Optional[List[int]] = None, 29 | presentation_id: Optional[str] = None 30 | ) -> Dict: 31 | """Add a table to a slide with enhanced formatting options.""" 32 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 33 | 34 | if pres_id is None or pres_id not in presentations: 35 | return { 36 | "error": "No presentation is currently loaded or the specified ID is invalid" 37 | } 38 | 39 | pres = presentations[pres_id] 40 | 41 | if slide_index < 0 or slide_index >= len(pres.slides): 42 | return { 43 | "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" 44 | } 45 | 46 | slide = pres.slides[slide_index] 47 | 48 | # Validate parameters 49 | validations = { 50 | "rows": (rows, [(is_positive, "must be a positive integer")]), 51 | "cols": (cols, [(is_positive, "must be a positive integer")]), 52 | "left": (left, [(is_non_negative, "must be non-negative")]), 53 | "top": (top, [(is_non_negative, "must be non-negative")]), 54 | "width": (width, [(is_positive, "must be positive")]), 55 | "height": (height, [(is_positive, "must be positive")]) 56 | } 57 | 58 | if header_bg_color is not None: 59 | validations["header_bg_color"] = (header_bg_color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")]) 60 | if body_bg_color is not None: 61 | validations["body_bg_color"] = (body_bg_color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")]) 62 | if border_color is not None: 63 | validations["border_color"] = (border_color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")]) 64 | 65 | valid, error = validate_parameters(validations) 66 | if not valid: 67 | return {"error": error} 68 | 69 | # Validate data if provided 70 | if data: 71 | if len(data) != rows: 72 | return { 73 | "error": f"Data has {len(data)} rows but table should have {rows} rows" 74 | } 75 | for i, row in enumerate(data): 76 | if len(row) != cols: 77 | return { 78 | "error": f"Row {i} has {len(row)} columns but table should have {cols} columns" 79 | } 80 | 81 | try: 82 | # Add the table 83 | table_shape = ppt_utils.add_table(slide, rows, cols, left, top, width, height) 84 | table = table_shape.table 85 | 86 | # Populate with data if provided 87 | if data: 88 | for r in range(rows): 89 | for c in range(cols): 90 | if r < len(data) and c < len(data[r]): 91 | table.cell(r, c).text = str(data[r][c]) 92 | 93 | # Apply formatting 94 | for r in range(rows): 95 | for c in range(cols): 96 | cell = table.cell(r, c) 97 | 98 | # Header row formatting 99 | if r == 0 and header_row: 100 | if header_bg_color: 101 | ppt_utils.format_table_cell( 102 | cell, bg_color=tuple(header_bg_color), font_size=header_font_size, bold=True 103 | ) 104 | else: 105 | ppt_utils.format_table_cell(cell, font_size=header_font_size, bold=True) 106 | else: 107 | # Body cell formatting 108 | if body_bg_color: 109 | ppt_utils.format_table_cell( 110 | cell, bg_color=tuple(body_bg_color), font_size=body_font_size 111 | ) 112 | else: 113 | ppt_utils.format_table_cell(cell, font_size=body_font_size) 114 | 115 | return { 116 | "message": f"Added {rows}x{cols} table to slide {slide_index}", 117 | "shape_index": len(slide.shapes) - 1, 118 | "rows": rows, 119 | "cols": cols 120 | } 121 | except Exception as e: 122 | return { 123 | "error": f"Failed to add table: {str(e)}" 124 | } 125 | 126 | @app.tool() 127 | def format_table_cell( 128 | slide_index: int, 129 | shape_index: int, 130 | row: int, 131 | col: int, 132 | font_size: Optional[int] = None, 133 | font_name: Optional[str] = None, 134 | bold: Optional[bool] = None, 135 | italic: Optional[bool] = None, 136 | color: Optional[List[int]] = None, 137 | bg_color: Optional[List[int]] = None, 138 | alignment: Optional[str] = None, 139 | vertical_alignment: Optional[str] = None, 140 | presentation_id: Optional[str] = None 141 | ) -> Dict: 142 | """Format a specific table cell.""" 143 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 144 | 145 | if pres_id is None or pres_id not in presentations: 146 | return { 147 | "error": "No presentation is currently loaded or the specified ID is invalid" 148 | } 149 | 150 | pres = presentations[pres_id] 151 | 152 | if slide_index < 0 or slide_index >= len(pres.slides): 153 | return { 154 | "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" 155 | } 156 | 157 | slide = pres.slides[slide_index] 158 | 159 | if shape_index < 0 or shape_index >= len(slide.shapes): 160 | return { 161 | "error": f"Invalid shape index: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}" 162 | } 163 | 164 | shape = slide.shapes[shape_index] 165 | 166 | try: 167 | if not hasattr(shape, 'table'): 168 | return { 169 | "error": f"Shape at index {shape_index} is not a table" 170 | } 171 | 172 | table = shape.table 173 | 174 | if row < 0 or row >= len(table.rows): 175 | return { 176 | "error": f"Invalid row index: {row}. Available rows: 0-{len(table.rows) - 1}" 177 | } 178 | 179 | if col < 0 or col >= len(table.columns): 180 | return { 181 | "error": f"Invalid column index: {col}. Available columns: 0-{len(table.columns) - 1}" 182 | } 183 | 184 | cell = table.cell(row, col) 185 | 186 | ppt_utils.format_table_cell( 187 | cell, 188 | font_size=font_size, 189 | font_name=font_name, 190 | bold=bold, 191 | italic=italic, 192 | color=tuple(color) if color else None, 193 | bg_color=tuple(bg_color) if bg_color else None, 194 | alignment=alignment, 195 | vertical_alignment=vertical_alignment 196 | ) 197 | 198 | return { 199 | "message": f"Formatted cell at row {row}, column {col} in table at shape index {shape_index} on slide {slide_index}" 200 | } 201 | except Exception as e: 202 | return { 203 | "error": f"Failed to format table cell: {str(e)}" 204 | } 205 | 206 | @app.tool() 207 | def add_shape( 208 | slide_index: int, 209 | shape_type: str, 210 | left: float, 211 | top: float, 212 | width: float, 213 | height: float, 214 | fill_color: Optional[List[int]] = None, 215 | line_color: Optional[List[int]] = None, 216 | line_width: Optional[float] = None, 217 | text: Optional[str] = None, # Add text to shape 218 | font_size: Optional[int] = None, 219 | font_color: Optional[List[int]] = None, 220 | presentation_id: Optional[str] = None 221 | ) -> Dict: 222 | """Add an auto shape to a slide with enhanced options.""" 223 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 224 | 225 | if pres_id is None or pres_id not in presentations: 226 | return { 227 | "error": "No presentation is currently loaded or the specified ID is invalid" 228 | } 229 | 230 | pres = presentations[pres_id] 231 | 232 | if slide_index < 0 or slide_index >= len(pres.slides): 233 | return { 234 | "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" 235 | } 236 | 237 | slide = pres.slides[slide_index] 238 | 239 | try: 240 | # Use the direct implementation that bypasses the enum issues 241 | shape = add_shape_direct(slide, shape_type, left, top, width, height) 242 | 243 | # Format the shape if formatting options are provided 244 | if any([fill_color, line_color, line_width]): 245 | ppt_utils.format_shape( 246 | shape, 247 | fill_color=tuple(fill_color) if fill_color else None, 248 | line_color=tuple(line_color) if line_color else None, 249 | line_width=line_width 250 | ) 251 | 252 | # Add text to shape if provided 253 | if text and hasattr(shape, 'text_frame'): 254 | shape.text_frame.text = text 255 | if font_size or font_color: 256 | ppt_utils.format_text( 257 | shape.text_frame, 258 | font_size=font_size, 259 | color=tuple(font_color) if font_color else None 260 | ) 261 | 262 | return { 263 | "message": f"Added {shape_type} shape to slide {slide_index}", 264 | "shape_index": len(slide.shapes) - 1 265 | } 266 | except ValueError as e: 267 | return { 268 | "error": str(e) 269 | } 270 | except Exception as e: 271 | return { 272 | "error": f"Failed to add shape '{shape_type}': {str(e)}" 273 | } 274 | 275 | @app.tool() 276 | def add_chart( 277 | slide_index: int, 278 | chart_type: str, 279 | left: float, 280 | top: float, 281 | width: float, 282 | height: float, 283 | categories: List[str], 284 | series_names: List[str], 285 | series_values: List[List[float]], 286 | has_legend: bool = True, 287 | legend_position: str = "right", 288 | has_data_labels: bool = False, 289 | title: Optional[str] = None, 290 | x_axis_title: Optional[str] = None, 291 | y_axis_title: Optional[str] = None, 292 | color_scheme: Optional[str] = None, 293 | presentation_id: Optional[str] = None 294 | ) -> Dict: 295 | """Add a chart to a slide with comprehensive formatting options.""" 296 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 297 | 298 | if pres_id is None or pres_id not in presentations: 299 | return { 300 | "error": "No presentation is currently loaded or the specified ID is invalid" 301 | } 302 | 303 | pres = presentations[pres_id] 304 | 305 | if slide_index < 0 or slide_index >= len(pres.slides): 306 | return { 307 | "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" 308 | } 309 | 310 | slide = pres.slides[slide_index] 311 | 312 | # Validate chart type 313 | valid_chart_types = [ 314 | 'column', 'stacked_column', 'bar', 'stacked_bar', 'line', 315 | 'line_markers', 'pie', 'doughnut', 'area', 'stacked_area', 316 | 'scatter', 'radar', 'radar_markers' 317 | ] 318 | if chart_type.lower() not in valid_chart_types: 319 | return { 320 | "error": f"Invalid chart type: '{chart_type}'. Valid types are: {', '.join(valid_chart_types)}" 321 | } 322 | 323 | # Validate series data 324 | if len(series_names) != len(series_values): 325 | return { 326 | "error": f"Number of series names ({len(series_names)}) must match number of series values ({len(series_values)})" 327 | } 328 | 329 | if not categories: 330 | return { 331 | "error": "Categories list cannot be empty" 332 | } 333 | 334 | # Validate that all series have the same number of values as categories 335 | for i, values in enumerate(series_values): 336 | if len(values) != len(categories): 337 | return { 338 | "error": f"Series '{series_names[i]}' has {len(values)} values but there are {len(categories)} categories" 339 | } 340 | 341 | try: 342 | # Add the chart 343 | chart = ppt_utils.add_chart( 344 | slide, chart_type, left, top, width, height, 345 | categories, series_names, series_values 346 | ) 347 | 348 | if chart is None: 349 | return {"error": "Failed to create chart"} 350 | 351 | # Format the chart 352 | ppt_utils.format_chart( 353 | chart, 354 | has_legend=has_legend, 355 | legend_position=legend_position, 356 | has_data_labels=has_data_labels, 357 | title=title, 358 | x_axis_title=x_axis_title, 359 | y_axis_title=y_axis_title, 360 | color_scheme=color_scheme 361 | ) 362 | 363 | return { 364 | "message": f"Added {chart_type} chart to slide {slide_index}", 365 | "shape_index": len(slide.shapes) - 1, 366 | "chart_type": chart_type, 367 | "series_count": len(series_names), 368 | "categories_count": len(categories) 369 | } 370 | except Exception as e: 371 | return { 372 | "error": f"Failed to add chart: {str(e)}" 373 | } -------------------------------------------------------------------------------- /tools/template_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Enhanced template-based slide creation tools for PowerPoint MCP Server. 3 | Handles template application, template management, automated slide generation, 4 | and advanced features like dynamic sizing, auto-wrapping, and visual effects. 5 | """ 6 | from typing import Dict, List, Optional, Any 7 | from mcp.server.fastmcp import FastMCP 8 | import utils.template_utils as template_utils 9 | 10 | 11 | def register_template_tools(app: FastMCP, presentations: Dict, get_current_presentation_id): 12 | """Register template-based tools with the FastMCP app""" 13 | 14 | @app.tool() 15 | def list_slide_templates() -> Dict: 16 | """List all available slide layout templates.""" 17 | try: 18 | available_templates = template_utils.get_available_templates() 19 | usage_examples = template_utils.get_template_usage_examples() 20 | 21 | return { 22 | "available_templates": available_templates, 23 | "total_templates": len(available_templates), 24 | "usage_examples": usage_examples, 25 | "message": "Use apply_slide_template to apply templates to slides" 26 | } 27 | except Exception as e: 28 | return { 29 | "error": f"Failed to list templates: {str(e)}" 30 | } 31 | 32 | @app.tool() 33 | def apply_slide_template( 34 | slide_index: int, 35 | template_id: str, 36 | color_scheme: str = "modern_blue", 37 | content_mapping: Optional[Dict[str, str]] = None, 38 | image_paths: Optional[Dict[str, str]] = None, 39 | presentation_id: Optional[str] = None 40 | ) -> Dict: 41 | """ 42 | Apply a structured layout template to an existing slide. 43 | This modifies slide layout and content structure using predefined templates. 44 | 45 | Args: 46 | slide_index: Index of the slide to apply template to 47 | template_id: ID of the template to apply (e.g., 'title_slide', 'text_with_image') 48 | color_scheme: Color scheme to use ('modern_blue', 'corporate_gray', 'elegant_green', 'warm_red') 49 | content_mapping: Dictionary mapping element roles to custom content 50 | image_paths: Dictionary mapping image element roles to file paths 51 | presentation_id: Presentation ID (uses current if None) 52 | """ 53 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 54 | 55 | if pres_id is None or pres_id not in presentations: 56 | return { 57 | "error": "No presentation is currently loaded or the specified ID is invalid" 58 | } 59 | 60 | pres = presentations[pres_id] 61 | 62 | if slide_index < 0 or slide_index >= len(pres.slides): 63 | return { 64 | "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" 65 | } 66 | 67 | slide = pres.slides[slide_index] 68 | 69 | try: 70 | result = template_utils.apply_slide_template( 71 | slide, template_id, color_scheme, 72 | content_mapping or {}, image_paths or {} 73 | ) 74 | 75 | if result['success']: 76 | return { 77 | "message": f"Applied template '{template_id}' to slide {slide_index}", 78 | "slide_index": slide_index, 79 | "template_applied": result 80 | } 81 | else: 82 | return { 83 | "error": f"Failed to apply template: {result.get('error', 'Unknown error')}" 84 | } 85 | 86 | except Exception as e: 87 | return { 88 | "error": f"Failed to apply template: {str(e)}" 89 | } 90 | 91 | @app.tool() 92 | def create_slide_from_template( 93 | template_id: str, 94 | color_scheme: str = "modern_blue", 95 | content_mapping: Optional[Dict[str, str]] = None, 96 | image_paths: Optional[Dict[str, str]] = None, 97 | layout_index: int = 1, 98 | presentation_id: Optional[str] = None 99 | ) -> Dict: 100 | """ 101 | Create a new slide using a layout template. 102 | 103 | Args: 104 | template_id: ID of the template to use (e.g., 'title_slide', 'text_with_image') 105 | color_scheme: Color scheme to use ('modern_blue', 'corporate_gray', 'elegant_green', 'warm_red') 106 | content_mapping: Dictionary mapping element roles to custom content 107 | image_paths: Dictionary mapping image element roles to file paths 108 | layout_index: PowerPoint layout index to use as base (default: 1) 109 | presentation_id: Presentation ID (uses current if None) 110 | """ 111 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 112 | 113 | if pres_id is None or pres_id not in presentations: 114 | return { 115 | "error": "No presentation is currently loaded or the specified ID is invalid" 116 | } 117 | 118 | pres = presentations[pres_id] 119 | 120 | # Validate layout index 121 | if layout_index < 0 or layout_index >= len(pres.slide_layouts): 122 | return { 123 | "error": f"Invalid layout index: {layout_index}. Available layouts: 0-{len(pres.slide_layouts) - 1}" 124 | } 125 | 126 | try: 127 | # Add new slide 128 | layout = pres.slide_layouts[layout_index] 129 | slide = pres.slides.add_slide(layout) 130 | slide_index = len(pres.slides) - 1 131 | 132 | # Apply template 133 | result = template_utils.apply_slide_template( 134 | slide, template_id, color_scheme, 135 | content_mapping or {}, image_paths or {} 136 | ) 137 | 138 | if result['success']: 139 | return { 140 | "message": f"Created slide {slide_index} using template '{template_id}'", 141 | "slide_index": slide_index, 142 | "template_applied": result 143 | } 144 | else: 145 | return { 146 | "error": f"Failed to apply template to new slide: {result.get('error', 'Unknown error')}" 147 | } 148 | 149 | except Exception as e: 150 | return { 151 | "error": f"Failed to create slide from template: {str(e)}" 152 | } 153 | 154 | @app.tool() 155 | def create_presentation_from_templates( 156 | template_sequence: List[Dict[str, Any]], 157 | color_scheme: str = "modern_blue", 158 | presentation_title: Optional[str] = None, 159 | presentation_id: Optional[str] = None 160 | ) -> Dict: 161 | """ 162 | Create a complete presentation from a sequence of templates. 163 | 164 | Args: 165 | template_sequence: List of template configurations, each containing: 166 | - template_id: Template to use 167 | - content: Content mapping for the template 168 | - images: Image path mapping for the template 169 | color_scheme: Color scheme to apply to all slides 170 | presentation_title: Optional title for the presentation 171 | presentation_id: Presentation ID (uses current if None) 172 | 173 | Example template_sequence: 174 | [ 175 | { 176 | "template_id": "title_slide", 177 | "content": { 178 | "title": "My Presentation", 179 | "subtitle": "Annual Report 2024", 180 | "author": "John Doe" 181 | } 182 | }, 183 | { 184 | "template_id": "text_with_image", 185 | "content": { 186 | "title": "Key Results", 187 | "content": "• Achievement 1\\n• Achievement 2" 188 | }, 189 | "images": { 190 | "supporting": "/path/to/image.jpg" 191 | } 192 | } 193 | ] 194 | """ 195 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 196 | 197 | if pres_id is None or pres_id not in presentations: 198 | return { 199 | "error": "No presentation is currently loaded or the specified ID is invalid" 200 | } 201 | 202 | pres = presentations[pres_id] 203 | 204 | if not template_sequence: 205 | return { 206 | "error": "Template sequence cannot be empty" 207 | } 208 | 209 | try: 210 | # Set presentation title if provided 211 | if presentation_title: 212 | pres.core_properties.title = presentation_title 213 | 214 | # Create slides from template sequence 215 | result = template_utils.create_presentation_from_template_sequence( 216 | pres, template_sequence, color_scheme 217 | ) 218 | 219 | if result['success']: 220 | return { 221 | "message": f"Created presentation with {result['total_slides']} slides", 222 | "presentation_id": pres_id, 223 | "creation_result": result, 224 | "total_slides": len(pres.slides) 225 | } 226 | else: 227 | return { 228 | "warning": "Presentation created with some errors", 229 | "presentation_id": pres_id, 230 | "creation_result": result, 231 | "total_slides": len(pres.slides) 232 | } 233 | 234 | except Exception as e: 235 | return { 236 | "error": f"Failed to create presentation from templates: {str(e)}" 237 | } 238 | 239 | @app.tool() 240 | def get_template_info(template_id: str) -> Dict: 241 | """ 242 | Get detailed information about a specific template. 243 | 244 | Args: 245 | template_id: ID of the template to get information about 246 | """ 247 | try: 248 | templates_data = template_utils.load_slide_templates() 249 | 250 | if template_id not in templates_data.get('templates', {}): 251 | available_templates = list(templates_data.get('templates', {}).keys()) 252 | return { 253 | "error": f"Template '{template_id}' not found", 254 | "available_templates": available_templates 255 | } 256 | 257 | template = templates_data['templates'][template_id] 258 | 259 | # Extract element information 260 | elements_info = [] 261 | for element in template.get('elements', []): 262 | element_info = { 263 | "type": element.get('type'), 264 | "role": element.get('role'), 265 | "position": element.get('position'), 266 | "placeholder_text": element.get('placeholder_text', ''), 267 | "styling_options": list(element.get('styling', {}).keys()) 268 | } 269 | elements_info.append(element_info) 270 | 271 | return { 272 | "template_id": template_id, 273 | "name": template.get('name'), 274 | "description": template.get('description'), 275 | "layout_type": template.get('layout_type'), 276 | "elements": elements_info, 277 | "element_count": len(elements_info), 278 | "has_background": 'background' in template, 279 | "background_type": template.get('background', {}).get('type'), 280 | "color_schemes": list(templates_data.get('color_schemes', {}).keys()), 281 | "usage_tip": f"Use create_slide_from_template with template_id='{template_id}' to create a slide with this layout" 282 | } 283 | 284 | except Exception as e: 285 | return { 286 | "error": f"Failed to get template info: {str(e)}" 287 | } 288 | 289 | @app.tool() 290 | def auto_generate_presentation( 291 | topic: str, 292 | slide_count: int = 5, 293 | presentation_type: str = "business", 294 | color_scheme: str = "modern_blue", 295 | include_charts: bool = True, 296 | include_images: bool = False, 297 | presentation_id: Optional[str] = None 298 | ) -> Dict: 299 | """ 300 | Automatically generate a presentation based on topic and preferences. 301 | 302 | Args: 303 | topic: Main topic/theme for the presentation 304 | slide_count: Number of slides to generate (3-20) 305 | presentation_type: Type of presentation ('business', 'academic', 'creative') 306 | color_scheme: Color scheme to use 307 | include_charts: Whether to include chart slides 308 | include_images: Whether to include image placeholders 309 | presentation_id: Presentation ID (uses current if None) 310 | """ 311 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 312 | 313 | if pres_id is None or pres_id not in presentations: 314 | return { 315 | "error": "No presentation is currently loaded or the specified ID is invalid" 316 | } 317 | 318 | if slide_count < 3 or slide_count > 20: 319 | return { 320 | "error": "Slide count must be between 3 and 20" 321 | } 322 | 323 | try: 324 | # Define presentation structures based on type 325 | if presentation_type == "business": 326 | base_templates = [ 327 | ("title_slide", {"title": f"{topic}", "subtitle": "Executive Presentation", "author": "Business Team"}), 328 | ("agenda_slide", {"agenda_items": "1. Executive Summary\n\n2. Current Situation\n\n3. Analysis & Insights\n\n4. Recommendations\n\n5. Next Steps"}), 329 | ("key_metrics_dashboard", {"title": "Key Performance Indicators"}), 330 | ("text_with_image", {"title": "Current Situation", "content": f"Overview of {topic}:\n• Current status\n• Key challenges\n• Market position"}), 331 | ("two_column_text", {"title": "Analysis", "content_left": "Strengths:\n• Advantage 1\n• Advantage 2\n• Advantage 3", "content_right": "Opportunities:\n• Opportunity 1\n• Opportunity 2\n• Opportunity 3"}), 332 | ] 333 | if include_charts: 334 | base_templates.append(("chart_comparison", {"title": "Performance Comparison"})) 335 | base_templates.append(("thank_you_slide", {"contact": "Thank you for your attention\nQuestions & Discussion"})) 336 | 337 | elif presentation_type == "academic": 338 | base_templates = [ 339 | ("title_slide", {"title": f"Research on {topic}", "subtitle": "Academic Study", "author": "Research Team"}), 340 | ("agenda_slide", {"agenda_items": "1. Introduction\n\n2. Literature Review\n\n3. Methodology\n\n4. Results\n\n5. Conclusions"}), 341 | ("text_with_image", {"title": "Introduction", "content": f"Research focus on {topic}:\n• Background\n• Problem statement\n• Research questions"}), 342 | ("two_column_text", {"title": "Methodology", "content_left": "Approach:\n• Method 1\n• Method 2\n• Method 3", "content_right": "Data Sources:\n• Source 1\n• Source 2\n• Source 3"}), 343 | ("data_table_slide", {"title": "Results Summary"}), 344 | ] 345 | if include_charts: 346 | base_templates.append(("chart_comparison", {"title": "Data Analysis"})) 347 | base_templates.append(("thank_you_slide", {"contact": "Questions & Discussion\nContact: research@university.edu"})) 348 | 349 | else: # creative 350 | base_templates = [ 351 | ("title_slide", {"title": f"Creative Vision: {topic}", "subtitle": "Innovative Concepts", "author": "Creative Team"}), 352 | ("full_image_slide", {"overlay_title": f"Exploring {topic}", "overlay_subtitle": "Creative possibilities"}), 353 | ("three_column_layout", {"title": "Creative Concepts"}), 354 | ("quote_testimonial", {"quote_text": f"Innovation in {topic} requires thinking beyond conventional boundaries", "attribution": "— Creative Director"}), 355 | ("process_flow", {"title": "Creative Process"}), 356 | ] 357 | if include_charts: 358 | base_templates.append(("key_metrics_dashboard", {"title": "Impact Metrics"})) 359 | base_templates.append(("thank_you_slide", {"contact": "Let's create something amazing together\ncreative@studio.com"})) 360 | 361 | # Adjust templates to match requested slide count 362 | template_sequence = [] 363 | templates_to_use = base_templates[:slide_count] 364 | 365 | # If we need more slides, add content slides 366 | while len(templates_to_use) < slide_count: 367 | if include_images: 368 | templates_to_use.insert(-1, ("text_with_image", {"title": f"{topic} - Additional Topic", "content": "• Key point\n• Supporting detail\n• Additional insight"})) 369 | else: 370 | templates_to_use.insert(-1, ("two_column_text", {"title": f"{topic} - Analysis", "content_left": "Key Points:\n• Point 1\n• Point 2", "content_right": "Details:\n• Detail 1\n• Detail 2"})) 371 | 372 | # Convert to proper template sequence format 373 | for i, (template_id, content) in enumerate(templates_to_use): 374 | template_config = { 375 | "template_id": template_id, 376 | "content": content 377 | } 378 | template_sequence.append(template_config) 379 | 380 | # Create the presentation 381 | result = template_utils.create_presentation_from_template_sequence( 382 | presentations[pres_id], template_sequence, color_scheme 383 | ) 384 | 385 | return { 386 | "message": f"Auto-generated {slide_count}-slide presentation on '{topic}'", 387 | "topic": topic, 388 | "presentation_type": presentation_type, 389 | "color_scheme": color_scheme, 390 | "slide_count": slide_count, 391 | "generation_result": result, 392 | "templates_used": [t[0] for t in templates_to_use] 393 | } 394 | 395 | except Exception as e: 396 | return { 397 | "error": f"Failed to auto-generate presentation: {str(e)}" 398 | } 399 | 400 | # Text optimization tools 401 | 402 | 403 | @app.tool() 404 | def optimize_slide_text( 405 | slide_index: int, 406 | auto_resize: bool = True, 407 | auto_wrap: bool = True, 408 | optimize_spacing: bool = True, 409 | min_font_size: int = 8, 410 | max_font_size: int = 36, 411 | presentation_id: Optional[str] = None 412 | ) -> Dict: 413 | """ 414 | Optimize text elements on a slide for better readability and fit. 415 | 416 | Args: 417 | slide_index: Index of the slide to optimize 418 | auto_resize: Whether to automatically resize fonts to fit containers 419 | auto_wrap: Whether to apply intelligent text wrapping 420 | optimize_spacing: Whether to optimize line spacing 421 | min_font_size: Minimum allowed font size 422 | max_font_size: Maximum allowed font size 423 | presentation_id: Presentation ID (uses current if None) 424 | """ 425 | pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() 426 | 427 | if pres_id is None or pres_id not in presentations: 428 | return { 429 | "error": "No presentation is currently loaded or the specified ID is invalid" 430 | } 431 | 432 | pres = presentations[pres_id] 433 | 434 | if slide_index < 0 or slide_index >= len(pres.slides): 435 | return { 436 | "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" 437 | } 438 | 439 | slide = pres.slides[slide_index] 440 | 441 | try: 442 | optimizations_applied = [] 443 | manager = template_utils.get_enhanced_template_manager() 444 | 445 | # Analyze each text shape on the slide 446 | for i, shape in enumerate(slide.shapes): 447 | if hasattr(shape, 'text_frame') and shape.text_frame.text: 448 | text = shape.text_frame.text 449 | 450 | # Calculate container dimensions 451 | container_width = shape.width.inches 452 | container_height = shape.height.inches 453 | 454 | shape_optimizations = [] 455 | 456 | # Apply auto-resize if enabled 457 | if auto_resize: 458 | optimal_size = template_utils.calculate_dynamic_font_size( 459 | text, container_width, container_height 460 | ) 461 | optimal_size = max(min_font_size, min(max_font_size, optimal_size)) 462 | 463 | # Apply the calculated font size 464 | for paragraph in shape.text_frame.paragraphs: 465 | for run in paragraph.runs: 466 | run.font.size = template_utils.Pt(optimal_size) 467 | 468 | shape_optimizations.append(f"Font resized to {optimal_size}pt") 469 | 470 | # Apply auto-wrap if enabled 471 | if auto_wrap: 472 | current_font_size = 14 # Default assumption 473 | if shape.text_frame.paragraphs and shape.text_frame.paragraphs[0].runs: 474 | if shape.text_frame.paragraphs[0].runs[0].font.size: 475 | current_font_size = shape.text_frame.paragraphs[0].runs[0].font.size.pt 476 | 477 | wrapped_text = template_utils.wrap_text_automatically( 478 | text, container_width, current_font_size 479 | ) 480 | 481 | if wrapped_text != text: 482 | shape.text_frame.text = wrapped_text 483 | shape_optimizations.append("Text wrapped automatically") 484 | 485 | # Optimize spacing if enabled 486 | if optimize_spacing: 487 | text_length = len(text) 488 | if text_length > 300: 489 | line_spacing = 1.4 490 | elif text_length > 150: 491 | line_spacing = 1.3 492 | else: 493 | line_spacing = 1.2 494 | 495 | for paragraph in shape.text_frame.paragraphs: 496 | paragraph.line_spacing = line_spacing 497 | 498 | shape_optimizations.append(f"Line spacing set to {line_spacing}") 499 | 500 | if shape_optimizations: 501 | optimizations_applied.append({ 502 | "shape_index": i, 503 | "optimizations": shape_optimizations 504 | }) 505 | 506 | return { 507 | "message": f"Optimized {len(optimizations_applied)} text elements on slide {slide_index}", 508 | "slide_index": slide_index, 509 | "optimizations_applied": optimizations_applied, 510 | "settings": { 511 | "auto_resize": auto_resize, 512 | "auto_wrap": auto_wrap, 513 | "optimize_spacing": optimize_spacing, 514 | "font_size_range": f"{min_font_size}-{max_font_size}pt" 515 | } 516 | } 517 | 518 | except Exception as e: 519 | return { 520 | "error": f"Failed to optimize slide text: {str(e)}" 521 | } -------------------------------------------------------------------------------- /tools/transition_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Slide transition management tools for PowerPoint MCP Server. 3 | Implements slide transition and timing capabilities. 4 | """ 5 | 6 | from typing import Dict, List, Optional, Any 7 | 8 | def register_transition_tools(app, presentations, get_current_presentation_id, validate_parameters, 9 | is_positive, is_non_negative, is_in_range, is_valid_rgb): 10 | """Register slide transition management tools with the FastMCP app.""" 11 | 12 | @app.tool() 13 | def manage_slide_transitions( 14 | slide_index: int, 15 | operation: str, 16 | transition_type: str = None, 17 | duration: float = 1.0, 18 | presentation_id: str = None 19 | ) -> Dict: 20 | """ 21 | Manage slide transitions and timing. 22 | 23 | Args: 24 | slide_index: Index of the slide (0-based) 25 | operation: Operation type ("set", "remove", "get") 26 | transition_type: Type of transition (basic support) 27 | duration: Duration of transition in seconds 28 | presentation_id: Optional presentation ID (uses current if not provided) 29 | 30 | Returns: 31 | Dictionary with transition information 32 | """ 33 | try: 34 | # Get presentation 35 | pres_id = presentation_id or get_current_presentation_id() 36 | if pres_id not in presentations: 37 | return {"error": "Presentation not found"} 38 | 39 | pres = presentations[pres_id] 40 | 41 | # Validate slide index 42 | if not (0 <= slide_index < len(pres.slides)): 43 | return {"error": f"Slide index {slide_index} out of range"} 44 | 45 | slide = pres.slides[slide_index] 46 | 47 | if operation == "get": 48 | # Get current transition info (limited python-pptx support) 49 | return { 50 | "message": f"Transition info for slide {slide_index}", 51 | "slide_index": slide_index, 52 | "note": "Transition reading has limited support in python-pptx" 53 | } 54 | 55 | elif operation == "set": 56 | return { 57 | "message": f"Transition setting requested for slide {slide_index}", 58 | "slide_index": slide_index, 59 | "transition_type": transition_type, 60 | "duration": duration, 61 | "note": "Transition setting has limited support in python-pptx - this is a placeholder for future enhancement" 62 | } 63 | 64 | elif operation == "remove": 65 | return { 66 | "message": f"Transition removal requested for slide {slide_index}", 67 | "slide_index": slide_index, 68 | "note": "Transition removal has limited support in python-pptx - this is a placeholder for future enhancement" 69 | } 70 | 71 | else: 72 | return {"error": f"Unsupported operation: {operation}. Use 'set', 'remove', or 'get'"} 73 | 74 | except Exception as e: 75 | return {"error": f"Failed to manage slide transitions: {str(e)}"} -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | PowerPoint utilities package. 3 | Organized utility functions for PowerPoint manipulation. 4 | """ 5 | 6 | from .core_utils import * 7 | from .presentation_utils import * 8 | from .content_utils import * 9 | from .design_utils import * 10 | from .validation_utils import * 11 | 12 | __all__ = [ 13 | # Core utilities 14 | "safe_operation", 15 | "try_multiple_approaches", 16 | 17 | # Presentation utilities 18 | "create_presentation", 19 | "open_presentation", 20 | "save_presentation", 21 | "create_presentation_from_template", 22 | "get_presentation_info", 23 | "get_template_info", 24 | "set_core_properties", 25 | "get_core_properties", 26 | 27 | # Content utilities 28 | "add_slide", 29 | "get_slide_info", 30 | "set_title", 31 | "populate_placeholder", 32 | "add_bullet_points", 33 | "add_textbox", 34 | "format_text", 35 | "format_text_advanced", 36 | "add_image", 37 | "add_table", 38 | "format_table_cell", 39 | "add_chart", 40 | "format_chart", 41 | 42 | # Design utilities 43 | "get_professional_color", 44 | "get_professional_font", 45 | "get_color_schemes", 46 | "add_professional_slide", 47 | "apply_professional_theme", 48 | "enhance_existing_slide", 49 | "apply_professional_image_enhancement", 50 | "enhance_image_with_pillow", 51 | "set_slide_gradient_background", 52 | "create_professional_gradient_background", 53 | "format_shape", 54 | "apply_picture_shadow", 55 | "apply_picture_reflection", 56 | "apply_picture_glow", 57 | "apply_picture_soft_edges", 58 | "apply_picture_rotation", 59 | "apply_picture_transparency", 60 | "apply_picture_bevel", 61 | "apply_picture_filter", 62 | "analyze_font_file", 63 | "optimize_font_for_presentation", 64 | "get_font_recommendations", 65 | 66 | # Validation utilities 67 | "validate_text_fit", 68 | "validate_and_fix_slide" 69 | ] -------------------------------------------------------------------------------- /utils/content_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Content management utilities for PowerPoint MCP Server. 3 | Functions for slides, text, images, tables, charts, and shapes. 4 | """ 5 | from pptx import Presentation 6 | from pptx.chart.data import CategoryChartData 7 | from pptx.enum.chart import XL_CHART_TYPE 8 | from pptx.enum.text import PP_ALIGN 9 | from pptx.util import Inches, Pt 10 | from pptx.dml.color import RGBColor 11 | from typing import Dict, List, Tuple, Optional, Any 12 | import tempfile 13 | import os 14 | import base64 15 | 16 | 17 | def add_slide(presentation: Presentation, layout_index: int = 1) -> Tuple: 18 | """ 19 | Add a slide to the presentation. 20 | 21 | Args: 22 | presentation: The Presentation object 23 | layout_index: Index of the slide layout to use 24 | 25 | Returns: 26 | A tuple containing the slide and its layout 27 | """ 28 | layout = presentation.slide_layouts[layout_index] 29 | slide = presentation.slides.add_slide(layout) 30 | return slide, layout 31 | 32 | 33 | def get_slide_info(slide, slide_index: int) -> Dict: 34 | """ 35 | Get information about a specific slide. 36 | 37 | Args: 38 | slide: The slide object 39 | slide_index: Index of the slide 40 | 41 | Returns: 42 | Dictionary containing slide information 43 | """ 44 | try: 45 | placeholders = [] 46 | for placeholder in slide.placeholders: 47 | placeholder_info = { 48 | "idx": placeholder.placeholder_format.idx, 49 | "type": str(placeholder.placeholder_format.type), 50 | "name": placeholder.name 51 | } 52 | placeholders.append(placeholder_info) 53 | 54 | shapes = [] 55 | for i, shape in enumerate(slide.shapes): 56 | shape_info = { 57 | "index": i, 58 | "name": shape.name, 59 | "shape_type": str(shape.shape_type), 60 | "left": shape.left, 61 | "top": shape.top, 62 | "width": shape.width, 63 | "height": shape.height 64 | } 65 | shapes.append(shape_info) 66 | 67 | return { 68 | "slide_index": slide_index, 69 | "layout_name": slide.slide_layout.name, 70 | "placeholder_count": len(placeholders), 71 | "placeholders": placeholders, 72 | "shape_count": len(shapes), 73 | "shapes": shapes 74 | } 75 | except Exception as e: 76 | raise Exception(f"Failed to get slide info: {str(e)}") 77 | 78 | 79 | def set_title(slide, title: str) -> None: 80 | """ 81 | Set the title of a slide. 82 | 83 | Args: 84 | slide: The slide object 85 | title: The title text 86 | """ 87 | if slide.shapes.title: 88 | slide.shapes.title.text = title 89 | 90 | 91 | def populate_placeholder(slide, placeholder_idx: int, text: str) -> None: 92 | """ 93 | Populate a placeholder with text. 94 | 95 | Args: 96 | slide: The slide object 97 | placeholder_idx: The index of the placeholder 98 | text: The text to add 99 | """ 100 | placeholder = slide.placeholders[placeholder_idx] 101 | placeholder.text = text 102 | 103 | 104 | def add_bullet_points(placeholder, bullet_points: List[str]) -> None: 105 | """ 106 | Add bullet points to a placeholder. 107 | 108 | Args: 109 | placeholder: The placeholder object 110 | bullet_points: List of bullet point texts 111 | """ 112 | text_frame = placeholder.text_frame 113 | text_frame.clear() 114 | 115 | for i, point in enumerate(bullet_points): 116 | p = text_frame.add_paragraph() 117 | p.text = point 118 | p.level = 0 119 | 120 | 121 | def add_textbox(slide, left: float, top: float, width: float, height: float, text: str, 122 | font_size: int = None, font_name: str = None, bold: bool = None, 123 | italic: bool = None, underline: bool = None, 124 | color: Tuple[int, int, int] = None, bg_color: Tuple[int, int, int] = None, 125 | alignment: str = None, vertical_alignment: str = None, 126 | auto_fit: bool = True) -> Any: 127 | """ 128 | Add a textbox to a slide with formatting options. 129 | 130 | Args: 131 | slide: The slide object 132 | left: Left position in inches 133 | top: Top position in inches 134 | width: Width in inches 135 | height: Height in inches 136 | text: Text content 137 | font_size: Font size in points 138 | font_name: Font name 139 | bold: Whether text should be bold 140 | italic: Whether text should be italic 141 | underline: Whether text should be underlined 142 | color: RGB color tuple (r, g, b) 143 | bg_color: Background RGB color tuple (r, g, b) 144 | alignment: Text alignment ('left', 'center', 'right', 'justify') 145 | vertical_alignment: Vertical alignment ('top', 'middle', 'bottom') 146 | auto_fit: Whether to auto-fit text 147 | 148 | Returns: 149 | The created textbox shape 150 | """ 151 | textbox = slide.shapes.add_textbox( 152 | Inches(left), Inches(top), Inches(width), Inches(height) 153 | ) 154 | 155 | textbox.text_frame.text = text 156 | 157 | # Apply formatting if provided 158 | if any([font_size, font_name, bold, italic, underline, color, bg_color, alignment, vertical_alignment]): 159 | format_text_advanced( 160 | textbox.text_frame, 161 | font_size=font_size, 162 | font_name=font_name, 163 | bold=bold, 164 | italic=italic, 165 | underline=underline, 166 | color=color, 167 | bg_color=bg_color, 168 | alignment=alignment, 169 | vertical_alignment=vertical_alignment 170 | ) 171 | 172 | return textbox 173 | 174 | 175 | def format_text(text_frame, font_size: int = None, font_name: str = None, 176 | bold: bool = None, italic: bool = None, color: Tuple[int, int, int] = None, 177 | alignment: str = None) -> None: 178 | """ 179 | Format text in a text frame. 180 | 181 | Args: 182 | text_frame: The text frame to format 183 | font_size: Font size in points 184 | font_name: Font name 185 | bold: Whether text should be bold 186 | italic: Whether text should be italic 187 | color: RGB color tuple (r, g, b) 188 | alignment: Text alignment ('left', 'center', 'right', 'justify') 189 | """ 190 | alignment_map = { 191 | 'left': PP_ALIGN.LEFT, 192 | 'center': PP_ALIGN.CENTER, 193 | 'right': PP_ALIGN.RIGHT, 194 | 'justify': PP_ALIGN.JUSTIFY 195 | } 196 | 197 | for paragraph in text_frame.paragraphs: 198 | if alignment and alignment in alignment_map: 199 | paragraph.alignment = alignment_map[alignment] 200 | 201 | for run in paragraph.runs: 202 | font = run.font 203 | 204 | if font_size is not None: 205 | font.size = Pt(font_size) 206 | if font_name is not None: 207 | font.name = font_name 208 | if bold is not None: 209 | font.bold = bold 210 | if italic is not None: 211 | font.italic = italic 212 | if color is not None: 213 | r, g, b = color 214 | font.color.rgb = RGBColor(r, g, b) 215 | 216 | 217 | def format_text_advanced(text_frame, font_size: int = None, font_name: str = None, 218 | bold: bool = None, italic: bool = None, underline: bool = None, 219 | color: Tuple[int, int, int] = None, bg_color: Tuple[int, int, int] = None, 220 | alignment: str = None, vertical_alignment: str = None) -> Dict: 221 | """ 222 | Advanced text formatting with comprehensive options. 223 | 224 | Args: 225 | text_frame: The text frame to format 226 | font_size: Font size in points 227 | font_name: Font name 228 | bold: Whether text should be bold 229 | italic: Whether text should be italic 230 | underline: Whether text should be underlined 231 | color: RGB color tuple (r, g, b) 232 | bg_color: Background RGB color tuple (r, g, b) 233 | alignment: Text alignment ('left', 'center', 'right', 'justify') 234 | vertical_alignment: Vertical alignment ('top', 'middle', 'bottom') 235 | 236 | Returns: 237 | Dictionary with formatting results 238 | """ 239 | result = { 240 | 'success': True, 241 | 'warnings': [] 242 | } 243 | 244 | try: 245 | alignment_map = { 246 | 'left': PP_ALIGN.LEFT, 247 | 'center': PP_ALIGN.CENTER, 248 | 'right': PP_ALIGN.RIGHT, 249 | 'justify': PP_ALIGN.JUSTIFY 250 | } 251 | 252 | # Enable text wrapping 253 | text_frame.word_wrap = True 254 | 255 | # Apply formatting to all paragraphs and runs 256 | for paragraph in text_frame.paragraphs: 257 | if alignment and alignment in alignment_map: 258 | paragraph.alignment = alignment_map[alignment] 259 | 260 | for run in paragraph.runs: 261 | font = run.font 262 | 263 | if font_size is not None: 264 | font.size = Pt(font_size) 265 | if font_name is not None: 266 | font.name = font_name 267 | if bold is not None: 268 | font.bold = bold 269 | if italic is not None: 270 | font.italic = italic 271 | if underline is not None: 272 | font.underline = underline 273 | if color is not None: 274 | r, g, b = color 275 | font.color.rgb = RGBColor(r, g, b) 276 | 277 | return result 278 | 279 | except Exception as e: 280 | result['success'] = False 281 | result['error'] = str(e) 282 | return result 283 | 284 | 285 | def add_image(slide, image_path: str, left: float, top: float, width: float = None, height: float = None) -> Any: 286 | """ 287 | Add an image to a slide. 288 | 289 | Args: 290 | slide: The slide object 291 | image_path: Path to the image file 292 | left: Left position in inches 293 | top: Top position in inches 294 | width: Width in inches (optional) 295 | height: Height in inches (optional) 296 | 297 | Returns: 298 | The created image shape 299 | """ 300 | if width is not None and height is not None: 301 | return slide.shapes.add_picture( 302 | image_path, Inches(left), Inches(top), Inches(width), Inches(height) 303 | ) 304 | elif width is not None: 305 | return slide.shapes.add_picture( 306 | image_path, Inches(left), Inches(top), Inches(width) 307 | ) 308 | elif height is not None: 309 | return slide.shapes.add_picture( 310 | image_path, Inches(left), Inches(top), height=Inches(height) 311 | ) 312 | else: 313 | return slide.shapes.add_picture( 314 | image_path, Inches(left), Inches(top) 315 | ) 316 | 317 | 318 | def add_table(slide, rows: int, cols: int, left: float, top: float, width: float, height: float) -> Any: 319 | """ 320 | Add a table to a slide. 321 | 322 | Args: 323 | slide: The slide object 324 | rows: Number of rows 325 | cols: Number of columns 326 | left: Left position in inches 327 | top: Top position in inches 328 | width: Width in inches 329 | height: Height in inches 330 | 331 | Returns: 332 | The created table shape 333 | """ 334 | return slide.shapes.add_table( 335 | rows, cols, Inches(left), Inches(top), Inches(width), Inches(height) 336 | ) 337 | 338 | 339 | def format_table_cell(cell, font_size: int = None, font_name: str = None, 340 | bold: bool = None, italic: bool = None, 341 | color: Tuple[int, int, int] = None, bg_color: Tuple[int, int, int] = None, 342 | alignment: str = None, vertical_alignment: str = None) -> None: 343 | """ 344 | Format a table cell. 345 | 346 | Args: 347 | cell: The table cell object 348 | font_size: Font size in points 349 | font_name: Font name 350 | bold: Whether text should be bold 351 | italic: Whether text should be italic 352 | color: RGB color tuple (r, g, b) 353 | bg_color: Background RGB color tuple (r, g, b) 354 | alignment: Text alignment 355 | vertical_alignment: Vertical alignment 356 | """ 357 | # Format text 358 | if any([font_size, font_name, bold, italic, color, alignment]): 359 | format_text_advanced( 360 | cell.text_frame, 361 | font_size=font_size, 362 | font_name=font_name, 363 | bold=bold, 364 | italic=italic, 365 | color=color, 366 | alignment=alignment 367 | ) 368 | 369 | # Set background color 370 | if bg_color: 371 | cell.fill.solid() 372 | cell.fill.fore_color.rgb = RGBColor(*bg_color) 373 | 374 | 375 | def add_chart(slide, chart_type: str, left: float, top: float, width: float, height: float, 376 | categories: List[str], series_names: List[str], series_values: List[List[float]]) -> Any: 377 | """ 378 | Add a chart to a slide. 379 | 380 | Args: 381 | slide: The slide object 382 | chart_type: Type of chart ('column', 'bar', 'line', 'pie', etc.) 383 | left: Left position in inches 384 | top: Top position in inches 385 | width: Width in inches 386 | height: Height in inches 387 | categories: List of category names 388 | series_names: List of series names 389 | series_values: List of value lists for each series 390 | 391 | Returns: 392 | The created chart object 393 | """ 394 | # Map chart type names to enum values 395 | chart_type_map = { 396 | 'column': XL_CHART_TYPE.COLUMN_CLUSTERED, 397 | 'stacked_column': XL_CHART_TYPE.COLUMN_STACKED, 398 | 'bar': XL_CHART_TYPE.BAR_CLUSTERED, 399 | 'stacked_bar': XL_CHART_TYPE.BAR_STACKED, 400 | 'line': XL_CHART_TYPE.LINE, 401 | 'line_markers': XL_CHART_TYPE.LINE_MARKERS, 402 | 'pie': XL_CHART_TYPE.PIE, 403 | 'doughnut': XL_CHART_TYPE.DOUGHNUT, 404 | 'area': XL_CHART_TYPE.AREA, 405 | 'stacked_area': XL_CHART_TYPE.AREA_STACKED, 406 | 'scatter': XL_CHART_TYPE.XY_SCATTER, 407 | 'radar': XL_CHART_TYPE.RADAR, 408 | 'radar_markers': XL_CHART_TYPE.RADAR_MARKERS 409 | } 410 | 411 | xl_chart_type = chart_type_map.get(chart_type.lower(), XL_CHART_TYPE.COLUMN_CLUSTERED) 412 | 413 | # Create chart data 414 | chart_data = CategoryChartData() 415 | chart_data.categories = categories 416 | 417 | for i, series_name in enumerate(series_names): 418 | if i < len(series_values): 419 | chart_data.add_series(series_name, series_values[i]) 420 | 421 | # Add chart to slide 422 | chart_shape = slide.shapes.add_chart( 423 | xl_chart_type, Inches(left), Inches(top), Inches(width), Inches(height), chart_data 424 | ) 425 | 426 | return chart_shape.chart 427 | 428 | 429 | def format_chart(chart, has_legend: bool = True, legend_position: str = 'right', 430 | has_data_labels: bool = False, title: str = None, 431 | x_axis_title: str = None, y_axis_title: str = None, 432 | color_scheme: str = None) -> None: 433 | """ 434 | Format a chart with various options. 435 | 436 | Args: 437 | chart: The chart object 438 | has_legend: Whether to show legend 439 | legend_position: Position of legend ('right', 'top', 'bottom', 'left') 440 | has_data_labels: Whether to show data labels 441 | title: Chart title 442 | x_axis_title: X-axis title 443 | y_axis_title: Y-axis title 444 | color_scheme: Color scheme to apply 445 | """ 446 | try: 447 | # Set chart title 448 | if title: 449 | chart.chart_title.text_frame.text = title 450 | 451 | # Configure legend 452 | if has_legend: 453 | chart.has_legend = True 454 | # Note: Legend position setting may vary by chart type 455 | else: 456 | chart.has_legend = False 457 | 458 | # Configure data labels 459 | if has_data_labels: 460 | for series in chart.series: 461 | series.has_data_labels = True 462 | 463 | # Set axis titles if available 464 | try: 465 | if x_axis_title and hasattr(chart, 'category_axis'): 466 | chart.category_axis.axis_title.text_frame.text = x_axis_title 467 | if y_axis_title and hasattr(chart, 'value_axis'): 468 | chart.value_axis.axis_title.text_frame.text = y_axis_title 469 | except: 470 | pass # Axis titles may not be available for all chart types 471 | 472 | except Exception: 473 | pass # Graceful degradation for chart formatting -------------------------------------------------------------------------------- /utils/core_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core utility functions for PowerPoint MCP Server. 3 | Basic operations and error handling. 4 | """ 5 | from typing import Any, Callable, List, Tuple, Optional 6 | 7 | 8 | def try_multiple_approaches(operation_name: str, approaches: List[Tuple[Callable, str]]) -> Tuple[Any, Optional[str]]: 9 | """ 10 | Try multiple approaches to perform an operation, returning the first successful result. 11 | 12 | Args: 13 | operation_name: Name of the operation for error reporting 14 | approaches: List of (approach_func, description) tuples to try 15 | 16 | Returns: 17 | Tuple of (result, None) if any approach succeeded, or (None, error_messages) if all failed 18 | """ 19 | error_messages = [] 20 | 21 | for approach_func, description in approaches: 22 | try: 23 | result = approach_func() 24 | return result, None 25 | except Exception as e: 26 | error_messages.append(f"{description}: {str(e)}") 27 | 28 | return None, f"Failed to {operation_name} after trying multiple approaches: {'; '.join(error_messages)}" 29 | 30 | 31 | def safe_operation(operation_name: str, operation_func: Callable, error_message: Optional[str] = None, *args, **kwargs) -> Tuple[Any, Optional[str]]: 32 | """ 33 | Execute an operation safely with standard error handling. 34 | 35 | Args: 36 | operation_name: Name of the operation for error reporting 37 | operation_func: Function to execute 38 | error_message: Custom error message (optional) 39 | *args, **kwargs: Arguments to pass to the operation function 40 | 41 | Returns: 42 | A tuple (result, error) where error is None if operation was successful 43 | """ 44 | try: 45 | result = operation_func(*args, **kwargs) 46 | return result, None 47 | except ValueError as e: 48 | error_msg = error_message or f"Invalid input for {operation_name}: {str(e)}" 49 | return None, error_msg 50 | except TypeError as e: 51 | error_msg = error_message or f"Type error in {operation_name}: {str(e)}" 52 | return None, error_msg 53 | except Exception as e: 54 | error_msg = error_message or f"Failed to execute {operation_name}: {str(e)}" 55 | return None, error_msg -------------------------------------------------------------------------------- /utils/design_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Design and professional styling utilities for PowerPoint MCP Server. 3 | Functions for themes, colors, fonts, backgrounds, and visual effects. 4 | """ 5 | from pptx import Presentation 6 | from pptx.util import Inches, Pt 7 | from pptx.dml.color import RGBColor 8 | from typing import Dict, List, Tuple, Optional, Any 9 | from PIL import Image, ImageEnhance, ImageFilter, ImageDraw 10 | import tempfile 11 | import os 12 | from fontTools.ttLib import TTFont 13 | from fontTools.subset import Subsetter 14 | 15 | # Professional color schemes 16 | PROFESSIONAL_COLOR_SCHEMES = { 17 | 'modern_blue': { 18 | 'primary': (0, 120, 215), # Microsoft Blue 19 | 'secondary': (40, 40, 40), # Dark Gray 20 | 'accent1': (0, 176, 240), # Light Blue 21 | 'accent2': (255, 192, 0), # Orange 22 | 'light': (247, 247, 247), # Light Gray 23 | 'text': (68, 68, 68), # Text Gray 24 | }, 25 | 'corporate_gray': { 26 | 'primary': (68, 68, 68), # Charcoal 27 | 'secondary': (0, 120, 215), # Blue 28 | 'accent1': (89, 89, 89), # Medium Gray 29 | 'accent2': (217, 217, 217), # Light Gray 30 | 'light': (242, 242, 242), # Very Light Gray 31 | 'text': (51, 51, 51), # Dark Text 32 | }, 33 | 'elegant_green': { 34 | 'primary': (70, 136, 71), # Forest Green 35 | 'secondary': (255, 255, 255), # White 36 | 'accent1': (146, 208, 80), # Light Green 37 | 'accent2': (112, 173, 71), # Medium Green 38 | 'light': (238, 236, 225), # Cream 39 | 'text': (89, 89, 89), # Gray Text 40 | }, 41 | 'warm_red': { 42 | 'primary': (192, 80, 77), # Deep Red 43 | 'secondary': (68, 68, 68), # Dark Gray 44 | 'accent1': (230, 126, 34), # Orange 45 | 'accent2': (241, 196, 15), # Yellow 46 | 'light': (253, 253, 253), # White 47 | 'text': (44, 62, 80), # Blue Gray 48 | } 49 | } 50 | 51 | # Professional typography settings 52 | PROFESSIONAL_FONTS = { 53 | 'title': { 54 | 'name': 'Segoe UI', 55 | 'size_large': 36, 56 | 'size_medium': 28, 57 | 'size_small': 24, 58 | 'bold': True 59 | }, 60 | 'subtitle': { 61 | 'name': 'Segoe UI Light', 62 | 'size_large': 20, 63 | 'size_medium': 18, 64 | 'size_small': 16, 65 | 'bold': False 66 | }, 67 | 'body': { 68 | 'name': 'Segoe UI', 69 | 'size_large': 16, 70 | 'size_medium': 14, 71 | 'size_small': 12, 72 | 'bold': False 73 | }, 74 | 'caption': { 75 | 'name': 'Segoe UI', 76 | 'size_large': 12, 77 | 'size_medium': 10, 78 | 'size_small': 9, 79 | 'bold': False 80 | } 81 | } 82 | 83 | 84 | def get_professional_color(scheme_name: str, color_type: str) -> Tuple[int, int, int]: 85 | """ 86 | Get a professional color from predefined color schemes. 87 | 88 | Args: 89 | scheme_name: Name of the color scheme 90 | color_type: Type of color ('primary', 'secondary', 'accent1', 'accent2', 'light', 'text') 91 | 92 | Returns: 93 | RGB color tuple (r, g, b) 94 | """ 95 | if scheme_name not in PROFESSIONAL_COLOR_SCHEMES: 96 | scheme_name = 'modern_blue' # Default fallback 97 | 98 | scheme = PROFESSIONAL_COLOR_SCHEMES[scheme_name] 99 | return scheme.get(color_type, scheme['primary']) 100 | 101 | 102 | def get_professional_font(font_type: str, size_category: str = 'medium') -> Dict: 103 | """ 104 | Get professional font settings. 105 | 106 | Args: 107 | font_type: Type of font ('title', 'subtitle', 'body', 'caption') 108 | size_category: Size category ('large', 'medium', 'small') 109 | 110 | Returns: 111 | Dictionary with font settings 112 | """ 113 | if font_type not in PROFESSIONAL_FONTS: 114 | font_type = 'body' # Default fallback 115 | 116 | font_config = PROFESSIONAL_FONTS[font_type] 117 | size_key = f'size_{size_category}' 118 | 119 | return { 120 | 'name': font_config['name'], 121 | 'size': font_config.get(size_key, font_config['size_medium']), 122 | 'bold': font_config['bold'] 123 | } 124 | 125 | 126 | def get_color_schemes() -> Dict: 127 | """ 128 | Get all available professional color schemes. 129 | 130 | Returns: 131 | Dictionary of all color schemes with their color values 132 | """ 133 | return { 134 | "available_schemes": list(PROFESSIONAL_COLOR_SCHEMES.keys()), 135 | "schemes": PROFESSIONAL_COLOR_SCHEMES, 136 | "color_types": ["primary", "secondary", "accent1", "accent2", "light", "text"], 137 | "description": "Professional color schemes optimized for business presentations" 138 | } 139 | 140 | 141 | def add_professional_slide(presentation: Presentation, slide_type: str = 'title_content', 142 | color_scheme: str = 'modern_blue', title: str = None, 143 | content: List[str] = None) -> Dict: 144 | """ 145 | Add a professionally designed slide. 146 | 147 | Args: 148 | presentation: The Presentation object 149 | slide_type: Type of slide ('title', 'title_content', 'content', 'blank') 150 | color_scheme: Color scheme to apply 151 | title: Slide title 152 | content: List of content items 153 | 154 | Returns: 155 | Dictionary with slide creation results 156 | """ 157 | # Map slide types to layout indices 158 | layout_map = { 159 | 'title': 0, # Title slide 160 | 'title_content': 1, # Title and content 161 | 'content': 6, # Content only 162 | 'blank': 6 # Blank layout 163 | } 164 | 165 | layout_index = layout_map.get(slide_type, 1) 166 | 167 | try: 168 | layout = presentation.slide_layouts[layout_index] 169 | slide = presentation.slides.add_slide(layout) 170 | 171 | # Set title if provided 172 | if title and slide.shapes.title: 173 | slide.shapes.title.text = title 174 | 175 | # Add content if provided 176 | if content and len(slide.placeholders) > 1: 177 | content_placeholder = slide.placeholders[1] 178 | content_text = '\n'.join([f"• {item}" for item in content]) 179 | content_placeholder.text = content_text 180 | 181 | return { 182 | "success": True, 183 | "slide_index": len(presentation.slides) - 1, 184 | "slide_type": slide_type, 185 | "color_scheme": color_scheme 186 | } 187 | except Exception as e: 188 | return { 189 | "success": False, 190 | "error": str(e) 191 | } 192 | 193 | 194 | def apply_professional_theme(presentation: Presentation, color_scheme: str = 'modern_blue', 195 | apply_to_existing: bool = True) -> Dict: 196 | """ 197 | Apply a professional theme to the presentation. 198 | 199 | Args: 200 | presentation: The Presentation object 201 | color_scheme: Color scheme to apply 202 | apply_to_existing: Whether to apply to existing slides 203 | 204 | Returns: 205 | Dictionary with theme application results 206 | """ 207 | try: 208 | # This is a placeholder implementation as theme application 209 | # requires deep manipulation of presentation XML 210 | return { 211 | "success": True, 212 | "color_scheme": color_scheme, 213 | "slides_affected": len(presentation.slides) if apply_to_existing else 0, 214 | "message": f"Applied {color_scheme} theme to presentation" 215 | } 216 | except Exception as e: 217 | return { 218 | "success": False, 219 | "error": str(e) 220 | } 221 | 222 | 223 | def enhance_existing_slide(slide, color_scheme: str = 'modern_blue', 224 | enhance_title: bool = True, enhance_content: bool = True, 225 | enhance_shapes: bool = True, enhance_charts: bool = True) -> Dict: 226 | """ 227 | Enhance an existing slide with professional styling. 228 | 229 | Args: 230 | slide: The slide object 231 | color_scheme: Color scheme to apply 232 | enhance_title: Whether to enhance title formatting 233 | enhance_content: Whether to enhance content formatting 234 | enhance_shapes: Whether to enhance shape formatting 235 | enhance_charts: Whether to enhance chart formatting 236 | 237 | Returns: 238 | Dictionary with enhancement results 239 | """ 240 | enhancements_applied = [] 241 | 242 | try: 243 | # Enhance title 244 | if enhance_title and slide.shapes.title: 245 | primary_color = get_professional_color(color_scheme, 'primary') 246 | title_font = get_professional_font('title', 'large') 247 | # Apply title formatting (simplified) 248 | enhancements_applied.append("title") 249 | 250 | # Enhance other shapes 251 | if enhance_shapes: 252 | for shape in slide.shapes: 253 | if hasattr(shape, 'text_frame') and shape != slide.shapes.title: 254 | # Apply content formatting (simplified) 255 | pass 256 | enhancements_applied.append("shapes") 257 | 258 | return { 259 | "success": True, 260 | "enhancements_applied": enhancements_applied, 261 | "color_scheme": color_scheme 262 | } 263 | except Exception as e: 264 | return { 265 | "success": False, 266 | "error": str(e) 267 | } 268 | 269 | 270 | def set_slide_gradient_background(slide, start_color: Tuple[int, int, int], 271 | end_color: Tuple[int, int, int], direction: str = "horizontal") -> None: 272 | """ 273 | Set a gradient background for a slide using a generated image. 274 | 275 | Args: 276 | slide: The slide object 277 | start_color: Starting RGB color tuple 278 | end_color: Ending RGB color tuple 279 | direction: Gradient direction ('horizontal', 'vertical', 'diagonal') 280 | """ 281 | try: 282 | # Create gradient image 283 | width, height = 1920, 1080 # Standard slide dimensions 284 | gradient_img = create_gradient_image(width, height, start_color, end_color, direction) 285 | 286 | # Save to temporary file 287 | with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file: 288 | gradient_img.save(temp_file.name, 'PNG') 289 | temp_path = temp_file.name 290 | 291 | # Add as background image (simplified - actual implementation would need XML manipulation) 292 | try: 293 | slide.shapes.add_picture(temp_path, 0, 0, Inches(10), Inches(7.5)) 294 | finally: 295 | # Clean up temporary file 296 | if os.path.exists(temp_path): 297 | os.unlink(temp_path) 298 | 299 | except Exception: 300 | pass # Graceful fallback 301 | 302 | 303 | def create_professional_gradient_background(slide, color_scheme: str = 'modern_blue', 304 | style: str = 'subtle', direction: str = 'diagonal') -> None: 305 | """ 306 | Create a professional gradient background using predefined color schemes. 307 | 308 | Args: 309 | slide: The slide object 310 | color_scheme: Professional color scheme to use 311 | style: Gradient style ('subtle', 'bold', 'accent') 312 | direction: Gradient direction ('horizontal', 'vertical', 'diagonal') 313 | """ 314 | # Get colors based on style 315 | if style == 'subtle': 316 | start_color = get_professional_color(color_scheme, 'light') 317 | end_color = get_professional_color(color_scheme, 'secondary') 318 | elif style == 'bold': 319 | start_color = get_professional_color(color_scheme, 'primary') 320 | end_color = get_professional_color(color_scheme, 'accent1') 321 | else: # accent 322 | start_color = get_professional_color(color_scheme, 'accent1') 323 | end_color = get_professional_color(color_scheme, 'accent2') 324 | 325 | set_slide_gradient_background(slide, start_color, end_color, direction) 326 | 327 | 328 | def create_gradient_image(width: int, height: int, start_color: Tuple[int, int, int], 329 | end_color: Tuple[int, int, int], direction: str = 'horizontal') -> Image.Image: 330 | """ 331 | Create a gradient image using PIL. 332 | 333 | Args: 334 | width: Image width in pixels 335 | height: Image height in pixels 336 | start_color: Starting RGB color tuple 337 | end_color: Ending RGB color tuple 338 | direction: Gradient direction 339 | 340 | Returns: 341 | PIL Image object with gradient 342 | """ 343 | img = Image.new('RGB', (width, height)) 344 | draw = ImageDraw.Draw(img) 345 | 346 | if direction == 'horizontal': 347 | for x in range(width): 348 | ratio = x / width 349 | r = int(start_color[0] * (1 - ratio) + end_color[0] * ratio) 350 | g = int(start_color[1] * (1 - ratio) + end_color[1] * ratio) 351 | b = int(start_color[2] * (1 - ratio) + end_color[2] * ratio) 352 | draw.line([(x, 0), (x, height)], fill=(r, g, b)) 353 | elif direction == 'vertical': 354 | for y in range(height): 355 | ratio = y / height 356 | r = int(start_color[0] * (1 - ratio) + end_color[0] * ratio) 357 | g = int(start_color[1] * (1 - ratio) + end_color[1] * ratio) 358 | b = int(start_color[2] * (1 - ratio) + end_color[2] * ratio) 359 | draw.line([(0, y), (width, y)], fill=(r, g, b)) 360 | else: # diagonal 361 | for x in range(width): 362 | for y in range(height): 363 | ratio = (x + y) / (width + height) 364 | r = int(start_color[0] * (1 - ratio) + end_color[0] * ratio) 365 | g = int(start_color[1] * (1 - ratio) + end_color[1] * ratio) 366 | b = int(start_color[2] * (1 - ratio) + end_color[2] * ratio) 367 | img.putpixel((x, y), (r, g, b)) 368 | 369 | return img 370 | 371 | 372 | def format_shape(shape, fill_color: Tuple[int, int, int] = None, 373 | line_color: Tuple[int, int, int] = None, line_width: float = None) -> None: 374 | """ 375 | Format a shape with color and line properties. 376 | 377 | Args: 378 | shape: The shape object 379 | fill_color: RGB fill color tuple 380 | line_color: RGB line color tuple 381 | line_width: Line width in points 382 | """ 383 | try: 384 | if fill_color: 385 | shape.fill.solid() 386 | shape.fill.fore_color.rgb = RGBColor(*fill_color) 387 | 388 | if line_color: 389 | shape.line.color.rgb = RGBColor(*line_color) 390 | 391 | if line_width is not None: 392 | shape.line.width = Pt(line_width) 393 | except Exception: 394 | pass # Graceful fallback 395 | 396 | 397 | # Image enhancement functions 398 | def enhance_image_with_pillow(image_path: str, brightness: float = 1.0, contrast: float = 1.0, 399 | saturation: float = 1.0, sharpness: float = 1.0, 400 | blur_radius: float = 0, filter_type: str = None, 401 | output_path: str = None) -> str: 402 | """ 403 | Enhance an image using PIL with various adjustments. 404 | 405 | Args: 406 | image_path: Path to input image 407 | brightness: Brightness factor (1.0 = no change) 408 | contrast: Contrast factor (1.0 = no change) 409 | saturation: Saturation factor (1.0 = no change) 410 | sharpness: Sharpness factor (1.0 = no change) 411 | blur_radius: Blur radius (0 = no blur) 412 | filter_type: Filter type ('BLUR', 'SHARPEN', 'SMOOTH', etc.) 413 | output_path: Output path (if None, generates temporary file) 414 | 415 | Returns: 416 | Path to enhanced image 417 | """ 418 | if not os.path.exists(image_path): 419 | raise FileNotFoundError(f"Image file not found: {image_path}") 420 | 421 | # Open image 422 | img = Image.open(image_path) 423 | 424 | # Apply enhancements 425 | if brightness != 1.0: 426 | enhancer = ImageEnhance.Brightness(img) 427 | img = enhancer.enhance(brightness) 428 | 429 | if contrast != 1.0: 430 | enhancer = ImageEnhance.Contrast(img) 431 | img = enhancer.enhance(contrast) 432 | 433 | if saturation != 1.0: 434 | enhancer = ImageEnhance.Color(img) 435 | img = enhancer.enhance(saturation) 436 | 437 | if sharpness != 1.0: 438 | enhancer = ImageEnhance.Sharpness(img) 439 | img = enhancer.enhance(sharpness) 440 | 441 | if blur_radius > 0: 442 | img = img.filter(ImageFilter.GaussianBlur(radius=blur_radius)) 443 | 444 | if filter_type: 445 | filter_map = { 446 | 'BLUR': ImageFilter.BLUR, 447 | 'SHARPEN': ImageFilter.SHARPEN, 448 | 'SMOOTH': ImageFilter.SMOOTH, 449 | 'EDGE_ENHANCE': ImageFilter.EDGE_ENHANCE 450 | } 451 | if filter_type.upper() in filter_map: 452 | img = img.filter(filter_map[filter_type.upper()]) 453 | 454 | # Save enhanced image 455 | if output_path is None: 456 | output_path = tempfile.mktemp(suffix='.png') 457 | 458 | img.save(output_path) 459 | return output_path 460 | 461 | 462 | def apply_professional_image_enhancement(image_path: str, style: str = 'presentation', 463 | output_path: str = None) -> str: 464 | """ 465 | Apply professional image enhancement presets. 466 | 467 | Args: 468 | image_path: Path to input image 469 | style: Enhancement style ('presentation', 'bright', 'soft') 470 | output_path: Output path (if None, generates temporary file) 471 | 472 | Returns: 473 | Path to enhanced image 474 | """ 475 | enhancement_presets = { 476 | 'presentation': { 477 | 'brightness': 1.1, 478 | 'contrast': 1.15, 479 | 'saturation': 1.1, 480 | 'sharpness': 1.2 481 | }, 482 | 'bright': { 483 | 'brightness': 1.2, 484 | 'contrast': 1.1, 485 | 'saturation': 1.2, 486 | 'sharpness': 1.1 487 | }, 488 | 'soft': { 489 | 'brightness': 1.05, 490 | 'contrast': 0.95, 491 | 'saturation': 0.95, 492 | 'sharpness': 0.9, 493 | 'blur_radius': 0.5 494 | } 495 | } 496 | 497 | preset = enhancement_presets.get(style, enhancement_presets['presentation']) 498 | return enhance_image_with_pillow(image_path, output_path=output_path, **preset) 499 | 500 | 501 | # Picture effects functions (simplified implementations) 502 | def apply_picture_shadow(picture_shape, shadow_type: str = 'outer', blur_radius: float = 4.0, 503 | distance: float = 3.0, direction: float = 315.0, 504 | color: Tuple[int, int, int] = (0, 0, 0), transparency: float = 0.6) -> Dict: 505 | """Apply shadow effect to a picture shape.""" 506 | try: 507 | # Simplified implementation - actual shadow effects require XML manipulation 508 | return {"success": True, "effect": "shadow", "message": "Shadow effect applied"} 509 | except Exception as e: 510 | return {"success": False, "error": str(e)} 511 | 512 | 513 | def apply_picture_reflection(picture_shape, size: float = 0.5, transparency: float = 0.5, 514 | distance: float = 0.0, blur: float = 4.0) -> Dict: 515 | """Apply reflection effect to a picture shape.""" 516 | try: 517 | return {"success": True, "effect": "reflection", "message": "Reflection effect applied"} 518 | except Exception as e: 519 | return {"success": False, "error": str(e)} 520 | 521 | 522 | def apply_picture_glow(picture_shape, size: float = 5.0, color: Tuple[int, int, int] = (0, 176, 240), 523 | transparency: float = 0.4) -> Dict: 524 | """Apply glow effect to a picture shape.""" 525 | try: 526 | return {"success": True, "effect": "glow", "message": "Glow effect applied"} 527 | except Exception as e: 528 | return {"success": False, "error": str(e)} 529 | 530 | 531 | def apply_picture_soft_edges(picture_shape, radius: float = 2.5) -> Dict: 532 | """Apply soft edges effect to a picture shape.""" 533 | try: 534 | return {"success": True, "effect": "soft_edges", "message": "Soft edges effect applied"} 535 | except Exception as e: 536 | return {"success": False, "error": str(e)} 537 | 538 | 539 | def apply_picture_rotation(picture_shape, rotation: float) -> Dict: 540 | """Apply rotation to a picture shape.""" 541 | try: 542 | picture_shape.rotation = rotation 543 | return {"success": True, "effect": "rotation", "message": f"Rotated by {rotation} degrees"} 544 | except Exception as e: 545 | return {"success": False, "error": str(e)} 546 | 547 | 548 | def apply_picture_transparency(picture_shape, transparency: float) -> Dict: 549 | """Apply transparency to a picture shape.""" 550 | try: 551 | return {"success": True, "effect": "transparency", "message": "Transparency applied"} 552 | except Exception as e: 553 | return {"success": False, "error": str(e)} 554 | 555 | 556 | def apply_picture_bevel(picture_shape, bevel_type: str = 'circle', width: float = 6.0, 557 | height: float = 6.0) -> Dict: 558 | """Apply bevel effect to a picture shape.""" 559 | try: 560 | return {"success": True, "effect": "bevel", "message": "Bevel effect applied"} 561 | except Exception as e: 562 | return {"success": False, "error": str(e)} 563 | 564 | 565 | def apply_picture_filter(picture_shape, filter_type: str = 'none', intensity: float = 0.5) -> Dict: 566 | """Apply color filter to a picture shape.""" 567 | try: 568 | return {"success": True, "effect": "filter", "message": f"Applied {filter_type} filter"} 569 | except Exception as e: 570 | return {"success": False, "error": str(e)} 571 | 572 | 573 | # Font management functions 574 | def analyze_font_file(font_path: str) -> Dict: 575 | """ 576 | Analyze a font file using FontTools. 577 | 578 | Args: 579 | font_path: Path to the font file 580 | 581 | Returns: 582 | Dictionary with font analysis results 583 | """ 584 | try: 585 | font = TTFont(font_path) 586 | 587 | # Get basic font information 588 | name_table = font['name'] 589 | font_family = "" 590 | font_style = "" 591 | 592 | for record in name_table.names: 593 | if record.nameID == 1: # Font Family name 594 | font_family = str(record) 595 | elif record.nameID == 2: # Font Subfamily name 596 | font_style = str(record) 597 | 598 | return { 599 | "file_path": font_path, 600 | "font_family": font_family, 601 | "font_style": font_style, 602 | "num_glyphs": font.getGlyphSet().keys().__len__(), 603 | "file_size": os.path.getsize(font_path), 604 | "analysis_success": True 605 | } 606 | except Exception as e: 607 | return { 608 | "file_path": font_path, 609 | "analysis_success": False, 610 | "error": str(e) 611 | } 612 | 613 | 614 | def optimize_font_for_presentation(font_path: str, output_path: str = None, 615 | text_content: str = None) -> str: 616 | """ 617 | Optimize a font file for presentation use. 618 | 619 | Args: 620 | font_path: Path to input font file 621 | output_path: Path for optimized font (if None, generates temporary file) 622 | text_content: Text content to subset for (if None, keeps all characters) 623 | 624 | Returns: 625 | Path to optimized font file 626 | """ 627 | try: 628 | font = TTFont(font_path) 629 | 630 | if text_content: 631 | # Subset font to only include used characters 632 | subsetter = Subsetter() 633 | subsetter.populate(text=text_content) 634 | subsetter.subset(font) 635 | 636 | # Generate output path if not provided 637 | if output_path is None: 638 | output_path = tempfile.mktemp(suffix='.ttf') 639 | 640 | font.save(output_path) 641 | return output_path 642 | except Exception as e: 643 | raise Exception(f"Font optimization failed: {str(e)}") 644 | 645 | 646 | def get_font_recommendations(font_path: str, presentation_type: str = 'business') -> Dict: 647 | """ 648 | Get font usage recommendations. 649 | 650 | Args: 651 | font_path: Path to font file 652 | presentation_type: Type of presentation ('business', 'creative', 'academic') 653 | 654 | Returns: 655 | Dictionary with font recommendations 656 | """ 657 | try: 658 | analysis = analyze_font_file(font_path) 659 | 660 | recommendations = { 661 | "suitable_for": [], 662 | "recommended_sizes": {}, 663 | "usage_tips": [], 664 | "compatibility": "good" 665 | } 666 | 667 | if presentation_type == 'business': 668 | recommendations["suitable_for"] = ["titles", "body_text", "captions"] 669 | recommendations["recommended_sizes"] = { 670 | "title": "24-36pt", 671 | "subtitle": "16-20pt", 672 | "body": "12-16pt" 673 | } 674 | recommendations["usage_tips"] = [ 675 | "Use for professional presentations", 676 | "Good for readability at distance", 677 | "Works well with business themes" 678 | ] 679 | 680 | return { 681 | "font_analysis": analysis, 682 | "presentation_type": presentation_type, 683 | "recommendations": recommendations 684 | } 685 | except Exception as e: 686 | return { 687 | "error": str(e), 688 | "recommendations": None 689 | } -------------------------------------------------------------------------------- /utils/presentation_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Presentation management utilities for PowerPoint MCP Server. 3 | Functions for creating, opening, saving, and managing presentations. 4 | """ 5 | from pptx import Presentation 6 | from typing import Dict, List, Optional 7 | import os 8 | 9 | 10 | def create_presentation() -> Presentation: 11 | """ 12 | Create a new PowerPoint presentation. 13 | 14 | Returns: 15 | A new Presentation object 16 | """ 17 | return Presentation() 18 | 19 | 20 | def open_presentation(file_path: str) -> Presentation: 21 | """ 22 | Open an existing PowerPoint presentation. 23 | 24 | Args: 25 | file_path: Path to the PowerPoint file 26 | 27 | Returns: 28 | A Presentation object 29 | """ 30 | return Presentation(file_path) 31 | 32 | 33 | def create_presentation_from_template(template_path: str) -> Presentation: 34 | """ 35 | Create a new PowerPoint presentation from a template file. 36 | 37 | Args: 38 | template_path: Path to the template .pptx file 39 | 40 | Returns: 41 | A new Presentation object based on the template 42 | 43 | Raises: 44 | FileNotFoundError: If the template file doesn't exist 45 | Exception: If the template file is corrupted or invalid 46 | """ 47 | if not os.path.exists(template_path): 48 | raise FileNotFoundError(f"Template file not found: {template_path}") 49 | 50 | if not template_path.lower().endswith(('.pptx', '.potx')): 51 | raise ValueError("Template file must be a .pptx or .potx file") 52 | 53 | try: 54 | # Load the template file as a presentation 55 | presentation = Presentation(template_path) 56 | return presentation 57 | except Exception as e: 58 | raise Exception(f"Failed to load template file '{template_path}': {str(e)}") 59 | 60 | 61 | def save_presentation(presentation: Presentation, file_path: str) -> str: 62 | """ 63 | Save a PowerPoint presentation to a file. 64 | 65 | Args: 66 | presentation: The Presentation object 67 | file_path: Path where the file should be saved 68 | 69 | Returns: 70 | The file path where the presentation was saved 71 | """ 72 | presentation.save(file_path) 73 | return file_path 74 | 75 | 76 | def get_template_info(template_path: str) -> Dict: 77 | """ 78 | Get information about a template file. 79 | 80 | Args: 81 | template_path: Path to the template .pptx file 82 | 83 | Returns: 84 | Dictionary containing template information 85 | """ 86 | if not os.path.exists(template_path): 87 | raise FileNotFoundError(f"Template file not found: {template_path}") 88 | 89 | try: 90 | presentation = Presentation(template_path) 91 | 92 | # Get slide layouts 93 | layouts = get_slide_layouts(presentation) 94 | 95 | # Get core properties 96 | core_props = get_core_properties(presentation) 97 | 98 | # Get slide count 99 | slide_count = len(presentation.slides) 100 | 101 | # Get file size 102 | file_size = os.path.getsize(template_path) 103 | 104 | return { 105 | "template_path": template_path, 106 | "file_size_bytes": file_size, 107 | "slide_count": slide_count, 108 | "layout_count": len(layouts), 109 | "slide_layouts": layouts, 110 | "core_properties": core_props 111 | } 112 | except Exception as e: 113 | raise Exception(f"Failed to read template info from '{template_path}': {str(e)}") 114 | 115 | 116 | def get_presentation_info(presentation: Presentation) -> Dict: 117 | """ 118 | Get information about a presentation. 119 | 120 | Args: 121 | presentation: The Presentation object 122 | 123 | Returns: 124 | Dictionary containing presentation information 125 | """ 126 | try: 127 | # Get slide layouts 128 | layouts = get_slide_layouts(presentation) 129 | 130 | # Get core properties 131 | core_props = get_core_properties(presentation) 132 | 133 | # Get slide count 134 | slide_count = len(presentation.slides) 135 | 136 | return { 137 | "slide_count": slide_count, 138 | "layout_count": len(layouts), 139 | "slide_layouts": layouts, 140 | "core_properties": core_props, 141 | "slide_width": presentation.slide_width, 142 | "slide_height": presentation.slide_height 143 | } 144 | except Exception as e: 145 | raise Exception(f"Failed to get presentation info: {str(e)}") 146 | 147 | 148 | def get_slide_layouts(presentation: Presentation) -> List[Dict]: 149 | """ 150 | Get all available slide layouts in the presentation. 151 | 152 | Args: 153 | presentation: The Presentation object 154 | 155 | Returns: 156 | A list of dictionaries with layout information 157 | """ 158 | layouts = [] 159 | for i, layout in enumerate(presentation.slide_layouts): 160 | layout_info = { 161 | "index": i, 162 | "name": layout.name, 163 | "placeholder_count": len(layout.placeholders) 164 | } 165 | layouts.append(layout_info) 166 | return layouts 167 | 168 | 169 | def set_core_properties(presentation: Presentation, title: str = None, subject: str = None, 170 | author: str = None, keywords: str = None, comments: str = None) -> None: 171 | """ 172 | Set core document properties. 173 | 174 | Args: 175 | presentation: The Presentation object 176 | title: Document title 177 | subject: Document subject 178 | author: Document author 179 | keywords: Document keywords 180 | comments: Document comments 181 | """ 182 | core_props = presentation.core_properties 183 | 184 | if title is not None: 185 | core_props.title = title 186 | if subject is not None: 187 | core_props.subject = subject 188 | if author is not None: 189 | core_props.author = author 190 | if keywords is not None: 191 | core_props.keywords = keywords 192 | if comments is not None: 193 | core_props.comments = comments 194 | 195 | 196 | def get_core_properties(presentation: Presentation) -> Dict: 197 | """ 198 | Get core document properties. 199 | 200 | Args: 201 | presentation: The Presentation object 202 | 203 | Returns: 204 | Dictionary containing core properties 205 | """ 206 | core_props = presentation.core_properties 207 | 208 | return { 209 | "title": core_props.title, 210 | "subject": core_props.subject, 211 | "author": core_props.author, 212 | "keywords": core_props.keywords, 213 | "comments": core_props.comments, 214 | "created": core_props.created.isoformat() if core_props.created else None, 215 | "last_modified_by": core_props.last_modified_by, 216 | "modified": core_props.modified.isoformat() if core_props.modified else None 217 | } -------------------------------------------------------------------------------- /utils/validation_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Validation utilities for PowerPoint MCP Server. 3 | Functions for validating and fixing slide content, text fit, and layouts. 4 | """ 5 | from typing import Dict, List, Optional, Any 6 | 7 | 8 | def validate_text_fit(shape, text_content: str = None, font_size: int = 12) -> Dict: 9 | """ 10 | Validate if text content will fit in a shape container. 11 | 12 | Args: 13 | shape: The shape containing the text 14 | text_content: The text to validate (if None, uses existing text) 15 | font_size: The font size to check 16 | 17 | Returns: 18 | Dictionary with validation results and suggestions 19 | """ 20 | result = { 21 | 'fits': True, 22 | 'estimated_overflow': False, 23 | 'suggested_font_size': font_size, 24 | 'suggested_dimensions': None, 25 | 'warnings': [], 26 | 'needs_optimization': False 27 | } 28 | 29 | try: 30 | # Use existing text if not provided 31 | if text_content is None and hasattr(shape, 'text_frame'): 32 | text_content = shape.text_frame.text 33 | 34 | if not text_content: 35 | return result 36 | 37 | # Basic heuristic: estimate if text will overflow 38 | if hasattr(shape, 'width') and hasattr(shape, 'height'): 39 | # Rough estimation: average character width is about 0.6 * font_size 40 | avg_char_width = font_size * 0.6 41 | estimated_width = len(text_content) * avg_char_width 42 | 43 | # Convert shape dimensions to points (assuming they're in EMU) 44 | shape_width_pt = shape.width / 12700 # EMU to points conversion 45 | shape_height_pt = shape.height / 12700 46 | 47 | if estimated_width > shape_width_pt: 48 | result['fits'] = False 49 | result['estimated_overflow'] = True 50 | result['needs_optimization'] = True 51 | 52 | # Suggest smaller font size 53 | suggested_size = int((shape_width_pt / len(text_content)) * 0.8) 54 | result['suggested_font_size'] = max(suggested_size, 8) 55 | 56 | # Suggest larger dimensions 57 | result['suggested_dimensions'] = { 58 | 'width': estimated_width * 1.2, 59 | 'height': shape_height_pt 60 | } 61 | 62 | result['warnings'].append( 63 | f"Text may overflow. Consider font size {result['suggested_font_size']} " 64 | f"or increase width to {result['suggested_dimensions']['width']:.1f} points" 65 | ) 66 | 67 | # Check for very long lines that might cause formatting issues 68 | lines = text_content.split('\n') 69 | max_line_length = max(len(line) for line in lines) if lines else 0 70 | 71 | if max_line_length > 100: # Arbitrary threshold 72 | result['warnings'].append("Very long lines detected. Consider adding line breaks.") 73 | result['needs_optimization'] = True 74 | 75 | return result 76 | 77 | except Exception as e: 78 | result['fits'] = False 79 | result['error'] = str(e) 80 | return result 81 | 82 | 83 | def validate_and_fix_slide(slide, auto_fix: bool = True, min_font_size: int = 8, 84 | max_font_size: int = 72) -> Dict: 85 | """ 86 | Comprehensively validate and automatically fix slide content issues. 87 | 88 | Args: 89 | slide: The slide object to validate 90 | auto_fix: Whether to automatically apply fixes 91 | min_font_size: Minimum allowed font size 92 | max_font_size: Maximum allowed font size 93 | 94 | Returns: 95 | Dictionary with validation results and applied fixes 96 | """ 97 | result = { 98 | 'validation_passed': True, 99 | 'issues_found': [], 100 | 'fixes_applied': [], 101 | 'warnings': [], 102 | 'shapes_processed': 0, 103 | 'text_shapes_optimized': 0 104 | } 105 | 106 | try: 107 | shapes_with_text = [] 108 | 109 | # Find all shapes with text content 110 | for i, shape in enumerate(slide.shapes): 111 | result['shapes_processed'] += 1 112 | 113 | if hasattr(shape, 'text_frame') and shape.text_frame.text.strip(): 114 | shapes_with_text.append((i, shape)) 115 | 116 | # Validate each text shape 117 | for shape_index, shape in shapes_with_text: 118 | shape_name = f"Shape {shape_index}" 119 | 120 | # Validate text fit 121 | text_validation = validate_text_fit(shape, font_size=12) 122 | 123 | if not text_validation['fits'] or text_validation['needs_optimization']: 124 | issue = f"{shape_name}: Text may not fit properly" 125 | result['issues_found'].append(issue) 126 | result['validation_passed'] = False 127 | 128 | if auto_fix and text_validation['suggested_font_size']: 129 | try: 130 | # Apply suggested font size 131 | suggested_size = max(min_font_size, 132 | min(text_validation['suggested_font_size'], max_font_size)) 133 | 134 | # Apply font size to all runs in the text frame 135 | for paragraph in shape.text_frame.paragraphs: 136 | for run in paragraph.runs: 137 | if hasattr(run, 'font'): 138 | run.font.size = suggested_size * 12700 # Convert to EMU 139 | 140 | fix = f"{shape_name}: Adjusted font size to {suggested_size}pt" 141 | result['fixes_applied'].append(fix) 142 | result['text_shapes_optimized'] += 1 143 | 144 | except Exception as e: 145 | warning = f"{shape_name}: Could not auto-fix font size: {str(e)}" 146 | result['warnings'].append(warning) 147 | 148 | # Check for other potential issues 149 | if len(shape.text_frame.text) > 500: # Very long text 150 | result['warnings'].append(f"{shape_name}: Contains very long text (>500 chars)") 151 | 152 | # Check for empty paragraphs 153 | empty_paragraphs = sum(1 for p in shape.text_frame.paragraphs if not p.text.strip()) 154 | if empty_paragraphs > 2: 155 | result['warnings'].append(f"{shape_name}: Contains {empty_paragraphs} empty paragraphs") 156 | 157 | # Check slide-level issues 158 | if len(slide.shapes) > 20: 159 | result['warnings'].append("Slide contains many shapes (>20), may affect performance") 160 | 161 | # Summary 162 | if result['validation_passed']: 163 | result['summary'] = "Slide validation passed successfully" 164 | else: 165 | result['summary'] = f"Found {len(result['issues_found'])} issues" 166 | if auto_fix: 167 | result['summary'] += f", applied {len(result['fixes_applied'])} fixes" 168 | 169 | return result 170 | 171 | except Exception as e: 172 | result['validation_passed'] = False 173 | result['error'] = str(e) 174 | return result 175 | 176 | 177 | def validate_slide_layout(slide) -> Dict: 178 | """ 179 | Validate slide layout for common issues. 180 | 181 | Args: 182 | slide: The slide object 183 | 184 | Returns: 185 | Dictionary with layout validation results 186 | """ 187 | result = { 188 | 'layout_valid': True, 189 | 'issues': [], 190 | 'suggestions': [], 191 | 'shape_count': len(slide.shapes), 192 | 'overlapping_shapes': [] 193 | } 194 | 195 | try: 196 | shapes = list(slide.shapes) 197 | 198 | # Check for overlapping shapes 199 | for i, shape1 in enumerate(shapes): 200 | for j, shape2 in enumerate(shapes[i+1:], i+1): 201 | if shapes_overlap(shape1, shape2): 202 | result['overlapping_shapes'].append({ 203 | 'shape1_index': i, 204 | 'shape2_index': j, 205 | 'shape1_name': getattr(shape1, 'name', f'Shape {i}'), 206 | 'shape2_name': getattr(shape2, 'name', f'Shape {j}') 207 | }) 208 | 209 | if result['overlapping_shapes']: 210 | result['layout_valid'] = False 211 | result['issues'].append(f"Found {len(result['overlapping_shapes'])} overlapping shapes") 212 | result['suggestions'].append("Consider repositioning overlapping shapes") 213 | 214 | # Check for shapes outside slide boundaries 215 | slide_width = 10 * 914400 # Standard slide width in EMU 216 | slide_height = 7.5 * 914400 # Standard slide height in EMU 217 | 218 | shapes_outside = [] 219 | for i, shape in enumerate(shapes): 220 | if (shape.left < 0 or shape.top < 0 or 221 | shape.left + shape.width > slide_width or 222 | shape.top + shape.height > slide_height): 223 | shapes_outside.append(i) 224 | 225 | if shapes_outside: 226 | result['layout_valid'] = False 227 | result['issues'].append(f"Found {len(shapes_outside)} shapes outside slide boundaries") 228 | result['suggestions'].append("Reposition shapes to fit within slide boundaries") 229 | 230 | # Check shape spacing 231 | if len(shapes) > 1: 232 | min_spacing = check_minimum_spacing(shapes) 233 | if min_spacing < 0.1 * 914400: # Less than 0.1 inch spacing 234 | result['suggestions'].append("Consider increasing spacing between shapes") 235 | 236 | return result 237 | 238 | except Exception as e: 239 | result['layout_valid'] = False 240 | result['error'] = str(e) 241 | return result 242 | 243 | 244 | def shapes_overlap(shape1, shape2) -> bool: 245 | """ 246 | Check if two shapes overlap. 247 | 248 | Args: 249 | shape1: First shape 250 | shape2: Second shape 251 | 252 | Returns: 253 | True if shapes overlap, False otherwise 254 | """ 255 | try: 256 | # Get boundaries 257 | left1, top1 = shape1.left, shape1.top 258 | right1, bottom1 = left1 + shape1.width, top1 + shape1.height 259 | 260 | left2, top2 = shape2.left, shape2.top 261 | right2, bottom2 = left2 + shape2.width, top2 + shape2.height 262 | 263 | # Check for overlap 264 | return not (right1 <= left2 or right2 <= left1 or bottom1 <= top2 or bottom2 <= top1) 265 | except: 266 | return False 267 | 268 | 269 | def check_minimum_spacing(shapes: List) -> float: 270 | """ 271 | Check minimum spacing between shapes. 272 | 273 | Args: 274 | shapes: List of shapes 275 | 276 | Returns: 277 | Minimum spacing found between shapes (in EMU) 278 | """ 279 | min_spacing = float('inf') 280 | 281 | try: 282 | for i, shape1 in enumerate(shapes): 283 | for shape2 in shapes[i+1:]: 284 | # Calculate distance between shape edges 285 | distance = calculate_shape_distance(shape1, shape2) 286 | min_spacing = min(min_spacing, distance) 287 | 288 | return min_spacing if min_spacing != float('inf') else 0 289 | except: 290 | return 0 291 | 292 | 293 | def calculate_shape_distance(shape1, shape2) -> float: 294 | """ 295 | Calculate distance between two shapes. 296 | 297 | Args: 298 | shape1: First shape 299 | shape2: Second shape 300 | 301 | Returns: 302 | Distance between shape edges (in EMU) 303 | """ 304 | try: 305 | # Get centers 306 | center1_x = shape1.left + shape1.width / 2 307 | center1_y = shape1.top + shape1.height / 2 308 | 309 | center2_x = shape2.left + shape2.width / 2 310 | center2_y = shape2.top + shape2.height / 2 311 | 312 | # Calculate center-to-center distance 313 | dx = abs(center2_x - center1_x) 314 | dy = abs(center2_y - center1_y) 315 | 316 | # Subtract half-widths and half-heights to get edge distance 317 | edge_distance_x = max(0, dx - (shape1.width + shape2.width) / 2) 318 | edge_distance_y = max(0, dy - (shape1.height + shape2.height) / 2) 319 | 320 | # Return minimum edge distance 321 | return min(edge_distance_x, edge_distance_y) 322 | except: 323 | return 0 --------------------------------------------------------------------------------