google-workspace-mcp 1.1.5__py3-none-any.whl → 1.2.0__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.
- google_workspace_mcp/__main__.py +5 -6
- google_workspace_mcp/models.py +486 -0
- google_workspace_mcp/services/calendar.py +14 -4
- google_workspace_mcp/services/drive.py +237 -14
- google_workspace_mcp/services/sheets_service.py +273 -35
- google_workspace_mcp/services/slides.py +42 -1829
- google_workspace_mcp/tools/calendar.py +116 -100
- google_workspace_mcp/tools/docs_tools.py +99 -57
- google_workspace_mcp/tools/drive.py +112 -92
- google_workspace_mcp/tools/gmail.py +131 -66
- google_workspace_mcp/tools/sheets_tools.py +137 -64
- google_workspace_mcp/tools/slides.py +295 -743
- {google_workspace_mcp-1.1.5.dist-info → google_workspace_mcp-1.2.0.dist-info}/METADATA +3 -2
- {google_workspace_mcp-1.1.5.dist-info → google_workspace_mcp-1.2.0.dist-info}/RECORD +16 -17
- google_workspace_mcp/tools/add_image.py +0 -1781
- google_workspace_mcp/utils/unit_conversion.py +0 -201
- {google_workspace_mcp-1.1.5.dist-info → google_workspace_mcp-1.2.0.dist-info}/WHEEL +0 -0
- {google_workspace_mcp-1.1.5.dist-info → google_workspace_mcp-1.2.0.dist-info}/entry_points.txt +0 -0
@@ -6,6 +6,16 @@ import logging
|
|
6
6
|
from typing import Any
|
7
7
|
|
8
8
|
from google_workspace_mcp.app import mcp # Import from central app module
|
9
|
+
from google_workspace_mcp.models import (
|
10
|
+
DriveFileContentOutput,
|
11
|
+
DriveFileDeletionOutput,
|
12
|
+
DriveFileUploadOutput,
|
13
|
+
DriveFolderCreationOutput,
|
14
|
+
DriveFolderFindOutput,
|
15
|
+
DriveFolderSearchOutput,
|
16
|
+
DriveSearchOutput,
|
17
|
+
DriveSharedDrivesOutput,
|
18
|
+
)
|
9
19
|
from google_workspace_mcp.services.drive import DriveService
|
10
20
|
|
11
21
|
logger = logging.getLogger(__name__)
|
@@ -16,82 +26,74 @@ logger = logging.getLogger(__name__)
|
|
16
26
|
|
17
27
|
@mcp.tool(
|
18
28
|
name="drive_search_files",
|
29
|
+
description="""Search for files in Google Drive with intelligent query handling.
|
30
|
+
|
31
|
+
QUERY FORMATS SUPPORTED:
|
32
|
+
1. Simple text searches (recommended): "sprint planning meeting notes" - searches content and filenames
|
33
|
+
2. Drive API syntax: name contains 'project' AND modifiedTime > '2024-01-01'
|
34
|
+
|
35
|
+
CRITICAL LIMITATIONS:
|
36
|
+
- Parentheses ( ) are NOT supported in file searches
|
37
|
+
- Mixed syntax (text + operators) is not allowed
|
38
|
+
- For OR logic with multiple terms, use separate tool calls
|
39
|
+
|
40
|
+
VALID EXAMPLES:
|
41
|
+
✅ "project documents 2024" (simple text)
|
42
|
+
✅ name contains 'sprint' (API syntax)
|
43
|
+
✅ fullText contains 'meeting' AND modifiedTime > '2024-01-01' (combined API syntax)
|
44
|
+
✅ mimeType = 'application/pdf' (file type filter)
|
45
|
+
|
46
|
+
INVALID EXAMPLES (will cause errors):
|
47
|
+
❌ "project (sprint OR planning)" - parentheses not supported
|
48
|
+
❌ "ArcLio sprint OR planning" - mixed text and operators
|
49
|
+
❌ "meeting modifiedTime > '2024-01-01'" - mixed syntax
|
50
|
+
|
51
|
+
COMMON FILE TYPE FILTERS:
|
52
|
+
- Google Docs: mimeType = 'application/vnd.google-apps.document'
|
53
|
+
- PDFs: mimeType = 'application/pdf'
|
54
|
+
- Images: mimeType contains 'image/'
|
55
|
+
|
56
|
+
DATE FORMAT: Use RFC 3339 format like '2024-01-01' for date searches.""",
|
19
57
|
)
|
20
58
|
async def drive_search_files(
|
21
59
|
query: str,
|
22
60
|
page_size: int = 10,
|
23
61
|
shared_drive_id: str | None = None,
|
24
|
-
|
25
|
-
include_trashed: bool = False,
|
26
|
-
) -> dict[str, Any]:
|
62
|
+
) -> DriveSearchOutput:
|
27
63
|
"""
|
28
|
-
Search for files in Google Drive
|
29
|
-
|
30
|
-
|
31
|
-
Examples:
|
32
|
-
- "budget report" → works as-is
|
33
|
-
- "John's Documents" → automatically handled
|
64
|
+
Search for files in Google Drive.
|
34
65
|
|
35
66
|
Args:
|
36
|
-
query: Search query
|
37
|
-
|
38
|
-
|
39
|
-
shared_drive_id: Optional shared drive ID to search within a specific shared drive.
|
40
|
-
include_shared_drives: Whether to include shared drives and folders in search (default True).
|
41
|
-
Set to False to search only personal files.
|
42
|
-
include_trashed: Whether to include trashed files in search results (default False).
|
67
|
+
query: Search query - either simple text or valid Drive API syntax (no parentheses)
|
68
|
+
page_size: Maximum number of files to return (1 to 1000, default 10)
|
69
|
+
shared_drive_id: Optional shared drive ID to search within specific shared drive
|
43
70
|
|
44
71
|
Returns:
|
45
|
-
|
72
|
+
DriveSearchOutput containing a list of matching files with metadata
|
46
73
|
"""
|
47
74
|
logger.info(
|
48
|
-
f"Executing drive_search_files with query: '{query}', page_size: {page_size}, "
|
49
|
-
f"shared_drive_id: {shared_drive_id}, include_shared_drives: {include_shared_drives}, "
|
50
|
-
f"include_trashed: {include_trashed}"
|
75
|
+
f"Executing drive_search_files with query: '{query}', page_size: {page_size}, shared_drive_id: {shared_drive_id}"
|
51
76
|
)
|
52
77
|
|
53
78
|
if not query or not query.strip():
|
54
79
|
raise ValueError("Query cannot be empty")
|
55
80
|
|
56
|
-
# Logic to build a robust query
|
57
|
-
# If the query looks like a simple term (no spaces, no operators), wrap it.
|
58
|
-
# Otherwise, assume the user has provided a full query expression.
|
59
|
-
clean_query = query.strip()
|
60
|
-
if (
|
61
|
-
" " not in clean_query
|
62
|
-
and ":" not in clean_query
|
63
|
-
and "=" not in clean_query
|
64
|
-
and ">" not in clean_query
|
65
|
-
and "<" not in clean_query
|
66
|
-
):
|
67
|
-
# This is likely a simple term, wrap it for a full-text search.
|
68
|
-
final_query = f"fullText contains '{clean_query.replace("'", "\\'")}'"
|
69
|
-
else:
|
70
|
-
# Assume it's a complex query and use it as-is.
|
71
|
-
final_query = clean_query.replace("'", "\\'")
|
72
|
-
|
73
|
-
# Append the trashed filter
|
74
|
-
if not include_trashed:
|
75
|
-
final_query = f"{final_query} and trashed=false"
|
76
|
-
|
77
81
|
drive_service = DriveService()
|
78
82
|
files = drive_service.search_files(
|
79
|
-
query=
|
80
|
-
page_size=page_size,
|
81
|
-
shared_drive_id=shared_drive_id,
|
82
|
-
include_shared_drives=include_shared_drives,
|
83
|
+
query=query, page_size=page_size, shared_drive_id=shared_drive_id
|
83
84
|
)
|
84
85
|
|
85
86
|
if isinstance(files, dict) and files.get("error"):
|
86
87
|
raise ValueError(f"Search failed: {files.get('message', 'Unknown error')}")
|
87
88
|
|
88
|
-
return
|
89
|
+
return DriveSearchOutput(files=files or [])
|
89
90
|
|
90
91
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
92
|
+
@mcp.tool(
|
93
|
+
name="drive_read_file_content",
|
94
|
+
description="Read the content of a file from Google Drive.",
|
95
|
+
)
|
96
|
+
async def drive_read_file_content(file_id: str) -> DriveFileContentOutput:
|
95
97
|
"""
|
96
98
|
Read the content of a file from Google Drive.
|
97
99
|
|
@@ -99,7 +101,7 @@ async def drive_read_file_content(file_id: str) -> dict[str, Any]:
|
|
99
101
|
file_id: The ID of the file to read.
|
100
102
|
|
101
103
|
Returns:
|
102
|
-
|
104
|
+
DriveFileContentOutput containing the file content and metadata.
|
103
105
|
"""
|
104
106
|
logger.info(f"Executing drive_read_file_content tool with file_id: '{file_id}'")
|
105
107
|
if not file_id or not file_id.strip():
|
@@ -114,18 +116,24 @@ async def drive_read_file_content(file_id: str) -> dict[str, Any]:
|
|
114
116
|
if isinstance(result, dict) and result.get("error"):
|
115
117
|
raise ValueError(result.get("message", "Error reading file"))
|
116
118
|
|
117
|
-
return
|
119
|
+
return DriveFileContentOutput(
|
120
|
+
file_id=result["file_id"],
|
121
|
+
name=result["name"],
|
122
|
+
content=result["content"],
|
123
|
+
mime_type=result["mime_type"],
|
124
|
+
)
|
118
125
|
|
119
126
|
|
120
|
-
|
121
|
-
|
122
|
-
|
127
|
+
@mcp.tool(
|
128
|
+
name="drive_upload_file",
|
129
|
+
description="Uploads a file to Google Drive by providing its content directly.",
|
130
|
+
)
|
123
131
|
async def drive_upload_file(
|
124
132
|
filename: str,
|
125
133
|
content_base64: str,
|
126
134
|
parent_folder_id: str | None = None,
|
127
135
|
shared_drive_id: str | None = None,
|
128
|
-
) ->
|
136
|
+
) -> DriveFileUploadOutput:
|
129
137
|
"""
|
130
138
|
Uploads a file to Google Drive using its base64 encoded content.
|
131
139
|
|
@@ -136,7 +144,7 @@ async def drive_upload_file(
|
|
136
144
|
shared_drive_id: Optional shared drive ID to upload the file to a shared drive.
|
137
145
|
|
138
146
|
Returns:
|
139
|
-
|
147
|
+
DriveFileUploadOutput containing the uploaded file metadata.
|
140
148
|
"""
|
141
149
|
logger.info(
|
142
150
|
f"Executing drive_upload_file with filename: '{filename}', parent_folder_id: {parent_folder_id}, shared_drive_id: {shared_drive_id}"
|
@@ -157,17 +165,23 @@ async def drive_upload_file(
|
|
157
165
|
if isinstance(result, dict) and result.get("error"):
|
158
166
|
raise ValueError(result.get("message", "Error uploading file"))
|
159
167
|
|
160
|
-
return
|
168
|
+
return DriveFileUploadOutput(
|
169
|
+
id=result["id"],
|
170
|
+
name=result["name"],
|
171
|
+
web_view_link=result["web_view_link"],
|
172
|
+
size=result["size"],
|
173
|
+
)
|
161
174
|
|
162
175
|
|
163
|
-
|
164
|
-
|
165
|
-
|
176
|
+
@mcp.tool(
|
177
|
+
name="drive_create_folder",
|
178
|
+
description="Create a new folder in Google Drive.",
|
179
|
+
)
|
166
180
|
async def drive_create_folder(
|
167
181
|
folder_name: str,
|
168
182
|
parent_folder_id: str | None = None,
|
169
183
|
shared_drive_id: str | None = None,
|
170
|
-
) ->
|
184
|
+
) -> DriveFolderCreationOutput:
|
171
185
|
"""
|
172
186
|
Create a new folder in Google Drive.
|
173
187
|
|
@@ -177,7 +191,7 @@ async def drive_create_folder(
|
|
177
191
|
shared_drive_id: Optional shared drive ID to create the folder in a shared drive.
|
178
192
|
|
179
193
|
Returns:
|
180
|
-
|
194
|
+
DriveFolderCreationOutput containing the created folder information.
|
181
195
|
"""
|
182
196
|
logger.info(
|
183
197
|
f"Executing drive_create_folder with folder_name: '{folder_name}', parent_folder_id: {parent_folder_id}, shared_drive_id: {shared_drive_id}"
|
@@ -198,15 +212,18 @@ async def drive_create_folder(
|
|
198
212
|
f"Folder creation failed: {result.get('message', 'Unknown error')}"
|
199
213
|
)
|
200
214
|
|
201
|
-
return
|
215
|
+
return DriveFolderCreationOutput(
|
216
|
+
id=result["id"], name=result["name"], web_view_link=result["web_view_link"]
|
217
|
+
)
|
202
218
|
|
203
219
|
|
204
|
-
|
205
|
-
|
206
|
-
|
220
|
+
@mcp.tool(
|
221
|
+
name="drive_delete_file",
|
222
|
+
description="Delete a file from Google Drive using its file ID.",
|
223
|
+
)
|
207
224
|
async def drive_delete_file(
|
208
225
|
file_id: str,
|
209
|
-
) ->
|
226
|
+
) -> DriveFileDeletionOutput:
|
210
227
|
"""
|
211
228
|
Delete a file from Google Drive.
|
212
229
|
|
@@ -214,7 +231,7 @@ async def drive_delete_file(
|
|
214
231
|
file_id: The ID of the file to delete.
|
215
232
|
|
216
233
|
Returns:
|
217
|
-
|
234
|
+
DriveFileDeletionOutput confirming the deletion.
|
218
235
|
"""
|
219
236
|
logger.info(f"Executing drive_delete_file with file_id: '{file_id}'")
|
220
237
|
if not file_id or not file_id.strip():
|
@@ -226,13 +243,18 @@ async def drive_delete_file(
|
|
226
243
|
if isinstance(result, dict) and result.get("error"):
|
227
244
|
raise ValueError(result.get("message", "Error deleting file"))
|
228
245
|
|
229
|
-
return
|
246
|
+
return DriveFileDeletionOutput(
|
247
|
+
success=result.get("success", True),
|
248
|
+
message=result.get("message", f"File '{file_id}' deleted successfully"),
|
249
|
+
file_id=file_id,
|
250
|
+
)
|
230
251
|
|
231
252
|
|
232
253
|
@mcp.tool(
|
233
254
|
name="drive_list_shared_drives",
|
255
|
+
description="Lists shared drives accessible by the user.",
|
234
256
|
)
|
235
|
-
async def drive_list_shared_drives(page_size: int = 100) ->
|
257
|
+
async def drive_list_shared_drives(page_size: int = 100) -> DriveSharedDrivesOutput:
|
236
258
|
"""
|
237
259
|
Lists shared drives (formerly Team Drives) that the user has access to.
|
238
260
|
|
@@ -240,8 +262,7 @@ async def drive_list_shared_drives(page_size: int = 100) -> dict[str, Any]:
|
|
240
262
|
page_size: Maximum number of shared drives to return (1 to 100, default 100).
|
241
263
|
|
242
264
|
Returns:
|
243
|
-
|
244
|
-
or an error message.
|
265
|
+
DriveSharedDrivesOutput containing a list of shared drives.
|
245
266
|
"""
|
246
267
|
logger.info(f"Executing drive_list_shared_drives tool with page_size: {page_size}")
|
247
268
|
|
@@ -252,9 +273,9 @@ async def drive_list_shared_drives(page_size: int = 100) -> dict[str, Any]:
|
|
252
273
|
raise ValueError(drives.get("message", "Error listing shared drives"))
|
253
274
|
|
254
275
|
if not drives:
|
255
|
-
|
276
|
+
drives = []
|
256
277
|
|
257
|
-
return
|
278
|
+
return DriveSharedDrivesOutput(count=len(drives), shared_drives=drives)
|
258
279
|
|
259
280
|
|
260
281
|
@mcp.tool(
|
@@ -264,7 +285,7 @@ async def drive_search_files_in_folder(
|
|
264
285
|
folder_id: str,
|
265
286
|
query: str = "",
|
266
287
|
page_size: int = 10,
|
267
|
-
) ->
|
288
|
+
) -> DriveFolderSearchOutput:
|
268
289
|
"""
|
269
290
|
Search for files or folders within a specific folder ID. Trashed files are excluded.
|
270
291
|
This works for both regular folders and Shared Drives (when using the Shared Drive's ID as the folder_id).
|
@@ -277,7 +298,7 @@ async def drive_search_files_in_folder(
|
|
277
298
|
page_size: Maximum number of files to return (1 to 1000, default 10).
|
278
299
|
|
279
300
|
Returns:
|
280
|
-
|
301
|
+
DriveFolderSearchOutput containing a list of files and folders.
|
281
302
|
"""
|
282
303
|
logger.info(
|
283
304
|
f"Executing drive_search_files_in_folder with folder_id: '{folder_id}', "
|
@@ -309,7 +330,7 @@ async def drive_search_files_in_folder(
|
|
309
330
|
f"Folder search failed: {files.get('message', 'Unknown error')}"
|
310
331
|
)
|
311
332
|
|
312
|
-
return
|
333
|
+
return DriveFolderSearchOutput(folder_id=folder_id, files=files or [])
|
313
334
|
|
314
335
|
|
315
336
|
# @mcp.tool(
|
@@ -358,7 +379,7 @@ async def drive_find_folder_by_name(
|
|
358
379
|
file_query: str = "",
|
359
380
|
page_size: int = 10,
|
360
381
|
shared_drive_id: str | None = None,
|
361
|
-
) ->
|
382
|
+
) -> DriveFolderFindOutput:
|
362
383
|
"""
|
363
384
|
Finds folders by name using a two-step search: first an exact match, then a partial match.
|
364
385
|
Automatically handles apostrophes in folder names and search queries. Trashed items are excluded.
|
@@ -375,7 +396,7 @@ async def drive_find_folder_by_name(
|
|
375
396
|
shared_drive_id: Optional shared drive ID to search within a specific shared drive.
|
376
397
|
|
377
398
|
Returns:
|
378
|
-
|
399
|
+
DriveFolderFindOutput containing folders found and optionally file search results.
|
379
400
|
"""
|
380
401
|
logger.info(
|
381
402
|
f"Executing drive_find_folder_by_name with folder_name: '{folder_name}', "
|
@@ -417,17 +438,17 @@ async def drive_find_folder_by_name(
|
|
417
438
|
f"Folder search failed: {folders.get('message', 'Unknown error')}"
|
418
439
|
)
|
419
440
|
|
420
|
-
result =
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
441
|
+
result = DriveFolderFindOutput(
|
442
|
+
folder_name=folder_name,
|
443
|
+
folders_found=folders or [],
|
444
|
+
folder_count=len(folders) if folders else 0,
|
445
|
+
)
|
425
446
|
|
426
447
|
if not include_files:
|
427
448
|
return result
|
428
449
|
|
429
450
|
if not folders:
|
430
|
-
result
|
451
|
+
result.message = f"No folders found with name matching '{folder_name}'"
|
431
452
|
return result
|
432
453
|
|
433
454
|
target_folder = folders[0]
|
@@ -444,9 +465,8 @@ async def drive_find_folder_by_name(
|
|
444
465
|
and ":" not in clean_file_query
|
445
466
|
and "=" not in clean_file_query
|
446
467
|
):
|
447
|
-
|
448
|
-
|
449
|
-
)
|
468
|
+
escaped_file_query = clean_file_query.replace("'", "\\'")
|
469
|
+
wrapped_file_query = f"fullText contains '{escaped_file_query}'"
|
450
470
|
else:
|
451
471
|
wrapped_file_query = clean_file_query.replace("'", "\\'")
|
452
472
|
combined_query = f"{wrapped_file_query} and {folder_constraint}"
|
@@ -462,8 +482,8 @@ async def drive_find_folder_by_name(
|
|
462
482
|
f"File search in folder failed: {files.get('message', 'Unknown error')}"
|
463
483
|
)
|
464
484
|
|
465
|
-
result
|
466
|
-
result
|
467
|
-
result
|
485
|
+
result.target_folder = target_folder
|
486
|
+
result.files = files or []
|
487
|
+
result.file_count = len(files) if files else 0
|
468
488
|
|
469
489
|
return result
|