google-workspace-mcp 1.0.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/models.py +486 -0
- google_workspace_mcp/services/calendar.py +14 -4
- google_workspace_mcp/services/drive.py +268 -18
- google_workspace_mcp/services/sheets_service.py +273 -35
- google_workspace_mcp/services/slides.py +242 -53
- google_workspace_mcp/tools/calendar.py +99 -88
- google_workspace_mcp/tools/docs_tools.py +67 -33
- google_workspace_mcp/tools/drive.py +288 -25
- google_workspace_mcp/tools/gmail.py +95 -39
- google_workspace_mcp/tools/sheets_tools.py +112 -46
- google_workspace_mcp/tools/slides.py +317 -46
- {google_workspace_mcp-1.0.5.dist-info → google_workspace_mcp-1.2.0.dist-info}/METADATA +4 -3
- {google_workspace_mcp-1.0.5.dist-info → google_workspace_mcp-1.2.0.dist-info}/RECORD +15 -14
- {google_workspace_mcp-1.0.5.dist-info → google_workspace_mcp-1.2.0.dist-info}/WHEEL +0 -0
- {google_workspace_mcp-1.0.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,23 +26,50 @@ logger = logging.getLogger(__name__)
|
|
16
26
|
|
17
27
|
@mcp.tool(
|
18
28
|
name="drive_search_files",
|
19
|
-
description="Search for files in Google Drive with
|
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.""",
|
20
57
|
)
|
21
58
|
async def drive_search_files(
|
22
59
|
query: str,
|
23
60
|
page_size: int = 10,
|
24
61
|
shared_drive_id: str | None = None,
|
25
|
-
) ->
|
62
|
+
) -> DriveSearchOutput:
|
26
63
|
"""
|
27
|
-
Search for files in Google Drive
|
64
|
+
Search for files in Google Drive.
|
28
65
|
|
29
66
|
Args:
|
30
|
-
query: Search query
|
31
|
-
page_size: Maximum number of files to return (1 to 1000, default 10)
|
32
|
-
shared_drive_id: Optional shared drive ID to search within
|
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
|
33
70
|
|
34
71
|
Returns:
|
35
|
-
|
72
|
+
DriveSearchOutput containing a list of matching files with metadata
|
36
73
|
"""
|
37
74
|
logger.info(
|
38
75
|
f"Executing drive_search_files with query: '{query}', page_size: {page_size}, shared_drive_id: {shared_drive_id}"
|
@@ -49,14 +86,14 @@ async def drive_search_files(
|
|
49
86
|
if isinstance(files, dict) and files.get("error"):
|
50
87
|
raise ValueError(f"Search failed: {files.get('message', 'Unknown error')}")
|
51
88
|
|
52
|
-
return
|
89
|
+
return DriveSearchOutput(files=files or [])
|
53
90
|
|
54
91
|
|
55
92
|
@mcp.tool(
|
56
93
|
name="drive_read_file_content",
|
57
94
|
description="Read the content of a file from Google Drive.",
|
58
95
|
)
|
59
|
-
async def drive_read_file_content(file_id: str) ->
|
96
|
+
async def drive_read_file_content(file_id: str) -> DriveFileContentOutput:
|
60
97
|
"""
|
61
98
|
Read the content of a file from Google Drive.
|
62
99
|
|
@@ -64,7 +101,7 @@ async def drive_read_file_content(file_id: str) -> dict[str, Any]:
|
|
64
101
|
file_id: The ID of the file to read.
|
65
102
|
|
66
103
|
Returns:
|
67
|
-
|
104
|
+
DriveFileContentOutput containing the file content and metadata.
|
68
105
|
"""
|
69
106
|
logger.info(f"Executing drive_read_file_content tool with file_id: '{file_id}'")
|
70
107
|
if not file_id or not file_id.strip():
|
@@ -79,7 +116,12 @@ async def drive_read_file_content(file_id: str) -> dict[str, Any]:
|
|
79
116
|
if isinstance(result, dict) and result.get("error"):
|
80
117
|
raise ValueError(result.get("message", "Error reading file"))
|
81
118
|
|
82
|
-
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
|
+
)
|
83
125
|
|
84
126
|
|
85
127
|
@mcp.tool(
|
@@ -91,7 +133,7 @@ async def drive_upload_file(
|
|
91
133
|
content_base64: str,
|
92
134
|
parent_folder_id: str | None = None,
|
93
135
|
shared_drive_id: str | None = None,
|
94
|
-
) ->
|
136
|
+
) -> DriveFileUploadOutput:
|
95
137
|
"""
|
96
138
|
Uploads a file to Google Drive using its base64 encoded content.
|
97
139
|
|
@@ -102,7 +144,7 @@ async def drive_upload_file(
|
|
102
144
|
shared_drive_id: Optional shared drive ID to upload the file to a shared drive.
|
103
145
|
|
104
146
|
Returns:
|
105
|
-
|
147
|
+
DriveFileUploadOutput containing the uploaded file metadata.
|
106
148
|
"""
|
107
149
|
logger.info(
|
108
150
|
f"Executing drive_upload_file with filename: '{filename}', parent_folder_id: {parent_folder_id}, shared_drive_id: {shared_drive_id}"
|
@@ -123,7 +165,12 @@ async def drive_upload_file(
|
|
123
165
|
if isinstance(result, dict) and result.get("error"):
|
124
166
|
raise ValueError(result.get("message", "Error uploading file"))
|
125
167
|
|
126
|
-
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
|
+
)
|
127
174
|
|
128
175
|
|
129
176
|
@mcp.tool(
|
@@ -134,7 +181,7 @@ async def drive_create_folder(
|
|
134
181
|
folder_name: str,
|
135
182
|
parent_folder_id: str | None = None,
|
136
183
|
shared_drive_id: str | None = None,
|
137
|
-
) ->
|
184
|
+
) -> DriveFolderCreationOutput:
|
138
185
|
"""
|
139
186
|
Create a new folder in Google Drive.
|
140
187
|
|
@@ -144,7 +191,7 @@ async def drive_create_folder(
|
|
144
191
|
shared_drive_id: Optional shared drive ID to create the folder in a shared drive.
|
145
192
|
|
146
193
|
Returns:
|
147
|
-
|
194
|
+
DriveFolderCreationOutput containing the created folder information.
|
148
195
|
"""
|
149
196
|
logger.info(
|
150
197
|
f"Executing drive_create_folder with folder_name: '{folder_name}', parent_folder_id: {parent_folder_id}, shared_drive_id: {shared_drive_id}"
|
@@ -165,7 +212,9 @@ async def drive_create_folder(
|
|
165
212
|
f"Folder creation failed: {result.get('message', 'Unknown error')}"
|
166
213
|
)
|
167
214
|
|
168
|
-
return
|
215
|
+
return DriveFolderCreationOutput(
|
216
|
+
id=result["id"], name=result["name"], web_view_link=result["web_view_link"]
|
217
|
+
)
|
169
218
|
|
170
219
|
|
171
220
|
@mcp.tool(
|
@@ -174,7 +223,7 @@ async def drive_create_folder(
|
|
174
223
|
)
|
175
224
|
async def drive_delete_file(
|
176
225
|
file_id: str,
|
177
|
-
) ->
|
226
|
+
) -> DriveFileDeletionOutput:
|
178
227
|
"""
|
179
228
|
Delete a file from Google Drive.
|
180
229
|
|
@@ -182,7 +231,7 @@ async def drive_delete_file(
|
|
182
231
|
file_id: The ID of the file to delete.
|
183
232
|
|
184
233
|
Returns:
|
185
|
-
|
234
|
+
DriveFileDeletionOutput confirming the deletion.
|
186
235
|
"""
|
187
236
|
logger.info(f"Executing drive_delete_file with file_id: '{file_id}'")
|
188
237
|
if not file_id or not file_id.strip():
|
@@ -194,14 +243,18 @@ async def drive_delete_file(
|
|
194
243
|
if isinstance(result, dict) and result.get("error"):
|
195
244
|
raise ValueError(result.get("message", "Error deleting file"))
|
196
245
|
|
197
|
-
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
|
+
)
|
198
251
|
|
199
252
|
|
200
253
|
@mcp.tool(
|
201
254
|
name="drive_list_shared_drives",
|
202
255
|
description="Lists shared drives accessible by the user.",
|
203
256
|
)
|
204
|
-
async def drive_list_shared_drives(page_size: int = 100) ->
|
257
|
+
async def drive_list_shared_drives(page_size: int = 100) -> DriveSharedDrivesOutput:
|
205
258
|
"""
|
206
259
|
Lists shared drives (formerly Team Drives) that the user has access to.
|
207
260
|
|
@@ -209,8 +262,7 @@ async def drive_list_shared_drives(page_size: int = 100) -> dict[str, Any]:
|
|
209
262
|
page_size: Maximum number of shared drives to return (1 to 100, default 100).
|
210
263
|
|
211
264
|
Returns:
|
212
|
-
|
213
|
-
or an error message.
|
265
|
+
DriveSharedDrivesOutput containing a list of shared drives.
|
214
266
|
"""
|
215
267
|
logger.info(f"Executing drive_list_shared_drives tool with page_size: {page_size}")
|
216
268
|
|
@@ -221,6 +273,217 @@ async def drive_list_shared_drives(page_size: int = 100) -> dict[str, Any]:
|
|
221
273
|
raise ValueError(drives.get("message", "Error listing shared drives"))
|
222
274
|
|
223
275
|
if not drives:
|
224
|
-
|
276
|
+
drives = []
|
277
|
+
|
278
|
+
return DriveSharedDrivesOutput(count=len(drives), shared_drives=drives)
|
279
|
+
|
280
|
+
|
281
|
+
@mcp.tool(
|
282
|
+
name="drive_search_files_in_folder",
|
283
|
+
)
|
284
|
+
async def drive_search_files_in_folder(
|
285
|
+
folder_id: str,
|
286
|
+
query: str = "",
|
287
|
+
page_size: int = 10,
|
288
|
+
) -> DriveFolderSearchOutput:
|
289
|
+
"""
|
290
|
+
Search for files or folders within a specific folder ID. Trashed files are excluded.
|
291
|
+
This works for both regular folders and Shared Drives (when using the Shared Drive's ID as the folder_id).
|
292
|
+
|
293
|
+
Args:
|
294
|
+
folder_id: The ID of the folder or Shared Drive to search within.
|
295
|
+
query: Optional search query string, following Google Drive API syntax.
|
296
|
+
If empty, returns all items.
|
297
|
+
Example to find only sub-folders: "mimeType = 'application/vnd.google-apps.folder'"
|
298
|
+
page_size: Maximum number of files to return (1 to 1000, default 10).
|
299
|
+
|
300
|
+
Returns:
|
301
|
+
DriveFolderSearchOutput containing a list of files and folders.
|
302
|
+
"""
|
303
|
+
logger.info(
|
304
|
+
f"Executing drive_search_files_in_folder with folder_id: '{folder_id}', "
|
305
|
+
f"query: '{query}', page_size: {page_size}"
|
306
|
+
)
|
307
|
+
|
308
|
+
if not folder_id or not folder_id.strip():
|
309
|
+
raise ValueError("Folder ID cannot be empty")
|
310
|
+
|
311
|
+
# Build the search query to search within the specific folder
|
312
|
+
folder_query = f"'{folder_id}' in parents and trashed=false"
|
313
|
+
if query and query.strip():
|
314
|
+
# Automatically escape apostrophes in user query
|
315
|
+
escaped_query = query.strip().replace("'", "\\'")
|
316
|
+
# Combine folder constraint with user query
|
317
|
+
combined_query = f"{escaped_query} and {folder_query}"
|
318
|
+
else:
|
319
|
+
combined_query = folder_query
|
320
|
+
|
321
|
+
drive_service = DriveService()
|
322
|
+
files = drive_service.search_files(
|
323
|
+
query=combined_query,
|
324
|
+
page_size=page_size,
|
325
|
+
include_shared_drives=True, # Always include shared drives for folder searches
|
326
|
+
)
|
327
|
+
|
328
|
+
if isinstance(files, dict) and files.get("error"):
|
329
|
+
raise ValueError(
|
330
|
+
f"Folder search failed: {files.get('message', 'Unknown error')}"
|
331
|
+
)
|
332
|
+
|
333
|
+
return DriveFolderSearchOutput(folder_id=folder_id, files=files or [])
|
334
|
+
|
335
|
+
|
336
|
+
# @mcp.tool(
|
337
|
+
# name="drive_get_folder_info",
|
338
|
+
# )
|
339
|
+
async def drive_get_folder_info(folder_id: str) -> dict[str, Any]:
|
340
|
+
"""
|
341
|
+
Get detailed information about a folder in Google Drive.
|
342
|
+
|
343
|
+
Useful for understanding folder permissions and hierarchy.
|
344
|
+
|
345
|
+
Args:
|
346
|
+
folder_id: The ID of the folder to get information about.
|
347
|
+
|
348
|
+
Returns:
|
349
|
+
A dictionary containing folder metadata or an error message.
|
350
|
+
"""
|
351
|
+
logger.info(f"Executing drive_get_folder_info with folder_id: '{folder_id}'")
|
352
|
+
|
353
|
+
if not folder_id or not folder_id.strip():
|
354
|
+
raise ValueError("Folder ID cannot be empty")
|
355
|
+
|
356
|
+
drive_service = DriveService()
|
357
|
+
folder_info = drive_service.get_file_metadata(file_id=folder_id)
|
358
|
+
|
359
|
+
if isinstance(folder_info, dict) and folder_info.get("error"):
|
360
|
+
raise ValueError(
|
361
|
+
f"Failed to get folder info: {folder_info.get('message', 'Unknown error')}"
|
362
|
+
)
|
363
|
+
|
364
|
+
# Verify it's actually a folder
|
365
|
+
if folder_info.get("mimeType") != "application/vnd.google-apps.folder":
|
366
|
+
raise ValueError(
|
367
|
+
f"ID '{folder_id}' is not a folder (mimeType: {folder_info.get('mimeType')})"
|
368
|
+
)
|
369
|
+
|
370
|
+
return folder_info
|
371
|
+
|
372
|
+
|
373
|
+
@mcp.tool(
|
374
|
+
name="drive_find_folder_by_name",
|
375
|
+
)
|
376
|
+
async def drive_find_folder_by_name(
|
377
|
+
folder_name: str,
|
378
|
+
include_files: bool = False,
|
379
|
+
file_query: str = "",
|
380
|
+
page_size: int = 10,
|
381
|
+
shared_drive_id: str | None = None,
|
382
|
+
) -> DriveFolderFindOutput:
|
383
|
+
"""
|
384
|
+
Finds folders by name using a two-step search: first an exact match, then a partial match.
|
385
|
+
Automatically handles apostrophes in folder names and search queries. Trashed items are excluded.
|
386
|
+
|
387
|
+
Crucial Note: This tool finds **regular folders** within "My Drive" or a Shared Drive.
|
388
|
+
It **does not** find Shared Drives themselves. To list available Shared Drives,
|
389
|
+
use the `drive_list_shared_drives` tool.
|
390
|
+
|
391
|
+
Args:
|
392
|
+
folder_name: The name of the folder to search for.
|
393
|
+
include_files: Whether to also search for files within the found folder (default False).
|
394
|
+
file_query: Optional search query for files within the folder. Only used if include_files=True.
|
395
|
+
page_size: Maximum number of files to return (1 to 1000, default 10).
|
396
|
+
shared_drive_id: Optional shared drive ID to search within a specific shared drive.
|
397
|
+
|
398
|
+
Returns:
|
399
|
+
DriveFolderFindOutput containing folders found and optionally file search results.
|
400
|
+
"""
|
401
|
+
logger.info(
|
402
|
+
f"Executing drive_find_folder_by_name with folder_name: '{folder_name}', "
|
403
|
+
f"include_files: {include_files}, file_query: '{file_query}', "
|
404
|
+
f"page_size: {page_size}, shared_drive_id: {shared_drive_id}"
|
405
|
+
)
|
406
|
+
|
407
|
+
if not folder_name or not folder_name.strip():
|
408
|
+
raise ValueError("Folder name cannot be empty")
|
225
409
|
|
226
|
-
|
410
|
+
drive_service = DriveService()
|
411
|
+
escaped_folder_name = folder_name.strip().replace("'", "\\'")
|
412
|
+
|
413
|
+
# --- Step 1: Attempt Exact Match ---
|
414
|
+
logger.info(f"Step 1: Searching for exact folder name: '{escaped_folder_name}'")
|
415
|
+
exact_query = f"name = '{escaped_folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false"
|
416
|
+
folders = drive_service.search_files(
|
417
|
+
query=exact_query,
|
418
|
+
page_size=5,
|
419
|
+
shared_drive_id=shared_drive_id,
|
420
|
+
include_shared_drives=True,
|
421
|
+
)
|
422
|
+
|
423
|
+
# If no exact match, fall back to partial match
|
424
|
+
if not folders:
|
425
|
+
logger.info(
|
426
|
+
f"No exact match found. Step 2: Searching for folder name containing '{escaped_folder_name}'"
|
427
|
+
)
|
428
|
+
contains_query = f"name contains '{escaped_folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false"
|
429
|
+
folders = drive_service.search_files(
|
430
|
+
query=contains_query,
|
431
|
+
page_size=5,
|
432
|
+
shared_drive_id=shared_drive_id,
|
433
|
+
include_shared_drives=True,
|
434
|
+
)
|
435
|
+
|
436
|
+
if isinstance(folders, dict) and folders.get("error"):
|
437
|
+
raise ValueError(
|
438
|
+
f"Folder search failed: {folders.get('message', 'Unknown error')}"
|
439
|
+
)
|
440
|
+
|
441
|
+
result = DriveFolderFindOutput(
|
442
|
+
folder_name=folder_name,
|
443
|
+
folders_found=folders or [],
|
444
|
+
folder_count=len(folders) if folders else 0,
|
445
|
+
)
|
446
|
+
|
447
|
+
if not include_files:
|
448
|
+
return result
|
449
|
+
|
450
|
+
if not folders:
|
451
|
+
result.message = f"No folders found with name matching '{folder_name}'"
|
452
|
+
return result
|
453
|
+
|
454
|
+
target_folder = folders[0]
|
455
|
+
folder_id = target_folder["id"]
|
456
|
+
|
457
|
+
# Build the search query for files within the folder
|
458
|
+
folder_constraint = f"'{folder_id}' in parents and trashed=false"
|
459
|
+
|
460
|
+
if file_query and file_query.strip():
|
461
|
+
# Use the same smart query logic as drive_search_files
|
462
|
+
clean_file_query = file_query.strip()
|
463
|
+
if (
|
464
|
+
" " not in clean_file_query
|
465
|
+
and ":" not in clean_file_query
|
466
|
+
and "=" not in clean_file_query
|
467
|
+
):
|
468
|
+
escaped_file_query = clean_file_query.replace("'", "\\'")
|
469
|
+
wrapped_file_query = f"fullText contains '{escaped_file_query}'"
|
470
|
+
else:
|
471
|
+
wrapped_file_query = clean_file_query.replace("'", "\\'")
|
472
|
+
combined_query = f"{wrapped_file_query} and {folder_constraint}"
|
473
|
+
else:
|
474
|
+
combined_query = folder_constraint
|
475
|
+
|
476
|
+
files = drive_service.search_files(
|
477
|
+
query=combined_query, page_size=page_size, include_shared_drives=True
|
478
|
+
)
|
479
|
+
|
480
|
+
if isinstance(files, dict) and files.get("error"):
|
481
|
+
raise ValueError(
|
482
|
+
f"File search in folder failed: {files.get('message', 'Unknown error')}"
|
483
|
+
)
|
484
|
+
|
485
|
+
result.target_folder = target_folder
|
486
|
+
result.files = files or []
|
487
|
+
result.file_count = len(files) if files else 0
|
488
|
+
|
489
|
+
return result
|