mcpcn-office-powerpoint-mcp-server 2.1.1__py3-none-any.whl → 2.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ppt_mcp_server.py CHANGED
@@ -1,474 +1,474 @@
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
- import functools
11
- import inspect
12
-
13
- # import utils # Currently unused
14
- from tools import (
15
- register_presentation_tools,
16
- register_content_tools,
17
- register_structural_tools,
18
- register_professional_tools,
19
- register_template_tools,
20
- register_hyperlink_tools,
21
- register_chart_tools,
22
- register_connector_tools,
23
- register_master_tools,
24
- register_transition_tools
25
- )
26
-
27
- # Initialize the FastMCP server
28
- app = FastMCP(
29
- name="ppt-mcp-server"
30
- )
31
-
32
- # Global response wrapper: if a tool returns {"error": ...}, raise to mark outer isError=true
33
- _original_app_tool = app.tool
34
- def _tool_with_error_promotion(*t_args, **t_kwargs):
35
- def _decorator(func):
36
- @functools.wraps(func)
37
- def _wrapped(*args, **kwargs):
38
- result = func(*args, **kwargs)
39
- # Promote dict-with-error to exception so outer layer sets isError=true
40
- if isinstance(result, dict) and "error" in result and result["error"]:
41
- raise RuntimeError(str(result["error"]))
42
- return result
43
- # Preserve original callable signature for FastMCP parameter binding
44
- try:
45
- _wrapped.__signature__ = inspect.signature(func)
46
- except Exception:
47
- pass
48
- return _original_app_tool(*t_args, **t_kwargs)(_wrapped)
49
- return _decorator
50
-
51
- # Monkey patch app.tool before any registrations
52
- app.tool = _tool_with_error_promotion
53
-
54
- # Global state to store presentations in memory
55
- presentations = {}
56
- current_presentation_id = None
57
-
58
- # Template configuration
59
- def get_template_search_directories():
60
- """
61
- Get list of directories to search for templates.
62
- Uses environment variable PPT_TEMPLATE_PATH if set, otherwise uses default directories.
63
-
64
- Returns:
65
- List of directories to search for templates
66
- """
67
- template_env_path = os.environ.get('PPT_TEMPLATE_PATH')
68
-
69
- if template_env_path:
70
- # If environment variable is set, use it as the primary template directory
71
- # Support multiple paths separated by colon (Unix) or semicolon (Windows)
72
- import platform
73
- separator = ';' if platform.system() == "Windows" else ':'
74
- env_dirs = [path.strip() for path in template_env_path.split(separator) if path.strip()]
75
-
76
- # Verify that the directories exist
77
- valid_env_dirs = []
78
- for dir_path in env_dirs:
79
- expanded_path = os.path.expanduser(dir_path)
80
- if os.path.exists(expanded_path) and os.path.isdir(expanded_path):
81
- valid_env_dirs.append(expanded_path)
82
-
83
- if valid_env_dirs:
84
- # Add default fallback directories
85
- return valid_env_dirs + ['.', './templates', './assets', './resources']
86
- else:
87
- print(f"Warning: PPT_TEMPLATE_PATH directories not found: {template_env_path}")
88
-
89
- # Default search directories when no environment variable or invalid paths
90
- return ['.', './templates', './assets', './resources']
91
-
92
- # ---- Helper Functions ----
93
-
94
- def get_current_presentation():
95
- """Get the current presentation object or raise an error if none is loaded."""
96
- if current_presentation_id is None or current_presentation_id not in presentations:
97
- raise ValueError("No presentation is currently loaded. Please create or open a presentation first.")
98
- return presentations[current_presentation_id]
99
-
100
- def get_current_presentation_id():
101
- """Get the current presentation ID."""
102
- return current_presentation_id
103
-
104
- def set_current_presentation_id(pres_id):
105
- """Set the current presentation ID."""
106
- global current_presentation_id
107
- current_presentation_id = pres_id
108
-
109
- def validate_parameters(params):
110
- """
111
- Validate parameters against constraints.
112
-
113
- Args:
114
- params: Dictionary of parameter name: (value, constraints) pairs
115
-
116
- Returns:
117
- (True, None) if all valid, or (False, error_message) if invalid
118
- """
119
- for param_name, (value, constraints) in params.items():
120
- for constraint_func, error_msg in constraints:
121
- if not constraint_func(value):
122
- return False, f"Parameter '{param_name}': {error_msg}"
123
- return True, None
124
-
125
- def is_positive(value):
126
- """Check if a value is positive."""
127
- return value > 0
128
-
129
- def is_non_negative(value):
130
- """Check if a value is non-negative."""
131
- return value >= 0
132
-
133
- def is_in_range(min_val, max_val):
134
- """Create a function that checks if a value is in a range."""
135
- return lambda x: min_val <= x <= max_val
136
-
137
- def is_in_list(valid_list):
138
- """Create a function that checks if a value is in a list."""
139
- return lambda x: x in valid_list
140
-
141
- def is_valid_rgb(color_list):
142
- """Check if a color list is a valid RGB tuple."""
143
- if not isinstance(color_list, list) or len(color_list) != 3:
144
- return False
145
- return all(isinstance(c, int) and 0 <= c <= 255 for c in color_list)
146
-
147
- def add_shape_direct(slide, shape_type: str, left: float, top: float, width: float, height: float) -> Any:
148
- """
149
- Add an auto shape to a slide using direct integer values instead of enum objects.
150
-
151
- This implementation provides a reliable alternative that bypasses potential
152
- enum-related issues in the python-pptx library.
153
-
154
- Args:
155
- slide: The slide object
156
- shape_type: Shape type string (e.g., 'rectangle', 'oval', 'triangle')
157
- left: Left position in inches
158
- top: Top position in inches
159
- width: Width in inches
160
- height: Height in inches
161
-
162
- Returns:
163
- The created shape
164
- """
165
- from pptx.util import Inches
166
-
167
- # Direct mapping of shape types to their integer values
168
- # These values are directly from the MS Office VBA documentation
169
- shape_type_map = {
170
- 'rectangle': 1,
171
- 'rounded_rectangle': 2,
172
- 'oval': 9,
173
- 'diamond': 4,
174
- 'triangle': 5, # This is ISOSCELES_TRIANGLE
175
- 'right_triangle': 6,
176
- 'pentagon': 56,
177
- 'hexagon': 10,
178
- 'heptagon': 11,
179
- 'octagon': 12,
180
- 'star': 12, # This is STAR_5_POINTS (value 12)
181
- 'arrow': 13,
182
- 'cloud': 35,
183
- 'heart': 21,
184
- 'lightning_bolt': 22,
185
- 'sun': 23,
186
- 'moon': 24,
187
- 'smiley_face': 17,
188
- 'no_symbol': 19,
189
- 'flowchart_process': 112,
190
- 'flowchart_decision': 114,
191
- 'flowchart_data': 115,
192
- 'flowchart_document': 119
193
- }
194
-
195
- # Check if shape type is valid before trying to use it
196
- shape_type_lower = str(shape_type).lower()
197
- if shape_type_lower not in shape_type_map:
198
- available_shapes = ', '.join(sorted(shape_type_map.keys()))
199
- raise ValueError(f"Unsupported shape type: '{shape_type}'. Available shape types: {available_shapes}")
200
-
201
- # Get the integer value for the shape type
202
- shape_value = shape_type_map[shape_type_lower]
203
-
204
- # Create the shape using the direct integer value
205
- try:
206
- # The integer value is passed directly to add_shape
207
- shape = slide.shapes.add_shape(
208
- shape_value, Inches(left), Inches(top), Inches(width), Inches(height)
209
- )
210
- return shape
211
- except Exception as e:
212
- raise ValueError(f"Failed to create '{shape_type}' shape using direct value {shape_value}: {str(e)}")
213
-
214
- # ---- Custom presentation management wrapper ----
215
-
216
- class PresentationManager:
217
- """Wrapper to handle presentation state updates."""
218
-
219
- def __init__(self, presentations_dict):
220
- self.presentations = presentations_dict
221
-
222
- def store_presentation(self, pres, pres_id):
223
- """Store a presentation and set it as current."""
224
- self.presentations[pres_id] = pres
225
- set_current_presentation_id(pres_id)
226
- return pres_id
227
-
228
- # ---- Register Tools ----
229
-
230
- # Create presentation manager wrapper
231
- presentation_manager = PresentationManager(presentations)
232
-
233
- # Wrapper functions to handle state management
234
- def create_presentation_wrapper(original_func):
235
- """Wrapper to handle presentation creation with state management."""
236
- def wrapper(*args, **kwargs):
237
- result = original_func(*args, **kwargs)
238
- if "presentation_id" in result and result["presentation_id"] in presentations:
239
- set_current_presentation_id(result["presentation_id"])
240
- return result
241
- return wrapper
242
-
243
- def open_presentation_wrapper(original_func):
244
- """Wrapper to handle presentation opening with state management."""
245
- def wrapper(*args, **kwargs):
246
- result = original_func(*args, **kwargs)
247
- if "presentation_id" in result and result["presentation_id"] in presentations:
248
- set_current_presentation_id(result["presentation_id"])
249
- return result
250
- return wrapper
251
-
252
- # Register all tool modules
253
- register_presentation_tools(
254
- app,
255
- presentations,
256
- get_current_presentation_id,
257
- get_template_search_directories
258
- )
259
-
260
- register_content_tools(
261
- app,
262
- presentations,
263
- get_current_presentation_id,
264
- validate_parameters,
265
- is_positive,
266
- is_non_negative,
267
- is_in_range,
268
- is_valid_rgb
269
- )
270
-
271
- register_structural_tools(
272
- app,
273
- presentations,
274
- get_current_presentation_id,
275
- validate_parameters,
276
- is_positive,
277
- is_non_negative,
278
- is_in_range,
279
- is_valid_rgb,
280
- add_shape_direct
281
- )
282
-
283
- register_professional_tools(
284
- app,
285
- presentations,
286
- get_current_presentation_id
287
- )
288
-
289
- register_template_tools(
290
- app,
291
- presentations,
292
- get_current_presentation_id
293
- )
294
-
295
- register_hyperlink_tools(
296
- app,
297
- presentations,
298
- get_current_presentation_id,
299
- validate_parameters,
300
- is_positive,
301
- is_non_negative,
302
- is_in_range,
303
- is_valid_rgb
304
- )
305
-
306
- register_chart_tools(
307
- app,
308
- presentations,
309
- get_current_presentation_id,
310
- validate_parameters,
311
- is_positive,
312
- is_non_negative,
313
- is_in_range,
314
- is_valid_rgb
315
- )
316
-
317
-
318
- register_connector_tools(
319
- app,
320
- presentations,
321
- get_current_presentation_id,
322
- validate_parameters,
323
- is_positive,
324
- is_non_negative,
325
- is_in_range,
326
- is_valid_rgb
327
- )
328
-
329
- register_master_tools(
330
- app,
331
- presentations,
332
- get_current_presentation_id,
333
- validate_parameters,
334
- is_positive,
335
- is_non_negative,
336
- is_in_range,
337
- is_valid_rgb
338
- )
339
-
340
- register_transition_tools(
341
- app,
342
- presentations,
343
- get_current_presentation_id,
344
- validate_parameters,
345
- is_positive,
346
- is_non_negative,
347
- is_in_range,
348
- is_valid_rgb
349
- )
350
-
351
-
352
- # ---- Additional Utility Tools ----
353
-
354
- @app.tool()
355
- def list_presentations() -> Dict:
356
- """List all loaded presentations."""
357
- return {
358
- "presentations": [
359
- {
360
- "id": pres_id,
361
- "slide_count": len(pres.slides),
362
- "is_current": pres_id == current_presentation_id
363
- }
364
- for pres_id, pres in presentations.items()
365
- ],
366
- "current_presentation_id": current_presentation_id,
367
- "total_presentations": len(presentations)
368
- }
369
-
370
- @app.tool()
371
- def switch_presentation(presentation_id: str) -> Dict:
372
- """Switch to a different loaded presentation."""
373
- if presentation_id not in presentations:
374
- return {
375
- "error": f"Presentation '{presentation_id}' not found. Available presentations: {list(presentations.keys())}"
376
- }
377
-
378
- global current_presentation_id
379
- old_id = current_presentation_id
380
- current_presentation_id = presentation_id
381
-
382
- return {
383
- "message": f"Switched from presentation '{old_id}' to '{presentation_id}'",
384
- "previous_presentation_id": old_id,
385
- "current_presentation_id": current_presentation_id
386
- }
387
-
388
- @app.tool()
389
- def get_server_info() -> Dict:
390
- """Get information about the MCP server."""
391
- return {
392
- "name": "PowerPoint MCP Server - Enhanced Edition",
393
- "version": "2.1.0",
394
- "total_tools": 32, # Organized into 11 specialized modules
395
- "loaded_presentations": len(presentations),
396
- "current_presentation": current_presentation_id,
397
- "features": [
398
- "Presentation Management (7 tools)",
399
- "Content Management (6 tools)",
400
- "Template Operations (7 tools)",
401
- "Structural Elements (4 tools)",
402
- "Professional Design (3 tools)",
403
- "Specialized Features (5 tools)"
404
- ],
405
- "improvements": [
406
- "32 specialized tools organized into 11 focused modules",
407
- "68+ utility functions across 7 organized utility modules",
408
- "Enhanced parameter handling and validation",
409
- "Unified operation interfaces with comprehensive coverage",
410
- "Advanced template system with auto-generation capabilities",
411
- "Professional design tools with multiple effects and styling",
412
- "Specialized features including hyperlinks, connectors, slide masters",
413
- "Dynamic text sizing and intelligent wrapping",
414
- "Advanced visual effects and styling",
415
- "Content-aware optimization and validation",
416
- "Complete PowerPoint lifecycle management",
417
- "Modular architecture for better maintainability"
418
- ],
419
- "new_enhanced_features": [
420
- "Hyperlink Management - Add, update, remove, and list hyperlinks in text",
421
- "Advanced Chart Data Updates - Replace chart data with new categories and series",
422
- "Advanced Text Run Formatting - Apply formatting to specific text runs",
423
- "Shape Connectors - Add connector lines and arrows between points",
424
- "Slide Master Management - Access and manage slide masters and layouts",
425
- "Slide Transitions - Basic transition management (placeholder for future)"
426
- ]
427
- }
428
-
429
- # ---- Main Function ----
430
- def main(transport: str = "stdio", port: int = 8000):
431
- if transport == "http":
432
- import asyncio
433
- # Set the port for HTTP transport
434
- app.settings.port = port
435
- # Start the FastMCP server with HTTP transport
436
- try:
437
- app.run(transport='streamable-http')
438
- except asyncio.exceptions.CancelledError:
439
- print("Server stopped by user.")
440
- except KeyboardInterrupt:
441
- print("Server stopped by user.")
442
- except Exception as e:
443
- print(f"Error starting server: {e}")
444
-
445
- elif transport == "sse":
446
- # Run the FastMCP server in SSE (Server Side Events) mode
447
- app.run(transport='sse')
448
-
449
- else:
450
- # Run the FastMCP server
451
- app.run(transport='stdio')
452
-
453
- if __name__ == "__main__":
454
- # Parse command line arguments
455
- parser = argparse.ArgumentParser(description="MCP Server for PowerPoint manipulation using python-pptx")
456
-
457
- parser.add_argument(
458
- "-t",
459
- "--transport",
460
- type=str,
461
- default="stdio",
462
- choices=["stdio", "http", "sse"],
463
- help="Transport method for the MCP server (default: stdio)"
464
- )
465
-
466
- parser.add_argument(
467
- "-p",
468
- "--port",
469
- type=int,
470
- default=8000,
471
- help="Port to run the MCP server on (default: 8000)"
472
- )
473
- args = parser.parse_args()
474
- main(args.transport, args.port)
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
+ import functools
11
+ import inspect
12
+
13
+ # import utils # Currently unused
14
+ from tools import (
15
+ register_presentation_tools,
16
+ register_content_tools,
17
+ register_structural_tools,
18
+ register_professional_tools,
19
+ register_template_tools,
20
+ register_hyperlink_tools,
21
+ register_chart_tools,
22
+ register_connector_tools,
23
+ register_master_tools,
24
+ register_transition_tools
25
+ )
26
+
27
+ # Initialize the FastMCP server
28
+ app = FastMCP(
29
+ name="ppt-mcp-server"
30
+ )
31
+
32
+ # Global response wrapper: if a tool returns {"error": ...}, raise to mark outer isError=true
33
+ _original_app_tool = app.tool
34
+ def _tool_with_error_promotion(*t_args, **t_kwargs):
35
+ def _decorator(func):
36
+ @functools.wraps(func)
37
+ def _wrapped(*args, **kwargs):
38
+ result = func(*args, **kwargs)
39
+ # Promote dict-with-error to exception so outer layer sets isError=true
40
+ if isinstance(result, dict) and "error" in result and result["error"]:
41
+ raise RuntimeError(str(result["error"]))
42
+ return result
43
+ # Preserve original callable signature for FastMCP parameter binding
44
+ try:
45
+ _wrapped.__signature__ = inspect.signature(func)
46
+ except Exception:
47
+ pass
48
+ return _original_app_tool(*t_args, **t_kwargs)(_wrapped)
49
+ return _decorator
50
+
51
+ # Monkey patch app.tool before any registrations
52
+ app.tool = _tool_with_error_promotion
53
+
54
+ # Global state to store presentations in memory
55
+ presentations = {}
56
+ current_presentation_id = None
57
+
58
+ # Template configuration
59
+ def get_template_search_directories():
60
+ """
61
+ Get list of directories to search for templates.
62
+ Uses environment variable PPT_TEMPLATE_PATH if set, otherwise uses default directories.
63
+
64
+ Returns:
65
+ List of directories to search for templates
66
+ """
67
+ template_env_path = os.environ.get('PPT_TEMPLATE_PATH')
68
+
69
+ if template_env_path:
70
+ # If environment variable is set, use it as the primary template directory
71
+ # Support multiple paths separated by colon (Unix) or semicolon (Windows)
72
+ import platform
73
+ separator = ';' if platform.system() == "Windows" else ':'
74
+ env_dirs = [path.strip() for path in template_env_path.split(separator) if path.strip()]
75
+
76
+ # Verify that the directories exist
77
+ valid_env_dirs = []
78
+ for dir_path in env_dirs:
79
+ expanded_path = os.path.expanduser(dir_path)
80
+ if os.path.exists(expanded_path) and os.path.isdir(expanded_path):
81
+ valid_env_dirs.append(expanded_path)
82
+
83
+ if valid_env_dirs:
84
+ # Add default fallback directories
85
+ return valid_env_dirs + ['.', './templates', './assets', './resources']
86
+ else:
87
+ print(f"Warning: PPT_TEMPLATE_PATH directories not found: {template_env_path}")
88
+
89
+ # Default search directories when no environment variable or invalid paths
90
+ return ['.', './templates', './assets', './resources']
91
+
92
+ # ---- Helper Functions ----
93
+
94
+ def get_current_presentation():
95
+ """Get the current presentation object or raise an error if none is loaded."""
96
+ if current_presentation_id is None or current_presentation_id not in presentations:
97
+ raise ValueError("No presentation is currently loaded. Please create or open a presentation first.")
98
+ return presentations[current_presentation_id]
99
+
100
+ def get_current_presentation_id():
101
+ """Get the current presentation ID."""
102
+ return current_presentation_id
103
+
104
+ def set_current_presentation_id(pres_id):
105
+ """Set the current presentation ID."""
106
+ global current_presentation_id
107
+ current_presentation_id = pres_id
108
+
109
+ def validate_parameters(params):
110
+ """
111
+ Validate parameters against constraints.
112
+
113
+ Args:
114
+ params: Dictionary of parameter name: (value, constraints) pairs
115
+
116
+ Returns:
117
+ (True, None) if all valid, or (False, error_message) if invalid
118
+ """
119
+ for param_name, (value, constraints) in params.items():
120
+ for constraint_func, error_msg in constraints:
121
+ if not constraint_func(value):
122
+ return False, f"Parameter '{param_name}': {error_msg}"
123
+ return True, None
124
+
125
+ def is_positive(value):
126
+ """Check if a value is positive."""
127
+ return value > 0
128
+
129
+ def is_non_negative(value):
130
+ """Check if a value is non-negative."""
131
+ return value >= 0
132
+
133
+ def is_in_range(min_val, max_val):
134
+ """Create a function that checks if a value is in a range."""
135
+ return lambda x: min_val <= x <= max_val
136
+
137
+ def is_in_list(valid_list):
138
+ """Create a function that checks if a value is in a list."""
139
+ return lambda x: x in valid_list
140
+
141
+ def is_valid_rgb(color_list):
142
+ """Check if a color list is a valid RGB tuple."""
143
+ if not isinstance(color_list, list) or len(color_list) != 3:
144
+ return False
145
+ return all(isinstance(c, int) and 0 <= c <= 255 for c in color_list)
146
+
147
+ def add_shape_direct(slide, shape_type: str, left: float, top: float, width: float, height: float) -> Any:
148
+ """
149
+ Add an auto shape to a slide using direct integer values instead of enum objects.
150
+
151
+ This implementation provides a reliable alternative that bypasses potential
152
+ enum-related issues in the python-pptx library.
153
+
154
+ Args:
155
+ slide: The slide object
156
+ shape_type: Shape type string (e.g., 'rectangle', 'oval', 'triangle')
157
+ left: Left position in inches
158
+ top: Top position in inches
159
+ width: Width in inches
160
+ height: Height in inches
161
+
162
+ Returns:
163
+ The created shape
164
+ """
165
+ from pptx.util import Inches
166
+
167
+ # Direct mapping of shape types to their integer values
168
+ # These values are directly from the MS Office VBA documentation
169
+ shape_type_map = {
170
+ 'rectangle': 1,
171
+ 'rounded_rectangle': 2,
172
+ 'oval': 9,
173
+ 'diamond': 4,
174
+ 'triangle': 5, # This is ISOSCELES_TRIANGLE
175
+ 'right_triangle': 6,
176
+ 'pentagon': 56,
177
+ 'hexagon': 10,
178
+ 'heptagon': 11,
179
+ 'octagon': 12,
180
+ 'star': 12, # This is STAR_5_POINTS (value 12)
181
+ 'arrow': 13,
182
+ 'cloud': 35,
183
+ 'heart': 21,
184
+ 'lightning_bolt': 22,
185
+ 'sun': 23,
186
+ 'moon': 24,
187
+ 'smiley_face': 17,
188
+ 'no_symbol': 19,
189
+ 'flowchart_process': 112,
190
+ 'flowchart_decision': 114,
191
+ 'flowchart_data': 115,
192
+ 'flowchart_document': 119
193
+ }
194
+
195
+ # Check if shape type is valid before trying to use it
196
+ shape_type_lower = str(shape_type).lower()
197
+ if shape_type_lower not in shape_type_map:
198
+ available_shapes = ', '.join(sorted(shape_type_map.keys()))
199
+ raise ValueError(f"Unsupported shape type: '{shape_type}'. Available shape types: {available_shapes}")
200
+
201
+ # Get the integer value for the shape type
202
+ shape_value = shape_type_map[shape_type_lower]
203
+
204
+ # Create the shape using the direct integer value
205
+ try:
206
+ # The integer value is passed directly to add_shape
207
+ shape = slide.shapes.add_shape(
208
+ shape_value, Inches(left), Inches(top), Inches(width), Inches(height)
209
+ )
210
+ return shape
211
+ except Exception as e:
212
+ raise ValueError(f"Failed to create '{shape_type}' shape using direct value {shape_value}: {str(e)}")
213
+
214
+ # ---- Custom presentation management wrapper ----
215
+
216
+ class PresentationManager:
217
+ """Wrapper to handle presentation state updates."""
218
+
219
+ def __init__(self, presentations_dict):
220
+ self.presentations = presentations_dict
221
+
222
+ def store_presentation(self, pres, pres_id):
223
+ """Store a presentation and set it as current."""
224
+ self.presentations[pres_id] = pres
225
+ set_current_presentation_id(pres_id)
226
+ return pres_id
227
+
228
+ # ---- Register Tools ----
229
+
230
+ # Create presentation manager wrapper
231
+ presentation_manager = PresentationManager(presentations)
232
+
233
+ # Wrapper functions to handle state management
234
+ def create_presentation_wrapper(original_func):
235
+ """Wrapper to handle presentation creation with state management."""
236
+ def wrapper(*args, **kwargs):
237
+ result = original_func(*args, **kwargs)
238
+ if "presentation_id" in result and result["presentation_id"] in presentations:
239
+ set_current_presentation_id(result["presentation_id"])
240
+ return result
241
+ return wrapper
242
+
243
+ def open_presentation_wrapper(original_func):
244
+ """Wrapper to handle presentation opening with state management."""
245
+ def wrapper(*args, **kwargs):
246
+ result = original_func(*args, **kwargs)
247
+ if "presentation_id" in result and result["presentation_id"] in presentations:
248
+ set_current_presentation_id(result["presentation_id"])
249
+ return result
250
+ return wrapper
251
+
252
+ # Register all tool modules
253
+ register_presentation_tools(
254
+ app,
255
+ presentations,
256
+ get_current_presentation_id,
257
+ get_template_search_directories
258
+ )
259
+
260
+ register_content_tools(
261
+ app,
262
+ presentations,
263
+ get_current_presentation_id,
264
+ validate_parameters,
265
+ is_positive,
266
+ is_non_negative,
267
+ is_in_range,
268
+ is_valid_rgb
269
+ )
270
+
271
+ register_structural_tools(
272
+ app,
273
+ presentations,
274
+ get_current_presentation_id,
275
+ validate_parameters,
276
+ is_positive,
277
+ is_non_negative,
278
+ is_in_range,
279
+ is_valid_rgb,
280
+ add_shape_direct
281
+ )
282
+
283
+ register_professional_tools(
284
+ app,
285
+ presentations,
286
+ get_current_presentation_id
287
+ )
288
+
289
+ register_template_tools(
290
+ app,
291
+ presentations,
292
+ get_current_presentation_id
293
+ )
294
+
295
+ register_hyperlink_tools(
296
+ app,
297
+ presentations,
298
+ get_current_presentation_id,
299
+ validate_parameters,
300
+ is_positive,
301
+ is_non_negative,
302
+ is_in_range,
303
+ is_valid_rgb
304
+ )
305
+
306
+ register_chart_tools(
307
+ app,
308
+ presentations,
309
+ get_current_presentation_id,
310
+ validate_parameters,
311
+ is_positive,
312
+ is_non_negative,
313
+ is_in_range,
314
+ is_valid_rgb
315
+ )
316
+
317
+
318
+ register_connector_tools(
319
+ app,
320
+ presentations,
321
+ get_current_presentation_id,
322
+ validate_parameters,
323
+ is_positive,
324
+ is_non_negative,
325
+ is_in_range,
326
+ is_valid_rgb
327
+ )
328
+
329
+ register_master_tools(
330
+ app,
331
+ presentations,
332
+ get_current_presentation_id,
333
+ validate_parameters,
334
+ is_positive,
335
+ is_non_negative,
336
+ is_in_range,
337
+ is_valid_rgb
338
+ )
339
+
340
+ register_transition_tools(
341
+ app,
342
+ presentations,
343
+ get_current_presentation_id,
344
+ validate_parameters,
345
+ is_positive,
346
+ is_non_negative,
347
+ is_in_range,
348
+ is_valid_rgb
349
+ )
350
+
351
+
352
+ # ---- Additional Utility Tools ----
353
+
354
+ @app.tool()
355
+ def list_presentations() -> Dict:
356
+ """List all loaded presentations."""
357
+ return {
358
+ "presentations": [
359
+ {
360
+ "id": pres_id,
361
+ "slide_count": len(pres.slides),
362
+ "is_current": pres_id == current_presentation_id
363
+ }
364
+ for pres_id, pres in presentations.items()
365
+ ],
366
+ "current_presentation_id": current_presentation_id,
367
+ "total_presentations": len(presentations)
368
+ }
369
+
370
+ @app.tool()
371
+ def switch_presentation(presentation_id: str) -> Dict:
372
+ """Switch to a different loaded presentation."""
373
+ if presentation_id not in presentations:
374
+ return {
375
+ "error": f"Presentation '{presentation_id}' not found. Available presentations: {list(presentations.keys())}"
376
+ }
377
+
378
+ global current_presentation_id
379
+ old_id = current_presentation_id
380
+ current_presentation_id = presentation_id
381
+
382
+ return {
383
+ "message": f"Switched from presentation '{old_id}' to '{presentation_id}'",
384
+ "previous_presentation_id": old_id,
385
+ "current_presentation_id": current_presentation_id
386
+ }
387
+
388
+ @app.tool()
389
+ def get_server_info() -> Dict:
390
+ """Get information about the MCP server."""
391
+ return {
392
+ "name": "PowerPoint MCP Server - Enhanced Edition",
393
+ "version": "2.1.0",
394
+ "total_tools": 32, # Organized into 11 specialized modules
395
+ "loaded_presentations": len(presentations),
396
+ "current_presentation": current_presentation_id,
397
+ "features": [
398
+ "Presentation Management (7 tools)",
399
+ "Content Management (6 tools)",
400
+ "Template Operations (7 tools)",
401
+ "Structural Elements (4 tools)",
402
+ "Professional Design (3 tools)",
403
+ "Specialized Features (5 tools)"
404
+ ],
405
+ "improvements": [
406
+ "32 specialized tools organized into 11 focused modules",
407
+ "68+ utility functions across 7 organized utility modules",
408
+ "Enhanced parameter handling and validation",
409
+ "Unified operation interfaces with comprehensive coverage",
410
+ "Advanced template system with auto-generation capabilities",
411
+ "Professional design tools with multiple effects and styling",
412
+ "Specialized features including hyperlinks, connectors, slide masters",
413
+ "Dynamic text sizing and intelligent wrapping",
414
+ "Advanced visual effects and styling",
415
+ "Content-aware optimization and validation",
416
+ "Complete PowerPoint lifecycle management",
417
+ "Modular architecture for better maintainability"
418
+ ],
419
+ "new_enhanced_features": [
420
+ "Hyperlink Management - Add, update, remove, and list hyperlinks in text",
421
+ "Advanced Chart Data Updates - Replace chart data with new categories and series",
422
+ "Advanced Text Run Formatting - Apply formatting to specific text runs",
423
+ "Shape Connectors - Add connector lines and arrows between points",
424
+ "Slide Master Management - Access and manage slide masters and layouts",
425
+ "Slide Transitions - Basic transition management (placeholder for future)"
426
+ ]
427
+ }
428
+
429
+ # ---- Main Function ----
430
+ def main(transport: str = "stdio", port: int = 8000):
431
+ if transport == "http":
432
+ import asyncio
433
+ # Set the port for HTTP transport
434
+ app.settings.port = port
435
+ # Start the FastMCP server with HTTP transport
436
+ try:
437
+ app.run(transport='streamable-http')
438
+ except asyncio.exceptions.CancelledError:
439
+ print("Server stopped by user.")
440
+ except KeyboardInterrupt:
441
+ print("Server stopped by user.")
442
+ except Exception as e:
443
+ print(f"Error starting server: {e}")
444
+
445
+ elif transport == "sse":
446
+ # Run the FastMCP server in SSE (Server Side Events) mode
447
+ app.run(transport='sse')
448
+
449
+ else:
450
+ # Run the FastMCP server
451
+ app.run(transport='stdio')
452
+
453
+ if __name__ == "__main__":
454
+ # Parse command line arguments
455
+ parser = argparse.ArgumentParser(description="MCP Server for PowerPoint manipulation using python-pptx")
456
+
457
+ parser.add_argument(
458
+ "-t",
459
+ "--transport",
460
+ type=str,
461
+ default="stdio",
462
+ choices=["stdio", "http", "sse"],
463
+ help="Transport method for the MCP server (default: stdio)"
464
+ )
465
+
466
+ parser.add_argument(
467
+ "-p",
468
+ "--port",
469
+ type=int,
470
+ default=8000,
471
+ help="Port to run the MCP server on (default: 8000)"
472
+ )
473
+ args = parser.parse_args()
474
+ main(args.transport, args.port)