google-workspace-mcp 1.0.5__py3-none-any.whl → 1.1.5__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.
@@ -14,10 +14,9 @@ logger = logging.getLogger(__name__)
14
14
  # --- Slides Tool Functions --- #
15
15
 
16
16
 
17
- @mcp.tool(
18
- name="get_presentation",
19
- description="Get a presentation by ID with its metadata and content.",
20
- )
17
+ # @mcp.tool(
18
+ # name="get_presentation",
19
+ # )
21
20
  async def get_presentation(presentation_id: str) -> dict[str, Any]:
22
21
  """
23
22
  Get presentation information including all slides and content.
@@ -42,10 +41,10 @@ async def get_presentation(presentation_id: str) -> dict[str, Any]:
42
41
  return result
43
42
 
44
43
 
45
- @mcp.tool(
46
- name="get_slides",
47
- description="Retrieves all slides from a presentation with their elements and notes.",
48
- )
44
+ # @mcp.tool(
45
+ # name="get_slides",
46
+ # description="Retrieves all slides from a presentation with their elements and notes.",
47
+ # )
49
48
  async def get_slides(presentation_id: str) -> dict[str, Any]:
50
49
  """
51
50
  Retrieves all slides from a presentation.
@@ -75,21 +74,24 @@ async def get_slides(presentation_id: str) -> dict[str, Any]:
75
74
 
76
75
  @mcp.tool(
77
76
  name="create_presentation",
78
- description="Creates a new Google Slides presentation with the specified title.",
79
77
  )
80
78
  async def create_presentation(
81
79
  title: str,
80
+ delete_default_slide: bool = False,
82
81
  ) -> dict[str, Any]:
83
82
  """
84
83
  Create a new presentation.
85
84
 
86
85
  Args:
87
86
  title: The title for the new presentation.
87
+ delete_default_slide: If True, deletes the default slide created by Google Slides API.
88
88
 
89
89
  Returns:
90
90
  Created presentation data or raises error.
91
91
  """
92
- logger.info(f"Executing create_presentation with title: '{title}'")
92
+ logger.info(
93
+ f"Executing create_presentation with title: '{title}', delete_default_slide: {delete_default_slide}"
94
+ )
93
95
  if not title or not title.strip():
94
96
  raise ValueError("Presentation title cannot be empty")
95
97
 
