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.
@@ -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
- # @mcp.tool(
18
- # name="get_presentation",
19
- # )
20
- async def get_presentation(presentation_id: str) -> dict[str, Any]:
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
- Presentation data dictionary or raises error.
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
- # Return raw service result
41
- return result
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
- # @mcp.tool(
45
- # name="get_slides",
46
- # description="Retrieves all slides from a presentation with their elements and notes.",
47
- # )
48
- async def get_slides(presentation_id: str) -> dict[str, Any]:
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
- A dictionary containing the list of slides or an error message.
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
- return {"message": "No slides found in this presentation."}
93
+ slides = []
70
94
 
71
- # Return raw service result
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
- delete_default_slide: bool = False,
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
- Created presentation data or raises error.
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
- # 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
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
- # @mcp.tool(
145
- # name="create_slide",
146
- # description="Adds a new slide to a Google Slides presentation with a specified layout.",
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 = "BLANK",
151
- ) -> dict[str, Any]:
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
- Response data confirming slide creation or raises error.
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 result
162
+ return SlidesCreateSlideOutput(
163
+ slide_id=result["slide_id"], presentation_id=presentation_id, layout=layout
164
+ )
176
165
 
177
166
 
178
- # @mcp.tool(
179
- # name="add_text_to_slide",
180
- # description="Adds text to a specified slide in a Google Slides presentation.",
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
- ) -> dict[str, Any]:
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
- Response data confirming text addition or raises error.
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 result
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
- # @mcp.tool(
236
- # name="add_formatted_text_to_slide",
237
- # description="Adds rich-formatted text (with bold, italic, etc.) to a slide.",
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
- ) -> dict[str, Any]:
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
- Response data confirming text addition or raises error.
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 result
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
- # @mcp.tool(
283
- # name="add_bulleted_list_to_slide",
284
- # description="Adds a bulleted list to a slide in a Google Slides presentation.",
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
- ) -> dict[str, Any]:
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
- Response data confirming list addition or raises error.
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 result
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
- image_url=image_url,
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
- # @mcp.tool(
410
- # name="add_table_to_slide",
411
- # description="Adds a table to a slide in a Google Slides presentation.",
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
- ) -> dict[str, Any]:
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
- Response data confirming table addition or raises error.
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 result
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
- # @mcp.tool(
469
- # name="add_slide_notes",
470
- # description="Adds presenter notes to a slide in a Google Slides presentation.",
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
- ) -> dict[str, Any]:
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
- Response data confirming notes addition or raises error.
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 result
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
- ) -> dict[str, Any]:
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 where the new slide will be created.
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
- Response data with the new slide ID or raises error.
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 result
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
- # @mcp.tool(
545
- # name="delete_slide",
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
- ) -> dict[str, Any]:
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
- Response data confirming slide deletion or raises error.
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 result
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
- # @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
- # )
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
- ) -> dict[str, Any]:
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
- Created presentation data or raises error.
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 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,
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
- # @mcp.tool(
683
- # name="slides_batch_update",
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
- requests: list[dict[str, Any]],
688
- ) -> dict[str, Any]:
567
+ ) -> SlidesSharePresentationOutput:
689
568
  """
690
- Apply a list of raw Google Slides API update requests to a presentation.
691
- For advanced users familiar with Slides API request structures.
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
- 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
- ]
578
+ SlidesSharePresentationOutput containing response data confirming the sharing operation.
720
579
  """
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
580
+ logger.info(
581
+ f"Executing share_presentation_with_domain for presentation ID: '{presentation_id}'"
731
582
  )
732
583
 
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")
584
+ if not presentation_id or not presentation_id.strip():
585
+ raise ValueError("Presentation ID cannot be empty.")
784
586
 
785
- if not isinstance(template_data, dict):
786
- raise ValueError("Template data must be a dictionary")
587
+ sharing_domain = "rizzbuzz.com"
787
588
 
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
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", "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"
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
- if elements and not isinstance(elements, list):
1013
- raise ValueError("Elements must be a list")
599
+ # Construct the shareable link
600
+ presentation_link = f"https://docs.google.com/presentation/d/{presentation_id}/"
1014
601
 
1015
- slides_service = SlidesService()
1016
- result = slides_service.create_slide_with_elements(
602
+ return SlidesSharePresentationOutput(
603
+ success=True,
604
+ message=f"Presentation successfully shared with the '{sharing_domain}' domain.",
1017
605
  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,
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
- # name="set_slide_background",
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
- 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.
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
- template_zones: Template zones from extract_template_zones_only
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
- Template zones with additional PT coordinates (x_pt, y_pt, width_pt, height_pt)
651
+ SlidesInsertChartOutput containing response data confirming the chart creation and embedding.
1135
652
  """
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
-
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
- result = slides_service.convert_template_zones_to_pt(template_zones)
1142
-
1143
- return {"success": True, "converted_zones": result}
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
- # @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.
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
- 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)
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
- 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")
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
- 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
- )
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
- if isinstance(result, dict) and result.get("error"):
1195
- raise ValueError(result.get("message", "Error updating text formatting"))
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
- return result
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