workspace-mcp 1.0.0__py3-none-any.whl → 1.0.2__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/service_decorator.py +31 -32
- core/server.py +3 -10
- core/utils.py +36 -0
- gcalendar/calendar_tools.py +308 -258
- gchat/chat_tools.py +131 -158
- gdocs/docs_tools.py +121 -149
- gdrive/drive_tools.py +168 -171
- gforms/forms_tools.py +118 -157
- gmail/gmail_tools.py +319 -400
- gsheets/sheets_tools.py +144 -197
- gslides/slides_tools.py +113 -157
- main.py +31 -25
- workspace_mcp-1.0.2.dist-info/METADATA +422 -0
- workspace_mcp-1.0.2.dist-info/RECORD +32 -0
- workspace_mcp-1.0.0.dist-info/METADATA +0 -29
- workspace_mcp-1.0.0.dist-info/RECORD +0 -32
- {workspace_mcp-1.0.0.dist-info → workspace_mcp-1.0.2.dist-info}/WHEEL +0 -0
- {workspace_mcp-1.0.0.dist-info → workspace_mcp-1.0.2.dist-info}/entry_points.txt +0 -0
- {workspace_mcp-1.0.0.dist-info → workspace_mcp-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {workspace_mcp-1.0.0.dist-info → workspace_mcp-1.0.2.dist-info}/top_level.txt +0 -0
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
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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
|
-
|
167
|
-
|
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
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
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
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
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
|
-
|
231
|
-
|
232
|
-
|
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
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
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
|
-
|
283
|
-
output_messages = []
|
263
|
+
output_messages = []
|
284
264
|
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
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
|
-
|
291
|
-
|
292
|
-
|
270
|
+
def _batch_callback(request_id, response, exception):
|
271
|
+
"""Callback for batch requests"""
|
272
|
+
results[request_id] = {"data": response, "error": exception}
|
293
273
|
|
294
|
-
|
295
|
-
|
296
|
-
|
274
|
+
# Try to use batch API
|
275
|
+
try:
|
276
|
+
batch = service.new_batch_http_request(callback=_batch_callback)
|
297
277
|
|
298
|
-
|
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
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
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
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
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
|
-
|
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
|
-
|
315
|
-
|
332
|
+
# Convert to results format
|
333
|
+
for mid, msg, error in fetch_results:
|
334
|
+
results[mid] = {"data": msg, "error": error}
|
316
335
|
|
317
|
-
|
318
|
-
|
319
|
-
|
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
|
-
|
324
|
-
|
325
|
-
|
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
|
-
#
|
353
|
-
|
354
|
-
results[mid] = {"data": msg, "error": error}
|
352
|
+
# Extract content based on format
|
353
|
+
payload = message.get("payload", {})
|
355
354
|
|
356
|
-
|
357
|
-
|
358
|
-
|
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"
|
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
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
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
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
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
|
-
|
491
|
-
|
492
|
-
|
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
|
-
|
455
|
+
# Add recipient if provided
|
456
|
+
if to:
|
457
|
+
message["to"] = to
|
500
458
|
|
501
|
-
|
502
|
-
draft_body = {"message": {"raw": raw_message}}
|
459
|
+
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
|
503
460
|
|
504
|
-
|
505
|
-
|
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
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
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
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
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
|
-
|
550
|
-
|
551
|
-
|
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
|
-
|
554
|
-
|
555
|
-
|
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
|
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
|
-
|
570
|
-
|
571
|
-
|
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
|
-
|
595
|
-
|
596
|
-
|
532
|
+
# Extract message body
|
533
|
+
payload = message.get("payload", {})
|
534
|
+
body_data = _extract_message_body(payload)
|
597
535
|
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
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
|
-
|
607
|
-
|
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
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
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
|
-
|
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
|
-
|
636
|
-
|
637
|
-
|
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
|
-
|
581
|
+
if not labels:
|
582
|
+
return "No labels found."
|
645
583
|
|
646
|
-
|
647
|
-
user_labels = []
|
584
|
+
lines = [f"Found {len(labels)} labels:", ""]
|
648
585
|
|
649
|
-
|
650
|
-
|
651
|
-
system_labels.append(label)
|
652
|
-
else:
|
653
|
-
user_labels.append(label)
|
586
|
+
system_labels = []
|
587
|
+
user_labels = []
|
654
588
|
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
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
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
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
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
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
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
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
|
-
|
739
|
-
|
740
|
-
|
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
|
-
|
745
|
-
|
746
|
-
)
|
747
|
-
|
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
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
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
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
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
|
-
|
795
|
-
|
796
|
-
|
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
|
-
|
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
|
-
|
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)}"
|