workspace-mcp 1.1.5__tar.gz → 1.1.6__tar.gz

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.
Files changed (44) hide show
  1. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/PKG-INFO +14 -7
  2. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/README.md +13 -6
  3. workspace_mcp-1.1.6/core/utils.py +296 -0
  4. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gTasks/tasks_tools.py +13 -0
  5. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gcalendar/calendar_tools.py +6 -6
  6. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gdocs/docs_tools.py +4 -4
  7. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gdrive/drive_tools.py +5 -5
  8. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gforms/forms_tools.py +5 -5
  9. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gmail/gmail_tools.py +199 -81
  10. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gsheets/sheets_tools.py +7 -7
  11. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gslides/slides_tools.py +25 -25
  12. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/pyproject.toml +1 -1
  13. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/workspace_mcp.egg-info/PKG-INFO +14 -7
  14. workspace_mcp-1.1.5/core/utils.py +0 -197
  15. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/LICENSE +0 -0
  16. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/auth/__init__.py +0 -0
  17. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/auth/google_auth.py +0 -0
  18. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/auth/oauth_callback_server.py +0 -0
  19. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/auth/oauth_responses.py +0 -0
  20. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/auth/scopes.py +0 -0
  21. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/auth/service_decorator.py +0 -0
  22. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/core/__init__.py +0 -0
  23. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/core/comments.py +0 -0
  24. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/core/context.py +0 -0
  25. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/core/server.py +0 -0
  26. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gTasks/__init__.py +0 -0
  27. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gcalendar/__init__.py +0 -0
  28. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gchat/__init__.py +0 -0
  29. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gchat/chat_tools.py +0 -0
  30. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gdocs/__init__.py +0 -0
  31. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gdrive/__init__.py +0 -0
  32. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gforms/__init__.py +0 -0
  33. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gmail/__init__.py +0 -0
  34. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gsheets/__init__.py +0 -0
  35. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/gslides/__init__.py +0 -0
  36. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/main.py +0 -0
  37. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/setup.cfg +0 -0
  38. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/tests/test_auth.py +0 -0
  39. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/tests/test_oauth_callback_server.py +0 -0
  40. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/workspace_mcp.egg-info/SOURCES.txt +0 -0
  41. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/workspace_mcp.egg-info/dependency_links.txt +0 -0
  42. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/workspace_mcp.egg-info/entry_points.txt +0 -0
  43. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/workspace_mcp.egg-info/requires.txt +0 -0
  44. {workspace_mcp-1.1.5 → workspace_mcp-1.1.6}/workspace_mcp.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workspace-mcp
