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
mcp_powerpoint_server.py
ADDED
|
@@ -0,0 +1,1348 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
import traceback
|
|
4
|
+
from typing import List, Optional, Dict, Any, Tuple, Union
|
|
5
|
+
from pptx import Presentation
|
|
6
|
+
from pptx.util import Inches, Pt
|
|
7
|
+
from pptx.dml.color import RGBColor
|
|
8
|
+
from pptx.enum.shapes import MSO_SHAPE_TYPE, MSO_AUTO_SHAPE_TYPE
|
|
9
|
+
from pptx.enum.dml import MSO_FILL_TYPE, MSO_LINE
|
|
10
|
+
from pptx.chart.data import CategoryChartData
|
|
11
|
+
from pptx.enum.chart import XL_CHART_TYPE
|
|
12
|
+
import os
|
|
13
|
+
import base64
|
|
14
|
+
from PIL import Image
|
|
15
|
+
import io
|
|
16
|
+
import numpy as np
|
|
17
|
+
import uuid
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
import tempfile
|
|
20
|
+
import shutil
|
|
21
|
+
import datetime
|
|
22
|
+
from mcp.server.fastmcp import FastMCP
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PowerPointContext:
|
|
26
|
+
def __init__(self, workspace_dir: str = "presentations"):
|
|
27
|
+
"""
|
|
28
|
+
Initialize the PowerPoint context with a workspace directory.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
workspace_dir (str): Directory containing PowerPoint files
|
|
32
|
+
"""
|
|
33
|
+
self.workspace_dir = Path(workspace_dir)
|
|
34
|
+
self.workspace_dir.mkdir(exist_ok=True)
|
|
35
|
+
self.presentations: Dict[str, Presentation] = {}
|
|
36
|
+
self.current_presentation: Optional[str] = None
|
|
37
|
+
# Create a template directory
|
|
38
|
+
self.template_dir = self.workspace_dir / "templates"
|
|
39
|
+
self.template_dir.mkdir(exist_ok=True)
|
|
40
|
+
# Element ID mapping
|
|
41
|
+
self.element_ids = {} # Maps presentation path -> slide index -> shape id -> internal id
|
|
42
|
+
|
|
43
|
+
def get_presentation(self, path: str) -> Presentation:
|
|
44
|
+
"""
|
|
45
|
+
Get a presentation from the workspace.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
path (str): Path to the presentation (relative to workspace or absolute)
|
|
49
|
+
"""
|
|
50
|
+
# Convert to Path object
|
|
51
|
+
path_obj = Path(path)
|
|
52
|
+
|
|
53
|
+
# If it's not an absolute path, make it relative to workspace
|
|
54
|
+
if not path_obj.is_absolute():
|
|
55
|
+
path_obj = self.workspace_dir / path_obj
|
|
56
|
+
|
|
57
|
+
path_str = str(path_obj)
|
|
58
|
+
|
|
59
|
+
if path_str not in self.presentations:
|
|
60
|
+
if path_obj.exists():
|
|
61
|
+
self.presentations[path_str] = Presentation(path_str)
|
|
62
|
+
else:
|
|
63
|
+
self.presentations[path_str] = Presentation()
|
|
64
|
+
|
|
65
|
+
self.current_presentation = path_str
|
|
66
|
+
return self.presentations[path_str]
|
|
67
|
+
|
|
68
|
+
def save_presentation(self, path: Optional[str] = None) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Save the current presentation.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
path (Optional[str]): Path to save to (if None, uses current path)
|
|
74
|
+
"""
|
|
75
|
+
if path:
|
|
76
|
+
path_obj = Path(path)
|
|
77
|
+
if not path_obj.is_absolute():
|
|
78
|
+
path_obj = self.workspace_dir / path_obj
|
|
79
|
+
save_path = str(path_obj)
|
|
80
|
+
else:
|
|
81
|
+
save_path = self.current_presentation
|
|
82
|
+
|
|
83
|
+
if save_path and save_path in self.presentations:
|
|
84
|
+
self.presentations[save_path].save(save_path)
|
|
85
|
+
|
|
86
|
+
def list_presentations(self) -> List[str]:
|
|
87
|
+
"""List all PowerPoint files in the workspace."""
|
|
88
|
+
return [str(f.relative_to(self.workspace_dir))
|
|
89
|
+
for f in self.workspace_dir.glob("*.pptx")]
|
|
90
|
+
|
|
91
|
+
def upload_presentation(self, file_path: str) -> str:
|
|
92
|
+
"""
|
|
93
|
+
Upload a new presentation to the workspace.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
file_path (str): Path to the file to upload
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
str: Path to the saved file
|
|
100
|
+
"""
|
|
101
|
+
if not file_path.endswith('.pptx'):
|
|
102
|
+
raise ValueError("Only .pptx files are supported")
|
|
103
|
+
|
|
104
|
+
source_path = Path(file_path)
|
|
105
|
+
if not source_path.exists():
|
|
106
|
+
raise FileNotFoundError(f"File {file_path} not found")
|
|
107
|
+
|
|
108
|
+
dest_path = self.workspace_dir / source_path.name
|
|
109
|
+
|
|
110
|
+
# Copy the file to the workspace
|
|
111
|
+
shutil.copy2(source_path, dest_path)
|
|
112
|
+
|
|
113
|
+
return str(dest_path.relative_to(self.workspace_dir))
|
|
114
|
+
|
|
115
|
+
# Element Management
|
|
116
|
+
def generate_element_id(self) -> str:
|
|
117
|
+
"""Generate a unique ID for an element."""
|
|
118
|
+
return str(uuid.uuid4())
|
|
119
|
+
|
|
120
|
+
def register_element(self, presentation_path: str, slide_index: int, shape) -> str:
|
|
121
|
+
"""Register a shape and get its unique ID."""
|
|
122
|
+
if presentation_path not in self.element_ids:
|
|
123
|
+
self.element_ids[presentation_path] = {}
|
|
124
|
+
|
|
125
|
+
if slide_index not in self.element_ids[presentation_path]:
|
|
126
|
+
self.element_ids[presentation_path][slide_index] = {}
|
|
127
|
+
|
|
128
|
+
# Use shape's internal ID if available, otherwise create one
|
|
129
|
+
shape_id = getattr(shape, "shape_id", id(shape))
|
|
130
|
+
|
|
131
|
+
if shape_id not in self.element_ids[presentation_path][slide_index]:
|
|
132
|
+
element_id = self.generate_element_id()
|
|
133
|
+
self.element_ids[presentation_path][slide_index][shape_id] = element_id
|
|
134
|
+
|
|
135
|
+
return self.element_ids[presentation_path][slide_index][shape_id]
|
|
136
|
+
|
|
137
|
+
def get_shape_by_id(self, presentation: Presentation, slide_index: int, element_id: str):
|
|
138
|
+
"""Get a shape by its unique ID."""
|
|
139
|
+
try:
|
|
140
|
+
# Verify slide index is valid
|
|
141
|
+
if slide_index >= len(presentation.slides):
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
slide = presentation.slides[slide_index]
|
|
145
|
+
|
|
146
|
+
# Get the presentation path
|
|
147
|
+
presentation_path = self.current_presentation
|
|
148
|
+
if not presentation_path:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
# Check if we have element mappings for this presentation and slide
|
|
152
|
+
if (presentation_path not in self.element_ids or
|
|
153
|
+
slide_index not in self.element_ids[presentation_path]):
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
# Iterate through shapes and find matching element ID
|
|
157
|
+
for shape in slide.shapes:
|
|
158
|
+
shape_id = getattr(shape, "shape_id", id(shape))
|
|
159
|
+
if (shape_id in self.element_ids[presentation_path][slide_index] and
|
|
160
|
+
self.element_ids[presentation_path][slide_index][shape_id] == element_id):
|
|
161
|
+
return shape
|
|
162
|
+
|
|
163
|
+
# If we get here, no matching shape was found
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
print(f"Error in get_shape_by_id: {str(e)}")
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
def analyze_slide_content(self, presentation_path: str, slide) -> Dict[str, Any]:
|
|
171
|
+
"""Analyze the content of a slide and return structured information."""
|
|
172
|
+
content = {
|
|
173
|
+
"text_boxes": [],
|
|
174
|
+
"images": [],
|
|
175
|
+
"shapes": [],
|
|
176
|
+
"charts": [],
|
|
177
|
+
"tables": [],
|
|
178
|
+
"layout": None
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# Get slide layout
|
|
182
|
+
content["layout"] = slide.slide_layout.name if slide.slide_layout else None
|
|
183
|
+
|
|
184
|
+
# Analyze shapes
|
|
185
|
+
for shape in slide.shapes:
|
|
186
|
+
# Register this shape to get a unique ID
|
|
187
|
+
element_id = self.register_element(presentation_path,
|
|
188
|
+
slide.slides.index(slide),
|
|
189
|
+
shape)
|
|
190
|
+
|
|
191
|
+
shape_info = {
|
|
192
|
+
"id": element_id,
|
|
193
|
+
"type": str(shape.shape_type),
|
|
194
|
+
"position": {"x": shape.left / Inches(1), "y": shape.top / Inches(1)},
|
|
195
|
+
"size": {"width": shape.width / Inches(1), "height": shape.height / Inches(1)}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
|
|
199
|
+
content["images"].append(shape_info)
|
|
200
|
+
elif shape.shape_type == MSO_SHAPE_TYPE.TABLE:
|
|
201
|
+
content["tables"].append(shape_info)
|
|
202
|
+
elif shape.shape_type == MSO_SHAPE_TYPE.CHART:
|
|
203
|
+
content["charts"].append(shape_info)
|
|
204
|
+
elif hasattr(shape, "text") and shape.text.strip():
|
|
205
|
+
shape_info["text"] = shape.text.strip()
|
|
206
|
+
content["text_boxes"].append(shape_info)
|
|
207
|
+
else:
|
|
208
|
+
content["shapes"].append(shape_info)
|
|
209
|
+
|
|
210
|
+
return content
|
|
211
|
+
|
|
212
|
+
def find_element(self, presentation_path: str, slide_index: int,
|
|
213
|
+
element_type: str = "any", search_text: Optional[str] = None,
|
|
214
|
+
position: Optional[Dict[str, float]] = None) -> List[Dict[str, Any]]:
|
|
215
|
+
"""
|
|
216
|
+
Find elements on a slide based on type, text content, or position.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
presentation_path: Path to the presentation
|
|
220
|
+
slide_index: Index of the slide to search
|
|
221
|
+
element_type: Type of element to find (text, shape, image, chart, table, any)
|
|
222
|
+
search_text: Text to search for in element content
|
|
223
|
+
position: Position criteria for finding elements
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
List of matching elements with confidence scores
|
|
227
|
+
"""
|
|
228
|
+
presentation = self.get_presentation(presentation_path)
|
|
229
|
+
slide = presentation.slides[slide_index]
|
|
230
|
+
|
|
231
|
+
results = []
|
|
232
|
+
|
|
233
|
+
for shape in slide.shapes:
|
|
234
|
+
# Skip if filtering by type and this doesn't match
|
|
235
|
+
if element_type != "any":
|
|
236
|
+
if element_type == "text" and (not hasattr(shape, "text") or not shape.text.strip()):
|
|
237
|
+
continue
|
|
238
|
+
if element_type == "image" and shape.shape_type != MSO_SHAPE_TYPE.PICTURE:
|
|
239
|
+
continue
|
|
240
|
+
if element_type == "chart" and shape.shape_type != MSO_SHAPE_TYPE.CHART:
|
|
241
|
+
continue
|
|
242
|
+
if element_type == "table" and shape.shape_type != MSO_SHAPE_TYPE.TABLE:
|
|
243
|
+
continue
|
|
244
|
+
if element_type == "shape" and (shape.shape_type == MSO_SHAPE_TYPE.PICTURE or
|
|
245
|
+
shape.shape_type == MSO_SHAPE_TYPE.CHART or
|
|
246
|
+
shape.shape_type == MSO_SHAPE_TYPE.TABLE or
|
|
247
|
+
(hasattr(shape, "text") and shape.text.strip())):
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
# Check text content if specified
|
|
251
|
+
text_match = False
|
|
252
|
+
confidence = 1.0
|
|
253
|
+
|
|
254
|
+
if search_text and hasattr(shape, "text"):
|
|
255
|
+
shape_text = shape.text.strip().lower()
|
|
256
|
+
search_lower = search_text.lower()
|
|
257
|
+
|
|
258
|
+
if shape_text == search_lower:
|
|
259
|
+
text_match = True
|
|
260
|
+
confidence = 1.0
|
|
261
|
+
elif search_lower in shape_text:
|
|
262
|
+
text_match = True
|
|
263
|
+
# Calculate confidence based on how much of the text matches
|
|
264
|
+
confidence = len(search_lower) / len(shape_text)
|
|
265
|
+
else:
|
|
266
|
+
continue # Text doesn't match at all
|
|
267
|
+
|
|
268
|
+
# Check position if specified
|
|
269
|
+
position_match = True
|
|
270
|
+
if position:
|
|
271
|
+
shape_x = shape.left / Inches(1)
|
|
272
|
+
shape_y = shape.top / Inches(1)
|
|
273
|
+
|
|
274
|
+
# Calculate distance from target position
|
|
275
|
+
target_x = position.get("x")
|
|
276
|
+
target_y = position.get("y")
|
|
277
|
+
proximity = position.get("proximity", 1.0)
|
|
278
|
+
|
|
279
|
+
if target_x is not None and target_y is not None:
|
|
280
|
+
distance = ((shape_x - target_x) ** 2 + (shape_y - target_y) ** 2) ** 0.5
|
|
281
|
+
if distance > proximity:
|
|
282
|
+
position_match = False
|
|
283
|
+
else:
|
|
284
|
+
# Adjust confidence based on proximity
|
|
285
|
+
position_confidence = 1.0 - (distance / proximity)
|
|
286
|
+
confidence *= position_confidence
|
|
287
|
+
|
|
288
|
+
# If all criteria match, add to results
|
|
289
|
+
if (search_text is None or text_match) and position_match:
|
|
290
|
+
element_id = self.register_element(presentation_path, slide_index, shape)
|
|
291
|
+
|
|
292
|
+
element = {
|
|
293
|
+
"id": element_id,
|
|
294
|
+
"type": self._get_shape_type_name(shape),
|
|
295
|
+
"text": shape.text.strip() if hasattr(shape, "text") else None,
|
|
296
|
+
"position": {"x": shape.left / Inches(1), "y": shape.top / Inches(1)},
|
|
297
|
+
"size": {"width": shape.width / Inches(1), "height": shape.height / Inches(1)},
|
|
298
|
+
"confidence": confidence
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
results.append(element)
|
|
302
|
+
|
|
303
|
+
# Sort by confidence
|
|
304
|
+
results.sort(key=lambda x: x["confidence"], reverse=True)
|
|
305
|
+
return results
|
|
306
|
+
|
|
307
|
+
def _get_shape_type_name(self, shape) -> str:
|
|
308
|
+
"""Convert shape type to a user-friendly name."""
|
|
309
|
+
if shape.shape_type == MSO_SHAPE_TYPE.AUTO_SHAPE:
|
|
310
|
+
for name, value in vars(MSO_AUTO_SHAPE_TYPE).items():
|
|
311
|
+
if not name.startswith("__") and value == shape.auto_shape_type:
|
|
312
|
+
return name.lower()
|
|
313
|
+
return "auto_shape"
|
|
314
|
+
elif shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
|
|
315
|
+
return "image"
|
|
316
|
+
elif shape.shape_type == MSO_SHAPE_TYPE.CHART:
|
|
317
|
+
return "chart"
|
|
318
|
+
elif shape.shape_type == MSO_SHAPE_TYPE.TABLE:
|
|
319
|
+
return "table"
|
|
320
|
+
else:
|
|
321
|
+
for name, value in vars(MSO_SHAPE_TYPE).items():
|
|
322
|
+
if not name.startswith("__") and value == shape.shape_type:
|
|
323
|
+
return name.lower()
|
|
324
|
+
return "unknown"
|
|
325
|
+
|
|
326
|
+
def edit_element(self, presentation_path: str, slide_index: int,
|
|
327
|
+
element_id: str, properties: Dict[str, Any]) -> Dict[str, Any]:
|
|
328
|
+
"""
|
|
329
|
+
Edit properties of a specific element on a slide.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
presentation_path: Path to the presentation
|
|
333
|
+
slide_index: Index of the slide
|
|
334
|
+
element_id: Unique ID of the element to edit
|
|
335
|
+
properties: Properties to modify (text, position, size, rotation, etc.)
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Updated properties of the element
|
|
339
|
+
"""
|
|
340
|
+
try:
|
|
341
|
+
# First verify the presentation exists and can be loaded
|
|
342
|
+
presentation = self.get_presentation(presentation_path)
|
|
343
|
+
if not presentation:
|
|
344
|
+
return {"error": f"Could not load presentation: {presentation_path}"}
|
|
345
|
+
|
|
346
|
+
# Verify the slide index is valid
|
|
347
|
+
if slide_index >= len(presentation.slides):
|
|
348
|
+
return {"error": f"Invalid slide index {slide_index}. Presentation has {len(presentation.slides)} slides."}
|
|
349
|
+
|
|
350
|
+
# Get the shape and handle None case explicitly
|
|
351
|
+
shape = self.get_shape_by_id(presentation, slide_index, element_id)
|
|
352
|
+
if not shape:
|
|
353
|
+
return {"error": f"Could not find element with ID {element_id} on slide {slide_index}"}
|
|
354
|
+
|
|
355
|
+
# Apply changes based on properties
|
|
356
|
+
if "text" in properties and hasattr(shape, "text_frame"):
|
|
357
|
+
shape.text_frame.text = properties["text"]
|
|
358
|
+
|
|
359
|
+
if "position" in properties:
|
|
360
|
+
position = properties["position"]
|
|
361
|
+
if "x" in position:
|
|
362
|
+
shape.left = Inches(position["x"])
|
|
363
|
+
if "y" in position:
|
|
364
|
+
shape.top = Inches(position["y"])
|
|
365
|
+
|
|
366
|
+
if "size" in properties:
|
|
367
|
+
size = properties["size"]
|
|
368
|
+
if "width" in size:
|
|
369
|
+
shape.width = Inches(size["width"])
|
|
370
|
+
if "height" in size:
|
|
371
|
+
shape.height = Inches(size["height"])
|
|
372
|
+
|
|
373
|
+
if "rotation" in properties:
|
|
374
|
+
shape.rotation = properties["rotation"]
|
|
375
|
+
|
|
376
|
+
if "transparency" in properties and hasattr(shape, "fill"):
|
|
377
|
+
alpha = int(255 * (100 - properties["transparency"]) / 100)
|
|
378
|
+
if hasattr(shape.fill.fore_color, "transparency"):
|
|
379
|
+
shape.fill.fore_color.transparency = (255 - alpha) / 255
|
|
380
|
+
|
|
381
|
+
# Return updated properties
|
|
382
|
+
updated_props = {
|
|
383
|
+
"text": shape.text if hasattr(shape, "text") else None,
|
|
384
|
+
"position": {"x": shape.left / Inches(1), "y": shape.top / Inches(1)},
|
|
385
|
+
"size": {"width": shape.width / Inches(1), "height": shape.height / Inches(1)},
|
|
386
|
+
"rotation": shape.rotation if hasattr(shape, "rotation") else None
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
# Save the changes
|
|
390
|
+
self.save_presentation(presentation_path)
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
"message": "Element updated successfully",
|
|
394
|
+
"properties": updated_props
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
except Exception as e:
|
|
398
|
+
# Add more context to the error message
|
|
399
|
+
error_msg = f"Error editing element: {str(e)}\n"
|
|
400
|
+
error_msg += f"Presentation: {presentation_path}\n"
|
|
401
|
+
error_msg += f"Slide: {slide_index}\n"
|
|
402
|
+
error_msg += f"Element ID: {element_id}\n"
|
|
403
|
+
error_msg += f"Properties: {properties}"
|
|
404
|
+
return {"error": error_msg}
|
|
405
|
+
|
|
406
|
+
def style_element(self, presentation_path: str, slide_index: int,
|
|
407
|
+
element_id: str, style_properties: Dict[str, Any]) -> bool:
|
|
408
|
+
"""
|
|
409
|
+
Apply styling to a specific element on a slide.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
presentation_path: Path to the presentation
|
|
413
|
+
slide_index: Index of the slide
|
|
414
|
+
element_id: Unique ID of the element to style
|
|
415
|
+
style_properties: Style properties to apply
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
True if styling was applied successfully
|
|
419
|
+
"""
|
|
420
|
+
presentation = self.get_presentation(presentation_path)
|
|
421
|
+
shape = self.get_shape_by_id(presentation, slide_index, element_id)
|
|
422
|
+
|
|
423
|
+
# Apply font styling
|
|
424
|
+
if "font" in style_properties and hasattr(shape, "text_frame"):
|
|
425
|
+
font_props = style_properties["font"]
|
|
426
|
+
|
|
427
|
+
for paragraph in shape.text_frame.paragraphs:
|
|
428
|
+
for run in paragraph.runs:
|
|
429
|
+
if "family" in font_props:
|
|
430
|
+
run.font.name = font_props["family"]
|
|
431
|
+
|
|
432
|
+
if "size" in font_props:
|
|
433
|
+
run.font.size = Pt(font_props["size"])
|
|
434
|
+
|
|
435
|
+
if "bold" in font_props:
|
|
436
|
+
run.font.bold = font_props["bold"]
|
|
437
|
+
|
|
438
|
+
if "italic" in font_props:
|
|
439
|
+
run.font.italic = font_props["italic"]
|
|
440
|
+
|
|
441
|
+
if "underline" in font_props:
|
|
442
|
+
run.font.underline = font_props["underline"]
|
|
443
|
+
|
|
444
|
+
if "color" in font_props:
|
|
445
|
+
color = font_props["color"].lstrip('#')
|
|
446
|
+
r, g, b = tuple(int(color[i:i+2], 16) for i in (0, 2, 4))
|
|
447
|
+
run.font.color.rgb = RGBColor(r, g, b)
|
|
448
|
+
|
|
449
|
+
# Apply fill styling
|
|
450
|
+
if "fill" in style_properties and hasattr(shape, "fill"):
|
|
451
|
+
fill_props = style_properties["fill"]
|
|
452
|
+
|
|
453
|
+
if fill_props.get("type") == "solid" and "color" in fill_props:
|
|
454
|
+
shape.fill.solid()
|
|
455
|
+
color = fill_props["color"].lstrip('#')
|
|
456
|
+
r, g, b = tuple(int(color[i:i+2], 16) for i in (0, 2, 4))
|
|
457
|
+
shape.fill.fore_color.rgb = RGBColor(r, g, b)
|
|
458
|
+
|
|
459
|
+
elif fill_props.get("type") == "gradient" and "gradient" in fill_props:
|
|
460
|
+
# PowerPoint API doesn't support setting gradient directly
|
|
461
|
+
# This would require more complex implementation
|
|
462
|
+
shape.fill.gradient()
|
|
463
|
+
|
|
464
|
+
# Set start color
|
|
465
|
+
start_color = fill_props["gradient"]["start_color"].lstrip('#')
|
|
466
|
+
r, g, b = tuple(int(start_color[i:i+2], 16) for i in (0, 2, 4))
|
|
467
|
+
shape.fill.gradient_stops[0].color.rgb = RGBColor(r, g, b)
|
|
468
|
+
|
|
469
|
+
# Set end color
|
|
470
|
+
end_color = fill_props["gradient"]["end_color"].lstrip('#')
|
|
471
|
+
r, g, b = tuple(int(end_color[i:i+2], 16) for i in (0, 2, 4))
|
|
472
|
+
shape.fill.gradient_stops[-1].color.rgb = RGBColor(r, g, b)
|
|
473
|
+
|
|
474
|
+
elif fill_props.get("type") == "none":
|
|
475
|
+
shape.fill.background()
|
|
476
|
+
|
|
477
|
+
# Apply line styling
|
|
478
|
+
if "line" in style_properties and hasattr(shape, "line"):
|
|
479
|
+
line_props = style_properties["line"]
|
|
480
|
+
|
|
481
|
+
if "color" in line_props:
|
|
482
|
+
color = line_props["color"].lstrip('#')
|
|
483
|
+
r, g, b = tuple(int(color[i:i+2], 16) for i in (0, 2, 4))
|
|
484
|
+
shape.line.color.rgb = RGBColor(r, g, b)
|
|
485
|
+
|
|
486
|
+
if "width" in line_props:
|
|
487
|
+
shape.line.width = Pt(line_props["width"])
|
|
488
|
+
|
|
489
|
+
if "style" in line_props:
|
|
490
|
+
# Map string style names to PowerPoint constants
|
|
491
|
+
style_map = {
|
|
492
|
+
"solid": MSO_LINE.SOLID,
|
|
493
|
+
"dash": MSO_LINE.DASH,
|
|
494
|
+
"dot": MSO_LINE.ROUND_DOT,
|
|
495
|
+
"dash-dot": MSO_LINE.DASH_DOT,
|
|
496
|
+
"none": None
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if line_props["style"] in style_map:
|
|
500
|
+
if style_map[line_props["style"]] is None:
|
|
501
|
+
shape.line.fill.background() # No line
|
|
502
|
+
else:
|
|
503
|
+
shape.line.dash_style = style_map[line_props["style"]]
|
|
504
|
+
|
|
505
|
+
return True
|
|
506
|
+
|
|
507
|
+
def find_slide_by_content(self, presentation_path: str, search_text: str) -> Optional[int]:
|
|
508
|
+
"""Find a slide index by searching through its content."""
|
|
509
|
+
presentation = self.get_presentation(presentation_path)
|
|
510
|
+
for idx, slide in enumerate(presentation.slides):
|
|
511
|
+
for shape in slide.shapes:
|
|
512
|
+
if hasattr(shape, "text") and search_text.lower() in shape.text.lower():
|
|
513
|
+
return idx
|
|
514
|
+
return None
|
|
515
|
+
|
|
516
|
+
def get_slide_preview(self, presentation_path: str, slide_index: int) -> str:
|
|
517
|
+
"""Generate a preview of the slide as a base64 encoded image."""
|
|
518
|
+
presentation = self.get_presentation(presentation_path)
|
|
519
|
+
slide = presentation.slides[slide_index]
|
|
520
|
+
|
|
521
|
+
# This is a placeholder - in a real implementation, you would need to
|
|
522
|
+
# use a proper PowerPoint rendering library or service
|
|
523
|
+
# For now, we'll create a simple visualization using PIL
|
|
524
|
+
width, height = 1920, 1080 # Standard slide dimensions
|
|
525
|
+
img = Image.new('RGB', (width, height), 'white')
|
|
526
|
+
|
|
527
|
+
# Draw shapes and text (simplified)
|
|
528
|
+
from PIL import ImageDraw, ImageFont
|
|
529
|
+
draw = ImageDraw.Draw(img)
|
|
530
|
+
|
|
531
|
+
for shape in slide.shapes:
|
|
532
|
+
if hasattr(shape, "text") and shape.text.strip():
|
|
533
|
+
# Convert PowerPoint coordinates to image coordinates
|
|
534
|
+
x = int(shape.left * width / 914400) # Convert EMU to pixels
|
|
535
|
+
y = int(shape.top * height / 685800)
|
|
536
|
+
draw.text((x, y), shape.text, fill='black')
|
|
537
|
+
|
|
538
|
+
# Convert to base64
|
|
539
|
+
buffered = io.BytesIO()
|
|
540
|
+
img.save(buffered, format="PNG")
|
|
541
|
+
return base64.b64encode(buffered.getvalue()).decode()
|
|
542
|
+
|
|
543
|
+
def add_shape(self, presentation_path: str, slide_index: int, shape_type: str,
|
|
544
|
+
position: Dict[str, float], size: Dict[str, float],
|
|
545
|
+
style_properties: Optional[Dict[str, Any]] = None) -> str:
|
|
546
|
+
"""
|
|
547
|
+
Add a new shape to a slide.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
presentation_path: Path to the presentation
|
|
551
|
+
slide_index: Index of the slide
|
|
552
|
+
shape_type: Type of shape to add
|
|
553
|
+
position: Position of the shape (x, y)
|
|
554
|
+
size: Size of the shape (width, height)
|
|
555
|
+
style_properties: Style properties for the shape
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
ID of the created shape
|
|
559
|
+
"""
|
|
560
|
+
presentation = self.get_presentation(presentation_path)
|
|
561
|
+
slide = presentation.slides[slide_index]
|
|
562
|
+
|
|
563
|
+
# Map shape type names to PowerPoint constants
|
|
564
|
+
shape_map = {
|
|
565
|
+
# Basic shapes
|
|
566
|
+
"rectangle": MSO_AUTO_SHAPE_TYPE.RECTANGLE,
|
|
567
|
+
"rounded_rectangle": MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE,
|
|
568
|
+
"oval": MSO_AUTO_SHAPE_TYPE.OVAL,
|
|
569
|
+
"triangle": MSO_AUTO_SHAPE_TYPE.TRIANGLE,
|
|
570
|
+
"right_triangle": MSO_AUTO_SHAPE_TYPE.RIGHT_TRIANGLE,
|
|
571
|
+
"diamond": MSO_AUTO_SHAPE_TYPE.DIAMOND,
|
|
572
|
+
"pentagon": MSO_AUTO_SHAPE_TYPE.PENTAGON,
|
|
573
|
+
"hexagon": MSO_AUTO_SHAPE_TYPE.HEXAGON,
|
|
574
|
+
"heptagon": MSO_AUTO_SHAPE_TYPE.HEPTAGON,
|
|
575
|
+
"octagon": MSO_AUTO_SHAPE_TYPE.OCTAGON,
|
|
576
|
+
"decagon": MSO_AUTO_SHAPE_TYPE.DECAGON,
|
|
577
|
+
"dodecagon": MSO_AUTO_SHAPE_TYPE.DODECAGON,
|
|
578
|
+
|
|
579
|
+
# Stars
|
|
580
|
+
"star4": MSO_AUTO_SHAPE_TYPE.STAR_4_POINT,
|
|
581
|
+
"star5": MSO_AUTO_SHAPE_TYPE.STAR_5_POINT,
|
|
582
|
+
"star6": MSO_AUTO_SHAPE_TYPE.STAR_6_POINT,
|
|
583
|
+
"star7": MSO_AUTO_SHAPE_TYPE.STAR_7_POINT,
|
|
584
|
+
"star8": MSO_AUTO_SHAPE_TYPE.STAR_8_POINT,
|
|
585
|
+
"star10": MSO_AUTO_SHAPE_TYPE.STAR_10_POINT,
|
|
586
|
+
"star12": MSO_AUTO_SHAPE_TYPE.STAR_12_POINT,
|
|
587
|
+
"star16": MSO_AUTO_SHAPE_TYPE.STAR_16_POINT,
|
|
588
|
+
"star24": MSO_AUTO_SHAPE_TYPE.STAR_24_POINT,
|
|
589
|
+
"star32": MSO_AUTO_SHAPE_TYPE.STAR_32_POINT,
|
|
590
|
+
|
|
591
|
+
# Arrows
|
|
592
|
+
"arrow": MSO_AUTO_SHAPE_TYPE.ARROW,
|
|
593
|
+
"left_arrow": MSO_AUTO_SHAPE_TYPE.LEFT_ARROW,
|
|
594
|
+
"right_arrow": MSO_AUTO_SHAPE_TYPE.RIGHT_ARROW,
|
|
595
|
+
"up_arrow": MSO_AUTO_SHAPE_TYPE.UP_ARROW,
|
|
596
|
+
"down_arrow": MSO_AUTO_SHAPE_TYPE.DOWN_ARROW,
|
|
597
|
+
"left_right_arrow": MSO_AUTO_SHAPE_TYPE.LEFT_RIGHT_ARROW,
|
|
598
|
+
"up_down_arrow": MSO_AUTO_SHAPE_TYPE.UP_DOWN_ARROW,
|
|
599
|
+
|
|
600
|
+
# Special shapes
|
|
601
|
+
"heart": MSO_AUTO_SHAPE_TYPE.HEART,
|
|
602
|
+
"lightning_bolt": MSO_AUTO_SHAPE_TYPE.LIGHTNING_BOLT,
|
|
603
|
+
"sun": MSO_AUTO_SHAPE_TYPE.SUN,
|
|
604
|
+
"moon": MSO_AUTO_SHAPE_TYPE.MOON,
|
|
605
|
+
"smiley_face": MSO_AUTO_SHAPE_TYPE.SMILEY_FACE,
|
|
606
|
+
"cloud": MSO_AUTO_SHAPE_TYPE.CLOUD,
|
|
607
|
+
|
|
608
|
+
# Process shapes
|
|
609
|
+
"flow_chart_process": MSO_AUTO_SHAPE_TYPE.FLOW_CHART_PROCESS,
|
|
610
|
+
"flow_chart_decision": MSO_AUTO_SHAPE_TYPE.FLOW_CHART_DECISION,
|
|
611
|
+
"flow_chart_connector": MSO_AUTO_SHAPE_TYPE.FLOW_CHART_CONNECTOR,
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if shape_type.lower() not in shape_map:
|
|
615
|
+
raise ValueError(f"Unsupported shape type: {shape_type}")
|
|
616
|
+
|
|
617
|
+
shape_type_enum = shape_map[shape_type.lower()]
|
|
618
|
+
left = Inches(position.get("x", 0))
|
|
619
|
+
top = Inches(position.get("y", 0))
|
|
620
|
+
width = Inches(size.get("width", 1))
|
|
621
|
+
height = Inches(size.get("height", 1))
|
|
622
|
+
|
|
623
|
+
shape = slide.shapes.add_shape(shape_type_enum, left, top, width, height)
|
|
624
|
+
|
|
625
|
+
# Register the shape to get a unique ID
|
|
626
|
+
element_id = self.register_element(presentation_path, slide_index, shape)
|
|
627
|
+
|
|
628
|
+
# Apply style properties if provided
|
|
629
|
+
if style_properties:
|
|
630
|
+
self.style_element(presentation_path, slide_index, element_id, style_properties)
|
|
631
|
+
|
|
632
|
+
return element_id
|
|
633
|
+
|
|
634
|
+
def connect_shapes(self, presentation_path: str, slide_index: int,
|
|
635
|
+
from_element_id: str, to_element_id: str,
|
|
636
|
+
connector_type: str = "straight",
|
|
637
|
+
style_properties: Optional[Dict[str, Any]] = None) -> str:
|
|
638
|
+
"""
|
|
639
|
+
Create a connector between two shapes on a slide.
|
|
640
|
+
|
|
641
|
+
Args:
|
|
642
|
+
presentation_path: Path to the presentation
|
|
643
|
+
slide_index: Index of the slide
|
|
644
|
+
from_element_id: ID of the starting element
|
|
645
|
+
to_element_id: ID of the ending element
|
|
646
|
+
connector_type: Type of connector to create (straight, elbow, curved)
|
|
647
|
+
style_properties: Style properties for the connector
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
ID of the created connector
|
|
651
|
+
"""
|
|
652
|
+
presentation = self.get_presentation(presentation_path)
|
|
653
|
+
slide = presentation.slides[slide_index]
|
|
654
|
+
|
|
655
|
+
# Get the shapes
|
|
656
|
+
from_shape = self.get_shape_by_id(presentation, slide_index, from_element_id)
|
|
657
|
+
to_shape = self.get_shape_by_id(presentation, slide_index, to_element_id)
|
|
658
|
+
|
|
659
|
+
# Map connector types to PowerPoint constants
|
|
660
|
+
connector_map = {
|
|
661
|
+
"straight": MSO_AUTO_SHAPE_TYPE.LINE_CONNECTOR_1,
|
|
662
|
+
"elbow": MSO_AUTO_SHAPE_TYPE.LINE_CONNECTOR_3,
|
|
663
|
+
"curved": MSO_AUTO_SHAPE_TYPE.CURVED_CONNECTOR_3
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if connector_type.lower() not in connector_map:
|
|
667
|
+
raise ValueError(f"Unsupported connector type: {connector_type}")
|
|
668
|
+
|
|
669
|
+
connector_type_enum = connector_map[connector_type.lower()]
|
|
670
|
+
|
|
671
|
+
# Add connector
|
|
672
|
+
# Note: python-pptx doesn't have direct support for connecting shapes
|
|
673
|
+
# We create a line in between as an approximation
|
|
674
|
+
start_x = from_shape.left + from_shape.width / 2
|
|
675
|
+
start_y = from_shape.top + from_shape.height / 2
|
|
676
|
+
end_x = to_shape.left + to_shape.width / 2
|
|
677
|
+
end_y = to_shape.top + to_shape.height / 2
|
|
678
|
+
|
|
679
|
+
# Create connector
|
|
680
|
+
connector = slide.shapes.add_connector(
|
|
681
|
+
connector_type_enum,
|
|
682
|
+
start_x, start_y, end_x - start_x, end_y - start_y
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# Register the connector to get a unique ID
|
|
686
|
+
element_id = self.register_element(presentation_path, slide_index, connector)
|
|
687
|
+
|
|
688
|
+
# Apply style properties if provided
|
|
689
|
+
if style_properties:
|
|
690
|
+
self.style_element(presentation_path, slide_index, element_id, style_properties)
|
|
691
|
+
|
|
692
|
+
return element_id
|
|
693
|
+
|
|
694
|
+
def get_company_financials(self, company_id: str,
|
|
695
|
+
metrics: List[str] = None,
|
|
696
|
+
years: List[int] = None) -> Dict[str, Any]:
|
|
697
|
+
"""
|
|
698
|
+
Get financial data for a company.
|
|
699
|
+
|
|
700
|
+
Args:
|
|
701
|
+
company_id: Company identifier
|
|
702
|
+
metrics: List of financial metrics to retrieve
|
|
703
|
+
years: List of years to retrieve data for
|
|
704
|
+
|
|
705
|
+
Returns:
|
|
706
|
+
Dictionary containing financial data
|
|
707
|
+
"""
|
|
708
|
+
if metrics is None:
|
|
709
|
+
metrics = ["revenue", "ebitda", "profit"]
|
|
710
|
+
|
|
711
|
+
# This is a placeholder - in a real implementation, you would:
|
|
712
|
+
# 1. Connect to a financial data API (e.g., Proff API for Norwegian companies)
|
|
713
|
+
# 2. Fetch the requested metrics for the specified company and years
|
|
714
|
+
# 3. Return the actual financial data
|
|
715
|
+
|
|
716
|
+
# For now, return dummy data
|
|
717
|
+
data = {
|
|
718
|
+
"company_id": company_id,
|
|
719
|
+
"metrics": {},
|
|
720
|
+
"years": years or [2022, 2023, 2024]
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
for metric in metrics:
|
|
724
|
+
data["metrics"][metric] = {
|
|
725
|
+
year: round(1000000 * (year - 2020) * (1 + 0.1 * (year - 2020)), 2)
|
|
726
|
+
for year in (years or [2022, 2023, 2024])
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return data
|
|
730
|
+
|
|
731
|
+
def create_financial_chart(self, presentation_path: str, slide_index: int,
|
|
732
|
+
chart_type: str, data: Dict[str, Any],
|
|
733
|
+
position: Dict[str, float], size: Dict[str, float],
|
|
734
|
+
title: str = None) -> str:
|
|
735
|
+
"""
|
|
736
|
+
Create a financial chart on a slide.
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
presentation_path: Path to the presentation
|
|
740
|
+
slide_index: Index of the slide
|
|
741
|
+
chart_type: Type of chart to create
|
|
742
|
+
data: Chart data
|
|
743
|
+
position: Position of the chart
|
|
744
|
+
size: Size of the chart
|
|
745
|
+
title: Chart title
|
|
746
|
+
|
|
747
|
+
Returns:
|
|
748
|
+
ID of the created chart
|
|
749
|
+
"""
|
|
750
|
+
presentation = self.get_presentation(presentation_path)
|
|
751
|
+
slide = presentation.slides[slide_index]
|
|
752
|
+
|
|
753
|
+
# Map chart type names to PowerPoint constants
|
|
754
|
+
chart_type_map = {
|
|
755
|
+
"line": XL_CHART_TYPE.LINE,
|
|
756
|
+
"bar": XL_CHART_TYPE.COLUMN_CLUSTERED,
|
|
757
|
+
"column": XL_CHART_TYPE.COLUMN_CLUSTERED,
|
|
758
|
+
"pie": XL_CHART_TYPE.PIE,
|
|
759
|
+
"area": XL_CHART_TYPE.AREA,
|
|
760
|
+
"scatter": XL_CHART_TYPE.XY_SCATTER,
|
|
761
|
+
"doughnut": XL_CHART_TYPE.DOUGHNUT,
|
|
762
|
+
"radar": XL_CHART_TYPE.RADAR,
|
|
763
|
+
"waterfall": XL_CHART_TYPE.COLUMN_STACKED,
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if chart_type.lower() not in chart_type_map:
|
|
767
|
+
raise ValueError(f"Unsupported chart type: {chart_type}")
|
|
768
|
+
|
|
769
|
+
# Create chart
|
|
770
|
+
left = Inches(position.get("x", 0))
|
|
771
|
+
top = Inches(position.get("y", 0))
|
|
772
|
+
width = Inches(size.get("width", 5))
|
|
773
|
+
height = Inches(size.get("height", 3))
|
|
774
|
+
|
|
775
|
+
chart = slide.shapes.add_chart(
|
|
776
|
+
chart_type_map[chart_type.lower()],
|
|
777
|
+
left, top, width, height
|
|
778
|
+
).chart
|
|
779
|
+
|
|
780
|
+
# Set chart data
|
|
781
|
+
chart_data = CategoryChartData()
|
|
782
|
+
|
|
783
|
+
# Add categories (years)
|
|
784
|
+
years = data.get("years", [2022, 2023, 2024])
|
|
785
|
+
chart_data.categories = years
|
|
786
|
+
|
|
787
|
+
# Add series
|
|
788
|
+
for metric_name, metric_values in data.get("metrics", {}).items():
|
|
789
|
+
chart_data.add_series(metric_name, [metric_values[year] for year in years])
|
|
790
|
+
|
|
791
|
+
chart.replace_data(chart_data)
|
|
792
|
+
|
|
793
|
+
# Set title if provided
|
|
794
|
+
if title:
|
|
795
|
+
chart.chart_title.text_frame.text = title
|
|
796
|
+
else:
|
|
797
|
+
chart.has_title = False
|
|
798
|
+
|
|
799
|
+
# Register the chart to get a unique ID
|
|
800
|
+
element_id = self.register_element(presentation_path, slide_index, chart)
|
|
801
|
+
|
|
802
|
+
return element_id
|
|
803
|
+
|
|
804
|
+
def create_comparison_table(self, presentation_path: str, slide_index: int,
|
|
805
|
+
companies: List[str], metrics: List[str],
|
|
806
|
+
position: Dict[str, float], title: str = None) -> str:
|
|
807
|
+
"""
|
|
808
|
+
Create a comparison table for companies.
|
|
809
|
+
|
|
810
|
+
Args:
|
|
811
|
+
presentation_path: Path to the presentation
|
|
812
|
+
slide_index: Index of the slide
|
|
813
|
+
companies: List of company names
|
|
814
|
+
metrics: List of metrics to compare
|
|
815
|
+
position: Position of the table
|
|
816
|
+
title: Table title
|
|
817
|
+
|
|
818
|
+
Returns:
|
|
819
|
+
ID of the created table
|
|
820
|
+
"""
|
|
821
|
+
presentation = self.get_presentation(presentation_path)
|
|
822
|
+
slide = presentation.slides[slide_index]
|
|
823
|
+
|
|
824
|
+
# Create table
|
|
825
|
+
rows = len(companies) + 1 # Header row + one row per company
|
|
826
|
+
cols = len(metrics) + 1 # First column for company name + one column per metric
|
|
827
|
+
|
|
828
|
+
left = Inches(position.get("x", 0))
|
|
829
|
+
top = Inches(position.get("y", 0))
|
|
830
|
+
width = Inches(size.get("width", 6))
|
|
831
|
+
height = Inches(size.get("height", 3))
|
|
832
|
+
|
|
833
|
+
table = slide.shapes.add_table(rows, cols, left, top, width, height).table
|
|
834
|
+
|
|
835
|
+
# Fill header row
|
|
836
|
+
table.cell(0, 0).text = "Company"
|
|
837
|
+
for col_idx, metric in enumerate(metrics):
|
|
838
|
+
table.cell(0, col_idx + 1).text = metric
|
|
839
|
+
|
|
840
|
+
# Fill data rows
|
|
841
|
+
for row_idx, company in enumerate(companies):
|
|
842
|
+
table.cell(row_idx + 1, 0).text = company
|
|
843
|
+
for col_idx, metric in enumerate(metrics):
|
|
844
|
+
# Generate dummy data for now
|
|
845
|
+
value = round(1000000 * (row_idx + 1) * (1 + 0.1 * col_idx), 2)
|
|
846
|
+
table.cell(row_idx + 1, col_idx + 1).text = str(value)
|
|
847
|
+
|
|
848
|
+
# Register the table to get a unique ID
|
|
849
|
+
element_id = self.register_element(presentation_path, slide_index, table)
|
|
850
|
+
|
|
851
|
+
return element_id
|
|
852
|
+
|
|
853
|
+
# Template operations
|
|
854
|
+
def list_templates(self) -> List[Dict[str, str]]:
|
|
855
|
+
"""List all available templates."""
|
|
856
|
+
templates = []
|
|
857
|
+
for template_file in self.template_dir.glob("*.pptx"):
|
|
858
|
+
templates.append({
|
|
859
|
+
"name": template_file.stem,
|
|
860
|
+
"path": str(template_file)
|
|
861
|
+
})
|
|
862
|
+
return templates
|
|
863
|
+
|
|
864
|
+
def apply_template(self, presentation_path: str, template_name: str,
|
|
865
|
+
options: Dict[str, bool] = None) -> Dict[str, Any]:
|
|
866
|
+
"""
|
|
867
|
+
Apply a template to a presentation.
|
|
868
|
+
|
|
869
|
+
Args:
|
|
870
|
+
presentation_path: Path to the presentation
|
|
871
|
+
template_name: Name of the template
|
|
872
|
+
options: Options for template application
|
|
873
|
+
|
|
874
|
+
Returns:
|
|
875
|
+
Information about applied elements
|
|
876
|
+
"""
|
|
877
|
+
if options is None:
|
|
878
|
+
options = {}
|
|
879
|
+
|
|
880
|
+
template_path = self.template_dir / f"{template_name}.pptx"
|
|
881
|
+
if not template_path.exists():
|
|
882
|
+
raise FileNotFoundError(f"Template not found: {template_name}")
|
|
883
|
+
|
|
884
|
+
template = Presentation(str(template_path))
|
|
885
|
+
presentation = self.get_presentation(presentation_path)
|
|
886
|
+
|
|
887
|
+
# For now, this is a simplified implementation
|
|
888
|
+
# In a full implementation, you would:
|
|
889
|
+
# 1. Copy slides from template to presentation
|
|
890
|
+
# 2. Apply master slides
|
|
891
|
+
# 3. Copy themes and styles
|
|
892
|
+
|
|
893
|
+
applied_elements = {
|
|
894
|
+
"slides": len(template.slides),
|
|
895
|
+
"themes": 1
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return applied_elements
|
|
899
|
+
|
|
900
|
+
def create_slide_from_template(self, presentation_path: str, template_name: str,
|
|
901
|
+
content: Dict[str, Any] = None) -> Dict[str, Any]:
|
|
902
|
+
"""
|
|
903
|
+
Create a new slide from a template.
|
|
904
|
+
|
|
905
|
+
Args:
|
|
906
|
+
presentation_path: Path to the presentation
|
|
907
|
+
template_name: Name of the template
|
|
908
|
+
content: Content to fill into the template
|
|
909
|
+
|
|
910
|
+
Returns:
|
|
911
|
+
Information about the created slide
|
|
912
|
+
"""
|
|
913
|
+
template_path = self.template_dir / f"{template_name}.pptx"
|
|
914
|
+
if not template_path.exists():
|
|
915
|
+
raise FileNotFoundError(f"Template not found: {template_name}")
|
|
916
|
+
|
|
917
|
+
template = Presentation(str(template_path))
|
|
918
|
+
presentation = self.get_presentation(presentation_path)
|
|
919
|
+
|
|
920
|
+
# Add a blank slide
|
|
921
|
+
slide_layout = presentation.slide_layouts[6] # Blank layout
|
|
922
|
+
slide = presentation.slides.add_slide(slide_layout)
|
|
923
|
+
|
|
924
|
+
# Copy content from template (simplified)
|
|
925
|
+
if template.slides:
|
|
926
|
+
template_slide = template.slides[0]
|
|
927
|
+
for shape in template_slide.shapes:
|
|
928
|
+
if hasattr(shape, "text") and shape.text.strip():
|
|
929
|
+
# Copy text shapes
|
|
930
|
+
new_shape = slide.shapes.add_textbox(
|
|
931
|
+
shape.left, shape.top, shape.width, shape.height
|
|
932
|
+
)
|
|
933
|
+
new_shape.text_frame.text = shape.text
|
|
934
|
+
|
|
935
|
+
slide_index = len(presentation.slides) - 1
|
|
936
|
+
|
|
937
|
+
# Populate placeholders if content is provided
|
|
938
|
+
populated_placeholders = []
|
|
939
|
+
if content:
|
|
940
|
+
for key, value in content.items():
|
|
941
|
+
populated_placeholders.append({"key": key, "value": value})
|
|
942
|
+
|
|
943
|
+
return {
|
|
944
|
+
"slide_index": slide_index,
|
|
945
|
+
"populated_placeholders": populated_placeholders
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
def save_as_template(self, presentation_path: str, slide_index: int,
|
|
949
|
+
template_name: str, template_description: str = "") -> Dict[str, Any]:
|
|
950
|
+
"""
|
|
951
|
+
Save a slide as a template.
|
|
952
|
+
|
|
953
|
+
Args:
|
|
954
|
+
presentation_path: Path to the presentation
|
|
955
|
+
slide_index: Index of the slide
|
|
956
|
+
template_name: Name for the template
|
|
957
|
+
template_description: Description of the template
|
|
958
|
+
|
|
959
|
+
Returns:
|
|
960
|
+
Information about the saved template
|
|
961
|
+
"""
|
|
962
|
+
presentation = self.get_presentation(presentation_path)
|
|
963
|
+
|
|
964
|
+
if slide_index >= len(presentation.slides):
|
|
965
|
+
raise IndexError(f"Slide index {slide_index} out of range")
|
|
966
|
+
|
|
967
|
+
# Create a new presentation with just this slide
|
|
968
|
+
template_presentation = Presentation()
|
|
969
|
+
slide = presentation.slides[slide_index]
|
|
970
|
+
template_presentation.slides.add_slide(slide.slide_layout)
|
|
971
|
+
|
|
972
|
+
# Copy all shapes from the original slide
|
|
973
|
+
for shape in slide.shapes:
|
|
974
|
+
# This is a simplified copy - in a full implementation,
|
|
975
|
+
# you would need to properly copy all shape properties
|
|
976
|
+
if hasattr(shape, "text"):
|
|
977
|
+
new_shape = template_presentation.slides[0].shapes.add_textbox(
|
|
978
|
+
shape.left, shape.top, shape.width, shape.height
|
|
979
|
+
)
|
|
980
|
+
new_shape.text_frame.text = shape.text
|
|
981
|
+
|
|
982
|
+
# Save the template
|
|
983
|
+
template_path = self.template_dir / f"{template_name}.pptx"
|
|
984
|
+
template_presentation.save(str(template_path))
|
|
985
|
+
|
|
986
|
+
return {
|
|
987
|
+
"template_name": template_name,
|
|
988
|
+
"template_path": str(template_path),
|
|
989
|
+
"description": template_description
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
# Initialize the context
|
|
994
|
+
context = PowerPointContext()
|
|
995
|
+
|
|
996
|
+
# Create the MCP server
|
|
997
|
+
mcp = FastMCP("PowerPoint MCP")
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
# Presentation Management Tools
|
|
1001
|
+
@mcp.tool()
|
|
1002
|
+
def list_presentations():
|
|
1003
|
+
"""List all PowerPoint files in the workspace."""
|
|
1004
|
+
try:
|
|
1005
|
+
presentations = context.list_presentations()
|
|
1006
|
+
return {"presentations": presentations}
|
|
1007
|
+
except Exception as e:
|
|
1008
|
+
return {"error": str(e)}
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
@mcp.tool()
|
|
1012
|
+
def upload_presentation(file_path: str):
|
|
1013
|
+
"""Upload a new presentation to the workspace."""
|
|
1014
|
+
try:
|
|
1015
|
+
saved_path = context.upload_presentation(file_path)
|
|
1016
|
+
return {
|
|
1017
|
+
"message": "Presentation uploaded successfully",
|
|
1018
|
+
"path": saved_path
|
|
1019
|
+
}
|
|
1020
|
+
except Exception as e:
|
|
1021
|
+
return {"error": str(e)}
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
@mcp.tool()
|
|
1025
|
+
def save_presentation(presentation_path: str = None):
|
|
1026
|
+
"""Save the current presentation."""
|
|
1027
|
+
try:
|
|
1028
|
+
context.save_presentation(presentation_path)
|
|
1029
|
+
return {"message": "Presentation saved successfully"}
|
|
1030
|
+
except Exception as e:
|
|
1031
|
+
return {"error": str(e)}
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
# Slide Operations
|
|
1035
|
+
@mcp.tool()
|
|
1036
|
+
def add_slide(presentation_path: str, layout_name: str = "Title and Content"):
|
|
1037
|
+
"""Add a new slide to the presentation."""
|
|
1038
|
+
try:
|
|
1039
|
+
presentation = context.get_presentation(presentation_path)
|
|
1040
|
+
slide_layout = presentation.slide_layouts.get_by_name(layout_name)
|
|
1041
|
+
slide = presentation.slides.add_slide(slide_layout)
|
|
1042
|
+
slide_index = len(presentation.slides) - 1
|
|
1043
|
+
return {
|
|
1044
|
+
"message": "Slide added successfully",
|
|
1045
|
+
"slide_index": slide_index
|
|
1046
|
+
}
|
|
1047
|
+
except Exception as e:
|
|
1048
|
+
return {"error": str(e)}
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
@mcp.tool()
|
|
1052
|
+
def delete_slide(presentation_path: str, slide_index: int):
|
|
1053
|
+
"""Delete a slide from the presentation."""
|
|
1054
|
+
try:
|
|
1055
|
+
presentation = context.get_presentation(presentation_path)
|
|
1056
|
+
if slide_index < len(presentation.slides):
|
|
1057
|
+
# Remove the slide
|
|
1058
|
+
rId = presentation.slides._sldIdLst[slide_index].rId
|
|
1059
|
+
presentation.part.drop_rel(rId)
|
|
1060
|
+
del presentation.slides._sldIdLst[slide_index]
|
|
1061
|
+
context.save_presentation(presentation_path)
|
|
1062
|
+
return {"message": "Slide deleted successfully"}
|
|
1063
|
+
else:
|
|
1064
|
+
return {"error": f"Slide index {slide_index} out of range"}
|
|
1065
|
+
except Exception as e:
|
|
1066
|
+
return {"error": str(e)}
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
@mcp.tool()
|
|
1070
|
+
def get_slide_count(presentation_path: str):
|
|
1071
|
+
"""Get the total number of slides in the presentation."""
|
|
1072
|
+
try:
|
|
1073
|
+
presentation = context.get_presentation(presentation_path)
|
|
1074
|
+
return {"slide_count": len(presentation.slides)}
|
|
1075
|
+
except Exception as e:
|
|
1076
|
+
return {"error": str(e)}
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
@mcp.tool()
|
|
1080
|
+
def analyze_slide(presentation_path: str, slide_index: int):
|
|
1081
|
+
"""Analyze the content of a slide."""
|
|
1082
|
+
try:
|
|
1083
|
+
presentation = context.get_presentation(presentation_path)
|
|
1084
|
+
if slide_index < len(presentation.slides):
|
|
1085
|
+
slide = presentation.slides[slide_index]
|
|
1086
|
+
content = context.analyze_slide_content(presentation_path, slide)
|
|
1087
|
+
return {"slide_index": slide_index, "content": content}
|
|
1088
|
+
else:
|
|
1089
|
+
return {"error": f"Slide index {slide_index} out of range"}
|
|
1090
|
+
except Exception as e:
|
|
1091
|
+
return {"error": str(e)}
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
@mcp.tool()
|
|
1095
|
+
def set_background_color(presentation_path: str, slide_index: int, color):
|
|
1096
|
+
"""Set the background color of a slide."""
|
|
1097
|
+
try:
|
|
1098
|
+
presentation = context.get_presentation(presentation_path)
|
|
1099
|
+
if slide_index < len(presentation.slides):
|
|
1100
|
+
slide = presentation.slides[slide_index]
|
|
1101
|
+
background = slide.background
|
|
1102
|
+
fill = background.fill
|
|
1103
|
+
|
|
1104
|
+
# Parse color (could be hex string or RGB tuple)
|
|
1105
|
+
if isinstance(color, str):
|
|
1106
|
+
color = color.lstrip('#')
|
|
1107
|
+
r, g, b = tuple(int(color[i:i+2], 16) for i in (0, 2, 4))
|
|
1108
|
+
elif isinstance(color, (list, tuple)) and len(color) == 3:
|
|
1109
|
+
r, g, b = color
|
|
1110
|
+
else:
|
|
1111
|
+
return {"error": "Invalid color format"}
|
|
1112
|
+
|
|
1113
|
+
fill.solid()
|
|
1114
|
+
fill.fore_color.rgb = RGBColor(r, g, b)
|
|
1115
|
+
context.save_presentation(presentation_path)
|
|
1116
|
+
return {"message": "Background color set successfully"}
|
|
1117
|
+
else:
|
|
1118
|
+
return {"error": f"Slide index {slide_index} out of range"}
|
|
1119
|
+
except Exception as e:
|
|
1120
|
+
return {"error": str(e)}
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
# Element Operations
|
|
1124
|
+
@mcp.tool()
|
|
1125
|
+
def add_text(presentation_path: str, slide_index: int, text: str,
|
|
1126
|
+
position: List[float] = [0.5, 0.5], font_size: float = 18):
|
|
1127
|
+
"""Add text to a slide."""
|
|
1128
|
+
try:
|
|
1129
|
+
presentation = context.get_presentation(presentation_path)
|
|
1130
|
+
if slide_index < len(presentation.slides):
|
|
1131
|
+
slide = presentation.slides[slide_index]
|
|
1132
|
+
left = Inches(position[0])
|
|
1133
|
+
top = Inches(position[1])
|
|
1134
|
+
width = Inches(5)
|
|
1135
|
+
height = Inches(1)
|
|
1136
|
+
|
|
1137
|
+
textbox = slide.shapes.add_textbox(left, top, width, height)
|
|
1138
|
+
text_frame = textbox.text_frame
|
|
1139
|
+
text_frame.text = text
|
|
1140
|
+
text_frame.paragraphs[0].font.size = Pt(font_size)
|
|
1141
|
+
|
|
1142
|
+
element_id = context.register_element(presentation_path, slide_index, textbox)
|
|
1143
|
+
context.save_presentation(presentation_path)
|
|
1144
|
+
return {
|
|
1145
|
+
"message": "Text added successfully",
|
|
1146
|
+
"element_id": element_id
|
|
1147
|
+
}
|
|
1148
|
+
else:
|
|
1149
|
+
return {"error": f"Slide index {slide_index} out of range"}
|
|
1150
|
+
except Exception as e:
|
|
1151
|
+
return {"error": str(e)}
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
@mcp.tool()
|
|
1155
|
+
def add_shape(presentation_path: str, slide_index: int, shape_type: str,
|
|
1156
|
+
position: Dict[str, float], size: Dict[str, float],
|
|
1157
|
+
style_properties: Dict[str, Any] = None):
|
|
1158
|
+
"""Add a shape to a slide."""
|
|
1159
|
+
try:
|
|
1160
|
+
element_id = context.add_shape(presentation_path, slide_index, shape_type,
|
|
1161
|
+
position, size, style_properties)
|
|
1162
|
+
context.save_presentation(presentation_path)
|
|
1163
|
+
return {
|
|
1164
|
+
"message": "Shape added successfully",
|
|
1165
|
+
"element_id": element_id
|
|
1166
|
+
}
|
|
1167
|
+
except Exception as e:
|
|
1168
|
+
return {"error": str(e)}
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
@mcp.tool()
|
|
1172
|
+
def edit_element(presentation_path: str, slide_index: int, element_id: str,
|
|
1173
|
+
properties: Dict[str, Any]):
|
|
1174
|
+
"""Edit an element's properties."""
|
|
1175
|
+
try:
|
|
1176
|
+
result = context.edit_element(presentation_path, slide_index, element_id, properties)
|
|
1177
|
+
return result
|
|
1178
|
+
except Exception as e:
|
|
1179
|
+
return {"error": str(e)}
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
@mcp.tool()
|
|
1183
|
+
def style_element(presentation_path: str, slide_index: int, element_id: str,
|
|
1184
|
+
style_properties: Dict[str, Any]):
|
|
1185
|
+
"""Apply styling to an element."""
|
|
1186
|
+
try:
|
|
1187
|
+
success = context.style_element(presentation_path, slide_index, element_id, style_properties)
|
|
1188
|
+
context.save_presentation(presentation_path)
|
|
1189
|
+
return {"message": "Styling applied successfully"} if success else {"error": "Styling failed"}
|
|
1190
|
+
except Exception as e:
|
|
1191
|
+
return {"error": str(e)}
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
@mcp.tool()
|
|
1195
|
+
def connect_shapes(presentation_path: str, slide_index: int, from_element_id: str,
|
|
1196
|
+
to_element_id: str, connector_type: str = "straight",
|
|
1197
|
+
style_properties: Dict[str, Any] = None):
|
|
1198
|
+
"""Connect two shapes with a connector."""
|
|
1199
|
+
try:
|
|
1200
|
+
element_id = context.connect_shapes(presentation_path, slide_index, from_element_id,
|
|
1201
|
+
to_element_id, connector_type, style_properties)
|
|
1202
|
+
context.save_presentation(presentation_path)
|
|
1203
|
+
return {
|
|
1204
|
+
"message": "Shapes connected successfully",
|
|
1205
|
+
"element_id": element_id
|
|
1206
|
+
}
|
|
1207
|
+
except Exception as e:
|
|
1208
|
+
return {"error": str(e)}
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
@mcp.tool()
|
|
1212
|
+
def find_element(presentation_path: str, slide_index: int, element_type: str = "any",
|
|
1213
|
+
search_text: str = None, position: Dict[str, float] = None):
|
|
1214
|
+
"""Find elements on a slide based on criteria."""
|
|
1215
|
+
try:
|
|
1216
|
+
results = context.find_element(presentation_path, slide_index, element_type,
|
|
1217
|
+
search_text, position)
|
|
1218
|
+
return {"results": results}
|
|
1219
|
+
except Exception as e:
|
|
1220
|
+
return {"error": str(e)}
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
# Financial Tools
|
|
1224
|
+
@mcp.tool()
|
|
1225
|
+
def get_company_financials(company_id: str, metrics: List[str] = None, years: List[int] = None):
|
|
1226
|
+
"""Get financial data for a company."""
|
|
1227
|
+
try:
|
|
1228
|
+
data = context.get_company_financials(company_id, metrics, years)
|
|
1229
|
+
return data
|
|
1230
|
+
except Exception as e:
|
|
1231
|
+
return {"error": str(e)}
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
@mcp.tool()
|
|
1235
|
+
def create_financial_chart(presentation_path: str, slide_index: int, chart_type: str, data: Dict[str, Any], position: Dict[str, float], size: Dict[str, float], title: str = None):
|
|
1236
|
+
"""Create a financial chart on a slide."""
|
|
1237
|
+
try:
|
|
1238
|
+
chart_id = context.create_financial_chart(presentation_path, slide_index, chart_type, data, position, size, title)
|
|
1239
|
+
return {
|
|
1240
|
+
"message": "Chart created successfully",
|
|
1241
|
+
"chart_id": chart_id
|
|
1242
|
+
}
|
|
1243
|
+
except Exception as e:
|
|
1244
|
+
return {"error": str(e)}
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
@mcp.tool()
|
|
1248
|
+
def create_comparison_table(presentation_path: str, slide_index: int, companies: List[str], metrics: List[str], position: Dict[str, float], title: str = None):
|
|
1249
|
+
"""Create a comparison table for companies."""
|
|
1250
|
+
try:
|
|
1251
|
+
table_id = context.create_comparison_table(presentation_path, slide_index, companies, metrics, position, title)
|
|
1252
|
+
return {
|
|
1253
|
+
"message": "Comparison table created successfully",
|
|
1254
|
+
"table_id": table_id
|
|
1255
|
+
}
|
|
1256
|
+
except Exception as e:
|
|
1257
|
+
return {"error": str(e)}
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
# Template Operations
|
|
1261
|
+
@mcp.tool()
|
|
1262
|
+
def list_templates():
|
|
1263
|
+
"""List all available templates."""
|
|
1264
|
+
try:
|
|
1265
|
+
templates = context.list_templates()
|
|
1266
|
+
return {"templates": templates}
|
|
1267
|
+
except Exception as e:
|
|
1268
|
+
return {"error": str(e)}
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
@mcp.tool()
|
|
1272
|
+
def apply_template(presentation_path: str, template_name: str, options: Dict[str, bool] = None):
|
|
1273
|
+
"""Apply a template to a presentation."""
|
|
1274
|
+
try:
|
|
1275
|
+
applied_elements = context.apply_template(presentation_path, template_name, options)
|
|
1276
|
+
return {
|
|
1277
|
+
"message": "Template applied successfully",
|
|
1278
|
+
"applied_elements": applied_elements
|
|
1279
|
+
}
|
|
1280
|
+
except Exception as e:
|
|
1281
|
+
return {"error": str(e)}
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
@mcp.tool()
|
|
1285
|
+
def create_slide_from_template(presentation_path: str, template_name: str, content: Dict[str, Any] = None):
|
|
1286
|
+
"""Create a new slide from a template."""
|
|
1287
|
+
try:
|
|
1288
|
+
result = context.create_slide_from_template(presentation_path, template_name, content)
|
|
1289
|
+
return {
|
|
1290
|
+
"message": "Slide created from template successfully",
|
|
1291
|
+
"slide_index": result["slide_index"],
|
|
1292
|
+
"populated_placeholders": result["populated_placeholders"]
|
|
1293
|
+
}
|
|
1294
|
+
except Exception as e:
|
|
1295
|
+
return {"error": str(e)}
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
@mcp.tool()
|
|
1299
|
+
def save_as_template(presentation_path: str, slide_index: int, template_name: str, template_description: str = ""):
|
|
1300
|
+
"""Save a slide as a template."""
|
|
1301
|
+
try:
|
|
1302
|
+
template_info = context.save_as_template(presentation_path, slide_index, template_name, template_description)
|
|
1303
|
+
return {
|
|
1304
|
+
"message": "Template saved successfully",
|
|
1305
|
+
"template_info": template_info
|
|
1306
|
+
}
|
|
1307
|
+
except Exception as e:
|
|
1308
|
+
return {"error": str(e)}
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
@mcp.tool()
|
|
1312
|
+
def debug_element_mappings(presentation_path: str, slide_index: int):
|
|
1313
|
+
"""Debug tool to inspect element mappings for a slide."""
|
|
1314
|
+
try:
|
|
1315
|
+
if presentation_path not in context.element_ids:
|
|
1316
|
+
return {"error": f"No elements registered for presentation: {presentation_path}"}
|
|
1317
|
+
|
|
1318
|
+
if slide_index not in context.element_ids[presentation_path]:
|
|
1319
|
+
return {"error": f"No elements registered for slide {slide_index}"}
|
|
1320
|
+
|
|
1321
|
+
mappings = context.element_ids[presentation_path][slide_index]
|
|
1322
|
+
return {
|
|
1323
|
+
"presentation": presentation_path,
|
|
1324
|
+
"slide": slide_index,
|
|
1325
|
+
"mappings": mappings
|
|
1326
|
+
}
|
|
1327
|
+
except Exception as e:
|
|
1328
|
+
return {"error": str(e)}
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
def main():
|
|
1332
|
+
print("Starting PowerPoint MCP Server...")
|
|
1333
|
+
print("Initializing workspace...")
|
|
1334
|
+
|
|
1335
|
+
# Initialize workspace
|
|
1336
|
+
if not os.path.exists(context.workspace_dir):
|
|
1337
|
+
os.makedirs(context.workspace_dir)
|
|
1338
|
+
print(f"Created workspace directory: {context.workspace_dir}")
|
|
1339
|
+
if not os.path.exists(os.path.join(context.workspace_dir, "templates")):
|
|
1340
|
+
os.makedirs(os.path.join(context.workspace_dir, "templates"))
|
|
1341
|
+
print("Created templates directory")
|
|
1342
|
+
|
|
1343
|
+
# Run the server
|
|
1344
|
+
mcp.run()
|
|
1345
|
+
|
|
1346
|
+
|
|
1347
|
+
if __name__ == "__main__":
|
|
1348
|
+
main()
|