google-workspace-mcp 1.1.5__py3-none-any.whl → 1.2.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.
- google_workspace_mcp/__main__.py +5 -6
- google_workspace_mcp/models.py +486 -0
- google_workspace_mcp/services/calendar.py +14 -4
- google_workspace_mcp/services/drive.py +237 -14
- google_workspace_mcp/services/sheets_service.py +273 -35
- google_workspace_mcp/services/slides.py +42 -1829
- google_workspace_mcp/tools/calendar.py +116 -100
- google_workspace_mcp/tools/docs_tools.py +99 -57
- google_workspace_mcp/tools/drive.py +112 -92
- google_workspace_mcp/tools/gmail.py +131 -66
- google_workspace_mcp/tools/sheets_tools.py +137 -64
- google_workspace_mcp/tools/slides.py +295 -743
- {google_workspace_mcp-1.1.5.dist-info → google_workspace_mcp-1.2.0.dist-info}/METADATA +3 -2
- {google_workspace_mcp-1.1.5.dist-info → google_workspace_mcp-1.2.0.dist-info}/RECORD +16 -17
- google_workspace_mcp/tools/add_image.py +0 -1781
- google_workspace_mcp/utils/unit_conversion.py +0 -201
- {google_workspace_mcp-1.1.5.dist-info → google_workspace_mcp-1.2.0.dist-info}/WHEEL +0 -0
- {google_workspace_mcp-1.1.5.dist-info → google_workspace_mcp-1.2.0.dist-info}/entry_points.txt +0 -0
@@ -6,6 +6,24 @@ import logging
|
|
6
6
|
from typing import Any
|
7
7
|
|
8
8
|
from google_workspace_mcp.app import mcp # Import from central app module
|
9
|
+
from google_workspace_mcp.models import (
|
10
|
+
SlidesAddFormattedTextOutput,
|
11
|
+
SlidesAddListOutput,
|
12
|
+
SlidesAddNotesOutput,
|
13
|
+
SlidesAddTableOutput,
|
14
|
+
SlidesAddTextOutput,
|
15
|
+
SlidesCreateFromMarkdownOutput,
|
16
|
+
SlidesCreatePresentationOutput,
|
17
|
+
SlidesCreateSlideOutput,
|
18
|
+
SlidesDeleteSlideOutput,
|
19
|
+
SlidesDuplicateSlideOutput,
|
20
|
+
SlidesGetPresentationOutput,
|
21
|
+
SlidesGetSlidesOutput,
|
22
|
+
SlidesInsertChartOutput,
|
23
|
+
SlidesSharePresentationOutput,
|
24
|
+
)
|
25
|
+
from google_workspace_mcp.services.drive import DriveService
|
26
|
+
from google_workspace_mcp.services.sheets_service import SheetsService
|
9
27
|
from google_workspace_mcp.services.slides import SlidesService
|
10
28
|
|
11
29
|
logger = logging.getLogger(__name__)
|
@@ -14,10 +32,11 @@ logger = logging.getLogger(__name__)
|
|
14
32
|
# --- Slides Tool Functions --- #
|
15
33
|
|
16
34
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
35
|
+
@mcp.tool(
|
36
|
+
name="get_presentation",
|
37
|
+
description="Get a presentation by ID with its metadata and content.",
|
38
|
+
)
|
39
|
+
async def get_presentation(presentation_id: str) -> SlidesGetPresentationOutput:
|
21
40
|
"""
|
22
41
|
Get presentation information including all slides and content.
|
23
42
|
|
@@ -25,7 +44,7 @@ async def get_presentation(presentation_id: str) -> dict[str, Any]:
|
|
25
44
|
presentation_id: The ID of the presentation.
|
26
45
|
|
27
46
|
Returns:
|
28
|
-
|
47
|
+
SlidesGetPresentationOutput containing presentation data.
|
29
48
|
"""
|
30
49
|
logger.info(f"Executing get_presentation tool with ID: '{presentation_id}'")
|
31
50
|
if not presentation_id or not presentation_id.strip():
|
@@ -37,15 +56,20 @@ async def get_presentation(presentation_id: str) -> dict[str, Any]:
|
|
37
56
|
if isinstance(result, dict) and result.get("error"):
|
38
57
|
raise ValueError(result.get("message", "Error getting presentation"))
|
39
58
|
|
40
|
-
|
41
|
-
|
59
|
+
return SlidesGetPresentationOutput(
|
60
|
+
presentation_id=result.get("presentationId", presentation_id),
|
61
|
+
title=result.get("title", ""),
|
62
|
+
slides=result.get("slides", []),
|
63
|
+
masters=result.get("masters", []),
|
64
|
+
layouts=result.get("layouts", []),
|
65
|
+
)
|
42
66
|
|
43
67
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
async def get_slides(presentation_id: str) ->
|
68
|
+
@mcp.tool(
|
69
|
+
name="get_slides",
|
70
|
+
description="Retrieves all slides from a presentation with their elements and notes.",
|
71
|
+
)
|
72
|
+
async def get_slides(presentation_id: str) -> SlidesGetSlidesOutput:
|
49
73
|
"""
|
50
74
|
Retrieves all slides from a presentation.
|
51
75
|
|
@@ -53,7 +77,7 @@ async def get_slides(presentation_id: str) -> dict[str, Any]:
|
|
53
77
|
presentation_id: The ID of the presentation.
|
54
78
|
|
55
79
|
Returns:
|
56
|
-
|
80
|
+
SlidesGetSlidesOutput containing the list of slides.
|
57
81
|
"""
|
58
82
|
logger.info(f"Executing get_slides tool from presentation: '{presentation_id}'")
|
59
83
|
if not presentation_id or not presentation_id.strip():
|
@@ -66,32 +90,28 @@ async def get_slides(presentation_id: str) -> dict[str, Any]:
|
|
66
90
|
raise ValueError(slides.get("message", "Error getting slides"))
|
67
91
|
|
68
92
|
if not slides:
|
69
|
-
|
93
|
+
slides = []
|
70
94
|
|
71
|
-
|
72
|
-
return {"count": len(slides), "slides": slides}
|
95
|
+
return SlidesGetSlidesOutput(count=len(slides), slides=slides)
|
73
96
|
|
74
97
|
|
75
98
|
@mcp.tool(
|
76
99
|
name="create_presentation",
|
100
|
+
description="Creates a new Google Slides presentation with the specified title.",
|
77
101
|
)
|
78
102
|
async def create_presentation(
|
79
103
|
title: str,
|
80
|
-
|
81
|
-
) -> dict[str, Any]:
|
104
|
+
) -> SlidesCreatePresentationOutput:
|
82
105
|
"""
|
83
106
|
Create a new presentation.
|
84
107
|
|
85
108
|
Args:
|
86
109
|
title: The title for the new presentation.
|
87
|
-
delete_default_slide: If True, deletes the default slide created by Google Slides API.
|
88
110
|
|
89
111
|
Returns:
|
90
|
-
|
112
|
+
SlidesCreatePresentationOutput containing created presentation data.
|
91
113
|
"""
|
92
|
-
logger.info(
|
93
|
-
f"Executing create_presentation with title: '{title}', delete_default_slide: {delete_default_slide}"
|
94
|
-
)
|
114
|
+
logger.info(f"Executing create_presentation with title: '{title}'")
|
95
115
|
if not title or not title.strip():
|
96
116
|
raise ValueError("Presentation title cannot be empty")
|
97
117
|
|
@@ -101,54 +121,21 @@ async def create_presentation(
|
|
101
121
|
if isinstance(result, dict) and result.get("error"):
|
102
122
|
raise ValueError(result.get("message", "Error creating presentation"))
|
103
123
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
124
|
+
return SlidesCreatePresentationOutput(
|
125
|
+
presentation_id=result["presentation_id"],
|
126
|
+
title=result["title"],
|
127
|
+
presentation_url=result["presentation_url"],
|
128
|
+
)
|
142
129
|
|
143
130
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
131
|
+
@mcp.tool(
|
132
|
+
name="create_slide",
|
133
|
+
description="Adds a new slide to a Google Slides presentation with a specified layout.",
|
134
|
+
)
|
148
135
|
async def create_slide(
|
149
136
|
presentation_id: str,
|
150
|
-
layout: str = "
|
151
|
-
) ->
|
137
|
+
layout: str = "TITLE_AND_BODY",
|
138
|
+
) -> SlidesCreateSlideOutput:
|
152
139
|
"""
|
153
140
|
Add a new slide to a presentation.
|
154
141
|
|
@@ -157,7 +144,7 @@ async def create_slide(
|
|
157
144
|
layout: The layout for the new slide (e.g., TITLE_AND_BODY, TITLE_ONLY, BLANK).
|
158
145
|
|
159
146
|
Returns:
|
160
|
-
|
147
|
+
SlidesCreateSlideOutput containing response data confirming slide creation.
|
161
148
|
"""
|
162
149
|
logger.info(
|
163
150
|
f"Executing create_slide in presentation '{presentation_id}' with layout '{layout}'"
|
@@ -172,13 +159,15 @@ async def create_slide(
|
|
172
159
|
if isinstance(result, dict) and result.get("error"):
|
173
160
|
raise ValueError(result.get("message", "Error creating slide"))
|
174
161
|
|
175
|
-
return
|
162
|
+
return SlidesCreateSlideOutput(
|
163
|
+
slide_id=result["slide_id"], presentation_id=presentation_id, layout=layout
|
164
|
+
)
|
176
165
|
|
177
166
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
167
|
+
@mcp.tool(
|
168
|
+
name="add_text_to_slide",
|
169
|
+
description="Adds text to a specified slide in a Google Slides presentation.",
|
170
|
+
)
|
182
171
|
async def add_text_to_slide(
|
183
172
|
presentation_id: str,
|
184
173
|
slide_id: str,
|
@@ -188,7 +177,7 @@ async def add_text_to_slide(
|
|
188
177
|
position_y: float = 100.0,
|
189
178
|
size_width: float = 400.0,
|
190
179
|
size_height: float = 100.0,
|
191
|
-
) ->
|
180
|
+
) -> SlidesAddTextOutput:
|
192
181
|
"""
|
193
182
|
Add text to a slide by creating a text box.
|
194
183
|
|
@@ -203,7 +192,7 @@ async def add_text_to_slide(
|
|
203
192
|
size_height: Height of the text box (default 100.0 PT).
|
204
193
|
|
205
194
|
Returns:
|
206
|
-
|
195
|
+
SlidesAddTextOutput containing response data confirming text addition.
|
207
196
|
"""
|
208
197
|
logger.info(f"Executing add_text_to_slide on slide '{slide_id}'")
|
209
198
|
if not presentation_id or not slide_id or text is None:
|
@@ -229,13 +218,17 @@ async def add_text_to_slide(
|
|
229
218
|
if isinstance(result, dict) and result.get("error"):
|
230
219
|
raise ValueError(result.get("message", "Error adding text to slide"))
|
231
220
|
|
232
|
-
return
|
221
|
+
return SlidesAddTextOutput(
|
222
|
+
element_id=result.get("element_id", ""),
|
223
|
+
presentation_id=presentation_id,
|
224
|
+
slide_id=slide_id,
|
225
|
+
)
|
233
226
|
|
234
227
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
228
|
+
@mcp.tool(
|
229
|
+
name="add_formatted_text_to_slide",
|
230
|
+
description="Adds rich-formatted text (with bold, italic, etc.) to a slide.",
|
231
|
+
)
|
239
232
|
async def add_formatted_text_to_slide(
|
240
233
|
presentation_id: str,
|
241
234
|
slide_id: str,
|
@@ -244,7 +237,7 @@ async def add_formatted_text_to_slide(
|
|
244
237
|
position_y: float = 100.0,
|
245
238
|
size_width: float = 400.0,
|
246
239
|
size_height: float = 100.0,
|
247
|
-
) ->
|
240
|
+
) -> SlidesAddFormattedTextOutput:
|
248
241
|
"""
|
249
242
|
Add formatted text to a slide with markdown-style formatting.
|
250
243
|
|
@@ -258,7 +251,7 @@ async def add_formatted_text_to_slide(
|
|
258
251
|
size_height: Height of the text box (default 100.0 PT).
|
259
252
|
|
260
253
|
Returns:
|
261
|
-
|
254
|
+
SlidesAddFormattedTextOutput containing response data confirming text addition.
|
262
255
|
"""
|
263
256
|
logger.info(f"Executing add_formatted_text_to_slide on slide '{slide_id}'")
|
264
257
|
if not presentation_id or not slide_id or text is None:
|
@@ -276,13 +269,18 @@ async def add_formatted_text_to_slide(
|
|
276
269
|
if isinstance(result, dict) and result.get("error"):
|
277
270
|
raise ValueError(result.get("message", "Error adding formatted text to slide"))
|
278
271
|
|
279
|
-
return
|
272
|
+
return SlidesAddFormattedTextOutput(
|
273
|
+
element_id=result.get("element_id", ""),
|
274
|
+
presentation_id=presentation_id,
|
275
|
+
slide_id=slide_id,
|
276
|
+
formatting_applied=result.get("formatting_applied", True),
|
277
|
+
)
|
280
278
|
|
281
279
|
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
280
|
+
@mcp.tool(
|
281
|
+
name="add_bulleted_list_to_slide",
|
282
|
+
description="Adds a bulleted list to a slide in a Google Slides presentation.",
|
283
|
+
)
|
286
284
|
async def add_bulleted_list_to_slide(
|
287
285
|
presentation_id: str,
|
288
286
|
slide_id: str,
|
@@ -291,7 +289,7 @@ async def add_bulleted_list_to_slide(
|
|
291
289
|
position_y: float = 100.0,
|
292
290
|
size_width: float = 400.0,
|
293
291
|
size_height: float = 200.0,
|
294
|
-
) ->
|
292
|
+
) -> SlidesAddListOutput:
|
295
293
|
"""
|
296
294
|
Add a bulleted list to a slide.
|
297
295
|
|
@@ -305,7 +303,7 @@ async def add_bulleted_list_to_slide(
|
|
305
303
|
size_height: Height of the text box (default 200.0 PT).
|
306
304
|
|
307
305
|
Returns:
|
308
|
-
|
306
|
+
SlidesAddListOutput containing response data confirming list addition.
|
309
307
|
"""
|
310
308
|
logger.info(f"Executing add_bulleted_list_to_slide on slide '{slide_id}'")
|
311
309
|
if not presentation_id or not slide_id or not items:
|
@@ -323,93 +321,18 @@ async def add_bulleted_list_to_slide(
|
|
323
321
|
if isinstance(result, dict) and result.get("error"):
|
324
322
|
raise ValueError(result.get("message", "Error adding bulleted list to slide"))
|
325
323
|
|
326
|
-
return
|
327
|
-
|
328
|
-
|
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(
|
324
|
+
return SlidesAddListOutput(
|
325
|
+
element_id=result.get("element_id", ""),
|
395
326
|
presentation_id=presentation_id,
|
396
327
|
slide_id=slide_id,
|
397
|
-
|
398
|
-
position=(position_x, position_y),
|
399
|
-
size=size,
|
400
|
-
unit=unit,
|
328
|
+
items_count=len(items),
|
401
329
|
)
|
402
330
|
|
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
331
|
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
332
|
+
@mcp.tool(
|
333
|
+
name="add_table_to_slide",
|
334
|
+
description="Adds a table to a slide in a Google Slides presentation.",
|
335
|
+
)
|
413
336
|
async def add_table_to_slide(
|
414
337
|
presentation_id: str,
|
415
338
|
slide_id: str,
|
@@ -420,7 +343,7 @@ async def add_table_to_slide(
|
|
420
343
|
position_y: float = 100.0,
|
421
344
|
size_width: float = 400.0,
|
422
345
|
size_height: float = 200.0,
|
423
|
-
) ->
|
346
|
+
) -> SlidesAddTableOutput:
|
424
347
|
"""
|
425
348
|
Add a table to a slide.
|
426
349
|
|
@@ -436,7 +359,7 @@ async def add_table_to_slide(
|
|
436
359
|
size_height: Height of the table (default 200.0 PT).
|
437
360
|
|
438
361
|
Returns:
|
439
|
-
|
362
|
+
SlidesAddTableOutput containing response data confirming table addition.
|
440
363
|
"""
|
441
364
|
logger.info(f"Executing add_table_to_slide on slide '{slide_id}'")
|
442
365
|
if not presentation_id or not slide_id:
|
@@ -462,18 +385,24 @@ async def add_table_to_slide(
|
|
462
385
|
if isinstance(result, dict) and result.get("error"):
|
463
386
|
raise ValueError(result.get("message", "Error adding table to slide"))
|
464
387
|
|
465
|
-
return
|
388
|
+
return SlidesAddTableOutput(
|
389
|
+
element_id=result.get("element_id", ""),
|
390
|
+
presentation_id=presentation_id,
|
391
|
+
slide_id=slide_id,
|
392
|
+
rows=rows,
|
393
|
+
columns=columns,
|
394
|
+
)
|
466
395
|
|
467
396
|
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
397
|
+
@mcp.tool(
|
398
|
+
name="add_slide_notes",
|
399
|
+
description="Adds presenter notes to a slide in a Google Slides presentation.",
|
400
|
+
)
|
472
401
|
async def add_slide_notes(
|
473
402
|
presentation_id: str,
|
474
403
|
slide_id: str,
|
475
404
|
notes: str,
|
476
|
-
) ->
|
405
|
+
) -> SlidesAddNotesOutput:
|
477
406
|
"""
|
478
407
|
Add presenter notes to a slide.
|
479
408
|
|
@@ -483,7 +412,7 @@ async def add_slide_notes(
|
|
483
412
|
notes: The notes content to add.
|
484
413
|
|
485
414
|
Returns:
|
486
|
-
|
415
|
+
SlidesAddNotesOutput containing response data confirming notes addition.
|
487
416
|
"""
|
488
417
|
logger.info(f"Executing add_slide_notes on slide '{slide_id}'")
|
489
418
|
if not presentation_id or not slide_id or not notes:
|
@@ -499,30 +428,33 @@ async def add_slide_notes(
|
|
499
428
|
if isinstance(result, dict) and result.get("error"):
|
500
429
|
raise ValueError(result.get("message", "Error adding notes to slide"))
|
501
430
|
|
502
|
-
return
|
431
|
+
return SlidesAddNotesOutput(
|
432
|
+
success=result.get("success", True),
|
433
|
+
presentation_id=presentation_id,
|
434
|
+
slide_id=slide_id,
|
435
|
+
notes_length=len(notes),
|
436
|
+
)
|
503
437
|
|
504
438
|
|
505
439
|
@mcp.tool(
|
506
440
|
name="duplicate_slide",
|
441
|
+
description="Duplicates a slide in a Google Slides presentation.",
|
507
442
|
)
|
508
443
|
async def duplicate_slide(
|
509
444
|
presentation_id: str,
|
510
445
|
slide_id: str,
|
511
446
|
insert_at_index: int | None = None,
|
512
|
-
) ->
|
447
|
+
) -> SlidesDuplicateSlideOutput:
|
513
448
|
"""
|
514
449
|
Duplicate a slide in a presentation.
|
515
450
|
|
516
451
|
Args:
|
517
|
-
presentation_id: The ID of the presentation
|
452
|
+
presentation_id: The ID of the presentation.
|
518
453
|
slide_id: The ID of the slide to duplicate.
|
519
454
|
insert_at_index: Optional index where to insert the duplicated slide.
|
520
455
|
|
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
|
-
|
524
456
|
Returns:
|
525
|
-
|
457
|
+
SlidesDuplicateSlideOutput containing response data with the new slide ID.
|
526
458
|
"""
|
527
459
|
logger.info(f"Executing duplicate_slide for slide '{slide_id}'")
|
528
460
|
if not presentation_id or not slide_id:
|
@@ -538,16 +470,21 @@ async def duplicate_slide(
|
|
538
470
|
if isinstance(result, dict) and result.get("error"):
|
539
471
|
raise ValueError(result.get("message", "Error duplicating slide"))
|
540
472
|
|
541
|
-
return
|
473
|
+
return SlidesDuplicateSlideOutput(
|
474
|
+
new_slide_id=result["new_slide_id"],
|
475
|
+
presentation_id=presentation_id,
|
476
|
+
source_slide_id=slide_id,
|
477
|
+
)
|
542
478
|
|
543
479
|
|
544
|
-
|
545
|
-
|
546
|
-
|
480
|
+
@mcp.tool(
|
481
|
+
name="delete_slide",
|
482
|
+
description="Deletes a slide from a Google Slides presentation.",
|
483
|
+
)
|
547
484
|
async def delete_slide(
|
548
485
|
presentation_id: str,
|
549
486
|
slide_id: str,
|
550
|
-
) ->
|
487
|
+
) -> SlidesDeleteSlideOutput:
|
551
488
|
"""
|
552
489
|
Delete a slide from a presentation.
|
553
490
|
|
@@ -556,7 +493,7 @@ async def delete_slide(
|
|
556
493
|
slide_id: The ID of the slide to delete.
|
557
494
|
|
558
495
|
Returns:
|
559
|
-
|
496
|
+
SlidesDeleteSlideOutput containing response data confirming slide deletion.
|
560
497
|
"""
|
561
498
|
logger.info(
|
562
499
|
f"Executing delete_slide: slide '{slide_id}' from presentation '{presentation_id}'"
|
@@ -572,17 +509,21 @@ async def delete_slide(
|
|
572
509
|
if isinstance(result, dict) and result.get("error"):
|
573
510
|
raise ValueError(result.get("message", "Error deleting slide"))
|
574
511
|
|
575
|
-
return
|
512
|
+
return SlidesDeleteSlideOutput(
|
513
|
+
success=result.get("success", True),
|
514
|
+
presentation_id=presentation_id,
|
515
|
+
deleted_slide_id=slide_id,
|
516
|
+
)
|
576
517
|
|
577
518
|
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
519
|
+
@mcp.tool(
|
520
|
+
name="create_presentation_from_markdown",
|
521
|
+
description="Creates a Google Slides presentation from structured Markdown content with enhanced formatting support using markdowndeck.",
|
522
|
+
)
|
582
523
|
async def create_presentation_from_markdown(
|
583
524
|
title: str,
|
584
525
|
markdown_content: str,
|
585
|
-
) ->
|
526
|
+
) -> SlidesCreateFromMarkdownOutput:
|
586
527
|
"""
|
587
528
|
Create a Google Slides presentation from Markdown using the markdowndeck library.
|
588
529
|
|
@@ -591,7 +532,7 @@ async def create_presentation_from_markdown(
|
|
591
532
|
markdown_content: Markdown content structured for slides.
|
592
533
|
|
593
534
|
Returns:
|
594
|
-
|
535
|
+
SlidesCreateFromMarkdownOutput containing created presentation data.
|
595
536
|
"""
|
596
537
|
logger.info(f"Executing create_presentation_from_markdown with title '{title}'")
|
597
538
|
if (
|
@@ -612,586 +553,197 @@ async def create_presentation_from_markdown(
|
|
612
553
|
result.get("message", "Error creating presentation from Markdown")
|
613
554
|
)
|
614
555
|
|
615
|
-
return
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
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,
|
556
|
+
return SlidesCreateFromMarkdownOutput(
|
557
|
+
presentation_id=result["presentation_id"],
|
558
|
+
title=result["title"],
|
559
|
+
presentation_url=result["presentation_url"],
|
560
|
+
slides_created=result.get("slides_created", 0),
|
674
561
|
)
|
675
562
|
|
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
563
|
|
682
|
-
|
683
|
-
|
684
|
-
# )
|
685
|
-
async def slides_batch_update(
|
564
|
+
@mcp.tool(name="share_presentation_with_domain")
|
565
|
+
async def share_presentation_with_domain(
|
686
566
|
presentation_id: str,
|
687
|
-
|
688
|
-
) -> dict[str, Any]:
|
567
|
+
) -> SlidesSharePresentationOutput:
|
689
568
|
"""
|
690
|
-
|
691
|
-
|
569
|
+
Shares a Google Slides presentation with the entire organization domain.
|
570
|
+
The domain is configured by the server administrator.
|
571
|
+
|
572
|
+
This tool makes the presentation viewable by anyone in the organization.
|
692
573
|
|
693
574
|
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.)
|
575
|
+
presentation_id: The ID of the Google Slides presentation to share.
|
696
576
|
|
697
577
|
Returns:
|
698
|
-
|
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
|
-
]
|
578
|
+
SlidesSharePresentationOutput containing response data confirming the sharing operation.
|
720
579
|
"""
|
721
|
-
logger.info(
|
722
|
-
|
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
|
580
|
+
logger.info(
|
581
|
+
f"Executing share_presentation_with_domain for presentation ID: '{presentation_id}'"
|
731
582
|
)
|
732
583
|
|
733
|
-
if
|
734
|
-
raise ValueError(
|
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")
|
584
|
+
if not presentation_id or not presentation_id.strip():
|
585
|
+
raise ValueError("Presentation ID cannot be empty.")
|
784
586
|
|
785
|
-
|
786
|
-
raise ValueError("Template data must be a dictionary")
|
587
|
+
sharing_domain = "rizzbuzz.com"
|
787
588
|
|
788
|
-
|
789
|
-
result =
|
790
|
-
|
589
|
+
drive_service = DriveService()
|
590
|
+
result = drive_service.share_file_with_domain(
|
591
|
+
file_id=presentation_id, domain=sharing_domain, role="reader"
|
791
592
|
)
|
792
593
|
|
793
594
|
if isinstance(result, dict) and result.get("error"):
|
794
595
|
raise ValueError(
|
795
|
-
result.get("message", "
|
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"
|
596
|
+
result.get("message", "Failed to share presentation with domain.")
|
981
597
|
)
|
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
598
|
|
1012
|
-
|
1013
|
-
|
599
|
+
# Construct the shareable link
|
600
|
+
presentation_link = f"https://docs.google.com/presentation/d/{presentation_id}/"
|
1014
601
|
|
1015
|
-
|
1016
|
-
|
602
|
+
return SlidesSharePresentationOutput(
|
603
|
+
success=True,
|
604
|
+
message=f"Presentation successfully shared with the '{sharing_domain}' domain.",
|
1017
605
|
presentation_id=presentation_id,
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
background_image_url=background_image_url,
|
1022
|
-
create_slide=create_slide,
|
1023
|
-
layout=layout,
|
1024
|
-
insert_at_index=insert_at_index,
|
606
|
+
presentation_link=presentation_link,
|
607
|
+
domain=sharing_domain,
|
608
|
+
role="reader",
|
1025
609
|
)
|
1026
610
|
|
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
611
|
|
1033
|
-
# @mcp.tool(
|
1034
|
-
|
1035
|
-
# )
|
1036
|
-
async def set_slide_background(
|
612
|
+
# @mcp.tool(name="insert_chart_from_data")
|
613
|
+
async def insert_chart_from_data(
|
1037
614
|
presentation_id: str,
|
1038
615
|
slide_id: str,
|
1039
|
-
|
1040
|
-
|
1041
|
-
|
1042
|
-
|
1043
|
-
|
1044
|
-
|
1045
|
-
|
1046
|
-
|
1047
|
-
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
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.
|
616
|
+
chart_type: str,
|
617
|
+
data: list[list[Any]],
|
618
|
+
title: str,
|
619
|
+
position_x: float = 50.0,
|
620
|
+
position_y: float = 50.0,
|
621
|
+
size_width: float = 480.0,
|
622
|
+
size_height: float = 320.0,
|
623
|
+
) -> SlidesInsertChartOutput:
|
624
|
+
"""
|
625
|
+
Creates and embeds a native, theme-aware Google Chart into a slide from a data table.
|
626
|
+
This tool handles the entire process: creating a data sheet in a dedicated Drive folder,
|
627
|
+
generating the chart, and embedding it into the slide.
|
628
|
+
|
629
|
+
Supported `chart_type` values:
|
630
|
+
- 'BAR': For bar charts. The API creates a vertical column chart.
|
631
|
+
- 'LINE': For line charts.
|
632
|
+
- 'PIE': For pie charts.
|
633
|
+
- 'COLUMN': For vertical column charts (identical to 'BAR').
|
634
|
+
|
635
|
+
Required `data` format:
|
636
|
+
The data must be a list of lists, where the first inner list contains the column headers.
|
637
|
+
Example: [["Month", "Revenue"], ["Jan", 2500], ["Feb", 3100], ["Mar", 2800]]
|
1129
638
|
|
1130
639
|
Args:
|
1131
|
-
|
640
|
+
presentation_id: The ID of the presentation to add the chart to.
|
641
|
+
slide_id: The ID of the slide where the chart will be placed.
|
642
|
+
chart_type: The type of chart to create ('BAR', 'LINE', 'PIE', 'COLUMN').
|
643
|
+
data: A list of lists containing the chart data, with headers in the first row.
|
644
|
+
title: The title that will appear on the chart.
|
645
|
+
position_x: The X-coordinate for the chart's top-left corner on the slide (in points).
|
646
|
+
position_y: The Y-coordinate for the chart's top-left corner on the slide (in points).
|
647
|
+
size_width: The width of the chart on the slide (in points).
|
648
|
+
size_height: The height of the chart on the slide (in points).
|
1132
649
|
|
1133
650
|
Returns:
|
1134
|
-
|
651
|
+
SlidesInsertChartOutput containing response data confirming the chart creation and embedding.
|
1135
652
|
"""
|
1136
|
-
logger.info(
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
653
|
+
logger.info(
|
654
|
+
f"Executing insert_chart_from_data: type='{chart_type}', title='{title}'"
|
655
|
+
)
|
656
|
+
sheets_service = SheetsService()
|
1140
657
|
slides_service = SlidesService()
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
658
|
+
drive_service = DriveService()
|
659
|
+
|
660
|
+
spreadsheet_id = None
|
661
|
+
try:
|
662
|
+
# 1. Get the dedicated folder for storing data sheets
|
663
|
+
data_folder_id = drive_service._get_or_create_data_folder()
|
664
|
+
|
665
|
+
# 2. Create a temporary Google Sheet for the data
|
666
|
+
sheet_title = f"[Chart Data] - {title}"
|
667
|
+
sheet_result = sheets_service.create_spreadsheet(title=sheet_title)
|
668
|
+
if not sheet_result or sheet_result.get("error"):
|
669
|
+
raise RuntimeError(
|
670
|
+
f"Failed to create data sheet: {sheet_result.get('message')}"
|
671
|
+
)
|
1144
672
|
|
673
|
+
spreadsheet_id = sheet_result["spreadsheet_id"]
|
674
|
+
|
675
|
+
# Move the new sheet to the correct folder and remove it from root
|
676
|
+
drive_service.service.files().update(
|
677
|
+
fileId=spreadsheet_id,
|
678
|
+
addParents=data_folder_id,
|
679
|
+
removeParents="root",
|
680
|
+
fields="id, parents",
|
681
|
+
).execute()
|
682
|
+
logger.info(f"Moved data sheet {spreadsheet_id} to folder {data_folder_id}")
|
683
|
+
|
684
|
+
# 3. Write the data to the temporary sheet
|
685
|
+
num_rows = len(data)
|
686
|
+
num_cols = len(data[0]) if data else 0
|
687
|
+
if num_rows == 0 or num_cols < 2:
|
688
|
+
raise ValueError(
|
689
|
+
"Data must have at least one header row and one data column."
|
690
|
+
)
|
1145
691
|
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
1149
|
-
|
1150
|
-
|
1151
|
-
|
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.
|
692
|
+
range_a1 = f"Sheet1!A1:{chr(ord('A') + num_cols - 1)}{num_rows}"
|
693
|
+
write_result = sheets_service.write_range(spreadsheet_id, range_a1, data)
|
694
|
+
if not write_result or write_result.get("error"):
|
695
|
+
raise RuntimeError(
|
696
|
+
f"Failed to write data to sheet: {write_result.get('message')}"
|
697
|
+
)
|
1162
698
|
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
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)
|
699
|
+
# 4. Create the chart object within the sheet
|
700
|
+
metadata = sheets_service.get_spreadsheet_metadata(spreadsheet_id)
|
701
|
+
sheet_id_numeric = metadata["sheets"][0]["properties"]["sheetId"]
|
1173
702
|
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
703
|
+
# --- START OF FIX: Map user-friendly chart type to API-specific chart type ---
|
704
|
+
chart_type_upper = chart_type.upper()
|
705
|
+
if chart_type_upper in ["BAR", "COLUMN"]:
|
706
|
+
api_chart_type = "COLUMN"
|
707
|
+
elif chart_type_upper == "PIE":
|
708
|
+
api_chart_type = "PIE_CHART"
|
709
|
+
else:
|
710
|
+
api_chart_type = chart_type_upper
|
711
|
+
# --- END OF FIX ---
|
1180
712
|
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1192
|
-
|
713
|
+
chart_result = sheets_service.create_chart_on_sheet(
|
714
|
+
spreadsheet_id, sheet_id_numeric, api_chart_type, num_rows, num_cols, title
|
715
|
+
)
|
716
|
+
if not chart_result or chart_result.get("error"):
|
717
|
+
raise RuntimeError(
|
718
|
+
f"Failed to create chart in sheet: {chart_result.get('message')}"
|
719
|
+
)
|
720
|
+
chart_id = chart_result["chartId"]
|
721
|
+
|
722
|
+
# 5. Embed the chart into the Google Slide
|
723
|
+
embed_result = slides_service.embed_sheets_chart(
|
724
|
+
presentation_id,
|
725
|
+
slide_id,
|
726
|
+
spreadsheet_id,
|
727
|
+
chart_id,
|
728
|
+
position=(position_x, position_y),
|
729
|
+
size=(size_width, size_height),
|
730
|
+
)
|
731
|
+
if not embed_result or embed_result.get("error"):
|
732
|
+
raise RuntimeError(
|
733
|
+
f"Failed to embed chart into slide: {embed_result.get('message')}"
|
734
|
+
)
|
1193
735
|
|
1194
|
-
|
1195
|
-
|
736
|
+
return SlidesInsertChartOutput(
|
737
|
+
success=True,
|
738
|
+
message=f"Successfully added native '{title}' chart to slide.",
|
739
|
+
presentation_id=presentation_id,
|
740
|
+
slide_id=slide_id,
|
741
|
+
chart_element_id=embed_result.get("element_id"),
|
742
|
+
)
|
1196
743
|
|
1197
|
-
|
744
|
+
except Exception as e:
|
745
|
+
logger.error(f"Chart creation workflow failed: {e}", exc_info=True)
|
746
|
+
# Re-raise to ensure the MCP framework catches it and reports an error
|
747
|
+
raise RuntimeError(
|
748
|
+
f"An error occurred during the chart creation process: {e}"
|
749
|
+
) from e
|