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.
@@ -3,9 +3,19 @@ Gmail tools for Google Workspace MCP operations.
3
3
  """
4
4
 
5
5
  import logging
6
- from typing import Any
7
6
 
8
7
  from google_workspace_mcp.app import mcp
8
+ from google_workspace_mcp.models import (
9
+ GmailAttachmentOutput,
10
+ GmailBulkDeleteOutput,
11
+ GmailDraftCreationOutput,
12
+ GmailDraftDeletionOutput,
13
+ GmailDraftSendOutput,
14
+ GmailEmailSearchOutput,
15
+ GmailMessageDetailsOutput,
16
+ GmailReplyOutput,
17
+ GmailSendOutput,
18
+ )
9
19
  from google_workspace_mcp.services.gmail import GmailService
10
20
 
11
21
  logger = logging.getLogger(__name__)
@@ -14,10 +24,13 @@ logger = logging.getLogger(__name__)
14
24
  # --- Gmail Tool Functions --- #
15
25
 
16
26
 
17
- # @mcp.tool(
18
- # name="query_gmail_emails",
19
- # )
20
- async def query_gmail_emails(query: str, max_results: int = 100) -> dict[str, Any]:
27
+ @mcp.tool(
28
+ name="query_gmail_emails",
29
+ description="Query Gmail emails based on a search query.",
30
+ )
31
+ async def query_gmail_emails(
32
+ query: str, max_results: int = 100
33
+ ) -> GmailEmailSearchOutput:
21
34
  """
22
35
  Searches for Gmail emails using Gmail query syntax.
23
36
 
