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.
@@ -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
- include_shared_drives: bool = True,
25
- include_trashed: bool = False,
26
- ) -> dict[str, Any]:
62
+ ) -> DriveSearchOutput:
27
63
  """
28
- Search for files in Google Drive with optional shared drive support. Trashed files are excluded by default.
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 string. Can be a simple text search or complex query with operators.
37
- Apostrophes are automatically escaped for you.
38
- page_size: Maximum number of files to return (1 to 1000, default 10).
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
- A dictionary containing a list of files or an error message.
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=final_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 {"files": files}
89
+ return DriveSearchOutput(files=files or [])
89
90
 
90
91
 
91
- # @mcp.tool(
92
- # name="drive_read_file_content",
93
- # )
94
- async def drive_read_file_content(file_id: str) -> dict[str, Any]:
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
- A dictionary containing the file content and metadata or an error.
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 result
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
- # @mcp.tool(
121
- # name="drive_upload_file",
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
- ) -> dict[str, Any]:
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
- A dictionary containing the uploaded file metadata or an error.
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 result
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
- # @mcp.tool(
164
- # name="drive_create_folder",
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
- ) -> dict[str, Any]:
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
- A dictionary containing the created folder information.
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 result
215
+ return DriveFolderCreationOutput(
216
+ id=result["id"], name=result["name"], web_view_link=result["web_view_link"]
217
+ )
202
218
 
203
219
 
204
- # @mcp.tool(
205
- # name="drive_delete_file",
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
- ) -> dict[str, Any]:
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
- A dictionary confirming the deletion or an error.
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 result
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) -> dict[str, Any]:
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
- A dictionary containing a list of shared drives with their 'id' and 'name',
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
- return {"message": "No shared drives found or accessible."}
276
+ drives = []
256
277
 
257
- return {"count": len(drives), "shared_drives": drives}
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
- ) -> dict[str, Any]:
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
- A dictionary containing a list of files and folders.
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 {"folder_id": folder_id, "files": files}
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
- ) -> dict[str, Any]:
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
- A dictionary containing folders_found and, if requested, file search results.
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
- "folder_name": folder_name,
422
- "folders_found": folders,
423
- "folder_count": len(folders) if folders else 0,
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["message"] = f"No folders found with name matching '{folder_name}'"
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
- wrapped_file_query = (
448
- f"fullText contains '{clean_file_query.replace("'", "\\'")}'"
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["target_folder"] = target_folder
466
- result["files"] = files
467
- result["file_count"] = len(files) if files else 0
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