workspace-mcp 1.1.7__py3-none-any.whl → 1.1.9__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 +1 -1
- auth/oauth21/__init__.py +108 -0
- auth/oauth21/compat.py +422 -0
- auth/oauth21/config.py +380 -0
- auth/oauth21/discovery.py +232 -0
- auth/oauth21/example_config.py +303 -0
- auth/oauth21/handler.py +440 -0
- auth/oauth21/http.py +270 -0
- auth/oauth21/jwt.py +438 -0
- auth/oauth21/middleware.py +426 -0
- auth/oauth21/oauth2.py +353 -0
- auth/oauth21/sessions.py +519 -0
- auth/oauth21/tokens.py +392 -0
- auth/oauth_callback_server.py +1 -1
- auth/service_decorator.py +2 -5
- core/comments.py +0 -3
- core/server.py +35 -36
- core/utils.py +3 -4
- gcalendar/calendar_tools.py +4 -5
- gchat/chat_tools.py +0 -1
- gdocs/docs_tools.py +73 -16
- gdrive/drive_tools.py +1 -3
- gforms/forms_tools.py +0 -1
- gmail/gmail_tools.py +184 -70
- gsheets/sheets_tools.py +0 -2
- gslides/slides_tools.py +1 -3
- gtasks/tasks_tools.py +1 -2
- main.py +2 -2
- {workspace_mcp-1.1.7.dist-info → workspace_mcp-1.1.9.dist-info}/METADATA +3 -2
- workspace_mcp-1.1.9.dist-info/RECORD +48 -0
- workspace_mcp-1.1.7.dist-info/RECORD +0 -36
- {workspace_mcp-1.1.7.dist-info → workspace_mcp-1.1.9.dist-info}/WHEEL +0 -0
- {workspace_mcp-1.1.7.dist-info → workspace_mcp-1.1.9.dist-info}/entry_points.txt +0 -0
- {workspace_mcp-1.1.7.dist-info → workspace_mcp-1.1.9.dist-info}/licenses/LICENSE +0 -0
- {workspace_mcp-1.1.7.dist-info → workspace_mcp-1.1.9.dist-info}/top_level.txt +0 -0
gdocs/docs_tools.py
CHANGED
@@ -6,9 +6,7 @@ This module provides MCP tools for interacting with Google Docs API and managing
|
|
6
6
|
import logging
|
7
7
|
import asyncio
|
8
8
|
import io
|
9
|
-
from typing import List
|
10
9
|
|
11
|
-
from mcp import types
|
12
10
|
from googleapiclient.http import MediaIoBaseDownload
|
13
11
|
|
14
12
|
# Auth & server utilities
|
@@ -94,24 +92,83 @@ async def get_doc_content(
|
|
94
92
|
|
95
93
|
# Step 3: Process based on mimeType
|
96
94
|
if mime_type == "application/vnd.google-apps.document":
|
97
|
-
logger.info(
|
95
|
+
logger.info("[get_doc_content] Processing as native Google Doc.")
|
98
96
|
doc_data = await asyncio.to_thread(
|
99
|
-
docs_service.documents().get(
|
97
|
+
docs_service.documents().get(
|
98
|
+
documentId=document_id,
|
99
|
+
includeTabsContent=True
|
100
|
+
).execute
|
100
101
|
)
|
102
|
+
# Tab header format constant
|
103
|
+
TAB_HEADER_FORMAT = "\n--- TAB: {tab_name} ---\n"
|
104
|
+
|
105
|
+
def extract_text_from_elements(elements, tab_name=None, depth=0):
|
106
|
+
"""Extract text from document elements (paragraphs, tables, etc.)"""
|
107
|
+
# Prevent infinite recursion by limiting depth
|
108
|
+
if depth > 5:
|
109
|
+
return ""
|
110
|
+
text_lines = []
|
111
|
+
if tab_name:
|
112
|
+
text_lines.append(TAB_HEADER_FORMAT.format(tab_name=tab_name))
|
113
|
+
|
114
|
+
for element in elements:
|
115
|
+
if 'paragraph' in element:
|
116
|
+
paragraph = element.get('paragraph', {})
|
117
|
+
para_elements = paragraph.get('elements', [])
|
118
|
+
current_line_text = ""
|
119
|
+
for pe in para_elements:
|
120
|
+
text_run = pe.get('textRun', {})
|
121
|
+
if text_run and 'content' in text_run:
|
122
|
+
current_line_text += text_run['content']
|
123
|
+
if current_line_text.strip():
|
124
|
+
text_lines.append(current_line_text)
|
125
|
+
elif 'table' in element:
|
126
|
+
# Handle table content
|
127
|
+
table = element.get('table', {})
|
128
|
+
table_rows = table.get('tableRows', [])
|
129
|
+
for row in table_rows:
|
130
|
+
row_cells = row.get('tableCells', [])
|
131
|
+
for cell in row_cells:
|
132
|
+
cell_content = cell.get('content', [])
|
133
|
+
cell_text = extract_text_from_elements(cell_content, depth=depth + 1)
|
134
|
+
if cell_text.strip():
|
135
|
+
text_lines.append(cell_text)
|
136
|
+
return "".join(text_lines)
|
137
|
+
|
138
|
+
def process_tab_hierarchy(tab, level=0):
|
139
|
+
"""Process a tab and its nested child tabs recursively"""
|
140
|
+
tab_text = ""
|
141
|
+
|
142
|
+
if 'documentTab' in tab:
|
143
|
+
tab_title = tab.get('documentTab', {}).get('title', 'Untitled Tab')
|
144
|
+
# Add indentation for nested tabs to show hierarchy
|
145
|
+
if level > 0:
|
146
|
+
tab_title = " " * level + tab_title
|
147
|
+
tab_body = tab.get('documentTab', {}).get('body', {}).get('content', [])
|
148
|
+
tab_text += extract_text_from_elements(tab_body, tab_title)
|
149
|
+
|
150
|
+
# Process child tabs (nested tabs)
|
151
|
+
child_tabs = tab.get('childTabs', [])
|
152
|
+
for child_tab in child_tabs:
|
153
|
+
tab_text += process_tab_hierarchy(child_tab, level + 1)
|
154
|
+
|
155
|
+
return tab_text
|
156
|
+
|
157
|
+
processed_text_lines = []
|
158
|
+
|
159
|
+
# Process main document body
|
101
160
|
body_elements = doc_data.get('body', {}).get('content', [])
|
161
|
+
main_content = extract_text_from_elements(body_elements)
|
162
|
+
if main_content.strip():
|
163
|
+
processed_text_lines.append(main_content)
|
164
|
+
|
165
|
+
# Process all tabs
|
166
|
+
tabs = doc_data.get('tabs', [])
|
167
|
+
for tab in tabs:
|
168
|
+
tab_content = process_tab_hierarchy(tab)
|
169
|
+
if tab_content.strip():
|
170
|
+
processed_text_lines.append(tab_content)
|
102
171
|
|
103
|
-
processed_text_lines: List[str] = []
|
104
|
-
for element in body_elements:
|
105
|
-
if 'paragraph' in element:
|
106
|
-
paragraph = element.get('paragraph', {})
|
107
|
-
para_elements = paragraph.get('elements', [])
|
108
|
-
current_line_text = ""
|
109
|
-
for pe in para_elements:
|
110
|
-
text_run = pe.get('textRun', {})
|
111
|
-
if text_run and 'content' in text_run:
|
112
|
-
current_line_text += text_run['content']
|
113
|
-
if current_line_text.strip():
|
114
|
-
processed_text_lines.append(current_line_text)
|
115
172
|
body_text = "".join(processed_text_lines)
|
116
173
|
else:
|
117
174
|
logger.info(f"[get_doc_content] Processing as Drive file (e.g., .docx, other). MimeType: {mime_type}")
|
gdrive/drive_tools.py
CHANGED
@@ -6,10 +6,8 @@ This module provides MCP tools for interacting with Google Drive API.
|
|
6
6
|
import logging
|
7
7
|
import asyncio
|
8
8
|
import re
|
9
|
-
from typing import
|
9
|
+
from typing import Optional, Dict, Any
|
10
10
|
|
11
|
-
from mcp import types
|
12
|
-
from googleapiclient.errors import HttpError
|
13
11
|
from googleapiclient.http import MediaIoBaseDownload, MediaIoBaseUpload
|
14
12
|
import io
|
15
13
|
import httpx
|
gforms/forms_tools.py
CHANGED
gmail/gmail_tools.py
CHANGED
@@ -7,17 +7,16 @@ This module provides MCP tools for interacting with the Gmail API.
|
|
7
7
|
import logging
|
8
8
|
import asyncio
|
9
9
|
import base64
|
10
|
+
import ssl
|
10
11
|
from typing import Optional, List, Dict, Literal
|
11
12
|
|
12
13
|
from email.mime.text import MIMEText
|
13
14
|
|
14
|
-
from mcp import types
|
15
15
|
from fastapi import Body
|
16
16
|
|
17
17
|
from auth.service_decorator import require_google_service
|
18
18
|
from core.utils import handle_http_errors
|
19
19
|
from core.server import (
|
20
|
-
GMAIL_READONLY_SCOPE,
|
21
20
|
GMAIL_SEND_SCOPE,
|
22
21
|
GMAIL_COMPOSE_SCOPE,
|
23
22
|
GMAIL_MODIFY_SCOPE,
|
@@ -27,6 +26,9 @@ from core.server import (
|
|
27
26
|
|
28
27
|
logger = logging.getLogger(__name__)
|
29
28
|
|
29
|
+
GMAIL_BATCH_SIZE = 25
|
30
|
+
GMAIL_REQUEST_DELAY = 0.1
|
31
|
+
|
30
32
|
|
31
33
|
def _extract_message_body(payload):
|
32
34
|
"""
|
@@ -108,14 +110,40 @@ def _format_gmail_results_plain(messages: list, query: str) -> str:
|
|
108
110
|
]
|
109
111
|
|
110
112
|
for i, msg in enumerate(messages, 1):
|
111
|
-
|
112
|
-
|
113
|
+
# Handle potential null/undefined message objects
|
114
|
+
if not msg or not isinstance(msg, dict):
|
115
|
+
lines.extend([
|
116
|
+
f" {i}. Message: Invalid message data",
|
117
|
+
" Error: Message object is null or malformed",
|
118
|
+
"",
|
119
|
+
])
|
120
|
+
continue
|
121
|
+
|
122
|
+
# Handle potential null/undefined values from Gmail API
|
123
|
+
message_id = msg.get("id")
|
124
|
+
thread_id = msg.get("threadId")
|
125
|
+
|
126
|
+
# Convert None, empty string, or missing values to "unknown"
|
127
|
+
if not message_id:
|
128
|
+
message_id = "unknown"
|
129
|
+
if not thread_id:
|
130
|
+
thread_id = "unknown"
|
131
|
+
|
132
|
+
if message_id != "unknown":
|
133
|
+
message_url = _generate_gmail_web_url(message_id)
|
134
|
+
else:
|
135
|
+
message_url = "N/A"
|
136
|
+
|
137
|
+
if thread_id != "unknown":
|
138
|
+
thread_url = _generate_gmail_web_url(thread_id)
|
139
|
+
else:
|
140
|
+
thread_url = "N/A"
|
113
141
|
|
114
142
|
lines.extend(
|
115
143
|
[
|
116
|
-
f" {i}. Message ID: {
|
144
|
+
f" {i}. Message ID: {message_id}",
|
117
145
|
f" Web Link: {message_url}",
|
118
|
-
f" Thread ID: {
|
146
|
+
f" Thread ID: {thread_id}",
|
119
147
|
f" Thread Link: {thread_url}",
|
120
148
|
"",
|
121
149
|
]
|
@@ -161,7 +189,17 @@ async def search_gmail_messages(
|
|
161
189
|
.list(userId="me", q=query, maxResults=page_size)
|
162
190
|
.execute
|
163
191
|
)
|
192
|
+
|
193
|
+
# Handle potential null response (but empty dict {} is valid)
|
194
|
+
if response is None:
|
195
|
+
logger.warning("[search_gmail_messages] Null response from Gmail API")
|
196
|
+
return f"No response received from Gmail API for query: '{query}'"
|
197
|
+
|
164
198
|
messages = response.get("messages", [])
|
199
|
+
# Additional safety check for null messages array
|
200
|
+
if messages is None:
|
201
|
+
messages = []
|
202
|
+
|
165
203
|
formatted_output = _format_gmail_results_plain(messages, query)
|
166
204
|
|
167
205
|
logger.info(f"[search_gmail_messages] Found {len(messages)} messages")
|
@@ -247,10 +285,10 @@ async def get_gmail_messages_content_batch(
|
|
247
285
|
) -> str:
|
248
286
|
"""
|
249
287
|
Retrieves the content of multiple Gmail messages in a single batch request.
|
250
|
-
Supports up to
|
288
|
+
Supports up to 25 messages per batch to prevent SSL connection exhaustion.
|
251
289
|
|
252
290
|
Args:
|
253
|
-
message_ids (List[str]): List of Gmail message IDs to retrieve (max
|
291
|
+
message_ids (List[str]): List of Gmail message IDs to retrieve (max 25 per batch).
|
254
292
|
user_google_email (str): The user's Google email address. Required.
|
255
293
|
format (Literal["full", "metadata"]): Message format. "full" includes body, "metadata" only headers.
|
256
294
|
|
@@ -266,9 +304,9 @@ async def get_gmail_messages_content_batch(
|
|
266
304
|
|
267
305
|
output_messages = []
|
268
306
|
|
269
|
-
# Process in chunks
|
270
|
-
for chunk_start in range(0, len(message_ids),
|
271
|
-
chunk_ids = message_ids[chunk_start : chunk_start +
|
307
|
+
# Process in smaller chunks to prevent SSL connection exhaustion
|
308
|
+
for chunk_start in range(0, len(message_ids), GMAIL_BATCH_SIZE):
|
309
|
+
chunk_ids = message_ids[chunk_start : chunk_start + GMAIL_BATCH_SIZE]
|
272
310
|
results: Dict[str, Dict] = {}
|
273
311
|
|
274
312
|
def _batch_callback(request_id, response, exception):
|
@@ -303,44 +341,57 @@ async def get_gmail_messages_content_batch(
|
|
303
341
|
await asyncio.to_thread(batch.execute)
|
304
342
|
|
305
343
|
except Exception as batch_error:
|
306
|
-
# Fallback to
|
344
|
+
# Fallback to sequential processing instead of parallel to prevent SSL exhaustion
|
307
345
|
logger.warning(
|
308
|
-
f"[get_gmail_messages_content_batch] Batch API failed, falling back to
|
346
|
+
f"[get_gmail_messages_content_batch] Batch API failed, falling back to sequential processing: {batch_error}"
|
309
347
|
)
|
310
348
|
|
311
|
-
async def
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
.
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
349
|
+
async def fetch_message_with_retry(mid: str, max_retries: int = 3):
|
350
|
+
"""Fetch a single message with exponential backoff retry for SSL errors"""
|
351
|
+
for attempt in range(max_retries):
|
352
|
+
try:
|
353
|
+
if format == "metadata":
|
354
|
+
msg = await asyncio.to_thread(
|
355
|
+
service.users()
|
356
|
+
.messages()
|
357
|
+
.get(
|
358
|
+
userId="me",
|
359
|
+
id=mid,
|
360
|
+
format="metadata",
|
361
|
+
metadataHeaders=["Subject", "From"],
|
362
|
+
)
|
363
|
+
.execute
|
322
364
|
)
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
365
|
+
else:
|
366
|
+
msg = await asyncio.to_thread(
|
367
|
+
service.users()
|
368
|
+
.messages()
|
369
|
+
.get(userId="me", id=mid, format="full")
|
370
|
+
.execute
|
371
|
+
)
|
372
|
+
return mid, msg, None
|
373
|
+
except ssl.SSLError as ssl_error:
|
374
|
+
if attempt < max_retries - 1:
|
375
|
+
# Exponential backoff: 1s, 2s, 4s
|
376
|
+
delay = 2 ** attempt
|
377
|
+
logger.warning(
|
378
|
+
f"[get_gmail_messages_content_batch] SSL error for message {mid} on attempt {attempt + 1}: {ssl_error}. Retrying in {delay}s..."
|
379
|
+
)
|
380
|
+
await asyncio.sleep(delay)
|
381
|
+
else:
|
382
|
+
logger.error(
|
383
|
+
f"[get_gmail_messages_content_batch] SSL error for message {mid} on final attempt: {ssl_error}"
|
384
|
+
)
|
385
|
+
return mid, None, ssl_error
|
386
|
+
except Exception as e:
|
387
|
+
return mid, None, e
|
340
388
|
|
341
|
-
#
|
342
|
-
for mid
|
343
|
-
|
389
|
+
# Process messages sequentially with small delays to prevent connection exhaustion
|
390
|
+
for mid in chunk_ids:
|
391
|
+
mid_result, msg_data, error = await fetch_message_with_retry(mid)
|
392
|
+
results[mid_result] = {"data": msg_data, "error": error}
|
393
|
+
# Brief delay between requests to allow connection cleanup
|
394
|
+
await asyncio.sleep(GMAIL_REQUEST_DELAY)
|
344
395
|
|
345
396
|
# Process results for this chunk
|
346
397
|
for mid in chunk_ids:
|
@@ -582,10 +633,10 @@ async def get_gmail_threads_content_batch(
|
|
582
633
|
) -> str:
|
583
634
|
"""
|
584
635
|
Retrieves the content of multiple Gmail threads in a single batch request.
|
585
|
-
Supports up to
|
636
|
+
Supports up to 25 threads per batch to prevent SSL connection exhaustion.
|
586
637
|
|
587
638
|
Args:
|
588
|
-
thread_ids (List[str]): A list of Gmail thread IDs to retrieve. The function will automatically batch requests in chunks of
|
639
|
+
thread_ids (List[str]): A list of Gmail thread IDs to retrieve. The function will automatically batch requests in chunks of 25.
|
589
640
|
user_google_email (str): The user's Google email address. Required.
|
590
641
|
|
591
642
|
Returns:
|
@@ -604,9 +655,9 @@ async def get_gmail_threads_content_batch(
|
|
604
655
|
"""Callback for batch requests"""
|
605
656
|
results[request_id] = {"data": response, "error": exception}
|
606
657
|
|
607
|
-
# Process in chunks
|
608
|
-
for chunk_start in range(0, len(thread_ids),
|
609
|
-
chunk_ids = thread_ids[chunk_start : chunk_start +
|
658
|
+
# Process in smaller chunks to prevent SSL connection exhaustion
|
659
|
+
for chunk_start in range(0, len(thread_ids), GMAIL_BATCH_SIZE):
|
660
|
+
chunk_ids = thread_ids[chunk_start : chunk_start + GMAIL_BATCH_SIZE]
|
610
661
|
results: Dict[str, Dict] = {}
|
611
662
|
|
612
663
|
# Try to use batch API
|
@@ -621,31 +672,44 @@ async def get_gmail_threads_content_batch(
|
|
621
672
|
await asyncio.to_thread(batch.execute)
|
622
673
|
|
623
674
|
except Exception as batch_error:
|
624
|
-
# Fallback to
|
675
|
+
# Fallback to sequential processing instead of parallel to prevent SSL exhaustion
|
625
676
|
logger.warning(
|
626
|
-
f"[get_gmail_threads_content_batch] Batch API failed, falling back to
|
677
|
+
f"[get_gmail_threads_content_batch] Batch API failed, falling back to sequential processing: {batch_error}"
|
627
678
|
)
|
628
679
|
|
629
|
-
async def
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
.
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
680
|
+
async def fetch_thread_with_retry(tid: str, max_retries: int = 3):
|
681
|
+
"""Fetch a single thread with exponential backoff retry for SSL errors"""
|
682
|
+
for attempt in range(max_retries):
|
683
|
+
try:
|
684
|
+
thread = await asyncio.to_thread(
|
685
|
+
service.users()
|
686
|
+
.threads()
|
687
|
+
.get(userId="me", id=tid, format="full")
|
688
|
+
.execute
|
689
|
+
)
|
690
|
+
return tid, thread, None
|
691
|
+
except ssl.SSLError as ssl_error:
|
692
|
+
if attempt < max_retries - 1:
|
693
|
+
# Exponential backoff: 1s, 2s, 4s
|
694
|
+
delay = 2 ** attempt
|
695
|
+
logger.warning(
|
696
|
+
f"[get_gmail_threads_content_batch] SSL error for thread {tid} on attempt {attempt + 1}: {ssl_error}. Retrying in {delay}s..."
|
697
|
+
)
|
698
|
+
await asyncio.sleep(delay)
|
699
|
+
else:
|
700
|
+
logger.error(
|
701
|
+
f"[get_gmail_threads_content_batch] SSL error for thread {tid} on final attempt: {ssl_error}"
|
702
|
+
)
|
703
|
+
return tid, None, ssl_error
|
704
|
+
except Exception as e:
|
705
|
+
return tid, None, e
|
645
706
|
|
646
|
-
#
|
647
|
-
for tid
|
648
|
-
|
707
|
+
# Process threads sequentially with small delays to prevent connection exhaustion
|
708
|
+
for tid in chunk_ids:
|
709
|
+
tid_result, thread_data, error = await fetch_thread_with_retry(tid)
|
710
|
+
results[tid_result] = {"data": thread_data, "error": error}
|
711
|
+
# Brief delay between requests to allow connection cleanup
|
712
|
+
await asyncio.sleep(GMAIL_REQUEST_DELAY)
|
649
713
|
|
650
714
|
# Process results for this chunk
|
651
715
|
for tid in chunk_ids:
|
@@ -841,3 +905,53 @@ async def modify_gmail_message_labels(
|
|
841
905
|
actions.append(f"Removed labels: {', '.join(remove_label_ids)}")
|
842
906
|
|
843
907
|
return f"Message labels updated successfully!\nMessage ID: {message_id}\n{'; '.join(actions)}"
|
908
|
+
|
909
|
+
|
910
|
+
@server.tool()
|
911
|
+
@handle_http_errors("batch_modify_gmail_message_labels")
|
912
|
+
@require_google_service("gmail", GMAIL_MODIFY_SCOPE)
|
913
|
+
async def batch_modify_gmail_message_labels(
|
914
|
+
service,
|
915
|
+
user_google_email: str,
|
916
|
+
message_ids: List[str],
|
917
|
+
add_label_ids: Optional[List[str]] = None,
|
918
|
+
remove_label_ids: Optional[List[str]] = None,
|
919
|
+
) -> str:
|
920
|
+
"""
|
921
|
+
Adds or removes labels from multiple Gmail messages in a single batch request.
|
922
|
+
|
923
|
+
Args:
|
924
|
+
user_google_email (str): The user's Google email address. Required.
|
925
|
+
message_ids (List[str]): A list of message IDs to modify.
|
926
|
+
add_label_ids (Optional[List[str]]): List of label IDs to add to the messages.
|
927
|
+
remove_label_ids (Optional[List[str]]): List of label IDs to remove from the messages.
|
928
|
+
|
929
|
+
Returns:
|
930
|
+
str: Confirmation message of the label changes applied to the messages.
|
931
|
+
"""
|
932
|
+
logger.info(
|
933
|
+
f"[batch_modify_gmail_message_labels] Invoked. Email: '{user_google_email}', Message IDs: '{message_ids}'"
|
934
|
+
)
|
935
|
+
|
936
|
+
if not add_label_ids and not remove_label_ids:
|
937
|
+
raise Exception(
|
938
|
+
"At least one of add_label_ids or remove_label_ids must be provided."
|
939
|
+
)
|
940
|
+
|
941
|
+
body = {"ids": message_ids}
|
942
|
+
if add_label_ids:
|
943
|
+
body["addLabelIds"] = add_label_ids
|
944
|
+
if remove_label_ids:
|
945
|
+
body["removeLabelIds"] = remove_label_ids
|
946
|
+
|
947
|
+
await asyncio.to_thread(
|
948
|
+
service.users().messages().batchModify(userId="me", body=body).execute
|
949
|
+
)
|
950
|
+
|
951
|
+
actions = []
|
952
|
+
if add_label_ids:
|
953
|
+
actions.append(f"Added labels: {', '.join(add_label_ids)}")
|
954
|
+
if remove_label_ids:
|
955
|
+
actions.append(f"Removed labels: {', '.join(remove_label_ids)}")
|
956
|
+
|
957
|
+
return f"Labels updated for {len(message_ids)} messages: {'; '.join(actions)}"
|
gsheets/sheets_tools.py
CHANGED
gslides/slides_tools.py
CHANGED
@@ -6,10 +6,8 @@ This module provides MCP tools for interacting with Google Slides API.
|
|
6
6
|
|
7
7
|
import logging
|
8
8
|
import asyncio
|
9
|
-
from typing import List,
|
9
|
+
from typing import List, Dict, Any
|
10
10
|
|
11
|
-
from mcp import types
|
12
|
-
from googleapiclient.errors import HttpError
|
13
11
|
|
14
12
|
from auth.service_decorator import require_google_service
|
15
13
|
from core.server import server
|
gtasks/tasks_tools.py
CHANGED
@@ -6,9 +6,8 @@ This module provides MCP tools for interacting with Google Tasks API.
|
|
6
6
|
|
7
7
|
import logging
|
8
8
|
import asyncio
|
9
|
-
from typing import
|
9
|
+
from typing import Optional
|
10
10
|
|
11
|
-
from mcp import types
|
12
11
|
from googleapiclient.errors import HttpError
|
13
12
|
|
14
13
|
from auth.service_decorator import require_google_service
|
main.py
CHANGED
@@ -115,9 +115,9 @@ def main():
|
|
115
115
|
safe_print(f" {tool_icons[tool]} {tool.title()} - Google {tool.title()} API integration")
|
116
116
|
safe_print("")
|
117
117
|
|
118
|
-
safe_print(
|
118
|
+
safe_print("📊 Configuration Summary:")
|
119
119
|
safe_print(f" 🔧 Tools Enabled: {len(tools_to_import)}/{len(tool_imports)}")
|
120
|
-
safe_print(
|
120
|
+
safe_print(" 🔑 Auth Method: OAuth 2.0 with PKCE")
|
121
121
|
safe_print(f" 📝 Log Level: {logging.getLogger().getEffectiveLevel()}")
|
122
122
|
safe_print("")
|
123
123
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: workspace-mcp
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.9
|
4
4
|
Summary: Comprehensive, highly performant Google Workspace Streamable HTTP & SSE MCP Server for Calendar, Gmail, Docs, Sheets, Slides & Drive
|
5
5
|
Author-email: Taylor Wilsdon <taylor@taylorwilsdon.com>
|
6
6
|
License: MIT
|
@@ -37,6 +37,7 @@ Requires-Dist: google-auth-httplib2>=0.2.0
|
|
37
37
|
Requires-Dist: google-auth-oauthlib>=1.2.2
|
38
38
|
Requires-Dist: httpx>=0.28.1
|
39
39
|
Requires-Dist: pyjwt>=2.10.1
|
40
|
+
Requires-Dist: ruff>=0.12.4
|
40
41
|
Requires-Dist: tomlkit
|
41
42
|
Dynamic: license-file
|
42
43
|
|
@@ -118,7 +119,7 @@ A production-ready MCP server that integrates all major Google Workspace service
|
|
118
119
|
1. **Download:** Grab the latest `google_workspace_mcp.dxt` from the “Releases” page
|
119
120
|
2. **Install:** Double-click the file – Claude Desktop opens and prompts you to **Install**
|
120
121
|
3. **Configure:** In Claude Desktop → **Settings → Extensions → Google Workspace MCP**, paste your Google OAuth credentials
|
121
|
-
4. **Use it:** Start a new Claude chat and call any Google Workspace tool
|
122
|
+
4. **Use it:** Start a new Claude chat and call any Google Workspace tool
|
122
123
|
|
123
124
|
>
|
124
125
|
**Why DXT?**
|
@@ -0,0 +1,48 @@
|
|
1
|
+
main.py,sha256=3ugTmkMRAYjeKwlknFrJqv_6dPqimN44-fvTDWJdEiI,7599
|
2
|
+
auth/__init__.py,sha256=gPCU3GE-SLy91S3D3CbX-XfKBm6hteK_VSPKx7yjT5s,42
|
3
|
+
auth/google_auth.py,sha256=d9fSTA2shCkg5hL_OB1es9hUvz72sJT_MBvtLEGeAHk,32867
|
4
|
+
auth/oauth_callback_server.py,sha256=6MOmk4G5e0on1XSJosuLWBLd_WAndGLKBOmktzzock8,9836
|
5
|
+
auth/oauth_responses.py,sha256=qbirSB4d7mBRKcJKqGLrJxRAPaLHqObf9t-VMAq6UKA,7020
|
6
|
+
auth/scopes.py,sha256=v091tidkMnhB0pPWOr0O08mU_s9yxSwVZkpVOyvlSwY,3550
|
7
|
+
auth/service_decorator.py,sha256=crHax8t3EL4DXLPg4i3h9YHyfdpegRex880yNBTNqqI,15795
|
8
|
+
auth/oauth21/__init__.py,sha256=5SiZ9_fISDlCaNKIE0yRy7GaaEZRJlNy034YalgkPuI,2592
|
9
|
+
auth/oauth21/compat.py,sha256=PnpYmJrnUqd_XHOjT-CV6QeRwCGl-lKdStu0riciKfM,15830
|
10
|
+
auth/oauth21/config.py,sha256=UwveWiWnzP_n16NLvefGT4TZVj3LDhfnQyqmaja71ZI,13969
|
11
|
+
auth/oauth21/discovery.py,sha256=5ISMJezdPKHARwxP7IvEacQoMRGjaAbbt85se8Fvybo,9519
|
12
|
+
auth/oauth21/example_config.py,sha256=EWs7zffU9QIkIfBDILrd8iC0nAVoHdwr2fmuGIH1TH8,10168
|
13
|
+
auth/oauth21/handler.py,sha256=e430VrMa6NtbGHP765aaJYU3azT5FTprfv1vUkUDhWQ,15560
|
14
|
+
auth/oauth21/http.py,sha256=dozTcgZX3KZlhdq1maR40d4fPgYx04NdJWMQ60UeHXw,8608
|
15
|
+
auth/oauth21/jwt.py,sha256=94STdontA5aLQ_uldnaKK_Z-4WXscYEevcAqApHbDj4,14090
|
16
|
+
auth/oauth21/middleware.py,sha256=2UYzE-mnp64wTg4cTu7oNRgrnSwR7blvgMdywsnIqoI,15593
|
17
|
+
auth/oauth21/oauth2.py,sha256=nQuF3zJR-kCqy8K9kRA9EiNXy49c1_040RZ9kc4xH1M,13276
|
18
|
+
auth/oauth21/sessions.py,sha256=OpKCNpDSycKtZHAkCarsbTCBfTrfcR6Fc2Z2tDECTxc,17890
|
19
|
+
auth/oauth21/tokens.py,sha256=0BzYU0vdSXq8LiUpVQ8nncZ8UnvjAhXEVw0iY8NYIhs,14745
|
20
|
+
core/__init__.py,sha256=AHVKdPl6v4lUFm2R-KuGuAgEmCyfxseMeLGtntMcqCs,43
|
21
|
+
core/comments.py,sha256=ZiPxdZOsQYhxFwkClCDlcefP0iyLvO23qvN7OPLGSPk,11119
|
22
|
+
core/context.py,sha256=zNgPXf9EO2EMs9sQkfKiywoy6sEOksVNgOrJMA_c30Y,768
|
23
|
+
core/server.py,sha256=xXtHJ0BL_qVm10yZr2Xmr1SqlgnKfrv6mXP4WZ3qX4k,9756
|
24
|
+
core/utils.py,sha256=sebbgJtzgmtoj-5PNgAvKEjmPtIKmtMY6b-faVf4Oec,13101
|
25
|
+
gcalendar/__init__.py,sha256=D5fSdAwbeomoaj7XAdxSnIy-NVKNkpExs67175bOtfc,46
|
26
|
+
gcalendar/calendar_tools.py,sha256=Y-lWqO3cWv8mpmO7Tm9AQExG_EfPXXueoiIk1zRPeHc,22167
|
27
|
+
gchat/__init__.py,sha256=XBjH4SbtULfZHgFCxk3moel5XqG599HCgZWl_veIncg,88
|
28
|
+
gchat/chat_tools.py,sha256=89Gh_6-r8a6CJJjL43CqAmOB0POogzNLMJry4-U-KR4,7252
|
29
|
+
gdocs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
|
+
gdocs/docs_tools.py,sha256=OBcIj7Cv5GWeUSgl25D7sxgFNwPFR7PtH-7hh6eq0Mc,10959
|
31
|
+
gdrive/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
32
|
+
gdrive/drive_tools.py,sha256=8mQDL-o4hgimHcivizV5ZD58GitsBdpIZKG6yJflzEI,15237
|
33
|
+
gforms/__init__.py,sha256=pL91XixrEp9YjpM-AYwONIEfeCP2OumkEG0Io5V4boE,37
|
34
|
+
gforms/forms_tools.py,sha256=Gj8idOXmZUrKAFmFT1l2RJltV7tnThFw8ATrz1z872k,9575
|
35
|
+
gmail/__init__.py,sha256=l8PZ4_7Oet6ZE7tVu9oQ3-BaRAmI4YzAO86kf9uu6pU,60
|
36
|
+
gmail/gmail_tools.py,sha256=ZOggJNBGNLso6mqZOe0yUgGGxxU_p1Jn7qlcpNOP-_o,34161
|
37
|
+
gsheets/__init__.py,sha256=jFfhD52w_EOVw6N5guf_dIc9eP2khW_eS9UAPJg_K3k,446
|
38
|
+
gsheets/sheets_tools.py,sha256=JSL_2f6WMN312k5pQ9TFktb-WYJyqAXqW1e91nST_t0,11887
|
39
|
+
gslides/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
40
|
+
gslides/slides_tools.py,sha256=vbhdzqHtNHfkNXN3h2_E9j_rMc747OzUaXH5XBu6XQY,10017
|
41
|
+
gtasks/__init__.py,sha256=qwOWUzQbkYLSBrdhCqEkAWPH2lEOljk1mLtrlab9YZc,107
|
42
|
+
gtasks/tasks_tools.py,sha256=s95fEMI7-TxyP8K2JoDMiRWftnVE9Fkrrr4j6jVgxAE,26469
|
43
|
+
workspace_mcp-1.1.9.dist-info/licenses/LICENSE,sha256=bB8L7rIyRy5o-WHxGgvRuY8hUTzNu4h3DTkvyV8XFJo,1070
|
44
|
+
workspace_mcp-1.1.9.dist-info/METADATA,sha256=RgCFapZq3-gsUfr1NhBJnBEpKaxfY9enq7ni5_30K80,23567
|
45
|
+
workspace_mcp-1.1.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
46
|
+
workspace_mcp-1.1.9.dist-info/entry_points.txt,sha256=kPiEfOTuf-ptDM0Rf2OlyrFudGW7hCZGg4MCn2Foxs4,44
|
47
|
+
workspace_mcp-1.1.9.dist-info/top_level.txt,sha256=uAg7uV2mETWYRw5g80XtO1lhxVO1sY6_IihNdG_4n24,80
|
48
|
+
workspace_mcp-1.1.9.dist-info/RECORD,,
|
@@ -1,36 +0,0 @@
|
|
1
|
-
main.py,sha256=a4w_AcD_nSJo9697-75tZ3sU0tqOP1J8xTrXXD7qmns,7601
|
2
|
-
auth/__init__.py,sha256=gPCU3GE-SLy91S3D3CbX-XfKBm6hteK_VSPKx7yjT5s,42
|
3
|
-
auth/google_auth.py,sha256=h0QIEthpZMxw7dEijYQ5ntXESg2FHNGkDneEjJkdCn4,32868
|
4
|
-
auth/oauth_callback_server.py,sha256=kcgufdYU3e3ncSNouqgGyIiIOfFCXiR6CiOS8pTYuNo,9837
|
5
|
-
auth/oauth_responses.py,sha256=qbirSB4d7mBRKcJKqGLrJxRAPaLHqObf9t-VMAq6UKA,7020
|
6
|
-
auth/scopes.py,sha256=v091tidkMnhB0pPWOr0O08mU_s9yxSwVZkpVOyvlSwY,3550
|
7
|
-
auth/service_decorator.py,sha256=8UfJnST6oi5Mci2YUdiIocn8--0oAEXm74VrGMroqzQ,15846
|
8
|
-
core/__init__.py,sha256=AHVKdPl6v4lUFm2R-KuGuAgEmCyfxseMeLGtntMcqCs,43
|
9
|
-
core/comments.py,sha256=vVfZYjH0kwqFyXcwvBx3m0Ko4WmfTJTkfD3dCQbucuc,11215
|
10
|
-
core/context.py,sha256=zNgPXf9EO2EMs9sQkfKiywoy6sEOksVNgOrJMA_c30Y,768
|
11
|
-
core/server.py,sha256=En_sV6Z19kWx8SO4KAnh8Qg5v2HYw8f9f_WJdEGMDSA,9301
|
12
|
-
core/utils.py,sha256=QLgyM-XNJiAqmr-ac9SLXJBWpN8VxvNurxdW0Ib2N7s,13096
|
13
|
-
gcalendar/__init__.py,sha256=D5fSdAwbeomoaj7XAdxSnIy-NVKNkpExs67175bOtfc,46
|
14
|
-
gcalendar/calendar_tools.py,sha256=mlWIn26dw7fNGqiIb8NUQNnPsP0b0RJGzp4BvvC85Fk,22193
|
15
|
-
gchat/__init__.py,sha256=XBjH4SbtULfZHgFCxk3moel5XqG599HCgZWl_veIncg,88
|
16
|
-
gchat/chat_tools.py,sha256=cIeXBBxWkFCdQNJ23BkX8IoDho6J8ZcfLsPjctUWyfA,7274
|
17
|
-
gdocs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
|
-
gdocs/docs_tools.py,sha256=WrCjya2-MvM7b-DwFAhoXPcV2kNxq4I0V_xaqV3a1ZI,8494
|
19
|
-
gdrive/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
|
-
gdrive/drive_tools.py,sha256=NWna_uQ7dmXV4kRPX01A0P7so0sjRfq5awWczg6sloU,15310
|
21
|
-
gforms/__init__.py,sha256=pL91XixrEp9YjpM-AYwONIEfeCP2OumkEG0Io5V4boE,37
|
22
|
-
gforms/forms_tools.py,sha256=0zxXFd1JnHPjCHJp_d1id9UYGxbW3UCklBmVlZTRLHo,9597
|
23
|
-
gmail/__init__.py,sha256=l8PZ4_7Oet6ZE7tVu9oQ3-BaRAmI4YzAO86kf9uu6pU,60
|
24
|
-
gmail/gmail_tools.py,sha256=3RpnrjFX_E5d8KUKsyx_K8fWMkDdj01mxha9vZBGwn4,28825
|
25
|
-
gsheets/__init__.py,sha256=jFfhD52w_EOVw6N5guf_dIc9eP2khW_eS9UAPJg_K3k,446
|
26
|
-
gsheets/sheets_tools.py,sha256=dUXtlpjxhRnPTN4Y0mklrMtW4noqezBfj8Rga2ivw74,11954
|
27
|
-
gslides/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
28
|
-
gslides/slides_tools.py,sha256=m9FifqBLPFo_tdxH6mvH1-fDUarAnr_ejJ9oMlgKiYQ,10094
|
29
|
-
gtasks/__init__.py,sha256=qwOWUzQbkYLSBrdhCqEkAWPH2lEOljk1mLtrlab9YZc,107
|
30
|
-
gtasks/tasks_tools.py,sha256=6zmZsZ-8JWUpOIDhvxuBIJsjB9bH8LrdUX0yVlsjlM8,26508
|
31
|
-
workspace_mcp-1.1.7.dist-info/licenses/LICENSE,sha256=bB8L7rIyRy5o-WHxGgvRuY8hUTzNu4h3DTkvyV8XFJo,1070
|
32
|
-
workspace_mcp-1.1.7.dist-info/METADATA,sha256=uQGTgL89fLrgbowJiorKLgmxV340TMvAjNkIFgj61nU,23544
|
33
|
-
workspace_mcp-1.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
34
|
-
workspace_mcp-1.1.7.dist-info/entry_points.txt,sha256=kPiEfOTuf-ptDM0Rf2OlyrFudGW7hCZGg4MCn2Foxs4,44
|
35
|
-
workspace_mcp-1.1.7.dist-info/top_level.txt,sha256=uAg7uV2mETWYRw5g80XtO1lhxVO1sY6_IihNdG_4n24,80
|
36
|
-
workspace_mcp-1.1.7.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|