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.
- google_workspace_mcp/__main__.py +6 -5
- google_workspace_mcp/services/drive.py +39 -12
- google_workspace_mcp/services/slides.py +2033 -57
- google_workspace_mcp/tools/add_image.py +1781 -0
- google_workspace_mcp/tools/calendar.py +12 -17
- google_workspace_mcp/tools/docs_tools.py +24 -32
- google_workspace_mcp/tools/drive.py +264 -21
- google_workspace_mcp/tools/gmail.py +27 -36
- google_workspace_mcp/tools/sheets_tools.py +18 -25
- google_workspace_mcp/tools/slides.py +774 -55
- google_workspace_mcp/utils/unit_conversion.py +201 -0
- {google_workspace_mcp-1.0.5.dist-info → google_workspace_mcp-1.1.5.dist-info}/METADATA +2 -2
- {google_workspace_mcp-1.0.5.dist-info → google_workspace_mcp-1.1.5.dist-info}/RECORD +15 -13
- {google_workspace_mcp-1.0.5.dist-info → google_workspace_mcp-1.1.5.dist-info}/WHEEL +0 -0
- {google_workspace_mcp-1.0.5.dist-info → google_workspace_mcp-1.1.5.dist-info}/entry_points.txt +0 -0
@@ -14,10 +14,9 @@ logger = logging.getLogger(__name__)
|
|
14
14
|
# --- Slides Tool Functions --- #
|
15
15
|
|
16
16
|
|
17
|
-
@mcp.tool(
|
18
|
-
|
19
|
-
|
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
|
-
|
47
|
-
|
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(
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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 = "
|
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(
|
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
|
-
|
139
|
-
|
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(
|
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
|
-
|
194
|
-
|
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
|
-
|
241
|
-
|
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
|
-
|
288
|
-
|
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
|
-
|
347
|
-
|
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
|
-
|
421
|
-
|
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(
|
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(
|
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
|
-
|
452
|
-
|
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
|
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(
|
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
|
1195
|
+
raise ValueError(result.get("message", "Error updating text formatting"))
|
477
1196
|
|
478
1197
|
return result
|