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,221 @@
1
+ """
2
+ Drive tools for Google Drive operations.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any
7
+
8
+ from google_workspace_mcp.app import mcp # Import from central app module
9
+ from google_workspace_mcp.services.drive import DriveService
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ # --- Drive Tool Functions --- #
15
+
16
+
17
+ @mcp.tool(
18
+ name="drive_search_files",
19
+ description="Search for files in Google Drive with optional shared drive support.",
20
+ )
21
+ async def drive_search_files(
22
+ query: str,
23
+ page_size: int = 10,
24
+ shared_drive_id: str | None = None,
25
+ ) -> dict[str, Any]:
26
+ """
27
+ Search for files in Google Drive, optionally within a specific shared drive.
28
+
29
+ Args:
30
+ query: Search query string. Can be a simple text search or complex query with operators.
31
+ page_size: Maximum number of files to return (1 to 1000, default 10).
32
+ shared_drive_id: Optional shared drive ID to search within a specific shared drive.
33
+
34
+ Returns:
35
+ A dictionary containing a list of files or an error message.
36
+ """
37
+ logger.info(
38
+ f"Executing drive_search_files with query: '{query}', page_size: {page_size}, shared_drive_id: {shared_drive_id}"
39
+ )
40
+
41
+ if not query or not query.strip():
42
+ raise ValueError("Query cannot be empty")
43
+
44
+ drive_service = DriveService()
45
+ files = drive_service.search_files(
46
+ query=query, page_size=page_size, shared_drive_id=shared_drive_id
47
+ )
48
+
49
+ if isinstance(files, dict) and files.get("error"):
50
+ raise ValueError(f"Search failed: {files.get('message', 'Unknown error')}")
51
+
52
+ return {"files": files}
53
+
54
+
55
+ @mcp.tool(
56
+ name="drive_read_file_content",
57
+ description="Read the content of a file from Google Drive.",
58
+ )
59
+ async def drive_read_file_content(file_id: str) -> dict[str, Any]:
60
+ """
61
+ Read the content of a file from Google Drive.
62
+
63
+ Args:
64
+ file_id: The ID of the file to read.
65
+
66
+ Returns:
67
+ A dictionary containing the file content and metadata or an error.
68
+ """
69
+ logger.info(f"Executing drive_read_file_content tool with file_id: '{file_id}'")
70
+ if not file_id or not file_id.strip():
71
+ raise ValueError("File ID cannot be empty")
72
+
73
+ drive_service = DriveService()
74
+ result = drive_service.read_file_content(file_id=file_id)
75
+
76
+ if result is None:
77
+ raise ValueError("File not found or could not be read")
78
+
79
+ if isinstance(result, dict) and result.get("error"):
80
+ raise ValueError(result.get("message", "Error reading file"))
81
+
82
+ return result
83
+
84
+
85
+ @mcp.tool(
86
+ name="drive_upload_file",
87
+ description="Upload a local file to Google Drive. Requires a local file path.",
88
+ )
89
+ async def drive_upload_file(
90
+ file_path: str,
91
+ parent_folder_id: str | None = None,
92
+ shared_drive_id: str | None = None,
93
+ ) -> dict[str, Any]:
94
+ """
95
+ Upload a local file to Google Drive.
96
+
97
+ Args:
98
+ file_path: Path to the local file to upload.
99
+ parent_folder_id: Optional parent folder ID to upload the file to.
100
+ shared_drive_id: Optional shared drive ID to upload the file to a shared drive.
101
+
102
+ Returns:
103
+ A dictionary containing the uploaded file metadata or an error.
104
+ """
105
+ logger.info(
106
+ f"Executing drive_upload_file with path: '{file_path}', parent_folder_id: {parent_folder_id}, shared_drive_id: {shared_drive_id}"
107
+ )
108
+ if not file_path or not file_path.strip():
109
+ raise ValueError("File path cannot be empty")
110
+
111
+ drive_service = DriveService()
112
+ result = drive_service.upload_file(
113
+ file_path=file_path,
114
+ parent_folder_id=parent_folder_id,
115
+ shared_drive_id=shared_drive_id,
116
+ )
117
+
118
+ if isinstance(result, dict) and result.get("error"):
119
+ raise ValueError(result.get("message", "Error uploading file"))
120
+
121
+ return result
122
+
123
+
124
+ @mcp.tool(
125
+ name="drive_create_folder",
126
+ description="Create a new folder in Google Drive.",
127
+ )
128
+ async def drive_create_folder(
129
+ folder_name: str,
130
+ parent_folder_id: str | None = None,
131
+ shared_drive_id: str | None = None,
132
+ ) -> dict[str, Any]:
133
+ """
134
+ Create a new folder in Google Drive.
135
+
136
+ Args:
137
+ folder_name: The name for the new folder.
138
+ parent_folder_id: Optional parent folder ID to create the folder within.
139
+ shared_drive_id: Optional shared drive ID to create the folder in a shared drive.
140
+
141
+ Returns:
142
+ A dictionary containing the created folder information.
143
+ """
144
+ logger.info(
145
+ f"Executing drive_create_folder with folder_name: '{folder_name}', parent_folder_id: {parent_folder_id}, shared_drive_id: {shared_drive_id}"
146
+ )
147
+
148
+ if not folder_name or not folder_name.strip():
149
+ raise ValueError("Folder name cannot be empty")
150
+
151
+ drive_service = DriveService()
152
+ result = drive_service.create_folder(
153
+ folder_name=folder_name,
154
+ parent_folder_id=parent_folder_id,
155
+ shared_drive_id=shared_drive_id,
156
+ )
157
+
158
+ if isinstance(result, dict) and result.get("error"):
159
+ raise ValueError(
160
+ f"Folder creation failed: {result.get('message', 'Unknown error')}"
161
+ )
162
+
163
+ return result
164
+
165
+
166
+ @mcp.tool(
167
+ name="drive_delete_file",
168
+ description="Delete a file from Google Drive using its file ID.",
169
+ )
170
+ async def drive_delete_file(
171
+ file_id: str,
172
+ ) -> dict[str, Any]:
173
+ """
174
+ Delete a file from Google Drive.
175
+
176
+ Args:
177
+ file_id: The ID of the file to delete.
178
+
179
+ Returns:
180
+ A dictionary confirming the deletion or an error.
181
+ """
182
+ logger.info(f"Executing drive_delete_file with file_id: '{file_id}'")
183
+ if not file_id or not file_id.strip():
184
+ raise ValueError("File ID cannot be empty")
185
+
186
+ drive_service = DriveService()
187
+ result = drive_service.delete_file(file_id=file_id)
188
+
189
+ if isinstance(result, dict) and result.get("error"):
190
+ raise ValueError(result.get("message", "Error deleting file"))
191
+
192
+ return result
193
+
194
+
195
+ @mcp.tool(
196
+ name="drive_list_shared_drives",
197
+ description="Lists shared drives accessible by the user.",
198
+ )
199
+ async def drive_list_shared_drives(page_size: int = 100) -> dict[str, Any]:
200
+ """
201
+ Lists shared drives (formerly Team Drives) that the user has access to.
202
+
203
+ Args:
204
+ page_size: Maximum number of shared drives to return (1 to 100, default 100).
205
+
206
+ Returns:
207
+ A dictionary containing a list of shared drives with their 'id' and 'name',
208
+ or an error message.
209
+ """
210
+ logger.info(f"Executing drive_list_shared_drives tool with page_size: {page_size}")
211
+
212
+ drive_service = DriveService()
213
+ drives = drive_service.list_shared_drives(page_size=page_size)
214
+
215
+ if isinstance(drives, dict) and drives.get("error"):
216
+ raise ValueError(drives.get("message", "Error listing shared drives"))
217
+
218
+ if not drives:
219
+ return {"message": "No shared drives found or accessible."}
220
+
221
+ return {"count": len(drives), "shared_drives": drives}
@@ -0,0 +1,344 @@
1
+ """
2
+ Gmail tools for Google Workspace MCP operations.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any
7
+
8
+ from google_workspace_mcp.app import mcp
9
+ from google_workspace_mcp.services.gmail import GmailService
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ # --- Gmail Tool Functions --- #
15
+
16
+
17
+ @mcp.tool(
18
+ name="query_gmail_emails",
19
+ description="Query Gmail emails based on a search query.",
20
+ )
21
+ async def query_gmail_emails(query: str, max_results: int = 100) -> dict[str, Any]:
22
+ """
23
+ Searches for Gmail emails using Gmail query syntax.
24
+
25
+ Args:
26
+ query: Gmail search query (e.g., "is:unread from:example.com").
27
+ max_results: Maximum number of emails to return.
28
+
29
+ Returns:
30
+ A dictionary containing the list of matching emails or an error message.
31
+ """
32
+ logger.info(f"Executing query_gmail_emails tool with query: '{query}'")
33
+
34
+ gmail_service = GmailService()
35
+ emails = gmail_service.query_emails(query=query, max_results=max_results)
36
+
37
+ # Check if there's an error
38
+ if isinstance(emails, dict) and emails.get("error"):
39
+ raise ValueError(emails.get("message", "Error querying emails"))
40
+
41
+ # Return appropriate message if no results
42
+ if not emails:
43
+ return {"message": "No emails found for the query."}
44
+
45
+ return {"count": len(emails), "emails": emails}
46
+
47
+
48
+ @mcp.tool(
49
+ name="gmail_get_message_details",
50
+ description="Retrieves a complete Gmail email message by its ID.",
51
+ )
52
+ async def gmail_get_message_details(email_id: str) -> dict[str, Any]:
53
+ """
54
+ Retrieves a complete Gmail email message by its ID.
55
+
56
+ Args:
57
+ email_id: The ID of the Gmail message to retrieve.
58
+
59
+ Returns:
60
+ A dictionary containing the email details and attachments.
61
+ """
62
+ logger.info(f"Executing gmail_get_message_details tool with email_id: '{email_id}'")
63
+ if not email_id or not email_id.strip():
64
+ raise ValueError("Email ID cannot be empty")
65
+
66
+ gmail_service = GmailService()
67
+ result = gmail_service.get_email(email_id=email_id)
68
+
69
+ # Check for explicit error from service first
70
+ if isinstance(result, dict) and result.get("error"):
71
+ raise ValueError(result.get("message", "Error getting email"))
72
+
73
+ # Then check if email is missing (e.g., service returned None)
74
+ if not result:
75
+ raise ValueError(f"Failed to retrieve email with ID: {email_id}")
76
+
77
+ return result
78
+
79
+
80
+ @mcp.tool(
81
+ name="gmail_get_attachment_content",
82
+ description="Retrieves a specific attachment from a Gmail message.",
83
+ )
84
+ async def gmail_get_attachment_content(message_id: str, attachment_id: str) -> dict[str, Any]:
85
+ """
86
+ Retrieves a specific attachment from a Gmail message.
87
+
88
+ Args:
89
+ message_id: The ID of the email message.
90
+ attachment_id: The ID of the attachment to retrieve.
91
+
92
+ Returns:
93
+ A dictionary containing filename, mimeType, size, and base64 data.
94
+ """
95
+ logger.info(f"Executing gmail_get_attachment_content tool - Msg: {message_id}, Attach: {attachment_id}")
96
+ if not message_id or not attachment_id:
97
+ raise ValueError("Message ID and attachment ID are required")
98
+
99
+ gmail_service = GmailService()
100
+ result = gmail_service.get_attachment_content(message_id=message_id, attachment_id=attachment_id)
101
+
102
+ if not result or (isinstance(result, dict) and result.get("error")):
103
+ error_msg = "Error getting attachment"
104
+ if isinstance(result, dict):
105
+ error_msg = result.get("message", error_msg)
106
+ raise ValueError(error_msg)
107
+
108
+ # FastMCP should handle this dict, recognizing 'data' as content blob.
109
+ return result
110
+
111
+
112
+ @mcp.tool(
113
+ name="create_gmail_draft",
114
+ description="Creates a draft email message in Gmail.",
115
+ )
116
+ async def create_gmail_draft(
117
+ to: str,
118
+ subject: str,
119
+ body: str,
120
+ cc: list[str] | None = None,
121
+ bcc: list[str] | None = None,
122
+ ) -> dict[str, Any]:
123
+ """
124
+ Creates a draft email message in Gmail.
125
+
126
+ Args:
127
+ to: Email address of the recipient.
128
+ subject: Subject line of the email.
129
+ body: Body content of the email.
130
+ cc: Optional list of email addresses to CC.
131
+ bcc: Optional list of email addresses to BCC.
132
+
133
+ Returns:
134
+ A dictionary containing the created draft details.
135
+ """
136
+ logger.info("Executing create_gmail_draft")
137
+ if not to or not subject or not body: # Check for empty strings
138
+ raise ValueError("To, subject, and body are required")
139
+
140
+ gmail_service = GmailService()
141
+ # Pass bcc parameter even though service may not use it (for test compatibility)
142
+ result = gmail_service.create_draft(to=to, subject=subject, body=body, cc=cc, bcc=bcc)
143
+
144
+ if not result or (isinstance(result, dict) and result.get("error")):
145
+ error_msg = "Error creating draft"
146
+ if isinstance(result, dict):
147
+ error_msg = result.get("message", error_msg)
148
+ raise ValueError(error_msg)
149
+
150
+ return result
151
+
152
+
153
+ @mcp.tool(
154
+ name="delete_gmail_draft",
155
+ description="Deletes a Gmail draft email by its draft ID.",
156
+ )
157
+ async def delete_gmail_draft(
158
+ draft_id: str,
159
+ ) -> dict[str, Any]:
160
+ """
161
+ Deletes a specific draft email from Gmail.
162
+
163
+ Args:
164
+ draft_id: The ID of the draft to delete.
165
+
166
+ Returns:
167
+ A dictionary confirming the deletion.
168
+ """
169
+ logger.info(f"Executing delete_gmail_draft with draft_id: '{draft_id}'")
170
+ if not draft_id or not draft_id.strip():
171
+ raise ValueError("Draft ID is required")
172
+
173
+ gmail_service = GmailService()
174
+ success = gmail_service.delete_draft(draft_id=draft_id)
175
+
176
+ if not success:
177
+ # Attempt to check if the service returned an error dict
178
+ # (Assuming handle_api_error might return dict or False/None)
179
+ # This part might need adjustment based on actual service error handling
180
+ error_info = getattr(gmail_service, "last_error", None) # Hypothetical error capture
181
+ error_msg = "Failed to delete draft"
182
+ if isinstance(error_info, dict) and error_info.get("error"):
183
+ error_msg = error_info.get("message", error_msg)
184
+ raise ValueError(error_msg)
185
+
186
+ return {
187
+ "message": f"Draft with ID '{draft_id}' deleted successfully.",
188
+ "success": True,
189
+ }
190
+
191
+
192
+ @mcp.tool(
193
+ name="gmail_send_draft",
194
+ description="Sends an existing draft email from Gmail.",
195
+ )
196
+ async def gmail_send_draft(draft_id: str) -> dict[str, Any]:
197
+ """
198
+ Sends a specific draft email.
199
+
200
+ Args:
201
+ draft_id: The ID of the draft to send.
202
+
203
+ Returns:
204
+ A dictionary containing the details of the sent message or an error.
205
+ """
206
+ logger.info(f"Executing gmail_send_draft tool for draft_id: '{draft_id}'")
207
+ if not draft_id or not draft_id.strip():
208
+ raise ValueError("Draft ID cannot be empty.")
209
+
210
+ gmail_service = GmailService()
211
+ result = gmail_service.send_draft(draft_id=draft_id)
212
+
213
+ if isinstance(result, dict) and result.get("error"):
214
+ raise ValueError(result.get("message", "Error sending draft"))
215
+
216
+ if not result: # Should be caught by error dict check
217
+ raise ValueError(f"Failed to send draft '{draft_id}'")
218
+
219
+ return result
220
+
221
+
222
+ @mcp.tool(
223
+ name="gmail_reply_to_email",
224
+ description="Create a reply to an existing email. Can be sent or saved as draft.",
225
+ )
226
+ async def gmail_reply_to_email(
227
+ email_id: str,
228
+ reply_body: str,
229
+ send: bool = False,
230
+ reply_all: bool = False,
231
+ ) -> dict[str, Any]:
232
+ """
233
+ Creates a reply to an existing email thread.
234
+
235
+ Args:
236
+ email_id: The ID of the message being replied to.
237
+ reply_body: Body content of the reply.
238
+ send: If True, send the reply immediately. If False, save as draft.
239
+ reply_all: If True, reply to all recipients. If False, reply to sender only.
240
+
241
+ Returns:
242
+ A dictionary containing the sent message or created draft details.
243
+ """
244
+ logger.info(f"Executing gmail_reply_to_email to message: '{email_id}'")
245
+ if not email_id or not reply_body:
246
+ raise ValueError("Email ID and reply body are required")
247
+
248
+ gmail_service = GmailService()
249
+ result = gmail_service.reply_to_email(
250
+ email_id=email_id,
251
+ reply_body=reply_body,
252
+ reply_all=reply_all,
253
+ )
254
+
255
+ if not result or (isinstance(result, dict) and result.get("error")):
256
+ action = "send reply" if send else "create reply draft"
257
+ error_msg = f"Error trying to {action}"
258
+ if isinstance(result, dict):
259
+ error_msg = result.get("message", error_msg)
260
+ raise ValueError(error_msg)
261
+
262
+ return result
263
+
264
+
265
+ @mcp.tool(
266
+ name="gmail_bulk_delete_messages",
267
+ description="Delete multiple emails at once by providing a list of message IDs.",
268
+ )
269
+ async def gmail_bulk_delete_messages(
270
+ message_ids: list[str],
271
+ ) -> dict[str, Any]:
272
+ """
273
+ Deletes multiple Gmail emails using a list of message IDs.
274
+
275
+ Args:
276
+ message_ids: A list of email message IDs to delete.
277
+
278
+ Returns:
279
+ A dictionary summarizing the deletion result.
280
+ """
281
+ # Validation first - check if it's a list
282
+ if not isinstance(message_ids, list):
283
+ raise ValueError("Message IDs must be provided as a list")
284
+
285
+ # Then check if the list is empty
286
+ if not message_ids:
287
+ raise ValueError("Message IDs list cannot be empty")
288
+
289
+ logger.info(f"Executing gmail_bulk_delete_messages with {len(message_ids)} IDs")
290
+
291
+ gmail_service = GmailService()
292
+ result = gmail_service.bulk_delete_messages(message_ids=message_ids)
293
+
294
+ if not result or (isinstance(result, dict) and result.get("error")):
295
+ error_msg = "Error during bulk deletion"
296
+ if isinstance(result, dict):
297
+ error_msg = result.get("message", error_msg)
298
+ raise ValueError(error_msg)
299
+
300
+ return result
301
+
302
+
303
+ @mcp.tool(
304
+ name="gmail_send_email",
305
+ description="Composes and sends an email directly.",
306
+ )
307
+ async def gmail_send_email(
308
+ to: list[str],
309
+ subject: str,
310
+ body: str,
311
+ cc: list[str] | None = None,
312
+ bcc: list[str] | None = None,
313
+ ) -> dict[str, Any]:
314
+ """
315
+ Composes and sends an email message.
316
+
317
+ Args:
318
+ to: A list of primary recipient email addresses.
319
+ subject: The subject line of the email.
320
+ body: The plain text body content of the email.
321
+ cc: Optional. A list of CC recipient email addresses.
322
+ bcc: Optional. A list of BCC recipient email addresses.
323
+
324
+ Returns:
325
+ A dictionary containing the details of the sent message or an error.
326
+ """
327
+ logger.info(f"Executing gmail_send_email tool to: {to}, subject: '{subject}'")
328
+ if not to or not isinstance(to, list) or not all(isinstance(email, str) and email.strip() for email in to):
329
+ raise ValueError("Recipients 'to' must be a non-empty list of email strings.")
330
+ if not subject or not subject.strip():
331
+ raise ValueError("Subject cannot be empty.")
332
+ if body is None: # Allow empty string for body, but not None if it implies missing arg.
333
+ raise ValueError("Body cannot be None (can be an empty string).")
334
+
335
+ gmail_service = GmailService()
336
+ result = gmail_service.send_email(to=to, subject=subject, body=body, cc=cc, bcc=bcc)
337
+
338
+ if isinstance(result, dict) and result.get("error"):
339
+ raise ValueError(result.get("message", "Error sending email"))
340
+
341
+ if not result:
342
+ raise ValueError("Failed to send email")
343
+
344
+ return result