workspace-mcp 1.0.1__py3-none-any.whl → 1.0.3__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.
gmail/gmail_tools.py CHANGED
@@ -16,7 +16,7 @@ from fastapi import Body
16
16
  from googleapiclient.errors import HttpError
17
17
 
18
18
  from auth.service_decorator import require_google_service
19
-
19
+ from core.utils import handle_http_errors
20
20
  from core.server import (
21
21
  GMAIL_READONLY_SCOPE,
22
22
  GMAIL_SEND_SCOPE,
@@ -133,6 +133,7 @@ def _format_gmail_results_plain(messages: list, query: str) -> str:
133
133
 
134
134
  @server.tool()
135
135
  @require_google_service("gmail", "gmail_read")
136
+ @handle_http_errors("search_gmail_messages")
136
137
  async def search_gmail_messages(
137
138
  service, query: str, user_google_email: str, page_size: int = 10
138
139
  ) -> str:
@@ -150,31 +151,22 @@ async def search_gmail_messages(
150
151
  """
151
152
  logger.info(f"[search_gmail_messages] Email: '{user_google_email}', Query: '{query}'")
152
153
 
153
- try:
154
- response = await asyncio.to_thread(
155
- service.users()
156
- .messages()
157
- .list(userId="me", q=query, maxResults=page_size)
158
- .execute
159
- )
160
- messages = response.get("messages", [])
161
- formatted_output = _format_gmail_results_plain(messages, query)
162
-
163
- logger.info(f"[search_gmail_messages] Found {len(messages)} messages")
164
- return formatted_output
154
+ response = await asyncio.to_thread(
155
+ service.users()
156
+ .messages()
157
+ .list(userId="me", q=query, maxResults=page_size)
158
+ .execute
159
+ )
160
+ messages = response.get("messages", [])
161
+ formatted_output = _format_gmail_results_plain(messages, query)
165
162
 
166
- except HttpError as e:
167
- error_msg = f"Gmail API error: {e.reason}" if e.resp.status != 400 else f"Invalid query: '{query}'"
168
- logger.error(f"[search_gmail_messages] {error_msg}")
169
- raise Exception(error_msg)
170
- except Exception as e:
171
- error_msg = f"Error searching Gmail: {str(e)}"
172
- logger.error(f"[search_gmail_messages] {error_msg}")
173
- raise Exception(error_msg)
163
+ logger.info(f"[search_gmail_messages] Found {len(messages)} messages")
164
+ return formatted_output
174
165
 
175
166
 
176
167
  @server.tool()
177
168
  @require_google_service("gmail", "gmail_read")
169
+ @handle_http_errors("get_gmail_message_content")
178
170
  async def get_gmail_message_content(
179
171
  service, message_id: str, user_google_email: str
180
172
  ) -> str:
@@ -192,68 +184,57 @@ async def get_gmail_message_content(
192
184
  f"[get_gmail_message_content] Invoked. Message ID: '{message_id}', Email: '{user_google_email}'"
193
185
  )
194
186
 
195
- try:
196
- logger.info(f"[get_gmail_message_content] Using service for: {user_google_email}")
197
-
198
- # Fetch message metadata first to get headers
199
- message_metadata = await asyncio.to_thread(
200
- service.users()
201
- .messages()
202
- .get(
203
- userId="me",
204
- id=message_id,
205
- format="metadata",
206
- metadataHeaders=["Subject", "From"],
207
- )
208
- .execute
187
+ logger.info(f"[get_gmail_message_content] Using service for: {user_google_email}")
188
+
189
+ # Fetch message metadata first to get headers
190
+ message_metadata = await asyncio.to_thread(
191
+ service.users()
192
+ .messages()
193
+ .get(
194
+ userId="me",
195
+ id=message_id,
196
+ format="metadata",
197
+ metadataHeaders=["Subject", "From"],
209
198
  )
199
+ .execute
200
+ )
210
201
 
211
- headers = {
212
- h["name"]: h["value"]
213
- for h in message_metadata.get("payload", {}).get("headers", [])
214
- }
215
- subject = headers.get("Subject", "(no subject)")
216
- sender = headers.get("From", "(unknown sender)")
217
-
218
- # Now fetch the full message to get the body parts
219
- message_full = await asyncio.to_thread(
220
- service.users()
221
- .messages()
222
- .get(
223
- userId="me",
224
- id=message_id,
225
- format="full", # Request full payload for body
226
- )
227
- .execute
202
+ headers = {
203
+ h["name"]: h["value"]
204
+ for h in message_metadata.get("payload", {}).get("headers", [])
205
+ }
206
+ subject = headers.get("Subject", "(no subject)")
207
+ sender = headers.get("From", "(unknown sender)")
208
+
209
+ # Now fetch the full message to get the body parts
210
+ message_full = await asyncio.to_thread(
211
+ service.users()
212
+ .messages()
213
+ .get(
214
+ userId="me",
215
+ id=message_id,
216
+ format="full", # Request full payload for body
228
217
  )
218
+ .execute
219
+ )
229
220
 
230
- # Extract the plain text body using helper function
231
- payload = message_full.get("payload", {})
232
- body_data = _extract_message_body(payload)
221
+ # Extract the plain text body using helper function
222
+ payload = message_full.get("payload", {})
223
+ body_data = _extract_message_body(payload)
233
224
 
234
- content_text = "\n".join(
235
- [
236
- f"Subject: {subject}",
237
- f"From: {sender}",
238
- f"\n--- BODY ---\n{body_data or '[No text/plain body found]'}",
239
- ]
240
- )
241
- return content_text
242
-
243
- except HttpError as e:
244
- logger.error(
245
- f"[get_gmail_message_content] Gmail API error getting message content: {e}", exc_info=True
246
- )
247
- raise Exception(f"Gmail API error: {e}")
248
- except Exception as e:
249
- logger.exception(
250
- f"[get_gmail_message_content] Unexpected error getting Gmail message content: {e}"
251
- )
252
- raise Exception(f"Unexpected error: {e}")
225
+ content_text = "\n".join(
226
+ [
227
+ f"Subject: {subject}",
228
+ f"From: {sender}",
229
+ f"\n--- BODY ---\n{body_data or '[No text/plain body found]'}",
230
+ ]
231
+ )
232
+ return content_text
253
233
 
254
234
 
255
235
  @server.tool()
256
236
  @require_google_service("gmail", "gmail_read")
237
+ @handle_http_errors("get_gmail_messages_content_batch")
257
238
  async def get_gmail_messages_content_batch(
258
239
  service,
259
240
  message_ids: List[str],
@@ -279,145 +260,134 @@ async def get_gmail_messages_content_batch(
279
260
  if not message_ids:
280
261
  raise Exception("No message IDs provided")
281
262
 
282
- try:
283
- output_messages = []
263
+ output_messages = []
284
264
 
285
- # Process in chunks of 100 (Gmail batch limit)
286
- for chunk_start in range(0, len(message_ids), 100):
287
- chunk_ids = message_ids[chunk_start:chunk_start + 100]
288
- results: Dict[str, Dict] = {}
265
+ # Process in chunks of 100 (Gmail batch limit)
266
+ for chunk_start in range(0, len(message_ids), 100):
267
+ chunk_ids = message_ids[chunk_start:chunk_start + 100]
268
+ results: Dict[str, Dict] = {}
289
269
 
290
- def _batch_callback(request_id, response, exception):
291
- """Callback for batch requests"""
292
- results[request_id] = {"data": response, "error": exception}
270
+ def _batch_callback(request_id, response, exception):
271
+ """Callback for batch requests"""
272
+ results[request_id] = {"data": response, "error": exception}
293
273
 
294
- # Try to use batch API
295
- try:
296
- batch = service.new_batch_http_request(callback=_batch_callback)
274
+ # Try to use batch API
275
+ try:
276
+ batch = service.new_batch_http_request(callback=_batch_callback)
297
277
 
298
- for mid in chunk_ids:
278
+ for mid in chunk_ids:
279
+ if format == "metadata":
280
+ req = service.users().messages().get(
281
+ userId="me",
282
+ id=mid,
283
+ format="metadata",
284
+ metadataHeaders=["Subject", "From"]
285
+ )
286
+ else:
287
+ req = service.users().messages().get(
288
+ userId="me",
289
+ id=mid,
290
+ format="full"
291
+ )
292
+ batch.add(req, request_id=mid)
293
+
294
+ # Execute batch request
295
+ await asyncio.to_thread(batch.execute)
296
+
297
+ except Exception as batch_error:
298
+ # Fallback to asyncio.gather if batch API fails
299
+ logger.warning(
300
+ f"[get_gmail_messages_content_batch] Batch API failed, falling back to asyncio.gather: {batch_error}"
301
+ )
302
+
303
+ async def fetch_message(mid: str):
304
+ try:
299
305
  if format == "metadata":
300
- req = service.users().messages().get(
301
- userId="me",
302
- id=mid,
303
- format="metadata",
304
- metadataHeaders=["Subject", "From"]
306
+ msg = await asyncio.to_thread(
307
+ service.users().messages().get(
308
+ userId="me",
309
+ id=mid,
310
+ format="metadata",
311
+ metadataHeaders=["Subject", "From"]
312
+ ).execute
305
313
  )
306
314
  else:
307
- req = service.users().messages().get(
308
- userId="me",
309
- id=mid,
310
- format="full"
315
+ msg = await asyncio.to_thread(
316
+ service.users().messages().get(
317
+ userId="me",
318
+ id=mid,
319
+ format="full"
320
+ ).execute
311
321
  )
312
- batch.add(req, request_id=mid)
322
+ return mid, msg, None
323
+ except Exception as e:
324
+ return mid, None, e
325
+
326
+ # Fetch all messages in parallel
327
+ fetch_results = await asyncio.gather(
328
+ *[fetch_message(mid) for mid in chunk_ids],
329
+ return_exceptions=False
330
+ )
313
331
 
314
- # Execute batch request
315
- await asyncio.to_thread(batch.execute)
332
+ # Convert to results format
333
+ for mid, msg, error in fetch_results:
334
+ results[mid] = {"data": msg, "error": error}
316
335
 
317
- except Exception as batch_error:
318
- # Fallback to asyncio.gather if batch API fails
319
- logger.warning(
320
- f"[get_gmail_messages_content_batch] Batch API failed, falling back to asyncio.gather: {batch_error}"
321
- )
336
+ # Process results for this chunk
337
+ for mid in chunk_ids:
338
+ entry = results.get(mid, {"data": None, "error": "No result"})
322
339
 
323
- async def fetch_message(mid: str):
324
- try:
325
- if format == "metadata":
326
- msg = await asyncio.to_thread(
327
- service.users().messages().get(
328
- userId="me",
329
- id=mid,
330
- format="metadata",
331
- metadataHeaders=["Subject", "From"]
332
- ).execute
333
- )
334
- else:
335
- msg = await asyncio.to_thread(
336
- service.users().messages().get(
337
- userId="me",
338
- id=mid,
339
- format="full"
340
- ).execute
341
- )
342
- return mid, msg, None
343
- except Exception as e:
344
- return mid, None, e
345
-
346
- # Fetch all messages in parallel
347
- fetch_results = await asyncio.gather(
348
- *[fetch_message(mid) for mid in chunk_ids],
349
- return_exceptions=False
340
+ if entry["error"]:
341
+ output_messages.append(
342
+ f"⚠️ Message {mid}: {entry['error']}\n"
350
343
  )
344
+ else:
345
+ message = entry["data"]
346
+ if not message:
347
+ output_messages.append(
348
+ f"⚠️ Message {mid}: No data returned\n"
349
+ )
350
+ continue
351
351
 
352
- # Convert to results format
353
- for mid, msg, error in fetch_results:
354
- results[mid] = {"data": msg, "error": error}
352
+ # Extract content based on format
353
+ payload = message.get("payload", {})
355
354
 
356
- # Process results for this chunk
357
- for mid in chunk_ids:
358
- entry = results.get(mid, {"data": None, "error": "No result"})
355
+ if format == "metadata":
356
+ headers = _extract_headers(payload, ["Subject", "From"])
357
+ subject = headers.get("Subject", "(no subject)")
358
+ sender = headers.get("From", "(unknown sender)")
359
359
 
360
- if entry["error"]:
361
360
  output_messages.append(
362
- f"⚠️ Message {mid}: {entry['error']}\n"
361
+ f"Message ID: {mid}\n"
362
+ f"Subject: {subject}\n"
363
+ f"From: {sender}\n"
364
+ f"Web Link: {_generate_gmail_web_url(mid)}\n"
363
365
  )
364
366
  else:
365
- message = entry["data"]
366
- if not message:
367
- output_messages.append(
368
- f"⚠️ Message {mid}: No data returned\n"
369
- )
370
- continue
371
-
372
- # Extract content based on format
373
- payload = message.get("payload", {})
367
+ # Full format - extract body too
368
+ headers = _extract_headers(payload, ["Subject", "From"])
369
+ subject = headers.get("Subject", "(no subject)")
370
+ sender = headers.get("From", "(unknown sender)")
371
+ body = _extract_message_body(payload)
374
372
 
375
- if format == "metadata":
376
- headers = _extract_headers(payload, ["Subject", "From"])
377
- subject = headers.get("Subject", "(no subject)")
378
- sender = headers.get("From", "(unknown sender)")
379
-
380
- output_messages.append(
381
- f"Message ID: {mid}\n"
382
- f"Subject: {subject}\n"
383
- f"From: {sender}\n"
384
- f"Web Link: {_generate_gmail_web_url(mid)}\n"
385
- )
386
- else:
387
- # Full format - extract body too
388
- headers = _extract_headers(payload, ["Subject", "From"])
389
- subject = headers.get("Subject", "(no subject)")
390
- sender = headers.get("From", "(unknown sender)")
391
- body = _extract_message_body(payload)
392
-
393
- output_messages.append(
394
- f"Message ID: {mid}\n"
395
- f"Subject: {subject}\n"
396
- f"From: {sender}\n"
397
- f"Web Link: {_generate_gmail_web_url(mid)}\n"
398
- f"\n{body or '[No text/plain body found]'}\n"
399
- )
400
-
401
- # Combine all messages with separators
402
- final_output = f"Retrieved {len(message_ids)} messages:\n\n"
403
- final_output += "\n---\n\n".join(output_messages)
373
+ output_messages.append(
374
+ f"Message ID: {mid}\n"
375
+ f"Subject: {subject}\n"
376
+ f"From: {sender}\n"
377
+ f"Web Link: {_generate_gmail_web_url(mid)}\n"
378
+ f"\n{body or '[No text/plain body found]'}\n"
379
+ )
404
380
 
405
- return final_output
381
+ # Combine all messages with separators
382
+ final_output = f"Retrieved {len(message_ids)} messages:\n\n"
383
+ final_output += "\n---\n\n".join(output_messages)
406
384
 
407
- except HttpError as e:
408
- logger.error(
409
- f"[get_gmail_messages_content_batch] Gmail API error in batch retrieval: {e}", exc_info=True
410
- )
411
- raise Exception(f"Gmail API error: {e}")
412
- except Exception as e:
413
- logger.exception(
414
- f"[get_gmail_messages_content_batch] Unexpected error in batch retrieval: {e}"
415
- )
416
- raise Exception(f"Unexpected error: {e}")
385
+ return final_output
417
386
 
418
387
 
419
388
  @server.tool()
420
389
  @require_google_service("gmail", GMAIL_SEND_SCOPE)
390
+ @handle_http_errors("send_gmail_message")
421
391
  async def send_gmail_message(
422
392
  service,
423
393
  user_google_email: str,
@@ -437,33 +407,24 @@ async def send_gmail_message(
437
407
  Returns:
438
408
  str: Confirmation message with the sent email's message ID.
439
409
  """
440
- try:
441
- # Prepare the email
442
- message = MIMEText(body)
443
- message["to"] = to
444
- message["subject"] = subject
445
- raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
446
- send_body = {"raw": raw_message}
447
-
448
- # Send the message
449
- sent_message = await asyncio.to_thread(
450
- service.users().messages().send(userId="me", body=send_body).execute
451
- )
452
- message_id = sent_message.get("id")
453
- return f"Email sent! Message ID: {message_id}"
454
-
455
- except HttpError as e:
456
- logger.error(
457
- f"[send_gmail_message] Gmail API error sending message: {e}", exc_info=True
458
- )
459
- raise Exception(f"Gmail API error: {e}")
460
- except Exception as e:
461
- logger.exception(f"[send_gmail_message] Unexpected error sending Gmail message: {e}")
462
- raise Exception(f"Unexpected error: {e}")
410
+ # Prepare the email
411
+ message = MIMEText(body)
412
+ message["to"] = to
413
+ message["subject"] = subject
414
+ raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
415
+ send_body = {"raw": raw_message}
416
+
417
+ # Send the message
418
+ sent_message = await asyncio.to_thread(
419
+ service.users().messages().send(userId="me", body=send_body).execute
420
+ )
421
+ message_id = sent_message.get("id")
422
+ return f"Email sent! Message ID: {message_id}"
463
423
 
464
424
 
465
425
  @server.tool()
466
426
  @require_google_service("gmail", GMAIL_COMPOSE_SCOPE)
427
+ @handle_http_errors("draft_gmail_message")
467
428
  async def draft_gmail_message(
468
429
  service,
469
430
  user_google_email: str,
@@ -487,39 +448,30 @@ async def draft_gmail_message(
487
448
  f"[draft_gmail_message] Invoked. Email: '{user_google_email}', Subject: '{subject}'"
488
449
  )
489
450
 
490
- try:
491
- # Prepare the email
492
- message = MIMEText(body)
493
- message["subject"] = subject
494
-
495
- # Add recipient if provided
496
- if to:
497
- message["to"] = to
451
+ # Prepare the email
452
+ message = MIMEText(body)
453
+ message["subject"] = subject
498
454
 
499
- raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
455
+ # Add recipient if provided
456
+ if to:
457
+ message["to"] = to
500
458
 
501
- # Create a draft instead of sending
502
- draft_body = {"message": {"raw": raw_message}}
459
+ raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
503
460
 
504
- # Create the draft
505
- created_draft = await asyncio.to_thread(
506
- service.users().drafts().create(userId="me", body=draft_body).execute
507
- )
508
- draft_id = created_draft.get("id")
509
- return f"Draft created! Draft ID: {draft_id}"
461
+ # Create a draft instead of sending
462
+ draft_body = {"message": {"raw": raw_message}}
510
463
 
511
- except HttpError as e:
512
- logger.error(
513
- f"[draft_gmail_message] Gmail API error creating draft: {e}", exc_info=True
514
- )
515
- raise Exception(f"Gmail API error: {e}")
516
- except Exception as e:
517
- logger.exception(f"[draft_gmail_message] Unexpected error creating Gmail draft: {e}")
518
- raise Exception(f"Unexpected error: {e}")
464
+ # Create the draft
465
+ created_draft = await asyncio.to_thread(
466
+ service.users().drafts().create(userId="me", body=draft_body).execute
467
+ )
468
+ draft_id = created_draft.get("id")
469
+ return f"Draft created! Draft ID: {draft_id}"
519
470
 
520
471
 
521
472
  @server.tool()
522
473
  @require_google_service("gmail", "gmail_read")
474
+ @handle_http_errors("get_gmail_thread_content")
523
475
  async def get_gmail_thread_content(
524
476
  service, thread_id: str, user_google_email: str
525
477
  ) -> str:
@@ -537,89 +489,78 @@ async def get_gmail_thread_content(
537
489
  f"[get_gmail_thread_content] Invoked. Thread ID: '{thread_id}', Email: '{user_google_email}'"
538
490
  )
539
491
 
540
- try:
541
- # Fetch the complete thread with all messages
542
- thread_response = await asyncio.to_thread(
543
- service.users()
544
- .threads()
545
- .get(userId="me", id=thread_id, format="full")
546
- .execute
547
- )
492
+ # Fetch the complete thread with all messages
493
+ thread_response = await asyncio.to_thread(
494
+ service.users()
495
+ .threads()
496
+ .get(userId="me", id=thread_id, format="full")
497
+ .execute
498
+ )
548
499
 
549
- messages = thread_response.get("messages", [])
550
- if not messages:
551
- return f"No messages found in thread '{thread_id}'."
500
+ messages = thread_response.get("messages", [])
501
+ if not messages:
502
+ return f"No messages found in thread '{thread_id}'."
503
+
504
+ # Extract thread subject from the first message
505
+ first_message = messages[0]
506
+ first_headers = {
507
+ h["name"]: h["value"]
508
+ for h in first_message.get("payload", {}).get("headers", [])
509
+ }
510
+ thread_subject = first_headers.get("Subject", "(no subject)")
511
+
512
+ # Build the thread content
513
+ content_lines = [
514
+ f"Thread ID: {thread_id}",
515
+ f"Subject: {thread_subject}",
516
+ f"Messages: {len(messages)}",
517
+ "",
518
+ ]
552
519
 
553
- # Extract thread subject from the first message
554
- first_message = messages[0]
555
- first_headers = {
520
+ # Process each message in the thread
521
+ for i, message in enumerate(messages, 1):
522
+ # Extract headers
523
+ headers = {
556
524
  h["name"]: h["value"]
557
- for h in first_message.get("payload", {}).get("headers", [])
525
+ for h in message.get("payload", {}).get("headers", [])
558
526
  }
559
- thread_subject = first_headers.get("Subject", "(no subject)")
560
-
561
- # Build the thread content
562
- content_lines = [
563
- f"Thread ID: {thread_id}",
564
- f"Subject: {thread_subject}",
565
- f"Messages: {len(messages)}",
566
- "",
567
- ]
568
527
 
569
- # Process each message in the thread
570
- for i, message in enumerate(messages, 1):
571
- # Extract headers
572
- headers = {
573
- h["name"]: h["value"]
574
- for h in message.get("payload", {}).get("headers", [])
575
- }
576
-
577
- sender = headers.get("From", "(unknown sender)")
578
- date = headers.get("Date", "(unknown date)")
579
- subject = headers.get("Subject", "(no subject)")
580
-
581
- # Extract message body
582
- payload = message.get("payload", {})
583
- body_data = _extract_message_body(payload)
584
-
585
- # Add message to content
586
- content_lines.extend(
587
- [
588
- f"=== Message {i} ===",
589
- f"From: {sender}",
590
- f"Date: {date}",
591
- ]
592
- )
528
+ sender = headers.get("From", "(unknown sender)")
529
+ date = headers.get("Date", "(unknown date)")
530
+ subject = headers.get("Subject", "(no subject)")
593
531
 
594
- # Only show subject if it's different from thread subject
595
- if subject != thread_subject:
596
- content_lines.append(f"Subject: {subject}")
532
+ # Extract message body
533
+ payload = message.get("payload", {})
534
+ body_data = _extract_message_body(payload)
597
535
 
598
- content_lines.extend(
599
- [
600
- "",
601
- body_data or "[No text/plain body found]",
602
- "",
603
- ]
604
- )
536
+ # Add message to content
537
+ content_lines.extend(
538
+ [
539
+ f"=== Message {i} ===",
540
+ f"From: {sender}",
541
+ f"Date: {date}",
542
+ ]
543
+ )
605
544
 
606
- content_text = "\n".join(content_lines)
607
- return content_text
545
+ # Only show subject if it's different from thread subject
546
+ if subject != thread_subject:
547
+ content_lines.append(f"Subject: {subject}")
608
548
 
609
- except HttpError as e:
610
- logger.error(
611
- f"[get_gmail_thread_content] Gmail API error getting thread content: {e}", exc_info=True
612
- )
613
- raise Exception(f"Gmail API error: {e}")
614
- except Exception as e:
615
- logger.exception(
616
- f"[get_gmail_thread_content] Unexpected error getting Gmail thread content: {e}"
549
+ content_lines.extend(
550
+ [
551
+ "",
552
+ body_data or "[No text/plain body found]",
553
+ "",
554
+ ]
617
555
  )
618
- raise Exception(f"Unexpected error: {e}")
556
+
557
+ content_text = "\n".join(content_lines)
558
+ return content_text
619
559
 
620
560
 
621
561
  @server.tool()
622
562
  @require_google_service("gmail", "gmail_read")
563
+ @handle_http_errors("list_gmail_labels")
623
564
  async def list_gmail_labels(service, user_google_email: str) -> str:
624
565
  """
625
566
  Lists all labels in the user's Gmail account.
@@ -632,49 +573,42 @@ async def list_gmail_labels(service, user_google_email: str) -> str:
632
573
  """
633
574
  logger.info(f"[list_gmail_labels] Invoked. Email: '{user_google_email}'")
634
575
 
635
- try:
636
- response = await asyncio.to_thread(
637
- service.users().labels().list(userId="me").execute
638
- )
639
- labels = response.get("labels", [])
640
-
641
- if not labels:
642
- return "No labels found."
576
+ response = await asyncio.to_thread(
577
+ service.users().labels().list(userId="me").execute
578
+ )
579
+ labels = response.get("labels", [])
643
580
 
644
- lines = [f"Found {len(labels)} labels:", ""]
581
+ if not labels:
582
+ return "No labels found."
645
583
 
646
- system_labels = []
647
- user_labels = []
584
+ lines = [f"Found {len(labels)} labels:", ""]
648
585
 
649
- for label in labels:
650
- if label.get("type") == "system":
651
- system_labels.append(label)
652
- else:
653
- user_labels.append(label)
586
+ system_labels = []
587
+ user_labels = []
654
588
 
655
- if system_labels:
656
- lines.append("📂 SYSTEM LABELS:")
657
- for label in system_labels:
658
- lines.append(f" • {label['name']} (ID: {label['id']})")
659
- lines.append("")
589
+ for label in labels:
590
+ if label.get("type") == "system":
591
+ system_labels.append(label)
592
+ else:
593
+ user_labels.append(label)
660
594
 
661
- if user_labels:
662
- lines.append("🏷️ USER LABELS:")
663
- for label in user_labels:
664
- lines.append(f" • {label['name']} (ID: {label['id']})")
595
+ if system_labels:
596
+ lines.append("📂 SYSTEM LABELS:")
597
+ for label in system_labels:
598
+ lines.append(f" • {label['name']} (ID: {label['id']})")
599
+ lines.append("")
665
600
 
666
- return "\n".join(lines)
601
+ if user_labels:
602
+ lines.append("🏷️ USER LABELS:")
603
+ for label in user_labels:
604
+ lines.append(f" • {label['name']} (ID: {label['id']})")
667
605
 
668
- except HttpError as e:
669
- logger.error(f"[list_gmail_labels] Gmail API error listing labels: {e}", exc_info=True)
670
- raise Exception(f"Gmail API error: {e}")
671
- except Exception as e:
672
- logger.exception(f"[list_gmail_labels] Unexpected error listing Gmail labels: {e}")
673
- raise Exception(f"Unexpected error: {e}")
606
+ return "\n".join(lines)
674
607
 
675
608
 
676
609
  @server.tool()
677
610
  @require_google_service("gmail", GMAIL_LABELS_SCOPE)
611
+ @handle_http_errors("manage_gmail_label")
678
612
  async def manage_gmail_label(
679
613
  service,
680
614
  user_google_email: str,
@@ -706,56 +640,49 @@ async def manage_gmail_label(
706
640
  if action in ["update", "delete"] and not label_id:
707
641
  raise Exception("Label ID is required for update and delete actions.")
708
642
 
709
- try:
710
- if action == "create":
711
- label_object = {
712
- "name": name,
713
- "labelListVisibility": label_list_visibility,
714
- "messageListVisibility": message_list_visibility,
715
- }
716
- created_label = await asyncio.to_thread(
717
- service.users().labels().create(userId="me", body=label_object).execute
718
- )
719
- return f"Label created successfully!\nName: {created_label['name']}\nID: {created_label['id']}"
720
-
721
- elif action == "update":
722
- current_label = await asyncio.to_thread(
723
- service.users().labels().get(userId="me", id=label_id).execute
724
- )
643
+ if action == "create":
644
+ label_object = {
645
+ "name": name,
646
+ "labelListVisibility": label_list_visibility,
647
+ "messageListVisibility": message_list_visibility,
648
+ }
649
+ created_label = await asyncio.to_thread(
650
+ service.users().labels().create(userId="me", body=label_object).execute
651
+ )
652
+ return f"Label created successfully!\nName: {created_label['name']}\nID: {created_label['id']}"
725
653
 
726
- label_object = {
727
- "id": label_id,
728
- "name": name if name is not None else current_label["name"],
729
- "labelListVisibility": label_list_visibility,
730
- "messageListVisibility": message_list_visibility,
731
- }
654
+ elif action == "update":
655
+ current_label = await asyncio.to_thread(
656
+ service.users().labels().get(userId="me", id=label_id).execute
657
+ )
732
658
 
733
- updated_label = await asyncio.to_thread(
734
- service.users().labels().update(userId="me", id=label_id, body=label_object).execute
735
- )
736
- return f"Label updated successfully!\nName: {updated_label['name']}\nID: {updated_label['id']}"
659
+ label_object = {
660
+ "id": label_id,
661
+ "name": name if name is not None else current_label["name"],
662
+ "labelListVisibility": label_list_visibility,
663
+ "messageListVisibility": message_list_visibility,
664
+ }
737
665
 
738
- elif action == "delete":
739
- label = await asyncio.to_thread(
740
- service.users().labels().get(userId="me", id=label_id).execute
741
- )
742
- label_name = label["name"]
666
+ updated_label = await asyncio.to_thread(
667
+ service.users().labels().update(userId="me", id=label_id, body=label_object).execute
668
+ )
669
+ return f"Label updated successfully!\nName: {updated_label['name']}\nID: {updated_label['id']}"
743
670
 
744
- await asyncio.to_thread(
745
- service.users().labels().delete(userId="me", id=label_id).execute
746
- )
747
- return f"Label '{label_name}' (ID: {label_id}) deleted successfully!"
671
+ elif action == "delete":
672
+ label = await asyncio.to_thread(
673
+ service.users().labels().get(userId="me", id=label_id).execute
674
+ )
675
+ label_name = label["name"]
748
676
 
749
- except HttpError as e:
750
- logger.error(f"[manage_gmail_label] Gmail API error: {e}", exc_info=True)
751
- raise Exception(f"Gmail API error: {e}")
752
- except Exception as e:
753
- logger.exception(f"[manage_gmail_label] Unexpected error: {e}")
754
- raise Exception(f"Unexpected error: {e}")
677
+ await asyncio.to_thread(
678
+ service.users().labels().delete(userId="me", id=label_id).execute
679
+ )
680
+ return f"Label '{label_name}' (ID: {label_id}) deleted successfully!"
755
681
 
756
682
 
757
683
  @server.tool()
758
684
  @require_google_service("gmail", GMAIL_MODIFY_SCOPE)
685
+ @handle_http_errors("modify_gmail_message_labels")
759
686
  async def modify_gmail_message_labels(
760
687
  service,
761
688
  user_google_email: str,
@@ -780,28 +707,20 @@ async def modify_gmail_message_labels(
780
707
  if not add_label_ids and not remove_label_ids:
781
708
  raise Exception("At least one of add_label_ids or remove_label_ids must be provided.")
782
709
 
783
- try:
784
- body = {}
785
- if add_label_ids:
786
- body["addLabelIds"] = add_label_ids
787
- if remove_label_ids:
788
- body["removeLabelIds"] = remove_label_ids
789
-
790
- await asyncio.to_thread(
791
- service.users().messages().modify(userId="me", id=message_id, body=body).execute
792
- )
710
+ body = {}
711
+ if add_label_ids:
712
+ body["addLabelIds"] = add_label_ids
713
+ if remove_label_ids:
714
+ body["removeLabelIds"] = remove_label_ids
793
715
 
794
- actions = []
795
- if add_label_ids:
796
- actions.append(f"Added labels: {', '.join(add_label_ids)}")
797
- if remove_label_ids:
798
- actions.append(f"Removed labels: {', '.join(remove_label_ids)}")
716
+ await asyncio.to_thread(
717
+ service.users().messages().modify(userId="me", id=message_id, body=body).execute
718
+ )
799
719
 
800
- return f"Message labels updated successfully!\nMessage ID: {message_id}\n{'; '.join(actions)}"
720
+ actions = []
721
+ if add_label_ids:
722
+ actions.append(f"Added labels: {', '.join(add_label_ids)}")
723
+ if remove_label_ids:
724
+ actions.append(f"Removed labels: {', '.join(remove_label_ids)}")
801
725
 
802
- except HttpError as e:
803
- logger.error(f"[modify_gmail_message_labels] Gmail API error modifying message labels: {e}", exc_info=True)
804
- raise Exception(f"Gmail API error: {e}")
805
- except Exception as e:
806
- logger.exception(f"[modify_gmail_message_labels] Unexpected error modifying Gmail message labels: {e}")
807
- raise Exception(f"Unexpected error: {e}")
726
+ return f"Message labels updated successfully!\nMessage ID: {message_id}\n{'; '.join(actions)}"