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.
- auth/google_auth.py +6 -8
- auth/oauth_callback_server.py +22 -8
- core/server.py +1 -1
- core/utils.py +153 -54
- gcalendar/calendar_tools.py +7 -7
- gdocs/docs_tools.py +4 -5
- gdrive/drive_tools.py +5 -5
- gforms/forms_tools.py +22 -23
- gmail/gmail_tools.py +199 -82
- gsheets/sheets_tools.py +7 -7
- gslides/slides_tools.py +25 -25
- gtasks/tasks_tools.py +13 -0
- {workspace_mcp-1.1.4.dist-info → workspace_mcp-1.1.6.dist-info}/METADATA +62 -13
- {workspace_mcp-1.1.4.dist-info → workspace_mcp-1.1.6.dist-info}/RECORD +18 -18
- {workspace_mcp-1.1.4.dist-info → workspace_mcp-1.1.6.dist-info}/WHEEL +0 -0
- {workspace_mcp-1.1.4.dist-info → workspace_mcp-1.1.6.dist-info}/entry_points.txt +0 -0
- {workspace_mcp-1.1.4.dist-info → workspace_mcp-1.1.6.dist-info}/licenses/LICENSE +0 -0
- {workspace_mcp-1.1.4.dist-info → workspace_mcp-1.1.6.dist-info}/top_level.txt +0 -0
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
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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(
|
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 =
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
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 =
|
288
|
-
|
289
|
-
|
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()
|
315
|
+
service.users()
|
316
|
+
.messages()
|
317
|
+
.get(
|
308
318
|
userId="me",
|
309
319
|
id=mid,
|
310
320
|
format="metadata",
|
311
|
-
metadataHeaders=["Subject", "From"]
|
312
|
-
)
|
321
|
+
metadataHeaders=["Subject", "From"],
|
322
|
+
)
|
323
|
+
.execute
|
313
324
|
)
|
314
325
|
else:
|
315
326
|
msg = await asyncio.to_thread(
|
316
|
-
service.users()
|
317
|
-
|
318
|
-
|
319
|
-
|
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
|
-
|
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
|
-
|
479
|
+
Helper function to format thread content from Gmail API response.
|
480
480
|
|
481
481
|
Args:
|
482
|
-
|
483
|
-
|
482
|
+
thread_data (dict): Thread data from Gmail API
|
483
|
+
thread_id (str): Thread ID for display
|
484
484
|
|
485
485
|
Returns:
|
486
|
-
str:
|
486
|
+
str: Formatted thread content
|
487
487
|
"""
|
488
|
-
|
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
|
-
|
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("
|
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(
|
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()
|
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(
|
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(
|
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
|
|