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.
@@ -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()