workspace-mcp 1.1.4__py3-none-any.whl → 1.1.6__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.
gforms/forms_tools.py CHANGED
@@ -6,10 +6,9 @@ This module provides MCP tools for interacting with Google Forms API.
6
6
 
7
7
  import logging
8
8
  import asyncio
9
- from typing import List, Optional, Dict, Any
9
+ from typing import Optional, Dict, Any
10
10
 
11
11
  from mcp import types
12
- from googleapiclient.errors import HttpError
13
12
 
14
13
  from auth.service_decorator import require_google_service
15
14
  from core.server import server
@@ -19,8 +18,8 @@ logger = logging.getLogger(__name__)
19
18
 
20
19
 
21
20
  @server.tool()
22
- @require_google_service("forms", "forms")
23
21
  @handle_http_errors("create_form")
22
+ @require_google_service("forms", "forms")
24
23
  async def create_form(
25
24
  service,
26
25
  user_google_email: str,
@@ -47,10 +46,10 @@ async def create_form(
47
46
  "title": title
48
47
  }
49
48
  }
50
-
49
+
51
50
  if description:
52
51
  form_body["info"]["description"] = description
53
-
52
+
54
53
  if document_title:
55
54
  form_body["info"]["document_title"] = document_title
56
55
 
@@ -61,15 +60,15 @@ async def create_form(
61
60
  form_id = created_form.get("formId")
62
61
  edit_url = f"https://docs.google.com/forms/d/{form_id}/edit"
63
62
  responder_url = created_form.get("responderUri", f"https://docs.google.com/forms/d/{form_id}/viewform")
64
-
63
+
65
64
  confirmation_message = f"Successfully created form '{created_form.get('info', {}).get('title', title)}' for {user_google_email}. Form ID: {form_id}. Edit URL: {edit_url}. Responder URL: {responder_url}"
66
65
  logger.info(f"Form created successfully for {user_google_email}. ID: {form_id}")
67
66
  return confirmation_message
68
67
 
69
68
 
70
69
  @server.tool()
70
+ @handle_http_errors("get_form", is_read_only=True)
71
71
  @require_google_service("forms", "forms")
72
- @handle_http_errors("get_form")
73
72
  async def get_form(
74
73
  service,
75
74
  user_google_email: str,
@@ -95,10 +94,10 @@ async def get_form(
95
94
  title = form_info.get("title", "No Title")
96
95
  description = form_info.get("description", "No Description")
97
96
  document_title = form_info.get("documentTitle", title)
98
-
97
+
99
98
  edit_url = f"https://docs.google.com/forms/d/{form_id}/edit"
100
99
  responder_url = form.get("responderUri", f"https://docs.google.com/forms/d/{form_id}/viewform")
101
-
100
+
102
101
  items = form.get("items", [])
103
102
  questions_summary = []
104
103
  for i, item in enumerate(items, 1):
@@ -106,9 +105,9 @@ async def get_form(
106
105
  item_type = item.get("questionItem", {}).get("question", {}).get("required", False)
107
106
  required_text = " (Required)" if item_type else ""
108
107
  questions_summary.append(f" {i}. {item_title}{required_text}")
109
-
108
+
110
109
  questions_text = "\n".join(questions_summary) if questions_summary else " No questions found"
111
-
110
+
112
111
  result = f"""Form Details for {user_google_email}:
113
112
  - Title: "{title}"
114
113
  - Description: "{description}"
@@ -118,14 +117,14 @@ async def get_form(
118
117
  - Responder URL: {responder_url}
119
118
  - Questions ({len(items)} total):
120
119
  {questions_text}"""
121
-
120
+
122
121
  logger.info(f"Successfully retrieved form for {user_google_email}. ID: {form_id}")
123
122
  return result
124
123
 
125
124
 
126
125
  @server.tool()
127
- @require_google_service("forms", "forms")
128
126
  @handle_http_errors("set_publish_settings")
127
+ @require_google_service("forms", "forms")
129
128
  async def set_publish_settings(
130
129
  service,
131
130
  user_google_email: str,
@@ -162,8 +161,8 @@ async def set_publish_settings(
162
161
 
163
162
 
164
163
  @server.tool()
164
+ @handle_http_errors("get_form_response", is_read_only=True)
165
165
  @require_google_service("forms", "forms")
166
- @handle_http_errors("get_form_response")
167
166
  async def get_form_response(
168
167
  service,
169
168
  user_google_email: str,
@@ -190,7 +189,7 @@ async def get_form_response(
190
189
  response_id = response.get("responseId", "Unknown")
191
190
  create_time = response.get("createTime", "Unknown")
192
191
  last_submitted_time = response.get("lastSubmittedTime", "Unknown")
193
-
192
+
194
193
  answers = response.get("answers", {})
195
194
  answer_details = []
196
195
  for question_id, answer_data in answers.items():
@@ -200,9 +199,9 @@ async def get_form_response(
200
199
  answer_details.append(f" Question ID {question_id}: {answer_text}")
201
200
  else:
202
201
  answer_details.append(f" Question ID {question_id}: No answer provided")
203
-
202
+
204
203
  answers_text = "\n".join(answer_details) if answer_details else " No answers found"
205
-
204
+
206
205
  result = f"""Form Response Details for {user_google_email}:
207
206
  - Form ID: {form_id}
208
207
  - Response ID: {response_id}
@@ -210,14 +209,14 @@ async def get_form_response(
210
209
  - Last Submitted: {last_submitted_time}
211
210
  - Answers:
212
211
  {answers_text}"""
213
-
212
+
214
213
  logger.info(f"Successfully retrieved response for {user_google_email}. Response ID: {response_id}")
215
214
  return result
216
215
 
217
216
 
218
217
  @server.tool()
218
+ @handle_http_errors("list_form_responses", is_read_only=True)
219
219
  @require_google_service("forms", "forms")
220
- @handle_http_errors("list_form_responses")
221
220
  async def list_form_responses(
222
221
  service,
223
222
  user_google_email: str,
@@ -252,7 +251,7 @@ async def list_form_responses(
252
251
 
253
252
  responses = responses_result.get("responses", [])
254
253
  next_page_token = responses_result.get("nextPageToken")
255
-
254
+
256
255
  if not responses:
257
256
  return f"No responses found for form {form_id} for {user_google_email}."
258
257
 
@@ -261,19 +260,19 @@ async def list_form_responses(
261
260
  response_id = response.get("responseId", "Unknown")
262
261
  create_time = response.get("createTime", "Unknown")
263
262
  last_submitted_time = response.get("lastSubmittedTime", "Unknown")
264
-
263
+
265
264
  answers_count = len(response.get("answers", {}))
266
265
  response_details.append(
267
266
  f" {i}. Response ID: {response_id} | Created: {create_time} | Last Submitted: {last_submitted_time} | Answers: {answers_count}"
268
267
  )
269
268
 
270
269
  pagination_info = f"\nNext page token: {next_page_token}" if next_page_token else "\nNo more pages."
271
-
270
+
272
271
  result = f"""Form Responses for {user_google_email}:
273
272
  - Form ID: {form_id}
274
273
  - Total responses returned: {len(responses)}
275
274
  - Responses:
276
275
  {chr(10).join(response_details)}{pagination_info}"""
277
-
276
+
278
277
  logger.info(f"Successfully retrieved {len(responses)} responses for {user_google_email}. Form ID: {form_id}")
279
278
  return result
gmail/gmail_tools.py CHANGED
@@ -13,7 +13,6 @@ from email.mime.text import MIMEText
13
13
 
14
14
  from mcp import types
15
15
  from fastapi import Body
16
- from googleapiclient.errors import HttpError
17
16
 
18
17
  from auth.service_decorator import require_google_service
19
18
  from core.utils import handle_http_errors
@@ -112,28 +111,31 @@ def _format_gmail_results_plain(messages: list, query: str) -> str:
112
111
  message_url = _generate_gmail_web_url(msg["id"])
113
112
  thread_url = _generate_gmail_web_url(msg["threadId"])
114
113
 
115
- lines.extend([
116
- f" {i}. Message ID: {msg['id']}",
117
- f" Web Link: {message_url}",
118
- f" Thread ID: {msg['threadId']}",
119
- f" Thread Link: {thread_url}",
120
- ""
121
- ])
122
-
123
- lines.extend([
124
- "💡 USAGE:",
125
- " • Pass the Message IDs **as a list** to get_gmail_messages_content_batch()",
126
- " e.g. get_gmail_messages_content_batch(message_ids=[...])",
127
- " Pass the Thread IDs to get_gmail_thread_content() (single) _or_",
128
- " get_gmail_threads_content_batch() (coming soon)"
129
- ])
114
+ lines.extend(
115
+ [
116
+ f" {i}. Message ID: {msg['id']}",
117
+ f" Web Link: {message_url}",
118
+ f" Thread ID: {msg['threadId']}",
119
+ f" Thread Link: {thread_url}",
120
+ "",
121
+ ]
122
+ )
123
+
124
+ lines.extend(
125
+ [
126
+ "💡 USAGE:",
127
+ " Pass the Message IDs **as a list** to get_gmail_messages_content_batch()",
128
+ " e.g. get_gmail_messages_content_batch(message_ids=[...])",
129
+ " • Pass the Thread IDs to get_gmail_thread_content() (single) or get_gmail_threads_content_batch() (batch)",
130
+ ]
131
+ )
130
132
 
131
133
  return "\n".join(lines)
132
134
 
133
135
 
134
136
  @server.tool()
137
+ @handle_http_errors("search_gmail_messages", is_read_only=True)
135
138
  @require_google_service("gmail", "gmail_read")
136
- @handle_http_errors("search_gmail_messages")
137
139
  async def search_gmail_messages(
138
140
  service, query: str, user_google_email: str, page_size: int = 10
139
141
  ) -> str:
@@ -149,7 +151,9 @@ async def search_gmail_messages(
149
151
  Returns:
150
152
  str: LLM-friendly structured results with Message IDs, Thread IDs, and clickable Gmail web interface URLs for each found message.
151
153
  """
152
- logger.info(f"[search_gmail_messages] Email: '{user_google_email}', Query: '{query}'")
154
+ logger.info(
155
+ f"[search_gmail_messages] Email: '{user_google_email}', Query: '{query}'"
156
+ )
153
157
 
154
158
  response = await asyncio.to_thread(
155
159
  service.users()
@@ -165,8 +169,8 @@ async def search_gmail_messages(
165
169
 
166
170
 
167
171
  @server.tool()
172
+ @handle_http_errors("get_gmail_message_content", is_read_only=True)
168
173
  @require_google_service("gmail", "gmail_read")
169
- @handle_http_errors("get_gmail_message_content")
170
174
  async def get_gmail_message_content(
171
175
  service, message_id: str, user_google_email: str
172
176
  ) -> str:
@@ -233,8 +237,8 @@ async def get_gmail_message_content(
233
237
 
234
238
 
235
239
  @server.tool()
240
+ @handle_http_errors("get_gmail_messages_content_batch", is_read_only=True)
236
241
  @require_google_service("gmail", "gmail_read")
237
- @handle_http_errors("get_gmail_messages_content_batch")
238
242
  async def get_gmail_messages_content_batch(
239
243
  service,
240
244
  message_ids: List[str],
@@ -264,7 +268,7 @@ async def get_gmail_messages_content_batch(
264
268
 
265
269
  # Process in chunks of 100 (Gmail batch limit)
266
270
  for chunk_start in range(0, len(message_ids), 100):
267
- chunk_ids = message_ids[chunk_start:chunk_start + 100]
271
+ chunk_ids = message_ids[chunk_start : chunk_start + 100]
268
272
  results: Dict[str, Dict] = {}
269
273
 
270
274
  def _batch_callback(request_id, response, exception):
@@ -277,17 +281,21 @@ async def get_gmail_messages_content_batch(
277
281
 
278
282
  for mid in chunk_ids:
279
283
  if format == "metadata":
280
- req = service.users().messages().get(
281
- userId="me",
282
- id=mid,
283
- format="metadata",
284
- metadataHeaders=["Subject", "From"]
284
+ req = (
285
+ service.users()
286
+ .messages()
287
+ .get(
288
+ userId="me",
289
+ id=mid,
290
+ format="metadata",
291
+ metadataHeaders=["Subject", "From"],
292
+ )
285
293
  )
286
294
  else:
287
- req = service.users().messages().get(
288
- userId="me",
289
- id=mid,
290
- format="full"
295
+ req = (
296
+ service.users()
297
+ .messages()
298
+ .get(userId="me", id=mid, format="full")
291
299
  )
292
300
  batch.add(req, request_id=mid)
293
301
 
@@ -304,20 +312,22 @@ async def get_gmail_messages_content_batch(
304
312
  try:
305
313
  if format == "metadata":
306
314
  msg = await asyncio.to_thread(
307
- service.users().messages().get(
315
+ service.users()
316
+ .messages()
317
+ .get(
308
318
  userId="me",
309
319
  id=mid,
310
320
  format="metadata",
311
- metadataHeaders=["Subject", "From"]
312
- ).execute
321
+ metadataHeaders=["Subject", "From"],
322
+ )
323
+ .execute
313
324
  )
314
325
  else:
315
326
  msg = await asyncio.to_thread(
316
- service.users().messages().get(
317
- userId="me",
318
- id=mid,
319
- format="full"
320
- ).execute
327
+ service.users()
328
+ .messages()
329
+ .get(userId="me", id=mid, format="full")
330
+ .execute
321
331
  )
322
332
  return mid, msg, None
323
333
  except Exception as e:
@@ -325,8 +335,7 @@ async def get_gmail_messages_content_batch(
325
335
 
326
336
  # Fetch all messages in parallel
327
337
  fetch_results = await asyncio.gather(
328
- *[fetch_message(mid) for mid in chunk_ids],
329
- return_exceptions=False
338
+ *[fetch_message(mid) for mid in chunk_ids], return_exceptions=False
330
339
  )
331
340
 
332
341
  # Convert to results format
@@ -338,15 +347,11 @@ async def get_gmail_messages_content_batch(
338
347
  entry = results.get(mid, {"data": None, "error": "No result"})
339
348
 
340
349
  if entry["error"]:
341
- output_messages.append(
342
- f"⚠️ Message {mid}: {entry['error']}\n"
343
- )
350
+ output_messages.append(f"⚠️ Message {mid}: {entry['error']}\n")
344
351
  else:
345
352
  message = entry["data"]
346
353
  if not message:
347
- output_messages.append(
348
- f"⚠️ Message {mid}: No data returned\n"
349
- )
354
+ output_messages.append(f"⚠️ Message {mid}: No data returned\n")
350
355
  continue
351
356
 
352
357
  # Extract content based on format
@@ -386,8 +391,8 @@ async def get_gmail_messages_content_batch(
386
391
 
387
392
 
388
393
  @server.tool()
389
- @require_google_service("gmail", GMAIL_SEND_SCOPE)
390
394
  @handle_http_errors("send_gmail_message")
395
+ @require_google_service("gmail", GMAIL_SEND_SCOPE)
391
396
  async def send_gmail_message(
392
397
  service,
393
398
  user_google_email: str,
@@ -423,8 +428,8 @@ async def send_gmail_message(
423
428
 
424
429
 
425
430
  @server.tool()
426
- @require_google_service("gmail", GMAIL_COMPOSE_SCOPE)
427
431
  @handle_http_errors("draft_gmail_message")
432
+ @require_google_service("gmail", GMAIL_COMPOSE_SCOPE)
428
433
  async def draft_gmail_message(
429
434
  service,
430
435
  user_google_email: str,
@@ -469,35 +474,18 @@ async def draft_gmail_message(
469
474
  return f"Draft created! Draft ID: {draft_id}"
470
475
 
471
476
 
472
- @server.tool()
473
- @require_google_service("gmail", "gmail_read")
474
- @handle_http_errors("get_gmail_thread_content")
475
- async def get_gmail_thread_content(
476
- service, thread_id: str, user_google_email: str
477
- ) -> str:
477
+ def _format_thread_content(thread_data: dict, thread_id: str) -> str:
478
478
  """
479
- Retrieves the complete content of a Gmail conversation thread, including all messages.
479
+ Helper function to format thread content from Gmail API response.
480
480
 
481
481
  Args:
482
- thread_id (str): The unique ID of the Gmail thread to retrieve.
483
- user_google_email (str): The user's Google email address. Required.
482
+ thread_data (dict): Thread data from Gmail API
483
+ thread_id (str): Thread ID for display
484
484
 
485
485
  Returns:
486
- str: The complete thread content with all messages formatted for reading.
486
+ str: Formatted thread content
487
487
  """
488
- logger.info(
489
- f"[get_gmail_thread_content] Invoked. Thread ID: '{thread_id}', Email: '{user_google_email}'"
490
- )
491
-
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
- )
499
-
500
- messages = thread_response.get("messages", [])
488
+ messages = thread_data.get("messages", [])
501
489
  if not messages:
502
490
  return f"No messages found in thread '{thread_id}'."
503
491
 
@@ -521,8 +509,7 @@ async def get_gmail_thread_content(
521
509
  for i, message in enumerate(messages, 1):
522
510
  # Extract headers
523
511
  headers = {
524
- h["name"]: h["value"]
525
- for h in message.get("payload", {}).get("headers", [])
512
+ h["name"]: h["value"] for h in message.get("payload", {}).get("headers", [])
526
513
  }
527
514
 
528
515
  sender = headers.get("From", "(unknown sender)")
@@ -554,13 +541,134 @@ async def get_gmail_thread_content(
554
541
  ]
555
542
  )
556
543
 
557
- content_text = "\n".join(content_lines)
558
- return content_text
544
+ return "\n".join(content_lines)
559
545
 
560
546
 
561
547
  @server.tool()
562
548
  @require_google_service("gmail", "gmail_read")
563
- @handle_http_errors("list_gmail_labels")
549
+ @handle_http_errors("get_gmail_thread_content", is_read_only=True)
550
+ async def get_gmail_thread_content(
551
+ service, thread_id: str, user_google_email: str
552
+ ) -> str:
553
+ """
554
+ Retrieves the complete content of a Gmail conversation thread, including all messages.
555
+
556
+ Args:
557
+ thread_id (str): The unique ID of the Gmail thread to retrieve.
558
+ user_google_email (str): The user's Google email address. Required.
559
+
560
+ Returns:
561
+ str: The complete thread content with all messages formatted for reading.
562
+ """
563
+ logger.info(
564
+ f"[get_gmail_thread_content] Invoked. Thread ID: '{thread_id}', Email: '{user_google_email}'"
565
+ )
566
+
567
+ # Fetch the complete thread with all messages
568
+ thread_response = await asyncio.to_thread(
569
+ service.users().threads().get(userId="me", id=thread_id, format="full").execute
570
+ )
571
+
572
+ return _format_thread_content(thread_response, thread_id)
573
+
574
+
575
+ @server.tool()
576
+ @require_google_service("gmail", "gmail_read")
577
+ @handle_http_errors("get_gmail_threads_content_batch", is_read_only=True)
578
+ async def get_gmail_threads_content_batch(
579
+ service,
580
+ thread_ids: List[str],
581
+ user_google_email: str,
582
+ ) -> str:
583
+ """
584
+ Retrieves the content of multiple Gmail threads in a single batch request.
585
+ Supports up to 100 threads per request using Google's batch API.
586
+
587
+ Args:
588
+ thread_ids (List[str]): A list of Gmail thread IDs to retrieve. The function will automatically batch requests in chunks of 100.
589
+ user_google_email (str): The user's Google email address. Required.
590
+
591
+ Returns:
592
+ str: A formatted list of thread contents with separators.
593
+ """
594
+ logger.info(
595
+ f"[get_gmail_threads_content_batch] Invoked. Thread count: {len(thread_ids)}, Email: '{user_google_email}'"
596
+ )
597
+
598
+ if not thread_ids:
599
+ raise ValueError("No thread IDs provided")
600
+
601
+ output_threads = []
602
+
603
+ def _batch_callback(request_id, response, exception):
604
+ """Callback for batch requests"""
605
+ results[request_id] = {"data": response, "error": exception}
606
+
607
+ # Process in chunks of 100 (Gmail batch limit)
608
+ for chunk_start in range(0, len(thread_ids), 100):
609
+ chunk_ids = thread_ids[chunk_start : chunk_start + 100]
610
+ results: Dict[str, Dict] = {}
611
+
612
+ # Try to use batch API
613
+ try:
614
+ batch = service.new_batch_http_request(callback=_batch_callback)
615
+
616
+ for tid in chunk_ids:
617
+ req = service.users().threads().get(userId="me", id=tid, format="full")
618
+ batch.add(req, request_id=tid)
619
+
620
+ # Execute batch request
621
+ await asyncio.to_thread(batch.execute)
622
+
623
+ except Exception as batch_error:
624
+ # Fallback to asyncio.gather if batch API fails
625
+ logger.warning(
626
+ f"[get_gmail_threads_content_batch] Batch API failed, falling back to asyncio.gather: {batch_error}"
627
+ )
628
+
629
+ async def fetch_thread(tid: str):
630
+ try:
631
+ thread = await asyncio.to_thread(
632
+ service.users()
633
+ .threads()
634
+ .get(userId="me", id=tid, format="full")
635
+ .execute
636
+ )
637
+ return tid, thread, None
638
+ except Exception as e:
639
+ return tid, None, e
640
+
641
+ # Fetch all threads in parallel
642
+ fetch_results = await asyncio.gather(
643
+ *[fetch_thread(tid) for tid in chunk_ids], return_exceptions=False
644
+ )
645
+
646
+ # Convert to results format
647
+ for tid, thread, error in fetch_results:
648
+ results[tid] = {"data": thread, "error": error}
649
+
650
+ # Process results for this chunk
651
+ for tid in chunk_ids:
652
+ entry = results.get(tid, {"data": None, "error": "No result"})
653
+
654
+ if entry["error"]:
655
+ output_threads.append(f"⚠️ Thread {tid}: {entry['error']}\n")
656
+ else:
657
+ thread = entry["data"]
658
+ if not thread:
659
+ output_threads.append(f"⚠️ Thread {tid}: No data returned\n")
660
+ continue
661
+
662
+ output_threads.append(_format_thread_content(thread, tid))
663
+
664
+ # Combine all threads with separators
665
+ header = f"Retrieved {len(thread_ids)} threads:"
666
+ return header + "\n\n" + "\n---\n\n".join(output_threads)
667
+
668
+
669
+ @server.tool()
670
+ @handle_http_errors("list_gmail_labels", is_read_only=True)
671
+ @require_google_service("gmail", "gmail_read")
564
672
  async def list_gmail_labels(service, user_google_email: str) -> str:
565
673
  """
566
674
  Lists all labels in the user's Gmail account.
@@ -607,8 +715,8 @@ async def list_gmail_labels(service, user_google_email: str) -> str:
607
715
 
608
716
 
609
717
  @server.tool()
610
- @require_google_service("gmail", GMAIL_LABELS_SCOPE)
611
718
  @handle_http_errors("manage_gmail_label")
719
+ @require_google_service("gmail", GMAIL_LABELS_SCOPE)
612
720
  async def manage_gmail_label(
613
721
  service,
614
722
  user_google_email: str,
@@ -632,7 +740,9 @@ async def manage_gmail_label(
632
740
  Returns:
633
741
  str: Confirmation message of the label operation.
634
742
  """
635
- logger.info(f"[manage_gmail_label] Invoked. Email: '{user_google_email}', Action: '{action}'")
743
+ logger.info(
744
+ f"[manage_gmail_label] Invoked. Email: '{user_google_email}', Action: '{action}'"
745
+ )
636
746
 
637
747
  if action == "create" and not name:
638
748
  raise Exception("Label name is required for create action.")
@@ -664,7 +774,10 @@ async def manage_gmail_label(
664
774
  }
665
775
 
666
776
  updated_label = await asyncio.to_thread(
667
- service.users().labels().update(userId="me", id=label_id, body=label_object).execute
777
+ service.users()
778
+ .labels()
779
+ .update(userId="me", id=label_id, body=label_object)
780
+ .execute
668
781
  )
669
782
  return f"Label updated successfully!\nName: {updated_label['name']}\nID: {updated_label['id']}"
670
783
 
@@ -681,8 +794,8 @@ async def manage_gmail_label(
681
794
 
682
795
 
683
796
  @server.tool()
684
- @require_google_service("gmail", GMAIL_MODIFY_SCOPE)
685
797
  @handle_http_errors("modify_gmail_message_labels")
798
+ @require_google_service("gmail", GMAIL_MODIFY_SCOPE)
686
799
  async def modify_gmail_message_labels(
687
800
  service,
688
801
  user_google_email: str,
@@ -702,10 +815,14 @@ async def modify_gmail_message_labels(
702
815
  Returns:
703
816
  str: Confirmation message of the label changes applied to the message.
704
817
  """
705
- logger.info(f"[modify_gmail_message_labels] Invoked. Email: '{user_google_email}', Message ID: '{message_id}'")
818
+ logger.info(
819
+ f"[modify_gmail_message_labels] Invoked. Email: '{user_google_email}', Message ID: '{message_id}'"
820
+ )
706
821
 
707
822
  if not add_label_ids and not remove_label_ids:
708
- raise Exception("At least one of add_label_ids or remove_label_ids must be provided.")
823
+ raise Exception(
824
+ "At least one of add_label_ids or remove_label_ids must be provided."
825
+ )
709
826
 
710
827
  body = {}
711
828
  if add_label_ids:
gsheets/sheets_tools.py CHANGED
@@ -21,8 +21,8 @@ logger = logging.getLogger(__name__)
21
21
 
22
22
 
23
23
  @server.tool()
24
+ @handle_http_errors("list_spreadsheets", is_read_only=True)
24
25
  @require_google_service("drive", "drive_read")
25
- @handle_http_errors("list_spreadsheets")
26
26
  async def list_spreadsheets(
27
27
  service,
28
28
  user_google_email: str,
@@ -70,8 +70,8 @@ async def list_spreadsheets(
70
70
 
71
71
 
72
72
  @server.tool()
73
+ @handle_http_errors("get_spreadsheet_info", is_read_only=True)
73
74
  @require_google_service("sheets", "sheets_read")
74
- @handle_http_errors("get_spreadsheet_info")
75
75
  async def get_spreadsheet_info(
76
76
  service,
77
77
  user_google_email: str,
@@ -120,8 +120,8 @@ async def get_spreadsheet_info(
120
120
 
121
121
 
122
122
  @server.tool()
123
+ @handle_http_errors("read_sheet_values", is_read_only=True)
123
124
  @require_google_service("sheets", "sheets_read")
124
- @handle_http_errors("read_sheet_values")
125
125
  async def read_sheet_values(
126
126
  service,
127
127
  user_google_email: str,
@@ -170,8 +170,8 @@ async def read_sheet_values(
170
170
 
171
171
 
172
172
  @server.tool()
173
- @require_google_service("sheets", "sheets_write")
174
173
  @handle_http_errors("modify_sheet_values")
174
+ @require_google_service("sheets", "sheets_write")
175
175
  async def modify_sheet_values(
176
176
  service,
177
177
  user_google_email: str,
@@ -241,8 +241,8 @@ async def modify_sheet_values(
241
241
 
242
242
 
243
243
  @server.tool()
244
- @require_google_service("sheets", "sheets_write")
245
244
  @handle_http_errors("create_spreadsheet")
245
+ @require_google_service("sheets", "sheets_write")
246
246
  async def create_spreadsheet(
247
247
  service,
248
248
  user_google_email: str,
@@ -290,8 +290,8 @@ async def create_spreadsheet(
290
290
 
291
291
 
292
292
  @server.tool()
293
- @require_google_service("sheets", "sheets_write")
294
293
  @handle_http_errors("create_sheet")
294
+ @require_google_service("sheets", "sheets_write")
295
295
  async def create_sheet(
296
296
  service,
297
297
  user_google_email: str,
@@ -344,7 +344,7 @@ _comment_tools = create_comment_tools("spreadsheet", "spreadsheet_id")
344
344
 
345
345
  # Extract and register the functions
346
346
  read_sheet_comments = _comment_tools['read_comments']
347
- create_sheet_comment = _comment_tools['create_comment']
347
+ create_sheet_comment = _comment_tools['create_comment']
348
348
  reply_to_sheet_comment = _comment_tools['reply_to_comment']
349
349
  resolve_sheet_comment = _comment_tools['resolve_comment']
350
350