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
@@ -36,7 +36,9 @@ class SheetsService(BaseGoogleService):
|
|
36
36
|
try:
|
37
37
|
logger.info(f"Creating new Google Spreadsheet with title: '{title}'")
|
38
38
|
spreadsheet_body = {"properties": {"title": title}}
|
39
|
-
spreadsheet =
|
39
|
+
spreadsheet = (
|
40
|
+
self.service.spreadsheets().create(body=spreadsheet_body).execute()
|
41
|
+
)
|
40
42
|
spreadsheet_id = spreadsheet.get("spreadsheetId")
|
41
43
|
logger.info(
|
42
44
|
f"Successfully created spreadsheet: {spreadsheet.get('properties', {}).get('title')} (ID: {spreadsheet_id})"
|
@@ -71,8 +73,15 @@ class SheetsService(BaseGoogleService):
|
|
71
73
|
or an error dictionary.
|
72
74
|
"""
|
73
75
|
try:
|
74
|
-
logger.info(
|
75
|
-
|
76
|
+
logger.info(
|
77
|
+
f"Reading range '{range_a1}' from spreadsheet ID: {spreadsheet_id}"
|
78
|
+
)
|
79
|
+
result = (
|
80
|
+
self.service.spreadsheets()
|
81
|
+
.values()
|
82
|
+
.get(spreadsheetId=spreadsheet_id, range=range_a1)
|
83
|
+
.execute()
|
84
|
+
)
|
76
85
|
|
77
86
|
# result will contain 'range', 'majorDimension', 'values'
|
78
87
|
# 'values' is a list of lists.
|
@@ -82,15 +91,23 @@ class SheetsService(BaseGoogleService):
|
|
82
91
|
return {
|
83
92
|
"spreadsheet_id": spreadsheet_id,
|
84
93
|
"range_requested": range_a1, # The input range
|
85
|
-
"range_returned": result.get(
|
94
|
+
"range_returned": result.get(
|
95
|
+
"range"
|
96
|
+
), # The actual range returned by API
|
86
97
|
"major_dimension": result.get("majorDimension"),
|
87
|
-
"values": result.get(
|
98
|
+
"values": result.get(
|
99
|
+
"values", []
|
100
|
+
), # Default to empty list if no values
|
88
101
|
}
|
89
102
|
except HttpError as error:
|
90
|
-
logger.error(
|
103
|
+
logger.error(
|
104
|
+
f"Error reading range '{range_a1}' from spreadsheet {spreadsheet_id}: {error}"
|
105
|
+
)
|
91
106
|
return self.handle_api_error("read_range", error)
|
92
107
|
except Exception as e:
|
93
|
-
logger.exception(
|
108
|
+
logger.exception(
|
109
|
+
f"Unexpected error reading range '{range_a1}' from spreadsheet {spreadsheet_id}"
|
110
|
+
)
|
94
111
|
return {
|
95
112
|
"error": True,
|
96
113
|
"error_type": "unexpected_service_error",
|
@@ -140,17 +157,23 @@ class SheetsService(BaseGoogleService):
|
|
140
157
|
f"Successfully wrote to range '{result.get('updatedRange')}' in spreadsheet ID: {spreadsheet_id}. Updated {result.get('updatedCells')} cells."
|
141
158
|
)
|
142
159
|
return {
|
143
|
-
"spreadsheet_id": result.get(
|
160
|
+
"spreadsheet_id": result.get(
|
161
|
+
"spreadsheetId"
|
162
|
+
), # Or use the input spreadsheet_id
|
144
163
|
"updated_range": result.get("updatedRange"),
|
145
164
|
"updated_rows": result.get("updatedRows"),
|
146
165
|
"updated_columns": result.get("updatedColumns"),
|
147
166
|
"updated_cells": result.get("updatedCells"),
|
148
167
|
}
|
149
168
|
except HttpError as error:
|
150
|
-
logger.error(
|
169
|
+
logger.error(
|
170
|
+
f"Error writing to range '{range_a1}' in spreadsheet {spreadsheet_id}: {error}"
|
171
|
+
)
|
151
172
|
return self.handle_api_error("write_range", error)
|
152
173
|
except Exception as e:
|
153
|
-
logger.exception(
|
174
|
+
logger.exception(
|
175
|
+
f"Unexpected error writing to range '{range_a1}' in spreadsheet {spreadsheet_id}"
|
176
|
+
)
|
154
177
|
return {
|
155
178
|
"error": True,
|
156
179
|
"error_type": "unexpected_service_error",
|
@@ -200,17 +223,29 @@ class SheetsService(BaseGoogleService):
|
|
200
223
|
.execute()
|
201
224
|
)
|
202
225
|
# result typically includes: spreadsheetId, tableRange (if appended to a table), updates (if named ranges/etc. were affected)
|
203
|
-
logger.info(
|
226
|
+
logger.info(
|
227
|
+
f"Successfully appended rows to spreadsheet ID: {spreadsheet_id}. Updates: {result.get('updates')}"
|
228
|
+
)
|
204
229
|
return {
|
205
|
-
"spreadsheet_id": result.get(
|
206
|
-
|
207
|
-
|
230
|
+
"spreadsheet_id": result.get(
|
231
|
+
"spreadsheetId"
|
232
|
+
), # Or use the input spreadsheet_id
|
233
|
+
"table_range_updated": result.get(
|
234
|
+
"tableRange"
|
235
|
+
), # The range of the new data
|
236
|
+
"updates": result.get(
|
237
|
+
"updates"
|
238
|
+
), # Info about other updates, e.g. to named ranges
|
208
239
|
}
|
209
240
|
except HttpError as error:
|
210
|
-
logger.error(
|
241
|
+
logger.error(
|
242
|
+
f"Error appending rows to range '{range_a1}' in spreadsheet {spreadsheet_id}: {error}"
|
243
|
+
)
|
211
244
|
return self.handle_api_error("append_rows", error)
|
212
245
|
except Exception as e:
|
213
|
-
logger.exception(
|
246
|
+
logger.exception(
|
247
|
+
f"Unexpected error appending rows to range '{range_a1}' in spreadsheet {spreadsheet_id}"
|
248
|
+
)
|
214
249
|
return {
|
215
250
|
"error": True,
|
216
251
|
"error_type": "unexpected_service_error",
|
@@ -231,22 +266,35 @@ class SheetsService(BaseGoogleService):
|
|
231
266
|
A dictionary containing the cleared range and spreadsheet ID, or an error dictionary.
|
232
267
|
"""
|
233
268
|
try:
|
234
|
-
logger.info(
|
269
|
+
logger.info(
|
270
|
+
f"Clearing range '{range_a1}' in spreadsheet ID: {spreadsheet_id}"
|
271
|
+
)
|
235
272
|
# The body for clear is an empty JSON object {}.
|
236
273
|
result = (
|
237
|
-
self.service.spreadsheets()
|
274
|
+
self.service.spreadsheets()
|
275
|
+
.values()
|
276
|
+
.clear(spreadsheetId=spreadsheet_id, range=range_a1, body={})
|
277
|
+
.execute()
|
238
278
|
)
|
239
279
|
# result typically includes: spreadsheetId, clearedRange
|
240
|
-
logger.info(
|
280
|
+
logger.info(
|
281
|
+
f"Successfully cleared range '{result.get('clearedRange')}' in spreadsheet ID: {spreadsheet_id}."
|
282
|
+
)
|
241
283
|
return {
|
242
|
-
"spreadsheet_id": result.get(
|
284
|
+
"spreadsheet_id": result.get(
|
285
|
+
"spreadsheetId"
|
286
|
+
), # Or use the input spreadsheet_id
|
243
287
|
"cleared_range": result.get("clearedRange"),
|
244
288
|
}
|
245
289
|
except HttpError as error:
|
246
|
-
logger.error(
|
290
|
+
logger.error(
|
291
|
+
f"Error clearing range '{range_a1}' from spreadsheet {spreadsheet_id}: {error}"
|
292
|
+
)
|
247
293
|
return self.handle_api_error("clear_range", error)
|
248
294
|
except Exception as e:
|
249
|
-
logger.exception(
|
295
|
+
logger.exception(
|
296
|
+
f"Unexpected error clearing range '{range_a1}' from spreadsheet {spreadsheet_id}"
|
297
|
+
)
|
250
298
|
return {
|
251
299
|
"error": True,
|
252
300
|
"error_type": "unexpected_service_error",
|
@@ -267,10 +315,16 @@ class SheetsService(BaseGoogleService):
|
|
267
315
|
or an error dictionary.
|
268
316
|
"""
|
269
317
|
try:
|
270
|
-
logger.info(
|
318
|
+
logger.info(
|
319
|
+
f"Adding new sheet with title '{title}' to spreadsheet ID: {spreadsheet_id}"
|
320
|
+
)
|
271
321
|
requests = [{"addSheet": {"properties": {"title": title}}}]
|
272
322
|
body = {"requests": requests}
|
273
|
-
response =
|
323
|
+
response = (
|
324
|
+
self.service.spreadsheets()
|
325
|
+
.batchUpdate(spreadsheetId=spreadsheet_id, body=body)
|
326
|
+
.execute()
|
327
|
+
)
|
274
328
|
|
275
329
|
# The response contains a list of replies, one for each request.
|
276
330
|
# The addSheet reply contains the properties of the new sheet.
|
@@ -300,10 +354,14 @@ class SheetsService(BaseGoogleService):
|
|
300
354
|
"sheet_properties": new_sheet_properties,
|
301
355
|
}
|
302
356
|
except HttpError as error:
|
303
|
-
logger.error(
|
357
|
+
logger.error(
|
358
|
+
f"Error adding sheet '{title}' to spreadsheet {spreadsheet_id}: {error}"
|
359
|
+
)
|
304
360
|
return self.handle_api_error("add_sheet", error)
|
305
361
|
except Exception as e:
|
306
|
-
logger.exception(
|
362
|
+
logger.exception(
|
363
|
+
f"Unexpected error adding sheet '{title}' to spreadsheet {spreadsheet_id}"
|
364
|
+
)
|
307
365
|
return {
|
308
366
|
"error": True,
|
309
367
|
"error_type": "unexpected_service_error",
|
@@ -323,10 +381,16 @@ class SheetsService(BaseGoogleService):
|
|
323
381
|
A dictionary indicating success (spreadsheetId and deleted sheetId) or an error dictionary.
|
324
382
|
"""
|
325
383
|
try:
|
326
|
-
logger.info(
|
384
|
+
logger.info(
|
385
|
+
f"Deleting sheet ID: {sheet_id} from spreadsheet ID: {spreadsheet_id}"
|
386
|
+
)
|
327
387
|
requests = [{"deleteSheet": {"sheetId": sheet_id}}]
|
328
388
|
body = {"requests": requests}
|
329
|
-
response =
|
389
|
+
response = (
|
390
|
+
self.service.spreadsheets()
|
391
|
+
.batchUpdate(spreadsheetId=spreadsheet_id, body=body)
|
392
|
+
.execute()
|
393
|
+
)
|
330
394
|
|
331
395
|
# A successful deleteSheet request usually doesn't return detailed content in the reply.
|
332
396
|
# The overall response.spreadsheetId confirms the operation was on the correct spreadsheet.
|
@@ -339,10 +403,14 @@ class SheetsService(BaseGoogleService):
|
|
339
403
|
"success": True,
|
340
404
|
}
|
341
405
|
except HttpError as error:
|
342
|
-
logger.error(
|
406
|
+
logger.error(
|
407
|
+
f"Error deleting sheet ID {sheet_id} from spreadsheet {spreadsheet_id}: {error}"
|
408
|
+
)
|
343
409
|
return self.handle_api_error("delete_sheet", error)
|
344
410
|
except Exception as e:
|
345
|
-
logger.exception(
|
411
|
+
logger.exception(
|
412
|
+
f"Unexpected error deleting sheet ID {sheet_id} from spreadsheet {spreadsheet_id}"
|
413
|
+
)
|
346
414
|
return {
|
347
415
|
"error": True,
|
348
416
|
"error_type": "unexpected_service_error",
|
@@ -350,7 +418,9 @@ class SheetsService(BaseGoogleService):
|
|
350
418
|
"operation": "delete_sheet",
|
351
419
|
}
|
352
420
|
|
353
|
-
def get_spreadsheet_metadata(
|
421
|
+
def get_spreadsheet_metadata(
|
422
|
+
self, spreadsheet_id: str, fields: str | None = None
|
423
|
+
) -> dict[str, Any] | None:
|
354
424
|
"""
|
355
425
|
Retrieves metadata for a specific Google Spreadsheet.
|
356
426
|
|
@@ -364,24 +434,192 @@ class SheetsService(BaseGoogleService):
|
|
364
434
|
"""
|
365
435
|
try:
|
366
436
|
logger.info(
|
367
|
-
f"Fetching metadata for spreadsheet ID: {spreadsheet_id}"
|
437
|
+
f"Fetching metadata for spreadsheet ID: {spreadsheet_id}"
|
438
|
+
+ (f" with fields: {fields}" if fields else "")
|
368
439
|
)
|
369
440
|
if fields is None:
|
370
441
|
fields = "spreadsheetId,properties,sheets(properties(sheetId,title,index,sheetType,gridProperties))"
|
371
442
|
|
372
|
-
spreadsheet_metadata =
|
443
|
+
spreadsheet_metadata = (
|
444
|
+
self.service.spreadsheets()
|
445
|
+
.get(spreadsheetId=spreadsheet_id, fields=fields)
|
446
|
+
.execute()
|
447
|
+
)
|
373
448
|
logger.info(
|
374
449
|
f"Successfully fetched metadata for spreadsheet: {spreadsheet_metadata.get('properties', {}).get('title')}"
|
375
450
|
)
|
376
451
|
return spreadsheet_metadata
|
377
452
|
except HttpError as error:
|
378
|
-
logger.error(
|
453
|
+
logger.error(
|
454
|
+
f"Error fetching metadata for spreadsheet ID {spreadsheet_id}: {error}"
|
455
|
+
)
|
379
456
|
return self.handle_api_error("get_spreadsheet_metadata", error)
|
380
457
|
except Exception as e:
|
381
|
-
logger.exception(
|
458
|
+
logger.exception(
|
459
|
+
f"Unexpected error fetching metadata for spreadsheet ID {spreadsheet_id}"
|
460
|
+
)
|
382
461
|
return {
|
383
462
|
"error": True,
|
384
463
|
"error_type": "unexpected_service_error",
|
385
464
|
"message": str(e),
|
386
465
|
"operation": "get_spreadsheet_metadata",
|
387
466
|
}
|
467
|
+
|
468
|
+
def create_chart_on_sheet(
|
469
|
+
self,
|
470
|
+
spreadsheet_id: str,
|
471
|
+
sheet_id: int,
|
472
|
+
chart_type: str,
|
473
|
+
num_rows: int,
|
474
|
+
num_cols: int,
|
475
|
+
title: str,
|
476
|
+
) -> dict[str, Any] | None:
|
477
|
+
"""
|
478
|
+
Adds a new chart to a sheet, handling different chart types correctly.
|
479
|
+
|
480
|
+
Args:
|
481
|
+
spreadsheet_id: The ID of the Google Spreadsheet.
|
482
|
+
sheet_id: The numeric ID of the sheet to add the chart to.
|
483
|
+
chart_type: The API-specific type of chart ('COLUMN', 'LINE', 'PIE_CHART').
|
484
|
+
num_rows: The number of rows in the data range.
|
485
|
+
num_cols: The number of columns in the data range.
|
486
|
+
title: The title of the chart.
|
487
|
+
|
488
|
+
Returns:
|
489
|
+
A dictionary containing the properties of the newly created chart or an error dictionary.
|
490
|
+
"""
|
491
|
+
try:
|
492
|
+
logger.info(f"Constructing spec for chart '{title}' of type '{chart_type}'")
|
493
|
+
|
494
|
+
chart_spec = {"title": title}
|
495
|
+
|
496
|
+
if chart_type == "PIE_CHART":
|
497
|
+
chart_spec["pieChart"] = {
|
498
|
+
"domain": {
|
499
|
+
"sourceRange": {
|
500
|
+
"sources": [
|
501
|
+
{
|
502
|
+
"sheetId": sheet_id,
|
503
|
+
"startRowIndex": 0,
|
504
|
+
"endRowIndex": num_rows,
|
505
|
+
"startColumnIndex": 0,
|
506
|
+
"endColumnIndex": 1,
|
507
|
+
}
|
508
|
+
]
|
509
|
+
}
|
510
|
+
},
|
511
|
+
"series": {
|
512
|
+
"sourceRange": {
|
513
|
+
"sources": [
|
514
|
+
{
|
515
|
+
"sheetId": sheet_id,
|
516
|
+
"startRowIndex": 0,
|
517
|
+
"endRowIndex": num_rows,
|
518
|
+
"startColumnIndex": 1,
|
519
|
+
"endColumnIndex": 2,
|
520
|
+
}
|
521
|
+
]
|
522
|
+
}
|
523
|
+
},
|
524
|
+
"legendPosition": "LABELED_LEGEND",
|
525
|
+
}
|
526
|
+
else: # For BAR, COLUMN, LINE charts
|
527
|
+
# --- START OF FIX: Correctly structure the domain object ---
|
528
|
+
domain_spec = {
|
529
|
+
"domain": {
|
530
|
+
"sourceRange": {
|
531
|
+
"sources": [
|
532
|
+
{
|
533
|
+
"sheetId": sheet_id,
|
534
|
+
"startRowIndex": 0,
|
535
|
+
"endRowIndex": num_rows,
|
536
|
+
"startColumnIndex": 0,
|
537
|
+
"endColumnIndex": 1,
|
538
|
+
}
|
539
|
+
]
|
540
|
+
}
|
541
|
+
}
|
542
|
+
}
|
543
|
+
# --- END OF FIX ---
|
544
|
+
|
545
|
+
series = []
|
546
|
+
for i in range(1, num_cols):
|
547
|
+
series.append(
|
548
|
+
{
|
549
|
+
"series": {
|
550
|
+
"sourceRange": {
|
551
|
+
"sources": [
|
552
|
+
{
|
553
|
+
"sheetId": sheet_id,
|
554
|
+
"startRowIndex": 0,
|
555
|
+
"endRowIndex": num_rows,
|
556
|
+
"startColumnIndex": i,
|
557
|
+
"endColumnIndex": i + 1,
|
558
|
+
}
|
559
|
+
]
|
560
|
+
}
|
561
|
+
},
|
562
|
+
"targetAxis": "LEFT_AXIS",
|
563
|
+
}
|
564
|
+
)
|
565
|
+
|
566
|
+
chart_spec["basicChart"] = {
|
567
|
+
"chartType": chart_type,
|
568
|
+
"legendPosition": "BOTTOM_LEGEND",
|
569
|
+
"axis": [
|
570
|
+
{"position": "BOTTOM_AXIS"},
|
571
|
+
{"position": "LEFT_AXIS"},
|
572
|
+
],
|
573
|
+
"domains": [domain_spec], # Use the corrected domain object
|
574
|
+
"series": series,
|
575
|
+
}
|
576
|
+
|
577
|
+
requests = [
|
578
|
+
{
|
579
|
+
"addChart": {
|
580
|
+
"chart": {
|
581
|
+
"spec": chart_spec,
|
582
|
+
"position": {
|
583
|
+
"overlayPosition": {
|
584
|
+
"anchorCell": {
|
585
|
+
"sheetId": sheet_id,
|
586
|
+
"rowIndex": num_rows + 2,
|
587
|
+
"columnIndex": 0,
|
588
|
+
},
|
589
|
+
}
|
590
|
+
},
|
591
|
+
}
|
592
|
+
}
|
593
|
+
}
|
594
|
+
]
|
595
|
+
|
596
|
+
body = {"requests": requests}
|
597
|
+
# The google-api-python-client expects snake_case keys in the body,
|
598
|
+
# so we'll convert our camelCase spec to snake_case before sending.
|
599
|
+
# This is a good practice for robustness.
|
600
|
+
import humps
|
601
|
+
|
602
|
+
snake_body = humps.decamelize(body)
|
603
|
+
|
604
|
+
response = (
|
605
|
+
self.service.spreadsheets()
|
606
|
+
.batchUpdate(spreadsheetId=spreadsheet_id, body=snake_body)
|
607
|
+
.execute()
|
608
|
+
)
|
609
|
+
|
610
|
+
new_chart_properties = (
|
611
|
+
response.get("replies", [{}])[0].get("addChart", {}).get("chart")
|
612
|
+
)
|
613
|
+
if not new_chart_properties:
|
614
|
+
raise ValueError("Failed to create chart or parse response.")
|
615
|
+
|
616
|
+
logger.info(
|
617
|
+
f"Successfully created chart with ID: {new_chart_properties.get('chartId')}"
|
618
|
+
)
|
619
|
+
return new_chart_properties
|
620
|
+
|
621
|
+
except HttpError as error:
|
622
|
+
logger.error(f"Google API error in create_chart_on_sheet: {error.content}")
|
623
|
+
return self.handle_api_error("create_chart_on_sheet", error)
|
624
|
+
except Exception as e:
|
625
|
+
return self.handle_api_error("create_chart_on_sheet", e)
|