iflow-mcp-jenstangen1-pptx 0.1.0__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.
- .gitignore +34 -0
- PKG-INFO +359 -0
- README.md +344 -0
- iflow_mcp_jenstangen1_pptx-0.1.0.dist-info/METADATA +359 -0
- iflow_mcp_jenstangen1_pptx-0.1.0.dist-info/RECORD +18 -0
- iflow_mcp_jenstangen1_pptx-0.1.0.dist-info/WHEEL +4 -0
- iflow_mcp_jenstangen1_pptx-0.1.0.dist-info/entry_points.txt +2 -0
- language.json +1 -0
- mcp_excel_server_win32.py +554 -0
- mcp_powerpoint_server.py +1348 -0
- mcp_powerpoint_server_win32.py +766 -0
- package-lock.json +1054 -0
- package.json +5 -0
- package_name +1 -0
- push_info.json +5 -0
- pyproject.toml +26 -0
- requirements.txt +9 -0
- server_config.json +1 -0
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
import win32com.client
|
|
2
|
+
import pythoncom
|
|
3
|
+
from typing import List, Optional, Dict, Any, Union
|
|
4
|
+
from mcp.server.fastmcp import FastMCP
|
|
5
|
+
import os
|
|
6
|
+
import pywintypes # Import for specific exception types
|
|
7
|
+
|
|
8
|
+
# Constants from PowerPoint VBA Object Library (usually obtained via makepy)
|
|
9
|
+
# Using magic numbers for simplicity here, but using makepy is recommended practice
|
|
10
|
+
# Example: from win32com.client import constants
|
|
11
|
+
# ppSaveAsDefault = 1 (This might vary, better to use format-specific saves)
|
|
12
|
+
ppSaveAsOpenXMLPresentation = 24 # .pptx
|
|
13
|
+
msoShapeRectangle = 1
|
|
14
|
+
msoTextOrientationHorizontal = 1
|
|
15
|
+
msoPlaceholder = 14 # Shape type for placeholders
|
|
16
|
+
|
|
17
|
+
# Placeholder type constants (might vary slightly by PP version)
|
|
18
|
+
ppPlaceholderTitle = 1
|
|
19
|
+
ppPlaceholderBody = 2
|
|
20
|
+
ppPlaceholderCenterTitle = 13 # Often used for Title Only layout
|
|
21
|
+
ppPlaceholderSubtitle = 14
|
|
22
|
+
ppPlaceholderDate = 16
|
|
23
|
+
ppPlaceholderSlideNumber = 15
|
|
24
|
+
ppPlaceholderFooter = 17
|
|
25
|
+
ppPlaceholderHeader = 18
|
|
26
|
+
ppPlaceholderObject = 7 # Generic object/content placeholder
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Mapping for user-friendly placeholder names to constants
|
|
30
|
+
PLACEHOLDER_NAME_MAP = {
|
|
31
|
+
"title": ppPlaceholderTitle,
|
|
32
|
+
"body": ppPlaceholderBody,
|
|
33
|
+
"centertitle": ppPlaceholderCenterTitle,
|
|
34
|
+
"subtitle": ppPlaceholderSubtitle,
|
|
35
|
+
"date": ppPlaceholderDate,
|
|
36
|
+
"slidenumber": ppPlaceholderSlideNumber,
|
|
37
|
+
"footer": ppPlaceholderFooter,
|
|
38
|
+
"header": ppPlaceholderHeader,
|
|
39
|
+
"object": ppPlaceholderObject,
|
|
40
|
+
"content": ppPlaceholderObject, # Common synonym
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Mapping for user-friendly shape type names (reverse of _get_shape_type_name)
|
|
44
|
+
SHAPE_TYPE_NAME_MAP = {
|
|
45
|
+
"rectangle": 1,
|
|
46
|
+
"textbox": 17, # MSO_SHAPE_TYPE.TEXT_BOX (Note: might also be AutoShape with text)
|
|
47
|
+
"oval": 9, # MSO_SHAPE_TYPE.OVAL (Note: Check MSO AutoShape constants for more specific ovals)
|
|
48
|
+
"table": 19, # MSO_SHAPE_TYPE.TABLE
|
|
49
|
+
"chart": 3, # MSO_SHAPE_TYPE.CHART
|
|
50
|
+
"picture": 13, # MSO_SHAPE_TYPE.PICTURE
|
|
51
|
+
"line": 20, # MSO_SHAPE_TYPE.LINE
|
|
52
|
+
"connector": 10, # MSO_SHAPE_TYPE.CONNECTOR (check AutoShape types)
|
|
53
|
+
"placeholder": msoPlaceholder,
|
|
54
|
+
# Add more basic types as needed
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PowerPointEditorWin32:
|
|
59
|
+
def __init__(self):
|
|
60
|
+
self.app = None
|
|
61
|
+
self._connect_or_launch_powerpoint()
|
|
62
|
+
|
|
63
|
+
def _connect_or_launch_powerpoint(self):
|
|
64
|
+
"""Connects to a running instance of PowerPoint or launches a new one."""
|
|
65
|
+
try:
|
|
66
|
+
# Use the Pywin32 CoInitializeEx to avoid threading issues with COM
|
|
67
|
+
pythoncom.CoInitializeEx(pythoncom.COINIT_APARTMENTTHREADED)
|
|
68
|
+
self.app = win32com.client.GetActiveObject("PowerPoint.Application")
|
|
69
|
+
print("Connected to running PowerPoint application.")
|
|
70
|
+
except pywintypes.com_error:
|
|
71
|
+
try:
|
|
72
|
+
self.app = win32com.client.Dispatch("PowerPoint.Application")
|
|
73
|
+
self.app.Visible = True # Make the application visible
|
|
74
|
+
print("Launched new PowerPoint application.")
|
|
75
|
+
except Exception as e:
|
|
76
|
+
print(f"Error launching PowerPoint: {e}")
|
|
77
|
+
self.app = None
|
|
78
|
+
except Exception as e:
|
|
79
|
+
print(f"An unexpected error occurred: {e}")
|
|
80
|
+
self.app = None
|
|
81
|
+
|
|
82
|
+
def _ensure_connection(self):
|
|
83
|
+
"""Ensures the PowerPoint application object is valid."""
|
|
84
|
+
if self.app is None:
|
|
85
|
+
self._connect_or_launch_powerpoint()
|
|
86
|
+
if self.app is None:
|
|
87
|
+
raise ConnectionError("Could not connect to or launch PowerPoint.")
|
|
88
|
+
# Basic check if the app object seems responsive
|
|
89
|
+
try:
|
|
90
|
+
_ = self.app.Version
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(f"PowerPoint connection lost or unresponsive: {e}")
|
|
93
|
+
self._connect_or_launch_powerpoint() # Try reconnecting
|
|
94
|
+
if self.app is None:
|
|
95
|
+
raise ConnectionError("Could not reconnect to PowerPoint.")
|
|
96
|
+
|
|
97
|
+
def list_open_presentations(self) -> List[Dict[str, Any]]:
|
|
98
|
+
"""Lists all currently open presentations."""
|
|
99
|
+
self._ensure_connection()
|
|
100
|
+
presentations_info = []
|
|
101
|
+
try:
|
|
102
|
+
for i in range(1, self.app.Presentations.Count + 1):
|
|
103
|
+
pres = self.app.Presentations(i)
|
|
104
|
+
presentations_info.append({
|
|
105
|
+
"name": pres.Name,
|
|
106
|
+
"path": pres.FullName if pres.Path else None,
|
|
107
|
+
"slides": pres.Slides.Count,
|
|
108
|
+
"saved": pres.Saved,
|
|
109
|
+
"index": i # Provide the 1-based index for reference
|
|
110
|
+
})
|
|
111
|
+
except Exception as e:
|
|
112
|
+
print(f"Error listing presentations: {e}")
|
|
113
|
+
# Maybe attempt reconnect if it's a COM error
|
|
114
|
+
if "RPC server is unavailable" in str(e):
|
|
115
|
+
self._connect_or_launch_powerpoint()
|
|
116
|
+
# Retry might be needed here depending on strategy
|
|
117
|
+
raise
|
|
118
|
+
return presentations_info
|
|
119
|
+
|
|
120
|
+
def get_presentation(self, identifier: str) -> Optional[Any]:
|
|
121
|
+
"""
|
|
122
|
+
Gets a presentation object by its name, path, or 1-based index.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
identifier (str): The name (e.g., "Presentation1.pptx"),
|
|
126
|
+
full path, or 1-based index (as string or int).
|
|
127
|
+
"""
|
|
128
|
+
self._ensure_connection()
|
|
129
|
+
try:
|
|
130
|
+
# Try by index first if it's an integer
|
|
131
|
+
if isinstance(identifier, int) or identifier.isdigit():
|
|
132
|
+
idx = int(identifier)
|
|
133
|
+
if 1 <= idx <= self.app.Presentations.Count:
|
|
134
|
+
return self.app.Presentations(idx)
|
|
135
|
+
else:
|
|
136
|
+
print(f"Index {identifier} out of range.")
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
# Try by name or path
|
|
140
|
+
for i in range(1, self.app.Presentations.Count + 1):
|
|
141
|
+
pres = self.app.Presentations(i)
|
|
142
|
+
if pres.Name == identifier or pres.FullName == identifier:
|
|
143
|
+
return pres
|
|
144
|
+
print(f"Presentation '{identifier}' not found.")
|
|
145
|
+
return None
|
|
146
|
+
except Exception as e:
|
|
147
|
+
print(f"Error getting presentation '{identifier}': {e}")
|
|
148
|
+
raise
|
|
149
|
+
|
|
150
|
+
def save_presentation(self, identifier: str, save_path: Optional[str] = None):
|
|
151
|
+
"""
|
|
152
|
+
Saves the specified presentation.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
identifier (str): Name, path, or index of the presentation.
|
|
156
|
+
save_path (Optional[str]): Path to save to. If None, saves to its current path.
|
|
157
|
+
If the presentation is new, save_path is required.
|
|
158
|
+
"""
|
|
159
|
+
self._ensure_connection()
|
|
160
|
+
pres = self.get_presentation(identifier)
|
|
161
|
+
if not pres:
|
|
162
|
+
raise ValueError(f"Presentation '{identifier}' not found.")
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
if save_path:
|
|
166
|
+
# Ensure the directory exists
|
|
167
|
+
abs_path = os.path.abspath(save_path)
|
|
168
|
+
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
|
169
|
+
pres.SaveAs(abs_path, ppSaveAsOpenXMLPresentation)
|
|
170
|
+
print(f"Presentation saved as '{abs_path}'.")
|
|
171
|
+
elif pres.Path: # Can only save if it has a path already
|
|
172
|
+
pres.Save()
|
|
173
|
+
print(f"Presentation '{pres.Name}' saved.")
|
|
174
|
+
else:
|
|
175
|
+
raise ValueError("save_path is required for a new presentation.")
|
|
176
|
+
except Exception as e:
|
|
177
|
+
print(f"Error saving presentation '{identifier}': {e}")
|
|
178
|
+
raise
|
|
179
|
+
|
|
180
|
+
def add_slide(self, identifier: str, layout_index: int = 1) -> int:
|
|
181
|
+
"""
|
|
182
|
+
Adds a new slide to the presentation.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
identifier (str): Name, path, or index of the presentation.
|
|
186
|
+
layout_index (int): 1-based index of the slide layout to use.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
int: The 1-based index of the newly added slide.
|
|
190
|
+
"""
|
|
191
|
+
self._ensure_connection()
|
|
192
|
+
pres = self.get_presentation(identifier)
|
|
193
|
+
if not pres:
|
|
194
|
+
raise ValueError(f"Presentation '{identifier}' not found.")
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
# Ensure layout_index is valid
|
|
198
|
+
if not (1 <= layout_index <= pres.SlideMaster.CustomLayouts.Count):
|
|
199
|
+
print(f"Warning: Layout index {layout_index} invalid. Using layout 1.")
|
|
200
|
+
layout_index = 1
|
|
201
|
+
layout = pres.SlideMaster.CustomLayouts(layout_index)
|
|
202
|
+
|
|
203
|
+
# Add the slide (returns the new Slide object)
|
|
204
|
+
new_slide = pres.Slides.AddSlide(pres.Slides.Count + 1, layout)
|
|
205
|
+
print(f"Added slide {new_slide.SlideIndex} to '{pres.Name}'.")
|
|
206
|
+
return new_slide.SlideIndex
|
|
207
|
+
except Exception as e:
|
|
208
|
+
print(f"Error adding slide to '{identifier}': {e}")
|
|
209
|
+
raise
|
|
210
|
+
|
|
211
|
+
def get_slide(self, identifier: str, slide_index: int) -> Optional[Any]:
|
|
212
|
+
"""
|
|
213
|
+
Gets a slide object from a presentation.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
identifier (str): Name, path, or index of the presentation.
|
|
217
|
+
slide_index (int): 1-based index of the slide.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Optional[Any]: The slide object or None if not found.
|
|
221
|
+
"""
|
|
222
|
+
self._ensure_connection()
|
|
223
|
+
pres = self.get_presentation(identifier)
|
|
224
|
+
if not pres:
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
if 1 <= slide_index <= pres.Slides.Count:
|
|
229
|
+
return pres.Slides(slide_index)
|
|
230
|
+
else:
|
|
231
|
+
print(f"Slide index {slide_index} out of range for '{pres.Name}'.")
|
|
232
|
+
return None
|
|
233
|
+
except Exception as e:
|
|
234
|
+
print(f"Error getting slide {slide_index} from '{identifier}': {e}")
|
|
235
|
+
raise
|
|
236
|
+
|
|
237
|
+
def add_text_box(self, identifier: str, slide_index: int, text: str,
|
|
238
|
+
left: float, top: float, width: float, height: float) -> int:
|
|
239
|
+
"""
|
|
240
|
+
Adds a text box to a slide.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
identifier (str): Presentation identifier.
|
|
244
|
+
slide_index (int): 1-based slide index.
|
|
245
|
+
text (str): Text content for the box.
|
|
246
|
+
left, top, width, height (float): Position and size in points.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
int: The unique ID of the newly added shape.
|
|
250
|
+
"""
|
|
251
|
+
self._ensure_connection()
|
|
252
|
+
slide = self.get_slide(identifier, slide_index)
|
|
253
|
+
if not slide:
|
|
254
|
+
raise ValueError(f"Slide {slide_index} not found in presentation '{identifier}'.")
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
shape = slide.Shapes.AddTextbox(msoTextOrientationHorizontal, left, top, width, height)
|
|
258
|
+
shape.TextFrame.TextRange.Text = text
|
|
259
|
+
shape.Name = f"TextBox_{shape.Id}" # Assign a default name
|
|
260
|
+
print(f"Added text box (ID: {shape.Id}) to slide {slide_index}.")
|
|
261
|
+
return shape.Id
|
|
262
|
+
except Exception as e:
|
|
263
|
+
print(f"Error adding text box to slide {slide_index}: {e}")
|
|
264
|
+
raise
|
|
265
|
+
|
|
266
|
+
def add_shape(self, identifier: str, slide_index: int, shape_type: int,
|
|
267
|
+
left: float, top: float, width: float, height: float) -> int:
|
|
268
|
+
"""
|
|
269
|
+
Adds a basic auto shape to a slide.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
identifier (str): Presentation identifier.
|
|
273
|
+
slide_index (int): 1-based slide index.
|
|
274
|
+
shape_type (int): MSO AutoShapeType constant (e.g., msoShapeRectangle).
|
|
275
|
+
left, top, width, height (float): Position and size in points.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
int: The unique ID of the newly added shape.
|
|
279
|
+
"""
|
|
280
|
+
self._ensure_connection()
|
|
281
|
+
slide = self.get_slide(identifier, slide_index)
|
|
282
|
+
if not slide:
|
|
283
|
+
raise ValueError(f"Slide {slide_index} not found in presentation '{identifier}'.")
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
# Use AddShape for AutoShapes
|
|
287
|
+
shape = slide.Shapes.AddShape(shape_type, left, top, width, height)
|
|
288
|
+
shape.Name = f"Shape_{shape.Id}" # Assign a default name
|
|
289
|
+
print(f"Added shape (ID: {shape.Id}, Type: {shape_type}) to slide {slide_index}.")
|
|
290
|
+
return shape.Id
|
|
291
|
+
except Exception as e:
|
|
292
|
+
print(f"Error adding shape to slide {slide_index}: {e}")
|
|
293
|
+
raise
|
|
294
|
+
|
|
295
|
+
def get_shape_by_id(self, identifier: str, slide_index: int, shape_id: int) -> Optional[Any]:
|
|
296
|
+
"""Gets a shape object by its unique ID."""
|
|
297
|
+
self._ensure_connection()
|
|
298
|
+
slide = self.get_slide(identifier, slide_index)
|
|
299
|
+
if not slide:
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
# Iterate through shapes to find by ID
|
|
304
|
+
for i in range(1, slide.Shapes.Count + 1):
|
|
305
|
+
shape = slide.Shapes(i)
|
|
306
|
+
if shape.Id == shape_id:
|
|
307
|
+
return shape
|
|
308
|
+
print(f"Shape with ID {shape_id} not found on slide {slide_index}.")
|
|
309
|
+
return None
|
|
310
|
+
except Exception as e:
|
|
311
|
+
# Handle potential errors if shape is deleted during iteration etc.
|
|
312
|
+
print(f"Error finding shape ID {shape_id} on slide {slide_index}: {e}")
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
def get_shape_by_name(self, identifier: str, slide_index: int, shape_name: str) -> Optional[Any]:
|
|
316
|
+
"""Gets a shape object by its name."""
|
|
317
|
+
self._ensure_connection()
|
|
318
|
+
slide = self.get_slide(identifier, slide_index)
|
|
319
|
+
if not slide:
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
# Accessing by name directly might fail if name is not unique or contains odd chars
|
|
324
|
+
return slide.Shapes(shape_name)
|
|
325
|
+
except pywintypes.com_error as e:
|
|
326
|
+
# Handle common error for item not found
|
|
327
|
+
if e.hresult == -2147024809: # 0x80070057 (E_INVALIDARG often means not found by name)
|
|
328
|
+
print(f"Shape with name '{shape_name}' not found on slide {slide_index}.")
|
|
329
|
+
else:
|
|
330
|
+
print(f"Error finding shape name '{shape_name}' on slide {slide_index}: {e}")
|
|
331
|
+
return None
|
|
332
|
+
except Exception as e:
|
|
333
|
+
print(f"Error finding shape name '{shape_name}' on slide {slide_index}: {e}")
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
def find_shape_by_text(self, identifier: Union[str, int], slide_index: int, search_text: str, partial_match: bool = True) -> List[Dict[str, Any]]:
|
|
337
|
+
"""
|
|
338
|
+
Finds shapes on a slide containing specific text.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
identifier (Union[str, int]): Presentation identifier.
|
|
342
|
+
slide_index (int): 1-based slide index.
|
|
343
|
+
search_text (str): The text to search for (case-insensitive).
|
|
344
|
+
partial_match (bool): If True, finds shapes where search_text is a substring.
|
|
345
|
+
If False, requires an exact match (ignoring case).
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
List[Dict[str, Any]]: List of matching shapes with their info.
|
|
349
|
+
"""
|
|
350
|
+
self._ensure_connection()
|
|
351
|
+
matches = []
|
|
352
|
+
slide = self.get_slide(identifier, slide_index)
|
|
353
|
+
if not slide:
|
|
354
|
+
print(f"Cannot find shapes by text: Slide {slide_index} not found in presentation '{identifier}'.")
|
|
355
|
+
return []
|
|
356
|
+
|
|
357
|
+
search_lower = search_text.lower()
|
|
358
|
+
try:
|
|
359
|
+
for i in range(1, slide.Shapes.Count + 1):
|
|
360
|
+
shape = slide.Shapes(i)
|
|
361
|
+
shape_text = ""
|
|
362
|
+
has_text_frame = False
|
|
363
|
+
try:
|
|
364
|
+
# Check text frame exists and has text
|
|
365
|
+
if shape.HasTextFrame and shape.TextFrame.HasText:
|
|
366
|
+
has_text_frame = True
|
|
367
|
+
shape_text = shape.TextFrame.TextRange.Text
|
|
368
|
+
except Exception:
|
|
369
|
+
continue # Skip shapes that error on text access
|
|
370
|
+
|
|
371
|
+
if has_text_frame:
|
|
372
|
+
shape_text_lower = shape_text.lower()
|
|
373
|
+
found = False
|
|
374
|
+
if partial_match:
|
|
375
|
+
if search_lower in shape_text_lower:
|
|
376
|
+
found = True
|
|
377
|
+
else: # Exact match (case-insensitive)
|
|
378
|
+
if search_lower == shape_text_lower:
|
|
379
|
+
found = True
|
|
380
|
+
|
|
381
|
+
if found:
|
|
382
|
+
shape_data = self._get_shape_basic_info(shape)
|
|
383
|
+
shape_data["text_preview"] = shape_text[:100] + "..." if len(shape_text) > 100 else shape_text
|
|
384
|
+
matches.append(shape_data)
|
|
385
|
+
|
|
386
|
+
except Exception as e:
|
|
387
|
+
print(f"Error searching for text '{search_text}' on slide {slide_index}: {e}")
|
|
388
|
+
return matches
|
|
389
|
+
|
|
390
|
+
def find_shapes_by_type(self, identifier: Union[str, int], slide_index: int, shape_type_name: str) -> List[Dict[str, Any]]:
|
|
391
|
+
"""
|
|
392
|
+
Finds shapes on a slide matching a specific type name.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
identifier (Union[str, int]): Presentation identifier.
|
|
396
|
+
slide_index (int): 1-based slide index.
|
|
397
|
+
shape_type_name (str): The user-friendly name of the shape type (e.g., "rectangle", "textbox"). Case-insensitive.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
List[Dict[str, Any]]: List of matching shapes with their info.
|
|
401
|
+
"""
|
|
402
|
+
self._ensure_connection()
|
|
403
|
+
matches = []
|
|
404
|
+
slide = self.get_slide(identifier, slide_index)
|
|
405
|
+
if not slide:
|
|
406
|
+
print(f"Cannot find shapes by type: Slide {slide_index} not found in presentation '{identifier}'.")
|
|
407
|
+
return []
|
|
408
|
+
|
|
409
|
+
type_name_lower = shape_type_name.lower()
|
|
410
|
+
target_type_id = SHAPE_TYPE_NAME_MAP.get(type_name_lower)
|
|
411
|
+
|
|
412
|
+
if target_type_id is None:
|
|
413
|
+
print(f"Warning: Unknown shape type name '{shape_type_name}'. Supported types: {list(SHAPE_TYPE_NAME_MAP.keys())}")
|
|
414
|
+
return []
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
for i in range(1, slide.Shapes.Count + 1):
|
|
418
|
+
shape = slide.Shapes(i)
|
|
419
|
+
# Special handling for textbox which might be an AutoShape
|
|
420
|
+
is_match = False
|
|
421
|
+
if target_type_id == SHAPE_TYPE_NAME_MAP["textbox"]:
|
|
422
|
+
# Check MSO_SHAPE_TYPE.TEXT_BOX or if it's an AutoShape with text
|
|
423
|
+
if shape.Type == SHAPE_TYPE_NAME_MAP["textbox"]:
|
|
424
|
+
is_match = True
|
|
425
|
+
elif shape.Type == 1: # msoAutoShape
|
|
426
|
+
try:
|
|
427
|
+
if shape.HasTextFrame and shape.TextFrame.HasText:
|
|
428
|
+
# Could refine this - maybe check shape.AutoShapeType?
|
|
429
|
+
# For now, consider any autoshape with text as potential textbox match
|
|
430
|
+
is_match = True
|
|
431
|
+
except: pass # Ignore errors accessing text frame
|
|
432
|
+
elif shape.Type == target_type_id:
|
|
433
|
+
is_match = True
|
|
434
|
+
|
|
435
|
+
if is_match:
|
|
436
|
+
matches.append(self._get_shape_basic_info(shape))
|
|
437
|
+
|
|
438
|
+
except Exception as e:
|
|
439
|
+
print(f"Error searching for shape type '{shape_type_name}' on slide {slide_index}: {e}")
|
|
440
|
+
return matches
|
|
441
|
+
|
|
442
|
+
def get_placeholder_shape(self, identifier: Union[str, int], slide_index: int, placeholder_name: str) -> Optional[Dict[str, Any]]:
|
|
443
|
+
"""
|
|
444
|
+
Finds a specific placeholder shape on a slide by its common name.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
identifier (Union[str, int]): Presentation identifier.
|
|
448
|
+
slide_index (int): 1-based slide index.
|
|
449
|
+
placeholder_name (str): Common name of the placeholder (e.g., "title", "body", "footer"). Case-insensitive.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
Optional[Dict[str, Any]]: Info of the first matching placeholder shape, or None if not found.
|
|
453
|
+
"""
|
|
454
|
+
self._ensure_connection()
|
|
455
|
+
slide = self.get_slide(identifier, slide_index)
|
|
456
|
+
if not slide:
|
|
457
|
+
print(f"Cannot find placeholder: Slide {slide_index} not found in presentation '{identifier}'.")
|
|
458
|
+
return None
|
|
459
|
+
|
|
460
|
+
name_lower = placeholder_name.lower()
|
|
461
|
+
target_ph_type = PLACEHOLDER_NAME_MAP.get(name_lower)
|
|
462
|
+
|
|
463
|
+
if target_ph_type is None:
|
|
464
|
+
print(f"Warning: Unknown placeholder name '{placeholder_name}'. Supported names: {list(PLACEHOLDER_NAME_MAP.keys())}")
|
|
465
|
+
return None
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
for i in range(1, slide.Shapes.Count + 1):
|
|
469
|
+
shape = slide.Shapes(i)
|
|
470
|
+
try:
|
|
471
|
+
# Check if it's a placeholder and its type matches
|
|
472
|
+
if shape.Type == msoPlaceholder and shape.PlaceholderFormat.Type == target_ph_type:
|
|
473
|
+
print(f"Found placeholder '{placeholder_name}' (ID: {shape.Id}) on slide {slide_index}.")
|
|
474
|
+
return self._get_shape_basic_info(shape)
|
|
475
|
+
except Exception:
|
|
476
|
+
# Some shapes might error on PlaceholderFormat access if not placeholders
|
|
477
|
+
continue
|
|
478
|
+
except Exception as e:
|
|
479
|
+
print(f"Error searching for placeholder '{placeholder_name}' on slide {slide_index}: {e}")
|
|
480
|
+
|
|
481
|
+
print(f"Placeholder '{placeholder_name}' not found on slide {slide_index}.")
|
|
482
|
+
return None
|
|
483
|
+
|
|
484
|
+
def edit_element(self, identifier: str, slide_index: int, shape_identifier: Union[int, str],
|
|
485
|
+
properties: Dict[str, Any]) -> bool:
|
|
486
|
+
"""
|
|
487
|
+
Edits properties of a shape identified by ID or name.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
identifier (str): Presentation identifier.
|
|
491
|
+
slide_index (int): 1-based slide index.
|
|
492
|
+
shape_identifier (Union[int, str]): The shape's ID (int) or Name (str).
|
|
493
|
+
properties (Dict[str, Any]): Dictionary of properties to change.
|
|
494
|
+
Supported: 'text', 'left', 'top', 'width', 'height', 'name'.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
bool: True if successful, False otherwise.
|
|
498
|
+
"""
|
|
499
|
+
self._ensure_connection()
|
|
500
|
+
if isinstance(shape_identifier, int):
|
|
501
|
+
shape = self.get_shape_by_id(identifier, slide_index, shape_identifier)
|
|
502
|
+
elif isinstance(shape_identifier, str):
|
|
503
|
+
shape = self.get_shape_by_name(identifier, slide_index, shape_identifier)
|
|
504
|
+
else:
|
|
505
|
+
print("Invalid shape_identifier type. Use int (ID) or str (Name).")
|
|
506
|
+
return False
|
|
507
|
+
|
|
508
|
+
if not shape:
|
|
509
|
+
return False
|
|
510
|
+
|
|
511
|
+
try:
|
|
512
|
+
if 'text' in properties and shape.HasTextFrame and shape.TextFrame.HasText:
|
|
513
|
+
shape.TextFrame.TextRange.Text = str(properties['text'])
|
|
514
|
+
if 'left' in properties:
|
|
515
|
+
shape.Left = float(properties['left'])
|
|
516
|
+
if 'top' in properties:
|
|
517
|
+
shape.Top = float(properties['top'])
|
|
518
|
+
if 'width' in properties:
|
|
519
|
+
shape.Width = float(properties['width'])
|
|
520
|
+
if 'height' in properties:
|
|
521
|
+
shape.Height = float(properties['height'])
|
|
522
|
+
if 'name' in properties:
|
|
523
|
+
shape.Name = str(properties['name']) # Allow renaming
|
|
524
|
+
|
|
525
|
+
print(f"Edited properties for shape '{shape_identifier}' on slide {slide_index}.")
|
|
526
|
+
return True
|
|
527
|
+
except Exception as e:
|
|
528
|
+
print(f"Error editing shape '{shape_identifier}' on slide {slide_index}: {e}")
|
|
529
|
+
return False
|
|
530
|
+
|
|
531
|
+
def list_shapes(self, identifier: Union[str, int], slide_index: int) -> List[Dict[str, Any]]:
|
|
532
|
+
"""Lists shapes on a slide with their ID, Name, Type, and basic geometry."""
|
|
533
|
+
self._ensure_connection()
|
|
534
|
+
shapes_info = []
|
|
535
|
+
slide = self.get_slide(identifier, slide_index)
|
|
536
|
+
if not slide:
|
|
537
|
+
print(f"Cannot list shapes: Slide {slide_index} not found in presentation '{identifier}'.")
|
|
538
|
+
return []
|
|
539
|
+
|
|
540
|
+
try:
|
|
541
|
+
for i in range(1, slide.Shapes.Count + 1):
|
|
542
|
+
shape = slide.Shapes(i)
|
|
543
|
+
shapes_info.append(self._get_shape_basic_info(shape)) # Use helper
|
|
544
|
+
except Exception as e:
|
|
545
|
+
print(f"Error listing shapes on slide {slide_index}: {e}")
|
|
546
|
+
# Don't raise, just return what we have or empty list
|
|
547
|
+
return shapes_info
|
|
548
|
+
|
|
549
|
+
def _get_shape_basic_info(self, shape: Any) -> Dict[str, Any]:
|
|
550
|
+
"""Helper to get common info dictionary for a shape."""
|
|
551
|
+
info = {
|
|
552
|
+
"id": -1, "name": "Unknown", "type_id": -1, "type_name": "Unknown",
|
|
553
|
+
"left": 0, "top": 0, "width": 0, "height": 0,
|
|
554
|
+
"has_text": False, "is_placeholder": False
|
|
555
|
+
}
|
|
556
|
+
try:
|
|
557
|
+
info["id"] = shape.Id
|
|
558
|
+
info["name"] = shape.Name
|
|
559
|
+
info["type_id"] = shape.Type
|
|
560
|
+
info["type_name"] = self._get_shape_type_name(shape.Type)
|
|
561
|
+
info["left"] = shape.Left
|
|
562
|
+
info["top"] = shape.Top
|
|
563
|
+
info["width"] = shape.Width
|
|
564
|
+
info["height"] = shape.Height
|
|
565
|
+
|
|
566
|
+
# Check for text
|
|
567
|
+
try:
|
|
568
|
+
if shape.HasTextFrame and shape.TextFrame.HasText:
|
|
569
|
+
info["has_text"] = True
|
|
570
|
+
except Exception: pass
|
|
571
|
+
|
|
572
|
+
# Check if placeholder
|
|
573
|
+
try:
|
|
574
|
+
if shape.Type == msoPlaceholder:
|
|
575
|
+
info["is_placeholder"] = True
|
|
576
|
+
info["placeholder_type_id"] = shape.PlaceholderFormat.Type
|
|
577
|
+
# Add friendly name for placeholder type if possible
|
|
578
|
+
for name, ph_id in PLACEHOLDER_NAME_MAP.items():
|
|
579
|
+
if ph_id == info["placeholder_type_id"]:
|
|
580
|
+
info["placeholder_type_name"] = name
|
|
581
|
+
break
|
|
582
|
+
except Exception: pass
|
|
583
|
+
|
|
584
|
+
except Exception as e:
|
|
585
|
+
print(f"Error getting basic info for a shape: {e}")
|
|
586
|
+
# Return partial info if possible
|
|
587
|
+
return info
|
|
588
|
+
|
|
589
|
+
def _get_shape_type_name(self, type_id: int) -> str:
|
|
590
|
+
"""Returns a readable name for MSO_SHAPE_TYPE IDs (add more as needed)."""
|
|
591
|
+
# This is a simplified mapping. A full mapping would be extensive.
|
|
592
|
+
# Consider reversing SHAPE_TYPE_NAME_MAP for consistency
|
|
593
|
+
mapping = {
|
|
594
|
+
1: "Rectangle/AutoShape", # Generic AutoShape
|
|
595
|
+
17: "TextBox",
|
|
596
|
+
9: "Oval", # Generic Oval (likely AutoShape)
|
|
597
|
+
19: "Table",
|
|
598
|
+
3: "Chart",
|
|
599
|
+
13: "Picture",
|
|
600
|
+
20: "Line",
|
|
601
|
+
10: "Connector",
|
|
602
|
+
msoPlaceholder: "Placeholder",
|
|
603
|
+
# Add more from MSO_SHAPE_TYPE if needed
|
|
604
|
+
6: "Group",
|
|
605
|
+
7: "EmbeddedObject",
|
|
606
|
+
8: "FormControl",
|
|
607
|
+
11: "Freeform",
|
|
608
|
+
12: "Media",
|
|
609
|
+
15: "OLEControlObject",
|
|
610
|
+
16: "ScriptAnchor",
|
|
611
|
+
18: "Canvas",
|
|
612
|
+
21: "Ink",
|
|
613
|
+
22: "InkComment",
|
|
614
|
+
23: "Diagram", # SmartArt? Check MSO_SHAPE_TYPE constants
|
|
615
|
+
24: "WebVideo",
|
|
616
|
+
25: "ContentApp", # Office Add-in
|
|
617
|
+
26: "GraphicFrame", # Holds Table, Chart, SmartArt, etc.
|
|
618
|
+
27: "LinkedOLEObject",
|
|
619
|
+
28: "LinkedPicture",
|
|
620
|
+
29: "Model3D",
|
|
621
|
+
30: "LinkedContentApp",
|
|
622
|
+
}
|
|
623
|
+
return mapping.get(type_id, f"Unknown ({type_id})")
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
# --- MCP Server Setup ---
|
|
627
|
+
|
|
628
|
+
# Create the PowerPoint editor instance
|
|
629
|
+
editor = PowerPointEditorWin32()
|
|
630
|
+
|
|
631
|
+
# Create MCP server
|
|
632
|
+
mcp = FastMCP("PowerPoint MCP (Win32)")
|
|
633
|
+
|
|
634
|
+
@mcp.tool()
|
|
635
|
+
def list_open_presentations():
|
|
636
|
+
"""Lists currently open PowerPoint presentations."""
|
|
637
|
+
try:
|
|
638
|
+
return {"presentations": editor.list_open_presentations()}
|
|
639
|
+
except Exception as e:
|
|
640
|
+
return {"error": f"Failed to list presentations: {str(e)}"}
|
|
641
|
+
|
|
642
|
+
@mcp.tool()
|
|
643
|
+
def save_presentation(identifier: str, save_path: str = None):
|
|
644
|
+
"""Saves the specified presentation. Use index, name, or full path as identifier."""
|
|
645
|
+
try:
|
|
646
|
+
editor.save_presentation(identifier, save_path)
|
|
647
|
+
return {"message": f"Save command issued for presentation '{identifier}'."}
|
|
648
|
+
except Exception as e:
|
|
649
|
+
return {"error": f"Failed to save presentation '{identifier}': {str(e)}"}
|
|
650
|
+
|
|
651
|
+
@mcp.tool()
|
|
652
|
+
def add_slide(identifier: str, layout_index: int = 1):
|
|
653
|
+
"""Adds a slide to the specified presentation. Use index, name, or full path as identifier."""
|
|
654
|
+
try:
|
|
655
|
+
new_slide_index = editor.add_slide(identifier, layout_index)
|
|
656
|
+
return {"message": "Slide added successfully.", "slide_index": new_slide_index}
|
|
657
|
+
except Exception as e:
|
|
658
|
+
return {"error": f"Failed to add slide to '{identifier}': {str(e)}"}
|
|
659
|
+
|
|
660
|
+
@mcp.tool()
|
|
661
|
+
def add_text_box(identifier: str, slide_index: int, text: str,
|
|
662
|
+
left: float = 72, top: float = 72, width: float = 288, height: float = 72):
|
|
663
|
+
"""Adds a text box to a specific slide (dimensions in points)."""
|
|
664
|
+
try:
|
|
665
|
+
shape_id = editor.add_text_box(identifier, slide_index, text, left, top, width, height)
|
|
666
|
+
return {"message": "Text box added successfully.", "shape_id": shape_id}
|
|
667
|
+
except Exception as e:
|
|
668
|
+
return {"error": f"Failed to add text box to slide {slide_index} in '{identifier}': {str(e)}"}
|
|
669
|
+
|
|
670
|
+
@mcp.tool()
|
|
671
|
+
def add_rectangle(identifier: str, slide_index: int,
|
|
672
|
+
left: float = 72, top: float = 150, width: float = 144, height: float = 72):
|
|
673
|
+
"""Adds a rectangle shape to a specific slide (dimensions in points)."""
|
|
674
|
+
try:
|
|
675
|
+
# msoShapeRectangle = 1
|
|
676
|
+
shape_id = editor.add_shape(identifier, slide_index, msoShapeRectangle, left, top, width, height)
|
|
677
|
+
return {"message": "Rectangle added successfully.", "shape_id": shape_id}
|
|
678
|
+
except Exception as e:
|
|
679
|
+
return {"error": f"Failed to add rectangle to slide {slide_index} in '{identifier}': {str(e)}"}
|
|
680
|
+
|
|
681
|
+
@mcp.tool()
|
|
682
|
+
def edit_element(identifier: str, slide_index: int, shape_identifier: Union[int, str],
|
|
683
|
+
properties: Dict[str, Any]):
|
|
684
|
+
"""Edits a shape's properties (text, left, top, width, height, name). Identify shape by ID (int) or Name (str)."""
|
|
685
|
+
try:
|
|
686
|
+
success = editor.edit_element(identifier, slide_index, shape_identifier, properties)
|
|
687
|
+
if success:
|
|
688
|
+
return {"message": f"Element '{shape_identifier}' updated successfully."}
|
|
689
|
+
else:
|
|
690
|
+
# Editor method already prints errors, return a generic failure
|
|
691
|
+
return {"error": f"Failed to update element '{shape_identifier}'. See server logs for details."}
|
|
692
|
+
except Exception as e:
|
|
693
|
+
return {"error": f"Failed to edit element '{shape_identifier}' on slide {slide_index}: {str(e)}"}
|
|
694
|
+
|
|
695
|
+
@mcp.tool()
|
|
696
|
+
def list_shapes(identifier: str, slide_index: int):
|
|
697
|
+
"""Lists all shapes on a given slide with their ID, Name, and Type."""
|
|
698
|
+
try:
|
|
699
|
+
shapes = editor.list_shapes(identifier, slide_index)
|
|
700
|
+
return {"shapes": shapes}
|
|
701
|
+
except Exception as e:
|
|
702
|
+
return {"error": f"Failed to list shapes on slide {slide_index} in '{identifier}': {str(e)}"}
|
|
703
|
+
|
|
704
|
+
@mcp.tool()
|
|
705
|
+
def find_shape_by_text(identifier: Union[str, int], slide_index: int, search_text: str, partial_match: bool = True):
|
|
706
|
+
"""Finds shapes on a slide containing specific text (case-insensitive). Set partial_match=False for exact match."""
|
|
707
|
+
if not editor: return {"error": "PowerPoint editor not initialized."}
|
|
708
|
+
try:
|
|
709
|
+
matches = editor.find_shape_by_text(identifier, slide_index, search_text, partial_match)
|
|
710
|
+
return {"matches": matches}
|
|
711
|
+
except Exception as e:
|
|
712
|
+
return _handle_tool_error("find_shape_by_text", e)
|
|
713
|
+
|
|
714
|
+
@mcp.tool()
|
|
715
|
+
def find_shapes_by_type(identifier: Union[str, int], slide_index: int, shape_type_name: str):
|
|
716
|
+
"""Finds shapes on a slide by type name (e.g., 'rectangle', 'textbox', 'picture', 'placeholder')."""
|
|
717
|
+
if not editor: return {"error": "PowerPoint editor not initialized."}
|
|
718
|
+
supported_types = list(SHAPE_TYPE_NAME_MAP.keys())
|
|
719
|
+
if shape_type_name.lower() not in supported_types:
|
|
720
|
+
return {"error": f"Unsupported shape type name '{shape_type_name}'. Try one of: {supported_types}"}
|
|
721
|
+
try:
|
|
722
|
+
matches = editor.find_shapes_by_type(identifier, slide_index, shape_type_name)
|
|
723
|
+
return {"matches": matches}
|
|
724
|
+
except Exception as e:
|
|
725
|
+
return _handle_tool_error("find_shapes_by_type", e)
|
|
726
|
+
|
|
727
|
+
@mcp.tool()
|
|
728
|
+
def get_placeholder_shape(identifier: Union[str, int], slide_index: int, placeholder_name: str):
|
|
729
|
+
"""Gets a specific placeholder shape by name (e.g., 'title', 'body', 'footer', 'slidenumber')."""
|
|
730
|
+
if not editor: return {"error": "PowerPoint editor not initialized."}
|
|
731
|
+
supported_placeholders = list(PLACEHOLDER_NAME_MAP.keys())
|
|
732
|
+
if placeholder_name.lower() not in supported_placeholders:
|
|
733
|
+
return {"error": f"Unsupported placeholder name '{placeholder_name}'. Try one of: {supported_placeholders}"}
|
|
734
|
+
try:
|
|
735
|
+
match = editor.get_placeholder_shape(identifier, slide_index, placeholder_name)
|
|
736
|
+
if match:
|
|
737
|
+
return {"placeholder_found": True, "shape_info": match}
|
|
738
|
+
else:
|
|
739
|
+
return {"placeholder_found": False, "message": f"Placeholder '{placeholder_name}' not found on slide {slide_index}."}
|
|
740
|
+
except Exception as e:
|
|
741
|
+
return _handle_tool_error("get_placeholder_shape", e)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
# You might want to add cleanup for COM objects, although Python's garbage collection
|
|
745
|
+
# combined with pywin32's handling often manages this. Explicitly setting self.app = None
|
|
746
|
+
# and maybe calling pythoncom.CoUninitialize() on shutdown could be added for robustness.
|
|
747
|
+
|
|
748
|
+
if __name__ == "__main__":
|
|
749
|
+
print("Starting PowerPoint MCP Server (Win32)...")
|
|
750
|
+
# The editor instance is created globally, attempting connection immediately.
|
|
751
|
+
if editor.app is None:
|
|
752
|
+
print("Warning: Failed to connect to PowerPoint on startup.")
|
|
753
|
+
# Server will still run, but tools will fail until PowerPoint is available
|
|
754
|
+
# and a tool call triggers a reconnect attempt.
|
|
755
|
+
|
|
756
|
+
# Run the MCP server
|
|
757
|
+
mcp.run()
|
|
758
|
+
|
|
759
|
+
# Make sure pywin32 is installed: pip install pywin32
|
|
760
|
+
# You might need to run `python Scripts/pywin32_postinstall.py -install`
|
|
761
|
+
# from your Python environment's directory if COM doesn't work initially.
|
|
762
|
+
|
|
763
|
+
# To get constants like ppSaveAsOpenXMLPresentation, run makepy:
|
|
764
|
+
# import win32com.client
|
|
765
|
+
# win32com.client.gencache.EnsureModule('{91493440-5A91-11CF-8700-00AA0060263B}', 0, 2, 12) # For Office/PPT 16.0
|
|
766
|
+
# Then you can use: from win32com.client import constants
|