google-workspace-mcp 1.0.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.
Files changed (38) hide show
  1. google_workspace_mcp/__init__.py +3 -0
  2. google_workspace_mcp/__main__.py +43 -0
  3. google_workspace_mcp/app.py +8 -0
  4. google_workspace_mcp/auth/__init__.py +7 -0
  5. google_workspace_mcp/auth/gauth.py +62 -0
  6. google_workspace_mcp/config.py +60 -0
  7. google_workspace_mcp/prompts/__init__.py +3 -0
  8. google_workspace_mcp/prompts/calendar.py +36 -0
  9. google_workspace_mcp/prompts/drive.py +18 -0
  10. google_workspace_mcp/prompts/gmail.py +65 -0
  11. google_workspace_mcp/prompts/slides.py +40 -0
  12. google_workspace_mcp/resources/__init__.py +13 -0
  13. google_workspace_mcp/resources/calendar.py +79 -0
  14. google_workspace_mcp/resources/drive.py +93 -0
  15. google_workspace_mcp/resources/gmail.py +58 -0
  16. google_workspace_mcp/resources/sheets_resources.py +92 -0
  17. google_workspace_mcp/resources/slides.py +421 -0
  18. google_workspace_mcp/services/__init__.py +21 -0
  19. google_workspace_mcp/services/base.py +73 -0
  20. google_workspace_mcp/services/calendar.py +256 -0
  21. google_workspace_mcp/services/docs_service.py +388 -0
  22. google_workspace_mcp/services/drive.py +454 -0
  23. google_workspace_mcp/services/gmail.py +676 -0
  24. google_workspace_mcp/services/sheets_service.py +466 -0
  25. google_workspace_mcp/services/slides.py +959 -0
  26. google_workspace_mcp/tools/__init__.py +7 -0
  27. google_workspace_mcp/tools/calendar.py +229 -0
  28. google_workspace_mcp/tools/docs_tools.py +277 -0
  29. google_workspace_mcp/tools/drive.py +221 -0
  30. google_workspace_mcp/tools/gmail.py +344 -0
  31. google_workspace_mcp/tools/sheets_tools.py +322 -0
  32. google_workspace_mcp/tools/slides.py +478 -0
  33. google_workspace_mcp/utils/__init__.py +1 -0
  34. google_workspace_mcp/utils/markdown_slides.py +504 -0
  35. google_workspace_mcp-1.0.0.dist-info/METADATA +547 -0
  36. google_workspace_mcp-1.0.0.dist-info/RECORD +38 -0
  37. google_workspace_mcp-1.0.0.dist-info/WHEEL +4 -0
  38. google_workspace_mcp-1.0.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,466 @@