@@ -99,16 +101,53 @@ async def create_presentation(
99
101
  if isinstance(result, dict) and result.get("error"):
100
102
  raise ValueError(result.get("message", "Error creating presentation"))
101
103
 
102
- return result
103
-
104
-
105
- @mcp.tool(
106
- name="create_slide",
107
- description="Adds a new slide to a Google Slides presentation with a specified layout.",
108
- )
104
+ # Extract essential information
105
+ presentation_id = result.get("presentationId")
106
+ if not presentation_id:
107
+ raise ValueError("Failed to get presentation ID from creation result")
108
+
109
+ # Prepare clean response
110
+ clean_result = {
111
+ "presentationId": presentation_id,
112
+ "title": title,
113
+ "status": "created_successfully",
114
+ }
115
+
116
+ # If requested, delete the default slide
117
+ if delete_default_slide and presentation_id:
118
+ # Get the first slide ID and delete it
119
+ presentation_data = slides_service.get_presentation(
120
+ presentation_id=presentation_id
121
+ )
122
+ if presentation_data.get("slides") and len(presentation_data["slides"]) > 0:
123
+ first_slide_id = presentation_data["slides"][0]["objectId"]
124
+ delete_result = slides_service.delete_slide(
125
+ presentation_id=presentation_id, slide_id=first_slide_id
126
+ )
127
+ if isinstance(delete_result, dict) and delete_result.get("error"):
128
+ logger.warning(
129
+ f"Failed to delete default slide: {delete_result.get('message')}"
130
+ )
131
+ clean_result["default_slide_deleted"] = False
132
+ clean_result["delete_error"] = delete_result.get("message")
133
+ else:
134
+ logger.info("Successfully deleted default slide")
135
+ clean_result["default_slide_deleted"] = True
136
+ clean_result["status"] = "created_successfully_no_default_slide"
137
+ else:
138
+ clean_result["default_slide_deleted"] = False
139
+ clean_result["delete_error"] = "No slides found to delete"
140
+
141
+ return clean_result
142
+
143
+
144
+ # @mcp.tool(
145
+ # name="create_slide",
146
+ # description="Adds a new slide to a Google Slides presentation with a specified layout.",
147
+ # )
109
148
  async def create_slide(
110
149
  presentation_id: str,
111
- layout: str = "TITLE_AND_BODY",
150
+ layout: str = "BLANK",
112
151
  ) -> dict[str, Any]:
113
152
  """
114
153
  Add a new slide to a presentation.
@@ -120,7 +159,9 @@ async def create_slide(
120
159
  Returns:
121
160
  Response data confirming slide creation or raises error.
122
161
  """
123
- logger.info(f"Executing create_slide in presentation '{presentation_id}' with layout '{layout}'")
162
+ logger.info(
163
+ f"Executing create_slide in presentation '{presentation_id}' with layout '{layout}'"
164
+ )
124
165
  if not presentation_id or not presentation_id.strip():
125
166
  raise ValueError("Presentation ID cannot be empty")
126
167
  # Optional: Validate layout against known predefined layouts?
@@ -134,10 +175,10 @@ async def create_slide(
134
175
  return result
135
176
 
136
177
 
137
- @mcp.tool(
138
- name="add_text_to_slide",
139
- description="Adds text to a specified slide in a Google Slides presentation.",
140
- )
178
+ # @mcp.tool(
179
+ # name="add_text_to_slide",
180
+ # description="Adds text to a specified slide in a Google Slides presentation.",
181
+ # )
141
182
  async def add_text_to_slide(
142
183
  presentation_id: str,
143
184
  slide_id: str,
@@ -171,7 +212,9 @@ async def add_text_to_slide(
171
212
  # Validate shape_type
172
213
  valid_shape_types = {"TEXT_BOX"}
173
214
  if shape_type not in valid_shape_types:
174
- raise ValueError(f"Invalid shape_type '{shape_type}' provided. Must be one of {valid_shape_types}.")
215
+ raise ValueError(
216
+ f"Invalid shape_type '{shape_type}' provided. Must be one of {valid_shape_types}."
217
+ )
175
218
 
176
219
  slides_service = SlidesService()
177
220
  result = slides_service.add_text(
@@ -189,10 +232,10 @@ async def add_text_to_slide(
189
232
  return result
190
233
 
191
234
 
192
- @mcp.tool(
193
- name="add_formatted_text_to_slide",
194
- description="Adds rich-formatted text (with bold, italic, etc.) to a slide.",
195
- )
235
+ # @mcp.tool(
236
+ # name="add_formatted_text_to_slide",
237
+ # description="Adds rich-formatted text (with bold, italic, etc.) to a slide.",
238
+ # )
196
239
  async def add_formatted_text_to_slide(
197
240
  presentation_id: str,
198
241
  slide_id: str,
@@ -236,10 +279,10 @@ async def add_formatted_text_to_slide(
236
279
  return result
237
280
 
238
281
 
239
- @mcp.tool(
240
- name="add_bulleted_list_to_slide",
241
- description="Adds a bulleted list to a slide in a Google Slides presentation.",
242
- )
282
+ # @mcp.tool(
283
+ # name="add_bulleted_list_to_slide",
284
+ # description="Adds a bulleted list to a slide in a Google Slides presentation.",
285
+ # )
243
286
  async def add_bulleted_list_to_slide(
244
287
  presentation_id: str,
245
288
  slide_id: str,
@@ -283,10 +326,90 @@ async def add_bulleted_list_to_slide(
283
326
  return result
284
327
 
285
328
 
286
- @mcp.tool(
287
- name="add_table_to_slide",
288
- description="Adds a table to a slide in a Google Slides presentation.",
289
- )
329
+ # @mcp.tool(
330
+ # name="add_image_to_slide",
331
+ # description="Adds a single image to a slide from a publicly accessible URL with smart sizing. For creating complete slides with multiple elements, use create_slide_with_elements instead for better performance. For full-height coverage, only specify size_height. For full-width coverage, only specify size_width. For exact dimensions, specify both.",
332
+ # )
333
+ async def add_image_to_slide(
334
+ presentation_id: str,
335
+ slide_id: str,
336
+ image_url: str,
337
+ position_x: float = 100.0,
338
+ position_y: float = 100.0,
339
+ size_width: float | None = None,
340
+ size_height: float | None = None,
341
+ unit: str = "PT",
342
+ ) -> dict[str, Any]:
343
+ """
344
+ Add an image to a slide from a publicly accessible URL.
345
+
346
+ Args:
347
+ presentation_id: The ID of the presentation.
348
+ slide_id: The ID of the slide.
349
+ image_url: The publicly accessible URL of the image to add.
350
+ position_x: X coordinate for position (default 100.0).
351
+ position_y: Y coordinate for position (default 100.0).
352
+ size_width: Optional width of the image. If not specified, uses original size or scales proportionally with height.
353
+ size_height: Optional height of the image. If not specified, uses original size or scales proportionally with width.
354
+ unit: Unit type - "PT" for points or "EMU" for English Metric Units (default "PT").
355
+
356
+ Returns:
357
+ Response data confirming image addition or raises error.
358
+
359
+ Note:
360
+ Image Sizing Best Practices:
361
+ - For full-height coverage: Only specify size_height parameter
362
+ - For full-width coverage: Only specify size_width parameter
363
+ - For exact dimensions: Specify both size_height and size_width
364
+ - Omitting a dimension allows proportional auto-scaling while maintaining aspect ratio
365
+ """
366
+ logger.info(
367
+ f"Executing add_image_to_slide on slide '{slide_id}' with image '{image_url}'"
368
+ )
369
+ logger.info(f"Position: ({position_x}, {position_y}) {unit}")
370
+ if size_width and size_height:
371
+ logger.info(f"Size: {size_width} x {size_height} {unit}")
372
+
373
+ if not presentation_id or not slide_id or not image_url:
374
+ raise ValueError("Presentation ID, Slide ID, and Image URL are required")
375
+
376
+ # Basic URL validation
377
+ if not image_url.startswith(("http://", "https://")):
378
+ raise ValueError("Image URL must be a valid HTTP or HTTPS URL")
379
+
380
+ # Validate unit
381
+ if unit not in ["PT", "EMU"]:
382
+ raise ValueError(
383
+ "Unit must be either 'PT' (points) or 'EMU' (English Metric Units)"
384
+ )
385
+
386
+ slides_service = SlidesService()
387
+
388
+ # Prepare size parameter
389
+ size = None
390
+ if size_width is not None and size_height is not None:
391
+ size = (size_width, size_height)
392
+
393
+ # Use the enhanced add_image method with unit support
394
+ result = slides_service.add_image_with_unit(
395
+ presentation_id=presentation_id,
396
+ slide_id=slide_id,
397
+ image_url=image_url,
398
+ position=(position_x, position_y),
399
+ size=size,
400
+ unit=unit,
401
+ )
402
+
403
+ if isinstance(result, dict) and result.get("error"):
404
+ raise ValueError(result.get("message", "Error adding image to slide"))
405
+
406
+ return result
407
+
408
+
409
+ # @mcp.tool(
410
+ # name="add_table_to_slide",
411
+ # description="Adds a table to a slide in a Google Slides presentation.",
412
+ # )
290
413
  async def add_table_to_slide(
291
414
  presentation_id: str,
292
415
  slide_id: str,
@@ -342,10 +465,10 @@ async def add_table_to_slide(
342
465
  return result
343
466
 
344
467
 
345
- @mcp.tool(
346
- name="add_slide_notes",
347
- description="Adds presenter notes to a slide in a Google Slides presentation.",
348
- )
468
+ # @mcp.tool(
469
+ # name="add_slide_notes",
470
+ # description="Adds presenter notes to a slide in a Google Slides presentation.",
471
+ # )
349
472
  async def add_slide_notes(
350
473
  presentation_id: str,
351
474
  slide_id: str,
@@ -381,7 +504,6 @@ async def add_slide_notes(
381
504
 
382
505
  @mcp.tool(
383
506
  name="duplicate_slide",
384
- description="Duplicates a slide in a Google Slides presentation.",
385
507
  )
386
508
  async def duplicate_slide(
387
509
  presentation_id: str,
@@ -392,10 +514,13 @@ async def duplicate_slide(
392
514
  Duplicate a slide in a presentation.
393
515
 
394
516
  Args:
395
- presentation_id: The ID of the presentation.
517
+ presentation_id: The ID of the presentation where the new slide will be created.
396
518
  slide_id: The ID of the slide to duplicate.
397
519
  insert_at_index: Optional index where to insert the duplicated slide.
398
520
 
521
+ Crucial Note: The slide_id MUST belong to the SAME presentation specified by presentation_id.
522
+ You cannot duplicate a slide from one presentation into another using this tool.
523
+
399
524
  Returns:
400
525
  Response data with the new slide ID or raises error.
401
526
  """
@@ -416,10 +541,9 @@ async def duplicate_slide(
416
541
  return result
417
542
 
418
543
 
419
- @mcp.tool(
420
- name="delete_slide",
421
- description="Deletes a slide from a Google Slides presentation.",
422
- )
544
+ # @mcp.tool(
545
+ # name="delete_slide",
546
+ # )
423
547
  async def delete_slide(
424
548
  presentation_id: str,
425
549
  slide_id: str,
@@ -434,12 +558,16 @@ async def delete_slide(
434
558
  Returns:
435
559
  Response data confirming slide deletion or raises error.
436
560
  """
437
- logger.info(f"Executing delete_slide: slide '{slide_id}' from presentation '{presentation_id}'")
561
+ logger.info(
562
+ f"Executing delete_slide: slide '{slide_id}' from presentation '{presentation_id}'"
563
+ )
438
564
  if not presentation_id or not slide_id:
439
565
  raise ValueError("Presentation ID and Slide ID are required")
440
566
 
441
567
  slides_service = SlidesService()
442
- result = slides_service.delete_slide(presentation_id=presentation_id, slide_id=slide_id)
568
+ result = slides_service.delete_slide(
569
+ presentation_id=presentation_id, slide_id=slide_id
570
+ )
443
571
 
444
572
  if isinstance(result, dict) and result.get("error"):
445
573
  raise ValueError(result.get("message", "Error deleting slide"))
@@ -447,10 +575,10 @@ async def delete_slide(
447
575
  return result
448
576
 
449
577
 
450
- @mcp.tool(
451
- name="create_presentation_from_markdown",
452
- description="Creates a Google Slides presentation from structured Markdown content with enhanced formatting support using markdowndeck.",
453
- )
578
+ # @mcp.tool(
579
+ # name="create_presentation_from_markdown",
580
+ # description="Creates a Google Slides presentation from structured Markdown content with enhanced formatting support using markdowndeck.",
581
+ # )
454
582
  async def create_presentation_from_markdown(
455
583
  title: str,
456
584
  markdown_content: str,
@@ -466,13 +594,604 @@ async def create_presentation_from_markdown(
466
594
  Created presentation data or raises error.
467
595
  """
468
596
  logger.info(f"Executing create_presentation_from_markdown with title '{title}'")
469
- if not title or not title.strip() or not markdown_content or not markdown_content.strip():
597
+ if (
598
+ not title
599
+ or not title.strip()
600
+ or not markdown_content
601
+ or not markdown_content.strip()
602
+ ):
470
603
  raise ValueError("Title and markdown content are required")
471
604
 
472
605
  slides_service = SlidesService()
473
- result = slides_service.create_presentation_from_markdown(title=title, markdown_content=markdown_content)
606
+ result = slides_service.create_presentation_from_markdown(
607
+ title=title, markdown_content=markdown_content
608
+ )
609
+
610
+ if isinstance(result, dict) and result.get("error"):
611
+ raise ValueError(
612
+ result.get("message", "Error creating presentation from Markdown")
613
+ )
614
+
615
+ return result
616
+
617
+
618
+ # @mcp.tool(
619
+ # name="create_textbox_with_text",
620
+ # )
621
+ async def create_textbox_with_text(
622
+ presentation_id: str,
623
+ slide_id: str,
624
+ text: str,
625
+ position_x: float,
626
+ position_y: float,
627
+ size_width: float,
628
+ size_height: float,
629
+ unit: str = "EMU",
630
+ element_id: str | None = None,
631
+ font_family: str = "Arial",
632
+ font_size: float = 12,
633
+ text_alignment: str | None = None,
634
+ vertical_alignment: str | None = None,
635
+ ) -> dict[str, Any]:
636
+ """
637
+ Create a text box with text, font formatting, and alignment.
638
+
639
+ Args:
640
+ presentation_id: The ID of the presentation.
641
+ slide_id: The ID of the slide.
642
+ text: The text content to insert.
643
+ position_x: X coordinate for position.
644
+ position_y: Y coordinate for position.
645
+ size_width: Width of the text box.
646
+ size_height: Height of the text box.
647
+ unit: Unit type - "PT" for points or "EMU" for English Metric Units (default "EMU").
648
+ element_id: Optional custom element ID, auto-generated if not provided.
649
+ font_family: Font family (e.g., "Playfair Display", "Roboto", "Arial").
650
+ font_size: Font size in points (e.g., 25, 7.5).
651
+ text_alignment: Optional horizontal alignment ("LEFT", "CENTER", "RIGHT", "JUSTIFY").
652
+ vertical_alignment: Optional vertical alignment ("TOP", "MIDDLE", "BOTTOM").
653
+
654
+ Returns:
655
+ Response data confirming text box creation or raises error.
656
+ """
657
+ logger.info(f"Executing create_textbox_with_text on slide '{slide_id}'")
658
+ if not presentation_id or not slide_id or text is None:
659
+ raise ValueError("Presentation ID, Slide ID, and Text are required")
660
+
661
+ slides_service = SlidesService()
662
+ result = slides_service.create_textbox_with_text(
663
+ presentation_id=presentation_id,
664
+ slide_id=slide_id,
665
+ text=text,
666
+ position=(position_x, position_y),
667
+ size=(size_width, size_height),
668
+ unit=unit,
669
+ element_id=element_id,
670
+ font_family=font_family,
671
+ font_size=font_size,
672
+ text_alignment=text_alignment,
673
+ vertical_alignment=vertical_alignment,
674
+ )
675
+
676
+ if isinstance(result, dict) and result.get("error"):
677
+ raise ValueError(result.get("message", "Error creating text box with text"))
678
+
679
+ return result
680
+
681
+
682
+ # @mcp.tool(
683
+ # name="slides_batch_update",
684
+ # )
685
+ async def slides_batch_update(
686
+ presentation_id: str,
687
+ requests: list[dict[str, Any]],
688
+ ) -> dict[str, Any]:
689
+ """
690
+ Apply a list of raw Google Slides API update requests to a presentation.
691
+ For advanced users familiar with Slides API request structures.
692
+
693
+ Args:
694
+ presentation_id: The ID of the presentation
695
+ requests: List of Google Slides API request objects (e.g., createShape, insertText, updateTextStyle, createImage, etc.)
696
+
697
+ Returns:
698
+ Response data confirming batch operation or raises error
699
+
700
+ Example request structure:
701
+ [
702
+ {
703
+ "createShape": {
704
+ "objectId": "textbox1",
705
+ "shapeType": "TEXT_BOX",
706
+ "elementProperties": {
707
+ "pageObjectId": "slide_id",
708
+ "size": {"width": {"magnitude": 300, "unit": "PT"}, "height": {"magnitude": 50, "unit": "PT"}},
709
+ "transform": {"translateX": 100, "translateY": 100, "unit": "PT"}
710
+ }
711
+ }
712
+ },
713
+ {
714
+ "insertText": {
715
+ "objectId": "textbox1",
716
+ "text": "Hello World"
717
+ }
718
+ }
719
+ ]
720
+ """
721
+ logger.info(f"Executing slides_batch_update with {len(requests)} requests")
722
+ if not presentation_id or not requests:
723
+ raise ValueError("Presentation ID and requests list are required")
724
+
725
+ if not isinstance(requests, list):
726
+ raise ValueError("Requests must be a list of API request objects")
727
+
728
+ slides_service = SlidesService()
729
+ result = slides_service.batch_update(
730
+ presentation_id=presentation_id, requests=requests
731
+ )
732
+
733
+ if isinstance(result, dict) and result.get("error"):
734
+ raise ValueError(result.get("message", "Error executing batch update"))
735
+
736
+ return result
737
+
738
+
739
+ # @mcp.tool(
740
+ # name="create_slide_from_template_data",
741
+ # )
742
+ async def create_slide_from_template_data(
743
+ presentation_id: str,
744
+ slide_id: str,
745
+ template_data: dict[str, Any],
746
+ ) -> dict[str, Any]:
747
+ """
748
+ Create a complete slide from template data in a single batch operation.
749
+
750
+ Args:
751
+ presentation_id: The ID of the presentation
752
+ slide_id: The ID of the slide
753
+ template_data: Dictionary containing slide elements, example:
754
+ {
755
+ "title": {
756
+ "text": "John's Company Campaign",
757
+ "position": {"x": 32, "y": 35, "width": 330, "height": 40},
758
+ "style": {"fontSize": 18, "fontFamily": "Roboto"}
759
+ },
760
+ "description": {
761
+ "text": "Campaign description...",
762
+ "position": {"x": 32, "y": 95, "width": 330, "height": 160},
763
+ "style": {"fontSize": 12, "fontFamily": "Roboto"}
764
+ },
765
+ "stats": [
766
+ {"value": "43.4M", "label": "TOTAL IMPRESSIONS", "position": {"x": 374.5, "y": 268.5}},
767
+ {"value": "134K", "label": "TOTAL ENGAGEMENTS", "position": {"x": 516.5, "y": 268.5}},
768
+ {"value": "4.8B", "label": "AGGREGATE READERSHIP", "position": {"x": 374.5, "y": 350.5}},
769
+ {"value": "$9.1M", "label": "AD EQUIVALENCY", "position": {"x": 516.5, "y": 350.5}}
770
+ ],
771
+ "image": {
772
+ "url": "https://images.unsplash.com/...",
773
+ "position": {"x": 375, "y": 35},
774
+ "size": {"width": 285, "height": 215}
775
+ }
776
+ }
777
+
778
+ Returns:
779
+ Response data confirming slide creation or raises error
780
+ """
781
+ logger.info(f"Executing create_slide_from_template_data on slide '{slide_id}'")
782
+ if not presentation_id or not slide_id or not template_data:
783
+ raise ValueError("Presentation ID, Slide ID, and Template Data are required")
784
+
785
+ if not isinstance(template_data, dict):
786
+ raise ValueError("Template data must be a dictionary")
787
+
788
+ slides_service = SlidesService()
789
+ result = slides_service.create_slide_from_template_data(
790
+ presentation_id=presentation_id, slide_id=slide_id, template_data=template_data
791
+ )
792
+
793
+ if isinstance(result, dict) and result.get("error"):
794
+ raise ValueError(
795
+ result.get("message", "Error creating slide from template data")
796
+ )
797
+
798
+ return result
799
+
800
+
801
+ @mcp.tool(
802
+ name="create_slide_with_elements",
803
+ )
804
+ async def create_slide_with_elements(
805
+ presentation_id: str,
806
+ slide_id: str | None = None,
807
+ elements: list[dict[str, Any]] | None = None,
808
+ background_color: str | None = None,
809
+ background_image_url: str | None = None,
810
+ create_slide: bool = False,
811
+ layout: str = "BLANK",
812
+ insert_at_index: int | None = None,
813
+ ) -> dict[str, Any]:
814
+ """
815
+ Create a complete slide with multiple elements in one batch operation.
816
+ NOW SUPPORTS CREATING THE SLIDE ITSELF - eliminates the two-call pattern!
817
+
818
+ Args:
819
+ presentation_id: The ID of the presentation
820
+ slide_id: The ID of the slide (optional if create_slide=True)
821
+ elements: List of element dictionaries (optional, can create empty slide), example:
822
+ [
823
+ {
824
+ "type": "textbox",
825
+ "content": "Slide Title",
826
+ "position": {"x": 282, "y": 558, "width": 600, "height": 45},
827
+ "style": {
828
+ "fontSize": 25,
829
+ "fontFamily": "Playfair Display",
830
+ "bold": True,
831
+ "textAlignment": "CENTER",
832
+ "verticalAlignment": "MIDDLE",
833
+ "textColor": "#FFFFFF", # White text
834
+ "backgroundColor": "#FFFFFF80" # Semi-transparent white background
835
+ }
836
+ },
837
+ {
838
+ "type": "textbox",
839
+ "content": "Description text...",
840
+ "position": {"x": 282, "y": 1327, "width": 600, "height": 234},
841
+ "style": {
842
+ "fontSize": 12,
843
+ "fontFamily": "Roboto",
844
+ "color": "#000000", # Black text (alternative to textColor)
845
+ "foregroundColor": "#333333" # Dark gray text (alternative to textColor/color)
846
+ }
847
+ },
848
+ {
849
+ "type": "textbox",
850
+ "content": "43.4M\nTOTAL IMPRESSIONS",
851
+ "position": {"x": 333, "y": 4059, "width": 122, "height": 79},
852
+ "textRanges": [
853
+ {
854
+ "startIndex": 0,
855
+ "endIndex": 5,
856
+ "style": {
857
+ "fontSize": 25,
858
+ "fontFamily": "Playfair Display",
859
+ "bold": True,
860
+ "textColor": "#FF0000" # Red text for the number
861
+ }
862
+ },
863
+ {
864
+ "startIndex": 6,
865
+ "endIndex": 22,
866
+ "style": {
867
+ "fontSize": 7.5,
868
+ "fontFamily": "Roboto",
869
+ "backgroundColor": "#FFFF0080" # Semi-transparent yellow background for label
870
+ }
871
+ }
872
+ ],
873
+ "style": {"textAlignment": "CENTER"}
874
+ },
875
+ {
876
+ "type": "image",
877
+ "content": "https://drive.google.com/file/d/.../view",
878
+ "position": {"x": 675, "y": 0, "width": 238, "height": 514}
879
+ },
880
+ {
881
+ "type": "table",
882
+ "content": {
883
+ "headers": ["Category", "Metric"],
884
+ "rows": [
885
+ ["Reach & Visibility", "Total Impressions: 43,431,803"],
886
+ ["Engagement", "Total Engagements: 134,431"],
887
+ ["Media Value", "Ad Equivalency: $9.1 million"]
888
+ ]
889
+ },
890
+ "position": {"x": 100, "y": 300, "width": 400, "height": 200},
891
+ "style": {
892
+ "headerStyle": {
893
+ "bold": true,
894
+ "backgroundColor": "#ff6b6b"
895
+ },
896
+ "firstColumnBold": true,
897
+ "fontSize": 12,
898
+ "fontFamily": "Roboto"
899
+ }
900
+ }
901
+ ]
902
+ background_color: Optional slide background color (e.g., "#f8cdcd4f")
903
+ background_image_url: Optional slide background image URL (takes precedence over background_color)
904
+ Must be publicly accessible (e.g., "https://drive.google.com/uc?id=FILE_ID")
905
+ create_slide: If True, creates the slide first. If False, adds elements to existing slide. (default: False)
906
+ layout: Layout for new slide (BLANK, TITLE_AND_BODY, etc.) - only used if create_slide=True
907
+ insert_at_index: Position for new slide (only used if create_slide=True)
908
+
909
+ Text Color Support:
910
+ - "textColor" or "color": "#FFFFFF"
911
+ - "foregroundColor": "#333333"
912
+ - Supports 6-character and 8-character hex codes with alpha: "#FFFFFF", "#FFFFFF80"
913
+ - Supports CSS rgba() format: "rgba(255, 255, 255, 0.5)"
914
+ - Supports RGB objects: {"r": 255, "g": 255, "b": 255} or {"red": 1.0, "green": 1.0, "blue": 1.0}
915
+
916
+ Background Color Support:
917
+ - "backgroundColor": "#FFFFFF80" - Semi-transparent white background
918
+ - "backgroundColor": "rgba(255, 255, 255, 0.5)" - Semi-transparent white background (CSS format)
919
+ - Supports same color formats as text colors
920
+ - 8-character hex codes supported: "#FFFFFF80" (alpha channel properly applied)
921
+ - CSS rgba() format supported: "rgba(255, 255, 255, 0.5)" (alpha channel properly applied)
922
+ - CSS rgb() format supported: "rgb(255, 255, 255)" (fully opaque)
923
+ - Works in main "style" object for entire text box background
924
+ - Creates semi-transparent background for the entire text box shape
925
+
926
+ Background Image Requirements:
927
+ - Must be publicly accessible without authentication
928
+ - Maximum file size: 50 MB
929
+ - Maximum resolution: 25 megapixels
930
+ - Supported formats: PNG, JPEG, GIF only
931
+ - HTTPS URLs recommended
932
+ - Will automatically stretch to fill slide (may distort aspect ratio)
933
+
934
+ Advanced textRanges formatting:
935
+ For mixed formatting within a single textbox, use "textRanges" instead of "style":
936
+ - textRanges: Array of formatting ranges - now supports TWO formats:
937
+
938
+ FORMAT 1 - Content-based (RECOMMENDED - no index calculation needed):
939
+ "textRanges": [
940
+ {"content": "43.4M", "style": {"fontSize": 25, "bold": true}},
941
+ {"content": "TOTAL IMPRESSIONS", "style": {"fontSize": 7.5}}
942
+ ]
943
+
944
+ FORMAT 2 - Index-based (legacy support):
945
+ "textRanges": [
946
+ {"startIndex": 0, "endIndex": 5, "style": {"fontSize": 25, "bold": true}},
947
+ {"startIndex": 6, "endIndex": 23, "style": {"fontSize": 7.5}}
948
+ ]
949
+
950
+ - Content-based ranges automatically find text and calculate indices
951
+ - Index-based ranges include auto-correction for common off-by-one errors
952
+ - Allows different fonts, sizes, colors, and formatting for different parts of text
953
+ - Perfect for stats with large numbers + small labels in same textbox
954
+ - Each textRange can have its own textColor and backgroundColor
955
+
956
+ Table Formatting Best Practices:
957
+ - Use "firstColumnBold": true to emphasize categories/left column
958
+ - Headers: Bold with colored backgrounds (e.g., "#ff6b6b" for brand consistency)
959
+ - Structure: Clear headers with organized data rows
960
+
961
+ Usage Examples:
962
+ # NEW OPTIMIZED WAY - Single API call to create slide with elements:
963
+ result = await create_slide_with_elements(
964
+ presentation_id="abc123",
965
+ elements=[
966
+ {
967
+ "type": "textbox",
968
+ "content": "Slide Title",
969
+ "position": {"x": 100, "y": 100, "width": 400, "height": 50},
970
+ "style": {"fontSize": 18, "bold": True, "textColor": "#FFFFFF"}
971
+ },
972
+ {
973
+ "type": "image",
974
+ "content": "https://images.unsplash.com/...",
975
+ "position": {"x": 375, "y": 35, "width": 285, "height": 215}
976
+ }
977
+ ],
978
+ create_slide=True, # Creates slide AND adds elements
979
+ layout="BLANK",
980
+ background_color="#f8cdcd4f"
981
+ )
982
+ # Returns: {"slideId": "auto_generated_id", "slideCreated": True, "elementsAdded": 2}
983
+
984
+ # Add elements to existing slide (original behavior):
985
+ result = await create_slide_with_elements(
986
+ presentation_id="abc123",
987
+ slide_id="existing_slide_123",
988
+ elements=[...],
989
+ create_slide=False # Only adds elements (default)
990
+ )
991
+
992
+ # Create slide without elements (just background):
993
+ result = await create_slide_with_elements(
994
+ presentation_id="abc123",
995
+ create_slide=True,
996
+ background_image_url="https://images.unsplash.com/..."
997
+ )
998
+
999
+ Returns:
1000
+ Response data confirming slide creation or raises error
1001
+ """
1002
+ logger.info(
1003
+ f"Executing create_slide_with_elements (create_slide={create_slide}, elements={len(elements or [])})"
1004
+ )
1005
+
1006
+ if not presentation_id:
1007
+ raise ValueError("Presentation ID is required")
1008
+
1009
+ if not create_slide and not slide_id:
1010
+ raise ValueError("slide_id is required when create_slide=False")
1011
+
1012
+ if elements and not isinstance(elements, list):
1013
+ raise ValueError("Elements must be a list")
1014
+
1015
+ slides_service = SlidesService()
1016
+ result = slides_service.create_slide_with_elements(
1017
+ presentation_id=presentation_id,
1018
+ slide_id=slide_id,
1019
+ elements=elements,
1020
+ background_color=background_color,
1021
+ background_image_url=background_image_url,
1022
+ create_slide=create_slide,
1023
+ layout=layout,
1024
+ insert_at_index=insert_at_index,
1025
+ )
1026
+
1027
+ if isinstance(result, dict) and result.get("error"):
1028
+ raise ValueError(result.get("message", "Error creating slide with elements"))
1029
+
1030
+ return result
1031
+
1032
+
1033
+ # @mcp.tool(
1034
+ # name="set_slide_background",
1035
+ # )
1036
+ async def set_slide_background(
1037
+ presentation_id: str,
1038
+ slide_id: str,
1039
+ background_color: str | None = None,
1040
+ background_image_url: str | None = None,
1041
+ ) -> dict[str, Any]:
1042
+ """
1043
+ Set the background of a slide to either a solid color or an image.
1044
+
1045
+ Args:
1046
+ presentation_id: The ID of the presentation
1047
+ slide_id: The ID of the slide
1048
+ background_color: Optional background color (e.g., "#f8cdcd4f", "#ffffff")
1049
+ background_image_url: Optional background image URL (takes precedence over color)
1050
+ Must be publicly accessible (e.g., "https://drive.google.com/uc?id=FILE_ID")
1051
+
1052
+ Background Image Requirements:
1053
+ - Must be publicly accessible without authentication
1054
+ - Maximum file size: 50 MB
1055
+ - Maximum resolution: 25 megapixels
1056
+ - Supported formats: PNG, JPEG, GIF only
1057
+ - HTTPS URLs recommended
1058
+ - Will automatically stretch to fill entire slide
1059
+ - May distort aspect ratio to fit slide dimensions
1060
+
1061
+ Returns:
1062
+ Response data confirming background update or raises error
1063
+ """
1064
+ logger.info(f"Setting background for slide '{slide_id}'")
1065
+ if not presentation_id or not slide_id:
1066
+ raise ValueError("Presentation ID and Slide ID are required")
1067
+
1068
+ if not background_color and not background_image_url:
1069
+ raise ValueError(
1070
+ "Either background_color or background_image_url must be provided"
1071
+ )
1072
+
1073
+ slides_service = SlidesService()
1074
+
1075
+ # Create the appropriate background request
1076
+ if background_image_url:
1077
+ logger.info(f"Setting slide background image: {background_image_url}")
1078
+ requests = [
1079
+ {
1080
+ "updatePageProperties": {
1081
+ "objectId": slide_id,
1082
+ "pageProperties": {
1083
+ "pageBackgroundFill": {
1084
+ "stretchedPictureFill": {"contentUrl": background_image_url}
1085
+ }
1086
+ },
1087
+ "fields": "pageBackgroundFill",
1088
+ }
1089
+ }
1090
+ ]
1091
+ else:
1092
+ logger.info(f"Setting slide background color: {background_color}")
1093
+ requests = [
1094
+ {
1095
+ "updatePageProperties": {
1096
+ "objectId": slide_id,
1097
+ "pageProperties": {
1098
+ "pageBackgroundFill": {
1099
+ "solidFill": {
1100
+ "color": {
1101
+ "rgbColor": slides_service._hex_to_rgb(
1102
+ background_color or "#ffffff"
1103
+ )
1104
+ }
1105
+ }
1106
+ }
1107
+ },
1108
+ "fields": "pageBackgroundFill.solidFill.color",
1109
+ }
1110
+ }
1111
+ ]
1112
+
1113
+ result = slides_service.batch_update(presentation_id, requests)
1114
+
1115
+ if isinstance(result, dict) and result.get("error"):
1116
+ raise ValueError(result.get("message", "Error setting slide background"))
1117
+
1118
+ return result
1119
+
1120
+
1121
+ # @mcp.tool(
1122
+ # name="convert_template_zones_to_pt",
1123
+ # )
1124
+ async def convert_template_zones_to_pt(
1125
+ template_zones: dict[str, Any],
1126
+ ) -> dict[str, Any]:
1127
+ """
1128
+ Convert template zones coordinates from EMU to PT for easier slide element creation.
1129
+
1130
+ Args:
1131
+ template_zones: Template zones from extract_template_zones_only
1132
+
1133
+ Returns:
1134
+ Template zones with additional PT coordinates (x_pt, y_pt, width_pt, height_pt)
1135
+ """
1136
+ logger.info(f"Converting {len(template_zones)} template zones to PT coordinates")
1137
+ if not template_zones:
1138
+ raise ValueError("Template zones are required")
1139
+
1140
+ slides_service = SlidesService()
1141
+ result = slides_service.convert_template_zones_to_pt(template_zones)
1142
+
1143
+ return {"success": True, "converted_zones": result}
1144
+
1145
+
1146
+ # @mcp.tool(
1147
+ # name="update_text_formatting",
1148
+ # )
1149
+ async def update_text_formatting(
1150
+ presentation_id: str,
1151
+ element_id: str,
1152
+ formatted_text: str,
1153
+ font_size: float | None = None,
1154
+ font_family: str | None = None,
1155
+ text_alignment: str | None = None,
1156
+ vertical_alignment: str | None = None,
1157
+ start_index: int | None = None,
1158
+ end_index: int | None = None,
1159
+ ) -> dict[str, Any]:
1160
+ """
1161
+ Update formatting of text in an existing text box.
1162
+
1163
+ Args:
1164
+ presentation_id: The ID of the presentation
1165
+ element_id: The ID of the text box element
1166
+ formatted_text: Text with formatting markers (**, *, etc.)
1167
+ font_size: Optional font size in points (e.g., 25, 7.5)
1168
+ font_family: Optional font family (e.g., "Playfair Display", "Roboto", "Arial")
1169
+ text_alignment: Optional horizontal alignment ("LEFT", "CENTER", "RIGHT", "JUSTIFY")
1170
+ vertical_alignment: Optional vertical alignment ("TOP", "MIDDLE", "BOTTOM")
1171
+ start_index: Optional start index for applying formatting to specific range (0-based)
1172
+ end_index: Optional end index for applying formatting to specific range (exclusive)
1173
+
1174
+ Returns:
1175
+ Response data or error information
1176
+ """
1177
+ logger.info(f"Executing update_text_formatting on element '{element_id}'")
1178
+ if not presentation_id or not element_id or not formatted_text:
1179
+ raise ValueError("Presentation ID, Element ID, and Formatted Text are required")
1180
+
1181
+ slides_service = SlidesService()
1182
+ result = slides_service.update_text_formatting(
1183
+ presentation_id=presentation_id,
1184
+ element_id=element_id,
1185
+ formatted_text=formatted_text,
1186
+ font_size=font_size,
1187
+ font_family=font_family,
1188
+ text_alignment=text_alignment,
1189
+ vertical_alignment=vertical_alignment,
1190
+ start_index=start_index,
1191
+ end_index=end_index,
1192
+ )
474
1193
 
475
1194
  if isinstance(result, dict) and result.get("error"):
476
- raise ValueError(result.get("message", "Error creating presentation from Markdown"))
1195
+ raise ValueError(result.get("message", "Error updating text formatting"))
477
1196
 
478
1197
  return result