3
- Version: 1.1.5
3
+ Version: 1.1.6
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
@@ -47,11 +47,11 @@ Dynamic: license-file
47
47
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
48
48
  [![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
49
49
  [![PyPI](https://img.shields.io/pypi/v/workspace-mcp.svg)](https://pypi.org/project/workspace-mcp/)
50
- [![UV](https://img.shields.io/badge/Package%20Installer-UV-blueviolet)](https://github.com/astral-sh/uv)
50
+ [![PyPI Downloads](https://static.pepy.tech/badge/workspace-mcp/month)](https://pepy.tech/projects/workspace-mcp)
51
51
  [![Website](https://img.shields.io/badge/Website-workspacemcp.com-green.svg)](https://workspacemcp.com)
52
52
  [![Verified on MseeP](https://mseep.ai/badge.svg)](https://mseep.ai/app/eebbc4a6-0f8c-41b2-ace8-038e5516dba0)
53
53
 
54
- **This is the single most feature-complete Google Workspace MCP server**
54
+ **This is the single most feature-complete Google Workspace MCP server** now with 1-click Claude installation
55
55
 
56
56
  *Full natural language control over Google Calendar, Drive, Gmail, Docs, Sheets, Slides, Forms, Tasks, and Chat through all MCP clients, AI assistants and developer tools.*
57
57
 
@@ -122,11 +122,12 @@ A production-ready MCP server that integrates all major Google Workspace service
122
122
 
123
123
  >
124
124
  **Why DXT?**
125
- > Desktop Extensions (`.dxt`) bundle the server, dependencies, and manifest so users go from download → working MCP in **three clicks** – no terminal, no JSON editing, no version conflicts.
125
+ > Desktop Extensions (`.dxt`) bundle the server, dependencies, and manifest so users go from download → working MCP in **one click** – no terminal, no JSON editing, no version conflicts.
126
126
 
127
127
  #### Required Configuration
128
128
  <details>
129
129
  <summary>Environment - you will configure these in Claude itself, see screenshot:</summary>
130
+
130
131
  | Variable | Purpose |
131
132
  |----------|---------|
132
133
  | `GOOGLE_OAUTH_CLIENT_ID` | OAuth client ID from Google Cloud |
@@ -136,8 +137,10 @@ A production-ready MCP server that integrates all major Google Workspace service
136
137
 
137
138
  Claude Desktop stores these securely in the OS keychain; set them once in the extension pane.
138
139
  </details>
139
- Screenshot here
140
140
 
141
+ <div align="center">
142
+ <video width="832" src="https://github.com/user-attachments/assets/83cca4b3-5e94-448b-acb3-6e3a27341d3a"></video>
143
+ </div>
141
144
  ---
142
145
 
143
146
  ### 2. Advanced / Cross-Platform Installation
@@ -308,8 +311,12 @@ After running the script, just restart Claude Desktop and you're ready to go.
308
311
  "mcpServers": {
309
312
  "google_workspace": {
310
313
  "command": "uv",
311
- "args": ["run", "main.py"],
312
- "cwd": "/path/to/google_workspace_mcp",
314
+ "args": [
315
+ "run",
316
+ "--directory",
317
+ "/path/to/repo/google_workspace_mcp",
318
+ "main.py"
319
+ ],
313
320
  "env": {
314
321
  "GOOGLE_OAUTH_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
315
322
  "GOOGLE_OAUTH_CLIENT_SECRET": "your-client-secret",
@@ -5,11 +5,11 @@
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
7
7
  [![PyPI](https://img.shields.io/pypi/v/workspace-mcp.svg)](https://pypi.org/project/workspace-mcp/)
8
- [![UV](https://img.shields.io/badge/Package%20Installer-UV-blueviolet)](https://github.com/astral-sh/uv)
8
+ [![PyPI Downloads](https://static.pepy.tech/badge/workspace-mcp/month)](https://pepy.tech/projects/workspace-mcp)
9
9
  [![Website](https://img.shields.io/badge/Website-workspacemcp.com-green.svg)](https://workspacemcp.com)
10
10
  [![Verified on MseeP](https://mseep.ai/badge.svg)](https://mseep.ai/app/eebbc4a6-0f8c-41b2-ace8-038e5516dba0)
11
11
 
12
- **This is the single most feature-complete Google Workspace MCP server**
12
+ **This is the single most feature-complete Google Workspace MCP server** now with 1-click Claude installation
13
13
 
14
14
  *Full natural language control over Google Calendar, Drive, Gmail, Docs, Sheets, Slides, Forms, Tasks, and Chat through all MCP clients, AI assistants and developer tools.*
15
15
 
@@ -80,11 +80,12 @@ A production-ready MCP server that integrates all major Google Workspace service
80
80
 
81
81
  >
82
82
  **Why DXT?**
83
- > Desktop Extensions (`.dxt`) bundle the server, dependencies, and manifest so users go from download → working MCP in **three clicks** – no terminal, no JSON editing, no version conflicts.
83
+ > Desktop Extensions (`.dxt`) bundle the server, dependencies, and manifest so users go from download → working MCP in **one click** – no terminal, no JSON editing, no version conflicts.
84
84
 
85
85
  #### Required Configuration
86
86
  <details>
87
87
  <summary>Environment - you will configure these in Claude itself, see screenshot:</summary>
88
+
88
89
  | Variable | Purpose |
89
90
  |----------|---------|
90
91
  | `GOOGLE_OAUTH_CLIENT_ID` | OAuth client ID from Google Cloud |
@@ -94,8 +95,10 @@ A production-ready MCP server that integrates all major Google Workspace service
94
95
 
95
96
  Claude Desktop stores these securely in the OS keychain; set them once in the extension pane.
96
97
  </details>
97
- Screenshot here
98
98
 
99
+ <div align="center">
100
+ <video width="832" src="https://github.com/user-attachments/assets/83cca4b3-5e94-448b-acb3-6e3a27341d3a"></video>
101
+ </div>
99
102
  ---
100
103
 
101
104
  ### 2. Advanced / Cross-Platform Installation
@@ -266,8 +269,12 @@ After running the script, just restart Claude Desktop and you're ready to go.
266
269
  "mcpServers": {
267
270
  "google_workspace": {
268
271
  "command": "uv",
269
- "args": ["run", "main.py"],
270
- "cwd": "/path/to/google_workspace_mcp",
272
+ "args": [
273
+ "run",
274
+ "--directory",
275
+ "/path/to/repo/google_workspace_mcp",
276
+ "main.py"
277
+ ],
271
278
  "env": {
272
279
  "GOOGLE_OAUTH_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
273
280
  "GOOGLE_OAUTH_CLIENT_SECRET": "your-client-secret",
@@ -0,0 +1,296 @@
1
+ import io
2
+ import logging
3
+ import os
4
+ import tempfile
5
+ import zipfile, xml.etree.ElementTree as ET
6
+ import ssl
7
+ import time
8
+ import asyncio
9
+ import functools
10
+
11
+ from typing import List, Optional
12
+
13
+ from googleapiclient.errors import HttpError
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class TransientNetworkError(Exception):
19
+ """Custom exception for transient network errors after retries."""
20
+
21
+ pass
22
+
23
+
24
+ def check_credentials_directory_permissions(credentials_dir: str = None) -> None:
25
+ """
26
+ Check if the service has appropriate permissions to create and write to the .credentials directory.
27
+
28
+ Args:
29
+ credentials_dir: Path to the credentials directory (default: uses get_default_credentials_dir())
30
+
31
+ Raises:
32
+ PermissionError: If the service lacks necessary permissions
33
+ OSError: If there are other file system issues
34
+ """
35
+ if credentials_dir is None:
36
+ from auth.google_auth import get_default_credentials_dir
37
+
38
+ credentials_dir = get_default_credentials_dir()
39
+
40
+ try:
41
+ # Check if directory exists
42
+ if os.path.exists(credentials_dir):
43
+ # Directory exists, check if we can write to it
44
+ test_file = os.path.join(credentials_dir, ".permission_test")
45
+ try:
46
+ with open(test_file, "w") as f:
47
+ f.write("test")
48
+ os.remove(test_file)
49
+ logger.info(
50
+ f"Credentials directory permissions check passed: {os.path.abspath(credentials_dir)}"
51
+ )
52
+ except (PermissionError, OSError) as e:
53
+ raise PermissionError(
54
+ f"Cannot write to existing credentials directory '{os.path.abspath(credentials_dir)}': {e}"
55
+ )
56
+ else:
57
+ # Directory doesn't exist, try to create it and its parent directories
58
+ try:
59
+ os.makedirs(credentials_dir, exist_ok=True)
60
+ # Test writing to the new directory
61
+ test_file = os.path.join(credentials_dir, ".permission_test")
62
+ with open(test_file, "w") as f:
63
+ f.write("test")
64
+ os.remove(test_file)
65
+ logger.info(
66
+ f"Created credentials directory with proper permissions: {os.path.abspath(credentials_dir)}"
67
+ )
68
+ except (PermissionError, OSError) as e:
69
+ # Clean up if we created the directory but can't write to it
70
+ try:
71
+ if os.path.exists(credentials_dir):
72
+ os.rmdir(credentials_dir)
73
+ except:
74
+ pass
75
+ raise PermissionError(
76
+ f"Cannot create or write to credentials directory '{os.path.abspath(credentials_dir)}': {e}"
77
+ )
78
+
79
+ except PermissionError:
80
+ raise
81
+ except Exception as e:
82
+ raise OSError(
83
+ f"Unexpected error checking credentials directory permissions: {e}"
84
+ )
85
+
86
+
87
+ def extract_office_xml_text(file_bytes: bytes, mime_type: str) -> Optional[str]:
88
+ """
89
+ Very light-weight XML scraper for Word, Excel, PowerPoint files.
90
+ Returns plain-text if something readable is found, else None.
91
+ No external deps – just std-lib zipfile + ElementTree.
92
+ """
93
+ shared_strings: List[str] = []
94
+ ns_excel_main = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
95
+
96
+ try:
97
+ with zipfile.ZipFile(io.BytesIO(file_bytes)) as zf:
98
+ targets: List[str] = []
99
+ # Map MIME → iterable of XML files to inspect
100
+ if (
101
+ mime_type
102
+ == "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
103
+ ):
104
+ targets = ["word/document.xml"]
105
+ elif (
106
+ mime_type
107
+ == "application/vnd.openxmlformats-officedocument.presentationml.presentation"
108
+ ):
109
+ targets = [n for n in zf.namelist() if n.startswith("ppt/slides/slide")]
110
+ elif (
111
+ mime_type
112
+ == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
113
+ ):
114
+ targets = [
115
+ n
116
+ for n in zf.namelist()
117
+ if n.startswith("xl/worksheets/sheet") and "drawing" not in n
118
+ ]
119
+ # Attempt to parse sharedStrings.xml for Excel files
120
+ try:
121
+ shared_strings_xml = zf.read("xl/sharedStrings.xml")
122
+ shared_strings_root = ET.fromstring(shared_strings_xml)
123
+ for si_element in shared_strings_root.findall(
124
+ f"{{{ns_excel_main}}}si"
125
+ ):
126
+ text_parts = []
127
+ # Find all <t> elements, simple or within <r> runs, and concatenate their text
128
+ for t_element in si_element.findall(f".//{{{ns_excel_main}}}t"):
129
+ if t_element.text:
130
+ text_parts.append(t_element.text)
131
+ shared_strings.append("".join(text_parts))
132
+ except KeyError:
133
+ logger.info(
134
+ "No sharedStrings.xml found in Excel file (this is optional)."
135
+ )
136
+ except ET.ParseError as e:
137
+ logger.error(f"Error parsing sharedStrings.xml: {e}")
138
+ except (
139
+ Exception
140
+ ) as e: # Catch any other unexpected error during sharedStrings parsing
141
+ logger.error(
142
+ f"Unexpected error processing sharedStrings.xml: {e}",
143
+ exc_info=True,
144
+ )
145
+ else:
146
+ return None
147
+
148
+ pieces: List[str] = []
149
+ for member in targets:
150
+ try:
151
+ xml_content = zf.read(member)
152
+ xml_root = ET.fromstring(xml_content)
153
+ member_texts: List[str] = []
154
+
155
+ if (
156
+ mime_type
157
+ == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
158
+ ):
159
+ for cell_element in xml_root.findall(
160
+ f".//{{{ns_excel_main}}}c"
161
+ ): # Find all <c> elements
162
+ value_element = cell_element.find(
163
+ f"{{{ns_excel_main}}}v"
164
+ ) # Find <v> under <c>
165
+
166
+ # Skip if cell has no value element or value element has no text
167
+ if value_element is None or value_element.text is None:
168
+ continue
169
+
170
+ cell_type = cell_element.get("t")
171
+ if cell_type == "s": # Shared string
172
+ try:
173
+ ss_idx = int(value_element.text)
174
+ if 0 <= ss_idx < len(shared_strings):
175
+ member_texts.append(shared_strings[ss_idx])
176
+ else:
177
+ logger.warning(
178
+ f"Invalid shared string index {ss_idx} in {member}. Max index: {len(shared_strings)-1}"
179
+ )
180
+ except ValueError:
181
+ logger.warning(
182
+ f"Non-integer shared string index: '{value_element.text}' in {member}."
183
+ )
184
+ else: # Direct value (number, boolean, inline string if not 's')
185
+ member_texts.append(value_element.text)
186
+ else: # Word or PowerPoint
187
+ for elem in xml_root.iter():
188
+ # For Word: <w:t> where w is "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
189
+ # For PowerPoint: <a:t> where a is "http://schemas.openxmlformats.org/drawingml/2006/main"
190
+ if (
191
+ elem.tag.endswith("}t") and elem.text
192
+ ): # Check for any namespaced tag ending with 't'
193
+ cleaned_text = elem.text.strip()
194
+ if (
195
+ cleaned_text
196
+ ): # Add only if there's non-whitespace text
197
+ member_texts.append(cleaned_text)
198
+
199
+ if member_texts:
200
+ pieces.append(
201
+ " ".join(member_texts)
202
+ ) # Join texts from one member with spaces
203
+
204
+ except ET.ParseError as e:
205
+ logger.warning(
206
+ f"Could not parse XML in member '{member}' for {mime_type} file: {e}"
207
+ )
208
+ except Exception as e:
209
+ logger.error(
210
+ f"Error processing member '{member}' for {mime_type}: {e}",
211
+ exc_info=True,
212
+ )
213
+ # continue processing other members
214
+
215
+ if not pieces: # If no text was extracted at all
216
+ return None
217
+
218
+ # Join content from different members (sheets/slides) with double newlines for separation
219
+ text = "\n\n".join(pieces).strip()
220
+ return text or None # Ensure None is returned if text is empty after strip
221
+
222
+ except zipfile.BadZipFile:
223
+ logger.warning(f"File is not a valid ZIP archive (mime_type: {mime_type}).")
224
+ return None
225
+ except (
226
+ ET.ParseError
227
+ ) as e: # Catch parsing errors at the top level if zipfile itself is XML-like
228
+ logger.error(f"XML parsing error at a high level for {mime_type}: {e}")
229
+ return None
230
+ except Exception as e:
231
+ logger.error(
232
+ f"Failed to extract office XML text for {mime_type}: {e}", exc_info=True
233
+ )
234
+ return None
235
+
236
+
237
+ def handle_http_errors(tool_name: str, is_read_only: bool = False):
238
+ """
239
+ A decorator to handle Google API HttpErrors and transient SSL errors in a standardized way.
240
+
241
+ It wraps a tool function, catches HttpError, logs a detailed error message,
242
+ and raises a generic Exception with a user-friendly message.
243
+
244
+ If is_read_only is True, it will also catch ssl.SSLError and retry with
245
+ exponential backoff. After exhausting retries, it raises a TransientNetworkError.
246
+
247
+ Args:
248
+ tool_name (str): The name of the tool being decorated (e.g., 'list_calendars').
249
+ is_read_only (bool): If True, the operation is considered safe to retry on
250
+ transient network errors. Defaults to False.
251
+ """
252
+
253
+ def decorator(func):
254
+ @functools.wraps(func)
255
+ async def wrapper(*args, **kwargs):
256
+ max_retries = 3
257
+ base_delay = 1
258
+
259
+ for attempt in range(max_retries):
260
+ try:
261
+ return await func(*args, **kwargs)
262
+ except ssl.SSLError as e:
263
+ if is_read_only and attempt < max_retries - 1:
264
+ delay = base_delay * (2**attempt)
265
+ logger.warning(
266
+ f"SSL error in {tool_name} on attempt {attempt + 1}: {e}. Retrying in {delay} seconds..."
267
+ )
268
+ await asyncio.sleep(delay)
269
+ else:
270
+ logger.error(
271
+ f"SSL error in {tool_name} on final attempt: {e}. Raising exception."
272
+ )
273
+ raise TransientNetworkError(
274
+ f"A transient SSL error occurred in '{tool_name}' after {max_retries} attempts. "
275
+ "This is likely a temporary network or certificate issue. Please try again shortly."
276
+ ) from e
277
+ except HttpError as error:
278
+ user_google_email = kwargs.get("user_google_email", "N/A")
279
+ message = (
280
+ f"API error in {tool_name}: {error}. "
281
+ f"You might need to re-authenticate for user '{user_google_email}'. "
282
+ f"LLM: Try 'start_google_auth' with the user's email and the appropriate service_name."
283
+ )
284
+ logger.error(message, exc_info=True)
285
+ raise Exception(message) from error
286
+ except TransientNetworkError:
287
+ # Re-raise without wrapping to preserve the specific error type
288
+ raise
289
+ except Exception as e:
290
+ message = f"An unexpected error occurred in {tool_name}: {e}"
291
+ logger.exception(message)
292
+ raise Exception(message) from e
293
+
294
+ return wrapper
295
+
296
+ return decorator
@@ -13,12 +13,14 @@ from googleapiclient.errors import HttpError
13
13
 
14
14
  from auth.service_decorator import require_google_service
15
15
  from core.server import server
16
+ from core.utils import handle_http_errors
16
17
 
17
18
  logger = logging.getLogger(__name__)
18
19
 
19
20
 
20
21
  @server.tool()
21
22
  @require_google_service("tasks", "tasks_read")
23
+ @handle_http_errors("list_task_lists")
22
24
  async def list_task_lists(
23
25
  service,
24
26
  user_google_email: str,
@@ -78,6 +80,7 @@ async def list_task_lists(
78
80
 
79
81
  @server.tool()
80
82
  @require_google_service("tasks", "tasks_read")
83
+ @handle_http_errors("get_task_list")
81
84
  async def get_task_list(
82
85
  service,
83
86
  user_google_email: str,
@@ -121,6 +124,7 @@ async def get_task_list(
121
124
 
122
125
  @server.tool()
123
126
  @require_google_service("tasks", "tasks")
127
+ @handle_http_errors("create_task_list")
124
128
  async def create_task_list(
125
129
  service,
126
130
  user_google_email: str,
@@ -168,6 +172,7 @@ async def create_task_list(
168
172
 
169
173
  @server.tool()
170
174
  @require_google_service("tasks", "tasks")
175
+ @handle_http_errors("update_task_list")
171
176
  async def update_task_list(
172
177
  service,
173
178
  user_google_email: str,
@@ -217,6 +222,7 @@ async def update_task_list(
217
222
 
218
223
  @server.tool()
219
224
  @require_google_service("tasks", "tasks")
225
+ @handle_http_errors("delete_task_list")
220
226
  async def delete_task_list(
221
227
  service,
222
228
  user_google_email: str,
@@ -256,6 +262,7 @@ async def delete_task_list(
256
262
 
257
263
  @server.tool()
258
264
  @require_google_service("tasks", "tasks_read")
265
+ @handle_http_errors("list_tasks")
259
266
  async def list_tasks(
260
267
  service,
261
268
  user_google_email: str,
@@ -361,6 +368,7 @@ async def list_tasks(
361
368
 
362
369
  @server.tool()
363
370
  @require_google_service("tasks", "tasks_read")
371
+ @handle_http_errors("get_task")
364
372
  async def get_task(
365
373
  service,
366
374
  user_google_email: str,
@@ -421,6 +429,7 @@ async def get_task(
421
429
 
422
430
  @server.tool()
423
431
  @require_google_service("tasks", "tasks")
432
+ @handle_http_errors("create_task")
424
433
  async def create_task(
425
434
  service,
426
435
  user_google_email: str,
@@ -495,6 +504,7 @@ async def create_task(
495
504
 
496
505
  @server.tool()
497
506
  @require_google_service("tasks", "tasks")
507
+ @handle_http_errors("update_task")
498
508
  async def update_task(
499
509
  service,
500
510
  user_google_email: str,
@@ -576,6 +586,7 @@ async def update_task(
576
586
 
577
587
  @server.tool()
578
588
  @require_google_service("tasks", "tasks")
589
+ @handle_http_errors("delete_task")
579
590
  async def delete_task(
580
591
  service,
581
592
  user_google_email: str,
@@ -617,6 +628,7 @@ async def delete_task(
617
628
 
618
629
  @server.tool()
619
630
  @require_google_service("tasks", "tasks")
631
+ @handle_http_errors("move_task")
620
632
  async def move_task(
621
633
  service,
622
634
  user_google_email: str,
@@ -695,6 +707,7 @@ async def move_task(
695
707
 
696
708
  @server.tool()
697
709
  @require_google_service("tasks", "tasks")
710
+ @handle_http_errors("clear_completed_tasks")
698
711
  async def clear_completed_tasks(
699
712
  service,
700
713
  user_google_email: str,
@@ -80,8 +80,8 @@ def _correct_time_format_for_api(
80
80
 
81
81
 
82
82
  @server.tool()
83
+ @handle_http_errors("list_calendars", is_read_only=True)
83
84
  @require_google_service("calendar", "calendar_read")
84
- @handle_http_errors("list_calendars")
85
85
  async def list_calendars(service, user_google_email: str) -> str:
86
86
  """
87
87
  Retrieves a list of calendars accessible to the authenticated user.
@@ -114,8 +114,8 @@ async def list_calendars(service, user_google_email: str) -> str:
114
114
 
115
115
 
116
116
  @server.tool()
117
+ @handle_http_errors("get_events", is_read_only=True)
117
118
  @require_google_service("calendar", "calendar_read")
118
- @handle_http_errors("get_events")
119
119
  async def get_events(
120
120
  service,
121
121
  user_google_email: str,
@@ -202,8 +202,8 @@ async def get_events(
202
202
 
203
203
 
204
204
  @server.tool()
205
- @require_google_service("calendar", "calendar_events")
206
205
  @handle_http_errors("create_event")
206
+ @require_google_service("calendar", "calendar_events")
207
207
  async def create_event(
208
208
  service,
209
209
  user_google_email: str,
@@ -326,8 +326,8 @@ async def create_event(
326
326
 
327
327
 
328
328
  @server.tool()
329
- @require_google_service("calendar", "calendar_events")
330
329
  @handle_http_errors("modify_event")
330
+ @require_google_service("calendar", "calendar_events")
331
331
  async def modify_event(
332
332
  service,
333
333
  user_google_email: str,
@@ -446,8 +446,8 @@ async def modify_event(
446
446
 
447
447
 
448
448
  @server.tool()
449
- @require_google_service("calendar", "calendar_events")
450
449
  @handle_http_errors("delete_event")
450
+ @require_google_service("calendar", "calendar_events")
451
451
  async def delete_event(service, user_google_email: str, event_id: str, calendar_id: str = "primary") -> str:
452
452
  """
453
453
  Deletes an existing event.
@@ -500,8 +500,8 @@ async def delete_event(service, user_google_email: str, event_id: str, calendar_
500
500
 
501
501
 
502
502
  @server.tool()
503
+ @handle_http_errors("get_event", is_read_only=True)
503
504
  @require_google_service("calendar", "calendar_read")
504
- @handle_http_errors("get_event")
505
505
  async def get_event(
506
506
  service,
507
507
  user_google_email: str,
@@ -20,8 +20,8 @@ from core.comments import create_comment_tools
20
20
  logger = logging.getLogger(__name__)
21
21
 
22
22
  @server.tool()
23
+ @handle_http_errors("search_docs", is_read_only=True)
23
24
  @require_google_service("drive", "drive_read")
24
- @handle_http_errors("search_docs")
25
25
  async def search_docs(
26
26
  service,
27
27
  user_google_email: str,
@@ -57,11 +57,11 @@ async def search_docs(
57
57
  return "\n".join(output)
58
58
 
59
59
  @server.tool()
60
+ @handle_http_errors("get_doc_content", is_read_only=True)
60
61
  @require_multiple_services([
61
62
  {"service_type": "drive", "scopes": "drive_read", "param_name": "drive_service"},
62
63
  {"service_type": "docs", "scopes": "docs_read", "param_name": "docs_service"}
63
64
  ])
64
- @handle_http_errors("get_doc_content")
65
65
  async def get_doc_content(
66
66
  drive_service,
67
67
  docs_service,
@@ -157,8 +157,8 @@ async def get_doc_content(
157
157
  return header + body_text
158
158
 
159
159
  @server.tool()
160
+ @handle_http_errors("list_docs_in_folder", is_read_only=True)
160
161
  @require_google_service("drive", "drive_read")
161
- @handle_http_errors("list_docs_in_folder")
162
162
  async def list_docs_in_folder(
163
163
  service,
164
164
  user_google_email: str,
@@ -189,8 +189,8 @@ async def list_docs_in_folder(
189
189
  return "\n".join(out)
190
190
 
191
191
  @server.tool()
192
- @require_google_service("docs", "docs_write")
193
192
  @handle_http_errors("create_doc")
193
+ @require_google_service("docs", "docs_write")
194
194
  async def create_doc(
195
195
  service,
196
196
  user_google_email: str,
@@ -76,8 +76,8 @@ def _build_drive_list_params(
76
76
  return list_params
77
77
 
78
78
  @server.tool()
79
+ @handle_http_errors("search_drive_files", is_read_only=True)
79
80
  @require_google_service("drive", "drive_read")
80
- @handle_http_errors("search_drive_files")
81
81
  async def search_drive_files(
82
82
  service,
83
83
  user_google_email: str,
@@ -143,8 +143,8 @@ async def search_drive_files(
143
143
  return text_output
144
144
 
145
145
  @server.tool()
146
+ @handle_http_errors("get_drive_file_content", is_read_only=True)
146
147
  @require_google_service("drive", "drive_read")
147
- @handle_http_errors("get_drive_file_content")
148
148
  async def get_drive_file_content(
149
149
  service,
150
150
  user_google_email: str,
@@ -200,7 +200,7 @@ async def get_drive_file_content(
200
200
  "application/vnd.openxmlformats-officedocument.presentationml.presentation",
201
201
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
202
202
  }
203
-
203
+
204
204
  if mime_type in office_mime_types:
205
205
  office_text = extract_office_xml_text(file_content_bytes, mime_type)
206
206
  if office_text:
@@ -233,8 +233,8 @@ async def get_drive_file_content(
233
233
 
234
234
 
235
235
  @server.tool()
236
+ @handle_http_errors("list_drive_items", is_read_only=True)
236
237
  @require_google_service("drive", "drive_read")
237
- @handle_http_errors("list_drive_items")
238
238
  async def list_drive_items(
239
239
  service,
240
240
  user_google_email: str,
@@ -289,8 +289,8 @@ async def list_drive_items(
289
289
  return text_output
290
290
 
291
291
  @server.tool()
292
- @require_google_service("drive", "drive_file")
293
292
  @handle_http_errors("create_drive_file")
293
+ @require_google_service("drive", "drive_file")
294
294
  async def create_drive_file(
295
295
  service,
296
296
  user_google_email: str,