1
+ """
2
+ Google Sheets service implementation.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any
7
+
8
+ from googleapiclient.errors import HttpError
9
+
10
+ from google_workspace_mcp.services.base import BaseGoogleService
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class SheetsService(BaseGoogleService):
16
+ """
17
+ Service for interacting with the Google Sheets API.
18
+ """
19
+
20
+ def __init__(self):
21
+ """Initialize the Google Sheets service."""
22
+ super().__init__(service_name="sheets", version="v4")
23
+ # API calls will be like self.service.spreadsheets().<method>
24
+
25
+ def create_spreadsheet(self, title: str) -> dict[str, Any] | None:
26
+ """
27
+ Creates a new Google Spreadsheet with the specified title.
28
+
29
+ Args:
30
+ title: The title for the new spreadsheet.
31
+
32
+ Returns:
33
+ A dictionary containing the created spreadsheet's ID, title, and URL,
34
+ or an error dictionary.
35
+ """
36
+ try:
37
+ logger.info(f"Creating new Google Spreadsheet with title: '{title}'")
38
+ spreadsheet_body = {"properties": {"title": title}}
39
+ spreadsheet = (
40
+ self.service.spreadsheets().create(body=spreadsheet_body).execute()
41
+ )
42
+ spreadsheet_id = spreadsheet.get("spreadsheetId")
43
+ logger.info(
44
+ f"Successfully created spreadsheet: {spreadsheet.get('properties', {}).get('title')} (ID: {spreadsheet_id})"
45
+ )
46
+ return {
47
+ "spreadsheet_id": spreadsheet_id,
48
+ "title": spreadsheet.get("properties", {}).get("title"),
49
+ "spreadsheet_url": spreadsheet.get("spreadsheetUrl"),
50
+ }
51
+ except HttpError as error:
52
+ logger.error(f"Error creating spreadsheet '{title}': {error}")
53
+ return self.handle_api_error("create_spreadsheet", error)
54
+ except Exception as e:
55
+ logger.exception(f"Unexpected error creating spreadsheet '{title}'")
56
+ return {
57
+ "error": True,
58
+ "error_type": "unexpected_service_error",
59
+ "message": str(e),
60
+ "operation": "create_spreadsheet",
61
+ }
62
+
63
+ def read_range(self, spreadsheet_id: str, range_a1: str) -> dict[str, Any] | None:
64
+ """
65
+ Reads values from a specified range in a Google Spreadsheet.
66
+
67
+ Args:
68
+ spreadsheet_id: The ID of the Google Spreadsheet.
69
+ range_a1: The A1 notation of the range to read (e.g., "Sheet1!A1:C5", or "A1:C5" if only one sheet).
70
+
71
+ Returns:
72
+ A dictionary containing the 'range', 'majorDimension', and 'values' (list of lists),
73
+ or an error dictionary.
74
+ """
75
+ try:
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
+ )
85
+
86
+ # result will contain 'range', 'majorDimension', 'values'
87
+ # 'values' is a list of lists.
88
+ logger.info(
89
+ f"Successfully read range '{result.get('range')}' from spreadsheet ID: {spreadsheet_id}. Got {len(result.get('values', []))} rows."
90
+ )
91
+ return {
92
+ "spreadsheet_id": spreadsheet_id,
93
+ "range_requested": range_a1, # The input range
94
+ "range_returned": result.get(
95
+ "range"
96
+ ), # The actual range returned by API
97
+ "major_dimension": result.get("majorDimension"),
98
+ "values": result.get(
99
+ "values", []
100
+ ), # Default to empty list if no values
101
+ }
102
+ except HttpError as error:
103
+ logger.error(
104
+ f"Error reading range '{range_a1}' from spreadsheet {spreadsheet_id}: {error}"
105
+ )
106
+ return self.handle_api_error("read_range", error)
107
+ except Exception as e:
108
+ logger.exception(
109
+ f"Unexpected error reading range '{range_a1}' from spreadsheet {spreadsheet_id}"
110
+ )
111
+ return {
112
+ "error": True,
113
+ "error_type": "unexpected_service_error",
114
+ "message": str(e),
115
+ "operation": "read_range",
116
+ }
117
+
118
+ def write_range(
119
+ self,
120
+ spreadsheet_id: str,
121
+ range_a1: str,
122
+ values: list[list[Any]],
123
+ value_input_option: str = "USER_ENTERED",
124
+ ) -> dict[str, Any] | None:
125
+ """
126
+ Writes data to a specified range in a Google Spreadsheet.
127
+
128
+ Args:
129
+ spreadsheet_id: The ID of the Google Spreadsheet.
130
+ range_a1: The A1 notation of the range to write (e.g., "Sheet1!A1:C5").
131
+ values: A list of lists representing the data to write.
132
+ value_input_option: How the input data should be interpreted.
133
+ "USER_ENTERED" will try to parse values as if a user typed them (e.g., formulas).
134
+ "RAW" will take values literally. Defaults to "USER_ENTERED".
135
+ Returns:
136
+ A dictionary containing details of the update (e.g., updatedRange, updatedCells),
137
+ or an error dictionary.
138
+ """
139
+ try:
140
+ logger.info(
141
+ f"Writing to range '{range_a1}' in spreadsheet ID: {spreadsheet_id} with {len(values)} rows. Value input option: {value_input_option}"
142
+ )
143
+ body = {"values": values}
144
+ result = (
145
+ self.service.spreadsheets()
146
+ .values()
147
+ .update(
148
+ spreadsheetId=spreadsheet_id,
149
+ range=range_a1,
150
+ valueInputOption=value_input_option,
151
+ body=body,
152
+ )
153
+ .execute()
154
+ )
155
+ # result typically includes: spreadsheetId, updatedRange, updatedRows, updatedColumns, updatedCells
156
+ logger.info(
157
+ f"Successfully wrote to range '{result.get('updatedRange')}' in spreadsheet ID: {spreadsheet_id}. Updated {result.get('updatedCells')} cells."
158
+ )
159
+ return {
160
+ "spreadsheet_id": result.get(
161
+ "spreadsheetId"
162
+ ), # Or use the input spreadsheet_id
163
+ "updated_range": result.get("updatedRange"),
164
+ "updated_rows": result.get("updatedRows"),
165
+ "updated_columns": result.get("updatedColumns"),
166
+ "updated_cells": result.get("updatedCells"),
167
+ }
168
+ except HttpError as error:
169
+ logger.error(
170
+ f"Error writing to range '{range_a1}' in spreadsheet {spreadsheet_id}: {error}"
171
+ )
172
+ return self.handle_api_error("write_range", error)
173
+ except Exception as e:
174
+ logger.exception(
175
+ f"Unexpected error writing to range '{range_a1}' in spreadsheet {spreadsheet_id}"
176
+ )
177
+ return {
178
+ "error": True,
179
+ "error_type": "unexpected_service_error",
180
+ "message": str(e),
181
+ "operation": "write_range",
182
+ }
183
+
184
+ def append_rows(
185
+ self,
186
+ spreadsheet_id: str,
187
+ range_a1: str,
188
+ values: list[list[Any]],
189
+ value_input_option: str = "USER_ENTERED",
190
+ insert_data_option: str = "INSERT_ROWS",
191
+ ) -> dict[str, Any] | None:
192
+ """
193
+ Appends rows of data to a sheet or table within a Google Spreadsheet.
194
+
195
+ Args:
196
+ spreadsheet_id: The ID of the Google Spreadsheet.
197
+ range_a1: The A1 notation of a range indicating the sheet or table to append to
198
+ (e.g., "Sheet1" to append to the first empty row, or "Sheet1!A1:C1" to append after a table).
199
+ values: A list of lists representing the data rows to append.
200
+ value_input_option: How the input data should be interpreted. Defaults to "USER_ENTERED".
201
+ insert_data_option: How the new data should be inserted. "INSERT_ROWS" inserts new rows,
202
+ "OVERWRITE" overwrites existing rows if the append range points to them.
203
+ Defaults to "INSERT_ROWS".
204
+ Returns:
205
+ A dictionary containing details of the append operation (e.g., updates to named ranges, tableRange),
206
+ or an error dictionary.
207
+ """
208
+ try:
209
+ logger.info(
210
+ f"Appending rows to range '{range_a1}' in spreadsheet ID: {spreadsheet_id} with {len(values)} rows. Value input: {value_input_option}, Insert option: {insert_data_option}"
211
+ )
212
+ body = {"values": values}
213
+ result = (
214
+ self.service.spreadsheets()
215
+ .values()
216
+ .append(
217
+ spreadsheetId=spreadsheet_id,
218
+ range=range_a1,
219
+ valueInputOption=value_input_option,
220
+ insertDataOption=insert_data_option,
221
+ body=body,
222
+ )
223
+ .execute()
224
+ )
225
+ # result typically includes: spreadsheetId, tableRange (if appended to a table), updates (if named ranges/etc. were affected)
226
+ logger.info(
227
+ f"Successfully appended rows to spreadsheet ID: {spreadsheet_id}. Updates: {result.get('updates')}"
228
+ )
229
+ return {
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
239
+ }
240
+ except HttpError as error:
241
+ logger.error(
242
+ f"Error appending rows to range '{range_a1}' in spreadsheet {spreadsheet_id}: {error}"
243
+ )
244
+ return self.handle_api_error("append_rows", error)
245
+ except Exception as e:
246
+ logger.exception(
247
+ f"Unexpected error appending rows to range '{range_a1}' in spreadsheet {spreadsheet_id}"
248
+ )
249
+ return {
250
+ "error": True,
251
+ "error_type": "unexpected_service_error",
252
+ "message": str(e),
253
+ "operation": "append_rows",
254
+ }
255
+
256
+ def clear_range(self, spreadsheet_id: str, range_a1: str) -> dict[str, Any] | None:
257
+ """
258
+ Clears values from a specified range in a Google Spreadsheet.
259
+ Note: This typically clears values only, not formatting unless specified in a more complex request body.
260
+
261
+ Args:
262
+ spreadsheet_id: The ID of the Google Spreadsheet.
263
+ range_a1: The A1 notation of the range to clear (e.g., "Sheet1!A1:C5").
264
+
265
+ Returns:
266
+ A dictionary containing the cleared range and spreadsheet ID, or an error dictionary.
267
+ """
268
+ try:
269
+ logger.info(
270
+ f"Clearing range '{range_a1}' in spreadsheet ID: {spreadsheet_id}"
271
+ )
272
+ # The body for clear is an empty JSON object {}.
273
+ result = (
274
+ self.service.spreadsheets()
275
+ .values()
276
+ .clear(spreadsheetId=spreadsheet_id, range=range_a1, body={})
277
+ .execute()
278
+ )
279
+ # result typically includes: spreadsheetId, clearedRange
280
+ logger.info(
281
+ f"Successfully cleared range '{result.get('clearedRange')}' in spreadsheet ID: {spreadsheet_id}."
282
+ )
283
+ return {
284
+ "spreadsheet_id": result.get(
285
+ "spreadsheetId"
286
+ ), # Or use the input spreadsheet_id
287
+ "cleared_range": result.get("clearedRange"),
288
+ }
289
+ except HttpError as error:
290
+ logger.error(
291
+ f"Error clearing range '{range_a1}' from spreadsheet {spreadsheet_id}: {error}"
292
+ )
293
+ return self.handle_api_error("clear_range", error)
294
+ except Exception as e:
295
+ logger.exception(
296
+ f"Unexpected error clearing range '{range_a1}' from spreadsheet {spreadsheet_id}"
297
+ )
298
+ return {
299
+ "error": True,
300
+ "error_type": "unexpected_service_error",
301
+ "message": str(e),
302
+ "operation": "clear_range",
303
+ }
304
+
305
+ def add_sheet(self, spreadsheet_id: str, title: str) -> dict[str, Any] | None:
306
+ """
307
+ Adds a new sheet to the specified spreadsheet.
308
+
309
+ Args:
310
+ spreadsheet_id: The ID of the Google Spreadsheet.
311
+ title: The title for the new sheet.
312
+
313
+ Returns:
314
+ A dictionary containing properties of the newly added sheet (e.g., sheetId, title, index),
315
+ or an error dictionary.
316
+ """
317
+ try:
318
+ logger.info(
319
+ f"Adding new sheet with title '{title}' to spreadsheet ID: {spreadsheet_id}"
320
+ )
321
+ requests = [{"addSheet": {"properties": {"title": title}}}]
322
+ body = {"requests": requests}
323
+ response = (
324
+ self.service.spreadsheets()
325
+ .batchUpdate(spreadsheetId=spreadsheet_id, body=body)
326
+ .execute()
327
+ )
328
+
329
+ # The response contains a list of replies, one for each request.
330
+ # The addSheet reply contains the properties of the new sheet.
331
+ new_sheet_properties = None
332
+ if response and response.get("replies"):
333
+ for reply in response.get("replies"):
334
+ if "addSheet" in reply:
335
+ new_sheet_properties = reply["addSheet"].get("properties")
336
+ break
337
+
338
+ if not new_sheet_properties:
339
+ logger.error(
340
+ f"Failed to add sheet '{title}' or extract properties from response for spreadsheet {spreadsheet_id}."
341
+ )
342
+ return {
343
+ "error": True,
344
+ "error_type": "api_response_error",
345
+ "message": "Failed to add sheet or parse response.",
346
+ "operation": "add_sheet",
347
+ }
348
+
349
+ logger.info(
350
+ f"Successfully added sheet: {new_sheet_properties.get('title')} (ID: {new_sheet_properties.get('sheetId')}) to spreadsheet ID: {spreadsheet_id}"
351
+ )
352
+ return {
353
+ "spreadsheet_id": spreadsheet_id,
354
+ "sheet_properties": new_sheet_properties,
355
+ }
356
+ except HttpError as error:
357
+ logger.error(
358
+ f"Error adding sheet '{title}' to spreadsheet {spreadsheet_id}: {error}"
359
+ )
360
+ return self.handle_api_error("add_sheet", error)
361
+ except Exception as e:
362
+ logger.exception(
363
+ f"Unexpected error adding sheet '{title}' to spreadsheet {spreadsheet_id}"
364
+ )
365
+ return {
366
+ "error": True,
367
+ "error_type": "unexpected_service_error",
368
+ "message": str(e),
369
+ "operation": "add_sheet",
370
+ }
371
+
372
+ def delete_sheet(self, spreadsheet_id: str, sheet_id: int) -> dict[str, Any] | None:
373
+ """
374
+ Deletes a sheet from the specified spreadsheet.
375
+
376
+ Args:
377
+ spreadsheet_id: The ID of the Google Spreadsheet.
378
+ sheet_id: The numeric ID of the sheet to delete.
379
+
380
+ Returns:
381
+ A dictionary indicating success (spreadsheetId and deleted sheetId) or an error dictionary.
382
+ """
383
+ try:
384
+ logger.info(
385
+ f"Deleting sheet ID: {sheet_id} from spreadsheet ID: {spreadsheet_id}"
386
+ )
387
+ requests = [{"deleteSheet": {"sheetId": sheet_id}}]
388
+ body = {"requests": requests}
389
+ response = (
390
+ self.service.spreadsheets()
391
+ .batchUpdate(spreadsheetId=spreadsheet_id, body=body)
392
+ .execute()
393
+ )
394
+
395
+ # A successful deleteSheet request usually doesn't return detailed content in the reply.
396
+ # The overall response.spreadsheetId confirms the operation was on the correct spreadsheet.
397
+ logger.info(
398
+ f"Successfully submitted deletion request for sheet ID: {sheet_id} in spreadsheet ID: {spreadsheet_id}. Response: {response}"
399
+ )
400
+ return {
401
+ "spreadsheet_id": spreadsheet_id,
402
+ "deleted_sheet_id": sheet_id,
403
+ "success": True,
404
+ }
405
+ except HttpError as error:
406
+ logger.error(
407
+ f"Error deleting sheet ID {sheet_id} from spreadsheet {spreadsheet_id}: {error}"
408
+ )
409
+ return self.handle_api_error("delete_sheet", error)
410
+ except Exception as e:
411
+ logger.exception(
412
+ f"Unexpected error deleting sheet ID {sheet_id} from spreadsheet {spreadsheet_id}"
413
+ )
414
+ return {
415
+ "error": True,
416
+ "error_type": "unexpected_service_error",
417
+ "message": str(e),
418
+ "operation": "delete_sheet",
419
+ }
420
+
421
+ def get_spreadsheet_metadata(
422
+ self, spreadsheet_id: str, fields: str | None = None
423
+ ) -> dict[str, Any] | None:
424
+ """
425
+ Retrieves metadata for a specific Google Spreadsheet.
426
+
427
+ Args:
428
+ spreadsheet_id: The ID of the Google Spreadsheet.
429
+ fields: Optional. A string specifying which fields to include in the response,
430
+ e.g., "properties,sheets.properties,namedRanges". Defaults to basic properties and sheet info.
431
+
432
+ Returns:
433
+ A dictionary containing spreadsheet metadata or an error dictionary.
434
+ """
435
+ try:
436
+ logger.info(
437
+ f"Fetching metadata for spreadsheet ID: {spreadsheet_id}"
438
+ + (f" with fields: {fields}" if fields else "")
439
+ )
440
+ if fields is None:
441
+ fields = "spreadsheetId,properties,sheets(properties(sheetId,title,index,sheetType,gridProperties))"
442
+
443
+ spreadsheet_metadata = (
444
+ self.service.spreadsheets()
445
+ .get(spreadsheetId=spreadsheet_id, fields=fields)
446
+ .execute()
447
+ )
448
+ logger.info(
449
+ f"Successfully fetched metadata for spreadsheet: {spreadsheet_metadata.get('properties',{}).get('title')}"
450
+ )
451
+ return spreadsheet_metadata
452
+ except HttpError as error:
453
+ logger.error(
454
+ f"Error fetching metadata for spreadsheet ID {spreadsheet_id}: {error}"
455
+ )
456
+ return self.handle_api_error("get_spreadsheet_metadata", error)
457
+ except Exception as e:
458
+ logger.exception(
459
+ f"Unexpected error fetching metadata for spreadsheet ID {spreadsheet_id}"
460
+ )
461
+ return {
462
+ "error": True,
463
+ "error_type": "unexpected_service_error",
464
+ "message": str(e),
465
+ "operation": "get_spreadsheet_metadata",
466
+ }