@@ -26,7 +39,7 @@ async def query_gmail_emails(query: str, max_results: int = 100) -> dict[str, An
26
39
  max_results: Maximum number of emails to return.
27
40
 
28
41
  Returns:
29
- A dictionary containing the list of matching emails or an error message.
42
+ GmailEmailSearchOutput containing the list of matching emails.
30
43
  """
31
44
  logger.info(f"Executing query_gmail_emails tool with query: '{query}'")
32
45
 
@@ -39,15 +52,16 @@ async def query_gmail_emails(query: str, max_results: int = 100) -> dict[str, An
39
52
 
40
53
  # Return appropriate message if no results
41
54
  if not emails:
42
- return {"message": "No emails found for the query."}
55
+ emails = []
43
56
 
44
- return {"count": len(emails), "emails": emails}
57
+ return GmailEmailSearchOutput(count=len(emails), emails=emails)
45
58
 
46
59
 
47
- # @mcp.tool(
48
- # name="gmail_get_message_details",
49
- # )
50
- async def gmail_get_message_details(email_id: str) -> dict[str, Any]:
60
+ @mcp.tool(
61
+ name="gmail_get_message_details",
62
+ description="Retrieves a complete Gmail email message by its ID.",
63
+ )
64
+ async def gmail_get_message_details(email_id: str) -> GmailMessageDetailsOutput:
51
65
  """
52
66
  Retrieves a complete Gmail email message by its ID.
53
67
 
@@ -55,7 +69,7 @@ async def gmail_get_message_details(email_id: str) -> dict[str, Any]:
55
69
  email_id: The ID of the Gmail message to retrieve.
56
70
 
57
71
  Returns:
58
- A dictionary containing the email details and attachments.
72
+ GmailMessageDetailsOutput containing the email details and attachments.
59
73
  """
60
74
  logger.info(f"Executing gmail_get_message_details tool with email_id: '{email_id}'")
61
75
  if not email_id or not email_id.strip():
@@ -72,13 +86,25 @@ async def gmail_get_message_details(email_id: str) -> dict[str, Any]:
72
86
  if not result:
73
87
  raise ValueError(f"Failed to retrieve email with ID: {email_id}")
74
88
 
75
- return result
89
+ return GmailMessageDetailsOutput(
90
+ id=result["id"],
91
+ thread_id=result.get("thread_id", ""),
92
+ subject=result.get("subject", ""),
93
+ from_email=result.get("from_email", ""),
94
+ to_email=result.get("to_email", []),
95
+ date=result.get("date", ""),
96
+ body=result.get("body", ""),
97
+ attachments=result.get("attachments"),
98
+ )
76
99
 
77
100
 
78
- # @mcp.tool(
79
- # name="gmail_get_attachment_content",
80
- # )
81
- async def gmail_get_attachment_content(message_id: str, attachment_id: str) -> dict[str, Any]:
101
+ @mcp.tool(
102
+ name="gmail_get_attachment_content",
103
+ description="Retrieves a specific attachment from a Gmail message.",
104
+ )
105
+ async def gmail_get_attachment_content(
106
+ message_id: str, attachment_id: str
107
+ ) -> GmailAttachmentOutput:
82
108
  """
83
109
  Retrieves a specific attachment from a Gmail message.
84
110
 
@@ -87,14 +113,18 @@ async def gmail_get_attachment_content(message_id: str, attachment_id: str) -> d
87
113
  attachment_id: The ID of the attachment to retrieve.
88
114
 
89
115
  Returns:
90
- A dictionary containing filename, mimeType, size, and base64 data.
116
+ GmailAttachmentOutput containing filename, mimeType, size, and base64 data.
91
117
  """
92
- logger.info(f"Executing gmail_get_attachment_content tool - Msg: {message_id}, Attach: {attachment_id}")
118
+ logger.info(
119
+ f"Executing gmail_get_attachment_content tool - Msg: {message_id}, Attach: {attachment_id}"
120
+ )
93
121
  if not message_id or not attachment_id:
94
122
  raise ValueError("Message ID and attachment ID are required")
95
123
 
96
124
  gmail_service = GmailService()
97
- result = gmail_service.get_attachment_content(message_id=message_id, attachment_id=attachment_id)
125
+ result = gmail_service.get_attachment_content(
126
+ message_id=message_id, attachment_id=attachment_id
127
+ )
98
128
 
99
129
  if not result or (isinstance(result, dict) and result.get("error")):
100
130
  error_msg = "Error getting attachment"
@@ -102,20 +132,25 @@ async def gmail_get_attachment_content(message_id: str, attachment_id: str) -> d
102
132
  error_msg = result.get("message", error_msg)
103
133
  raise ValueError(error_msg)
104
134
 
105
- # FastMCP should handle this dict, recognizing 'data' as content blob.
106
- return result
135
+ return GmailAttachmentOutput(
136
+ filename=result["filename"],
137
+ mime_type=result["mime_type"],
138
+ size=result["size"],
139
+ data=result["data"],
140
+ )
107
141
 
108
142
 
109
- # @mcp.tool(
110
- # name="create_gmail_draft",
111
- # )
143
+ @mcp.tool(
144
+ name="create_gmail_draft",
145
+ description="Creates a draft email message in Gmail.",
146
+ )
112
147
  async def create_gmail_draft(
113
148
  to: str,
114
149
  subject: str,
115
150
  body: str,
116
151
  cc: list[str] | None = None,
117
152
  bcc: list[str] | None = None,
118
- ) -> dict[str, Any]:
153
+ ) -> GmailDraftCreationOutput:
119
154
  """
120
155
  Creates a draft email message in Gmail.
121
156
 
@@ -127,7 +162,7 @@ async def create_gmail_draft(
127
162
  bcc: Optional list of email addresses to BCC.
128
163
 
129
164
  Returns:
130
- A dictionary containing the created draft details.
165
+ GmailDraftCreationOutput containing the created draft details.
131
166
  """
132
167
  logger.info("Executing create_gmail_draft")
133
168
  if not to or not subject or not body: # Check for empty strings
@@ -135,7 +170,9 @@ async def create_gmail_draft(
135
170
 
136
171
  gmail_service = GmailService()
137
172
  # Pass bcc parameter even though service may not use it (for test compatibility)
138
- result = gmail_service.create_draft(to=to, subject=subject, body=body, cc=cc, bcc=bcc)
173
+ result = gmail_service.create_draft(
174
+ to=to, subject=subject, body=body, cc=cc, bcc=bcc
175
+ )
139
176
 
140
177
  if not result or (isinstance(result, dict) and result.get("error")):
141
178
  error_msg = "Error creating draft"
@@ -143,15 +180,16 @@ async def create_gmail_draft(
143
180
  error_msg = result.get("message", error_msg)
144
181
  raise ValueError(error_msg)
145
182
 
146
- return result
183
+ return GmailDraftCreationOutput(id=result["id"], message=result.get("message", {}))
147
184
 
148
185
 
149
- # @mcp.tool(
150
- # name="delete_gmail_draft",
151
- # )
186
+ @mcp.tool(
187
+ name="delete_gmail_draft",
188
+ description="Deletes a Gmail draft email by its draft ID.",
189
+ )
152
190
  async def delete_gmail_draft(
153
191
  draft_id: str,
154
- ) -> dict[str, Any]:
192
+ ) -> GmailDraftDeletionOutput:
155
193
  """
156
194
  Deletes a specific draft email from Gmail.
157
195
 
@@ -159,7 +197,7 @@ async def delete_gmail_draft(
159
197
  draft_id: The ID of the draft to delete.
160
198
 
161
199
  Returns:
162
- A dictionary confirming the deletion.
200
+ GmailDraftDeletionOutput confirming the deletion.
163
201
  """
164
202
  logger.info(f"Executing delete_gmail_draft with draft_id: '{draft_id}'")
165
203
  if not draft_id or not draft_id.strip():
@@ -172,22 +210,24 @@ async def delete_gmail_draft(
172
210
  # Attempt to check if the service returned an error dict
173
211
  # (Assuming handle_api_error might return dict or False/None)
174
212
  # This part might need adjustment based on actual service error handling
175
- error_info = getattr(gmail_service, "last_error", None) # Hypothetical error capture
213
+ error_info = getattr(
214
+ gmail_service, "last_error", None
215
+ ) # Hypothetical error capture
176
216
  error_msg = "Failed to delete draft"
177
217
  if isinstance(error_info, dict) and error_info.get("error"):
178
218
  error_msg = error_info.get("message", error_msg)
179
219
  raise ValueError(error_msg)
180
220
 
181
- return {
182
- "message": f"Draft with ID '{draft_id}' deleted successfully.",
183
- "success": True,
184
- }
221
+ return GmailDraftDeletionOutput(
222
+ message=f"Draft with ID '{draft_id}' deleted successfully.", success=True
223
+ )
185
224
 
186
225
 
187
- # @mcp.tool(
188
- # name="gmail_send_draft",
189
- # )
190
- async def gmail_send_draft(draft_id: str) -> dict[str, Any]:
226
+ @mcp.tool(
227
+ name="gmail_send_draft",
228
+ description="Sends an existing draft email from Gmail.",
229
+ )
230
+ async def gmail_send_draft(draft_id: str) -> GmailDraftSendOutput:
191
231
  """
192
232
  Sends a specific draft email.
193
233
 
@@ -195,7 +235,7 @@ async def gmail_send_draft(draft_id: str) -> dict[str, Any]:
195
235
  draft_id: The ID of the draft to send.
196
236
 
197
237
  Returns:
198
- A dictionary containing the details of the sent message or an error.
238
+ GmailDraftSendOutput containing the details of the sent message.
199
239
  """
200
240
  logger.info(f"Executing gmail_send_draft tool for draft_id: '{draft_id}'")
201
241
  if not draft_id or not draft_id.strip():
@@ -210,18 +250,23 @@ async def gmail_send_draft(draft_id: str) -> dict[str, Any]:
210
250
  if not result: # Should be caught by error dict check
211
251
  raise ValueError(f"Failed to send draft '{draft_id}'")
212
252
 
213
- return result
253
+ return GmailDraftSendOutput(
254
+ id=result["id"],
255
+ thread_id=result.get("thread_id", ""),
256
+ label_ids=result.get("label_ids", []),
257
+ )
214
258
 
215
259
 
216
- # @mcp.tool(
217
- # name="gmail_reply_to_email",
218
- # )
260
+ @mcp.tool(
261
+ name="gmail_reply_to_email",
262
+ description="Create a reply to an existing email. Can be sent or saved as draft.",
263
+ )
219
264
  async def gmail_reply_to_email(
220
265
  email_id: str,
221
266
  reply_body: str,
222
267
  send: bool = False,
223
268
  reply_all: bool = False,
224
- ) -> dict[str, Any]:
269
+ ) -> GmailReplyOutput:
225
270
  """
226
271
  Creates a reply to an existing email thread.
227
272
 
@@ -232,7 +277,7 @@ async def gmail_reply_to_email(
232
277
  reply_all: If True, reply to all recipients. If False, reply to sender only.
233
278
 
234
279
  Returns:
235
- A dictionary containing the sent message or created draft details.
280
+ GmailReplyOutput containing the sent message or created draft details.
236
281
  """
237
282
  logger.info(f"Executing gmail_reply_to_email to message: '{email_id}'")
238
283
  if not email_id or not reply_body:
@@ -252,15 +297,18 @@ async def gmail_reply_to_email(
252
297
  error_msg = result.get("message", error_msg)
253
298
  raise ValueError(error_msg)
254
299
 
255
- return result
300
+ return GmailReplyOutput(
301
+ id=result["id"], thread_id=result.get("thread_id", ""), in_reply_to=email_id
302
+ )
256
303
 
257
304
 
258
- # @mcp.tool(
259
- # name="gmail_bulk_delete_messages",
260
- # )
305
+ @mcp.tool(
306
+ name="gmail_bulk_delete_messages",
307
+ description="Delete multiple emails at once by providing a list of message IDs.",
308
+ )
261
309
  async def gmail_bulk_delete_messages(
262
310
  message_ids: list[str],
263
- ) -> dict[str, Any]:
311
+ ) -> GmailBulkDeleteOutput:
264
312
  """
265
313
  Deletes multiple Gmail emails using a list of message IDs.
266
314
 
@@ -268,7 +316,7 @@ async def gmail_bulk_delete_messages(
268
316
  message_ids: A list of email message IDs to delete.
269
317
 
270
318
  Returns:
271
- A dictionary summarizing the deletion result.
319
+ GmailBulkDeleteOutput summarizing the deletion result.
272
320
  """
273
321
  # Validation first - check if it's a list
274
322
  if not isinstance(message_ids, list):
@@ -289,19 +337,26 @@ async def gmail_bulk_delete_messages(
289
337
  error_msg = result.get("message", error_msg)
290
338
  raise ValueError(error_msg)
291
339
 
292
- return result
340
+ return GmailBulkDeleteOutput(
341
+ deleted_count=result.get("deleted_count", len(message_ids)),
342
+ success=result.get("success", True),
343
+ message=result.get(
344
+ "message", f"Successfully deleted {len(message_ids)} messages"
345
+ ),
346
+ )
293
347
 
294
348
 
295
- # @mcp.tool(
296
- # name="gmail_send_email",
297
- # )
349
+ @mcp.tool(
350
+ name="gmail_send_email",
351
+ description="Composes and sends an email directly.",
352
+ )
298
353
  async def gmail_send_email(
299
354
  to: list[str],
300
355
  subject: str,
301
356
  body: str,
302
357
  cc: list[str] | None = None,
303
358
  bcc: list[str] | None = None,
304
- ) -> dict[str, Any]:
359
+ ) -> GmailSendOutput:
305
360
  """
306
361
  Composes and sends an email message.
307
362
 
@@ -313,14 +368,20 @@ async def gmail_send_email(
313
368
  bcc: Optional. A list of BCC recipient email addresses.
314
369
 
315
370
  Returns:
316
- A dictionary containing the details of the sent message or an error.
371
+ GmailSendOutput containing the details of the sent message.
317
372
  """
318
373
  logger.info(f"Executing gmail_send_email tool to: {to}, subject: '{subject}'")
319
- if not to or not isinstance(to, list) or not all(isinstance(email, str) and email.strip() for email in to):
374
+ if (
375
+ not to
376
+ or not isinstance(to, list)
377
+ or not all(isinstance(email, str) and email.strip() for email in to)
378
+ ):
320
379
  raise ValueError("Recipients 'to' must be a non-empty list of email strings.")
321
380
  if not subject or not subject.strip():
322
381
  raise ValueError("Subject cannot be empty.")
323
- if body is None: # Allow empty string for body, but not None if it implies missing arg.
382
+ if (
383
+ body is None
384
+ ): # Allow empty string for body, but not None if it implies missing arg.
324
385
  raise ValueError("Body cannot be None (can be an empty string).")
325
386
 
326
387
  gmail_service = GmailService()
@@ -332,4 +393,8 @@ async def gmail_send_email(
332
393
  if not result:
333
394
  raise ValueError("Failed to send email")
334
395
 
335
- return result
396
+ return GmailSendOutput(
397
+ id=result["id"],
398
+ thread_id=result.get("thread_id", ""),
399
+ label_ids=result.get("label_ids", []),
400
+ )