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.
- {mcpcn_office_powerpoint_mcp_server-2.1.1.dist-info → mcpcn_office_powerpoint_mcp_server-2.1.2.dist-info}/METADATA +82 -30
- mcpcn_office_powerpoint_mcp_server-2.1.2.dist-info/RECORD +25 -0
- {mcpcn_office_powerpoint_mcp_server-2.1.1.dist-info → mcpcn_office_powerpoint_mcp_server-2.1.2.dist-info}/WHEEL +1 -1
- {mcpcn_office_powerpoint_mcp_server-2.1.1.dist-info → mcpcn_office_powerpoint_mcp_server-2.1.2.dist-info}/licenses/LICENSE +20 -20
- ppt_mcp_server.py +474 -474
- slide_layout_templates.json +3689 -3689
- tools/__init__.py +27 -27
- tools/chart_tools.py +81 -81
- tools/connector_tools.py +90 -90
- tools/content_tools.py +966 -778
- tools/hyperlink_tools.py +137 -137
- tools/master_tools.py +113 -113
- tools/presentation_tools.py +211 -211
- tools/professional_tools.py +289 -289
- tools/structural_tools.py +372 -372
- tools/template_tools.py +520 -520
- tools/transition_tools.py +74 -74
- utils/__init__.py +69 -68
- utils/content_utils.py +633 -578
- utils/core_utils.py +54 -54
- utils/design_utils.py +688 -688
- utils/presentation_utils.py +216 -216
- utils/template_utils.py +1142 -1142
- utils/validation_utils.py +322 -322
- mcpcn_office_powerpoint_mcp_server-2.1.1.dist-info/RECORD +0 -25
- {mcpcn_office_powerpoint_mcp_server-2.1.1.dist-info → mcpcn_office_powerpoint_mcp_server-2.1.2.dist-info}/entry_points.txt +0 -0
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)
|