google-workspace-mcp 1.0.4__py3-none-any.whl → 1.1.5__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 +6 -5
- google_workspace_mcp/services/drive.py +39 -12
- google_workspace_mcp/services/slides.py +2033 -57
- google_workspace_mcp/tools/add_image.py +1781 -0
- google_workspace_mcp/tools/calendar.py +12 -17
- google_workspace_mcp/tools/docs_tools.py +24 -32
- google_workspace_mcp/tools/drive.py +264 -21
- google_workspace_mcp/tools/gmail.py +27 -36
- google_workspace_mcp/tools/sheets_tools.py +18 -25
- google_workspace_mcp/tools/slides.py +774 -55
- google_workspace_mcp/utils/unit_conversion.py +201 -0
- {google_workspace_mcp-1.0.4.dist-info → google_workspace_mcp-1.1.5.dist-info}/METADATA +2 -2
- {google_workspace_mcp-1.0.4.dist-info → google_workspace_mcp-1.1.5.dist-info}/RECORD +15 -13
- {google_workspace_mcp-1.0.4.dist-info → google_workspace_mcp-1.1.5.dist-info}/WHEEL +0 -0
- {google_workspace_mcp-1.0.4.dist-info → google_workspace_mcp-1.1.5.dist-info}/entry_points.txt +0 -0
@@ -16,7 +16,6 @@ logger = logging.getLogger(__name__)
|
|
16
16
|
|
17
17
|
# @mcp.tool(
|
18
18
|
# name="list_calendars",
|
19
|
-
# description="Lists all calendars accessible by the user.",
|
20
19
|
# )
|
21
20
|
# async def list_calendars() -> dict[str, Any]:
|
22
21
|
# """
|
@@ -40,10 +39,9 @@ logger = logging.getLogger(__name__)
|
|
40
39
|
# return {"count": len(calendars), "calendars": calendars}
|
41
40
|
|
42
41
|
|
43
|
-
@mcp.tool(
|
44
|
-
|
45
|
-
|
46
|
-
)
|
42
|
+
# @mcp.tool(
|
43
|
+
# name="calendar_get_events",
|
44
|
+
# )
|
47
45
|
async def calendar_get_events(
|
48
46
|
time_min: str,
|
49
47
|
time_max: str,
|
@@ -92,10 +90,9 @@ async def calendar_get_events(
|
|
92
90
|
return {"count": len(events), "events": events}
|
93
91
|
|
94
92
|
|
95
|
-
@mcp.tool(
|
96
|
-
|
97
|
-
|
98
|
-
)
|
93
|
+
# @mcp.tool(
|
94
|
+
# name="calendar_get_event_details",
|
95
|
+
# )
|
99
96
|
async def calendar_get_event_details(event_id: str, calendar_id: str = "primary") -> dict[str, Any]:
|
100
97
|
"""
|
101
98
|
Retrieves details for a specific event in a Google Calendar.
|
@@ -126,10 +123,9 @@ async def calendar_get_event_details(event_id: str, calendar_id: str = "primary"
|
|
126
123
|
return event_details
|
127
124
|
|
128
125
|
|
129
|
-
@mcp.tool(
|
130
|
-
|
131
|
-
|
132
|
-
)
|
126
|
+
# @mcp.tool(
|
127
|
+
# name="create_calendar_event",
|
128
|
+
# )
|
133
129
|
async def create_calendar_event(
|
134
130
|
summary: str,
|
135
131
|
start_time: str,
|
@@ -184,10 +180,9 @@ async def create_calendar_event(
|
|
184
180
|
return result
|
185
181
|
|
186
182
|
|
187
|
-
@mcp.tool(
|
188
|
-
|
189
|
-
|
190
|
-
)
|
183
|
+
# @mcp.tool(
|
184
|
+
# name="delete_calendar_event",
|
185
|
+
# )
|
191
186
|
async def delete_calendar_event(
|
192
187
|
event_id: str,
|
193
188
|
calendar_id: str = "primary",
|
@@ -11,10 +11,9 @@ from google_workspace_mcp.services.docs_service import DocsService
|
|
11
11
|
logger = logging.getLogger(__name__)
|
12
12
|
|
13
13
|
|
14
|
-
@mcp.tool(
|
15
|
-
|
16
|
-
|
17
|
-
)
|
14
|
+
# @mcp.tool(
|
15
|
+
# name="docs_create_document",
|
16
|
+
# )
|
18
17
|
async def docs_create_document(title: str) -> dict[str, Any]:
|
19
18
|
"""
|
20
19
|
Creates a new, empty Google Document.
|
@@ -44,10 +43,9 @@ async def docs_create_document(title: str) -> dict[str, Any]:
|
|
44
43
|
return result
|
45
44
|
|
46
45
|
|
47
|
-
@mcp.tool(
|
48
|
-
|
49
|
-
|
50
|
-
)
|
46
|
+
# @mcp.tool(
|
47
|
+
# name="docs_get_document_metadata",
|
48
|
+
# )
|
51
49
|
async def docs_get_document_metadata(document_id: str) -> dict[str, Any]:
|
52
50
|
"""
|
53
51
|
Retrieves metadata for a specific Google Document.
|
@@ -77,10 +75,9 @@ async def docs_get_document_metadata(document_id: str) -> dict[str, Any]:
|
|
77
75
|
return result
|
78
76
|
|
79
77
|
|
80
|
-
@mcp.tool(
|
81
|
-
|
82
|
-
|
83
|
-
)
|
78
|
+
# @mcp.tool(
|
79
|
+
# name="docs_get_content_as_markdown",
|
80
|
+
# )
|
84
81
|
async def docs_get_content_as_markdown(document_id: str) -> dict[str, Any]:
|
85
82
|
"""
|
86
83
|
Retrieves the main textual content of a Google Document, converted to Markdown.
|
@@ -114,10 +111,9 @@ async def docs_get_content_as_markdown(document_id: str) -> dict[str, Any]:
|
|
114
111
|
return result
|
115
112
|
|
116
113
|
|
117
|
-
@mcp.tool(
|
118
|
-
|
119
|
-
|
120
|
-
)
|
114
|
+
# @mcp.tool(
|
115
|
+
# name="docs_append_text",
|
116
|
+
# )
|
121
117
|
async def docs_append_text(
|
122
118
|
document_id: str, text: str, ensure_newline: bool = True
|
123
119
|
) -> dict[str, Any]:
|
@@ -152,10 +148,9 @@ async def docs_append_text(
|
|
152
148
|
return result
|
153
149
|
|
154
150
|
|
155
|
-
@mcp.tool(
|
156
|
-
|
157
|
-
|
158
|
-
)
|
151
|
+
# @mcp.tool(
|
152
|
+
# name="docs_prepend_text",
|
153
|
+
# )
|
159
154
|
async def docs_prepend_text(
|
160
155
|
document_id: str, text: str, ensure_newline: bool = True
|
161
156
|
) -> dict[str, Any]:
|
@@ -190,10 +185,9 @@ async def docs_prepend_text(
|
|
190
185
|
return result
|
191
186
|
|
192
187
|
|
193
|
-
@mcp.tool(
|
194
|
-
|
195
|
-
|
196
|
-
)
|
188
|
+
# @mcp.tool(
|
189
|
+
# name="docs_insert_text",
|
190
|
+
# )
|
197
191
|
async def docs_insert_text(
|
198
192
|
document_id: str, text: str, index: int | None = None, segment_id: str | None = None
|
199
193
|
) -> dict[str, Any]:
|
@@ -234,10 +228,9 @@ async def docs_insert_text(
|
|
234
228
|
return result
|
235
229
|
|
236
230
|
|
237
|
-
@mcp.tool(
|
238
|
-
|
239
|
-
|
240
|
-
)
|
231
|
+
# @mcp.tool(
|
232
|
+
# name="docs_batch_update",
|
233
|
+
# )
|
241
234
|
async def docs_batch_update(document_id: str, requests: list[dict]) -> dict[str, Any]:
|
242
235
|
"""
|
243
236
|
Applies a list of Google Docs API update requests to the specified document.
|
@@ -277,10 +270,9 @@ async def docs_batch_update(document_id: str, requests: list[dict]) -> dict[str,
|
|
277
270
|
return result # Return the full response which includes documentId and replies
|
278
271
|
|
279
272
|
|
280
|
-
@mcp.tool(
|
281
|
-
|
282
|
-
|
283
|
-
)
|
273
|
+
# @mcp.tool(
|
274
|
+
# name="docs_insert_image",
|
275
|
+
# )
|
284
276
|
async def docs_insert_image(
|
285
277
|
document_id: str,
|
286
278
|
image_url: str,
|
@@ -16,34 +16,70 @@ logger = logging.getLogger(__name__)
|
|
16
16
|
|
17
17
|
@mcp.tool(
|
18
18
|
name="drive_search_files",
|
19
|
-
description="Search for files in Google Drive with optional shared drive support.",
|
20
19
|
)
|
21
20
|
async def drive_search_files(
|
22
21
|
query: str,
|
23
22
|
page_size: int = 10,
|
24
23
|
shared_drive_id: str | None = None,
|
24
|
+
include_shared_drives: bool = True,
|
25
|
+
include_trashed: bool = False,
|
25
26
|
) -> dict[str, Any]:
|
26
27
|
"""
|
27
|
-
Search for files in Google Drive
|
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
|
28
34
|
|
29
35
|
Args:
|
30
36
|
query: Search query string. Can be a simple text search or complex query with operators.
|
37
|
+
Apostrophes are automatically escaped for you.
|
31
38
|
page_size: Maximum number of files to return (1 to 1000, default 10).
|
32
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).
|
33
43
|
|
34
44
|
Returns:
|
35
45
|
A dictionary containing a list of files or an error message.
|
36
46
|
"""
|
37
47
|
logger.info(
|
38
|
-
f"Executing drive_search_files with query: '{query}', page_size: {page_size},
|
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}"
|
39
51
|
)
|
40
52
|
|
41
53
|
if not query or not query.strip():
|
42
54
|
raise ValueError("Query cannot be empty")
|
43
55
|
|
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
|
+
|
44
77
|
drive_service = DriveService()
|
45
78
|
files = drive_service.search_files(
|
46
|
-
query=
|
79
|
+
query=final_query,
|
80
|
+
page_size=page_size,
|
81
|
+
shared_drive_id=shared_drive_id,
|
82
|
+
include_shared_drives=include_shared_drives,
|
47
83
|
)
|
48
84
|
|
49
85
|
if isinstance(files, dict) and files.get("error"):
|
@@ -52,10 +88,9 @@ async def drive_search_files(
|
|
52
88
|
return {"files": files}
|
53
89
|
|
54
90
|
|
55
|
-
@mcp.tool(
|
56
|
-
|
57
|
-
|
58
|
-
)
|
91
|
+
# @mcp.tool(
|
92
|
+
# name="drive_read_file_content",
|
93
|
+
# )
|
59
94
|
async def drive_read_file_content(file_id: str) -> dict[str, Any]:
|
60
95
|
"""
|
61
96
|
Read the content of a file from Google Drive.
|
@@ -82,10 +117,9 @@ async def drive_read_file_content(file_id: str) -> dict[str, Any]:
|
|
82
117
|
return result
|
83
118
|
|
84
119
|
|
85
|
-
@mcp.tool(
|
86
|
-
|
87
|
-
|
88
|
-
)
|
120
|
+
# @mcp.tool(
|
121
|
+
# name="drive_upload_file",
|
122
|
+
# )
|
89
123
|
async def drive_upload_file(
|
90
124
|
filename: str,
|
91
125
|
content_base64: str,
|
@@ -126,10 +160,9 @@ async def drive_upload_file(
|
|
126
160
|
return result
|
127
161
|
|
128
162
|
|
129
|
-
@mcp.tool(
|
130
|
-
|
131
|
-
|
132
|
-
)
|
163
|
+
# @mcp.tool(
|
164
|
+
# name="drive_create_folder",
|
165
|
+
# )
|
133
166
|
async def drive_create_folder(
|
134
167
|
folder_name: str,
|
135
168
|
parent_folder_id: str | None = None,
|
@@ -168,10 +201,9 @@ async def drive_create_folder(
|
|
168
201
|
return result
|
169
202
|
|
170
203
|
|
171
|
-
@mcp.tool(
|
172
|
-
|
173
|
-
|
174
|
-
)
|
204
|
+
# @mcp.tool(
|
205
|
+
# name="drive_delete_file",
|
206
|
+
# )
|
175
207
|
async def drive_delete_file(
|
176
208
|
file_id: str,
|
177
209
|
) -> dict[str, Any]:
|
@@ -199,7 +231,6 @@ async def drive_delete_file(
|
|
199
231
|
|
200
232
|
@mcp.tool(
|
201
233
|
name="drive_list_shared_drives",
|
202
|
-
description="Lists shared drives accessible by the user.",
|
203
234
|
)
|
204
235
|
async def drive_list_shared_drives(page_size: int = 100) -> dict[str, Any]:
|
205
236
|
"""
|
@@ -224,3 +255,215 @@ async def drive_list_shared_drives(page_size: int = 100) -> dict[str, Any]:
|
|
224
255
|
return {"message": "No shared drives found or accessible."}
|
225
256
|
|
226
257
|
return {"count": len(drives), "shared_drives": drives}
|
258
|
+
|
259
|
+
|
260
|
+
@mcp.tool(
|
261
|
+
name="drive_search_files_in_folder",
|
262
|
+
)
|
263
|
+
async def drive_search_files_in_folder(
|
264
|
+
folder_id: str,
|
265
|
+
query: str = "",
|
266
|
+
page_size: int = 10,
|
267
|
+
) -> dict[str, Any]:
|
268
|
+
"""
|
269
|
+
Search for files or folders within a specific folder ID. Trashed files are excluded.
|
270
|
+
This works for both regular folders and Shared Drives (when using the Shared Drive's ID as the folder_id).
|
271
|
+
|
272
|
+
Args:
|
273
|
+
folder_id: The ID of the folder or Shared Drive to search within.
|
274
|
+
query: Optional search query string, following Google Drive API syntax.
|
275
|
+
If empty, returns all items.
|
276
|
+
Example to find only sub-folders: "mimeType = 'application/vnd.google-apps.folder'"
|
277
|
+
page_size: Maximum number of files to return (1 to 1000, default 10).
|
278
|
+
|
279
|
+
Returns:
|
280
|
+
A dictionary containing a list of files and folders.
|
281
|
+
"""
|
282
|
+
logger.info(
|
283
|
+
f"Executing drive_search_files_in_folder with folder_id: '{folder_id}', "
|
284
|
+
f"query: '{query}', page_size: {page_size}"
|
285
|
+
)
|
286
|
+
|
287
|
+
if not folder_id or not folder_id.strip():
|
288
|
+
raise ValueError("Folder ID cannot be empty")
|
289
|
+
|
290
|
+
# Build the search query to search within the specific folder
|
291
|
+
folder_query = f"'{folder_id}' in parents and trashed=false"
|
292
|
+
if query and query.strip():
|
293
|
+
# Automatically escape apostrophes in user query
|
294
|
+
escaped_query = query.strip().replace("'", "\\'")
|
295
|
+
# Combine folder constraint with user query
|
296
|
+
combined_query = f"{escaped_query} and {folder_query}"
|
297
|
+
else:
|
298
|
+
combined_query = folder_query
|
299
|
+
|
300
|
+
drive_service = DriveService()
|
301
|
+
files = drive_service.search_files(
|
302
|
+
query=combined_query,
|
303
|
+
page_size=page_size,
|
304
|
+
include_shared_drives=True, # Always include shared drives for folder searches
|
305
|
+
)
|
306
|
+
|
307
|
+
if isinstance(files, dict) and files.get("error"):
|
308
|
+
raise ValueError(
|
309
|
+
f"Folder search failed: {files.get('message', 'Unknown error')}"
|
310
|
+
)
|
311
|
+
|
312
|
+
return {"folder_id": folder_id, "files": files}
|
313
|
+
|
314
|
+
|
315
|
+
# @mcp.tool(
|
316
|
+
# name="drive_get_folder_info",
|
317
|
+
# )
|
318
|
+
async def drive_get_folder_info(folder_id: str) -> dict[str, Any]:
|
319
|
+
"""
|
320
|
+
Get detailed information about a folder in Google Drive.
|
321
|
+
|
322
|
+
Useful for understanding folder permissions and hierarchy.
|
323
|
+
|
324
|
+
Args:
|
325
|
+
folder_id: The ID of the folder to get information about.
|
326
|
+
|
327
|
+
Returns:
|
328
|
+
A dictionary containing folder metadata or an error message.
|
329
|
+
"""
|
330
|
+
logger.info(f"Executing drive_get_folder_info with folder_id: '{folder_id}'")
|
331
|
+
|
332
|
+
if not folder_id or not folder_id.strip():
|
333
|
+
raise ValueError("Folder ID cannot be empty")
|
334
|
+
|
335
|
+
drive_service = DriveService()
|
336
|
+
folder_info = drive_service.get_file_metadata(file_id=folder_id)
|
337
|
+
|
338
|
+
if isinstance(folder_info, dict) and folder_info.get("error"):
|
339
|
+
raise ValueError(
|
340
|
+
f"Failed to get folder info: {folder_info.get('message', 'Unknown error')}"
|
341
|
+
)
|
342
|
+
|
343
|
+
# Verify it's actually a folder
|
344
|
+
if folder_info.get("mimeType") != "application/vnd.google-apps.folder":
|
345
|
+
raise ValueError(
|
346
|
+
f"ID '{folder_id}' is not a folder (mimeType: {folder_info.get('mimeType')})"
|
347
|
+
)
|
348
|
+
|
349
|
+
return folder_info
|
350
|
+
|
351
|
+
|
352
|
+
@mcp.tool(
|
353
|
+
name="drive_find_folder_by_name",
|
354
|
+
)
|
355
|
+
async def drive_find_folder_by_name(
|
356
|
+
folder_name: str,
|
357
|
+
include_files: bool = False,
|
358
|
+
file_query: str = "",
|
359
|
+
page_size: int = 10,
|
360
|
+
shared_drive_id: str | None = None,
|
361
|
+
) -> dict[str, Any]:
|
362
|
+
"""
|
363
|
+
Finds folders by name using a two-step search: first an exact match, then a partial match.
|
364
|
+
Automatically handles apostrophes in folder names and search queries. Trashed items are excluded.
|
365
|
+
|
366
|
+
Crucial Note: This tool finds **regular folders** within "My Drive" or a Shared Drive.
|
367
|
+
It **does not** find Shared Drives themselves. To list available Shared Drives,
|
368
|
+
use the `drive_list_shared_drives` tool.
|
369
|
+
|
370
|
+
Args:
|
371
|
+
folder_name: The name of the folder to search for.
|
372
|
+
include_files: Whether to also search for files within the found folder (default False).
|
373
|
+
file_query: Optional search query for files within the folder. Only used if include_files=True.
|
374
|
+
page_size: Maximum number of files to return (1 to 1000, default 10).
|
375
|
+
shared_drive_id: Optional shared drive ID to search within a specific shared drive.
|
376
|
+
|
377
|
+
Returns:
|
378
|
+
A dictionary containing folders_found and, if requested, file search results.
|
379
|
+
"""
|
380
|
+
logger.info(
|
381
|
+
f"Executing drive_find_folder_by_name with folder_name: '{folder_name}', "
|
382
|
+
f"include_files: {include_files}, file_query: '{file_query}', "
|
383
|
+
f"page_size: {page_size}, shared_drive_id: {shared_drive_id}"
|
384
|
+
)
|
385
|
+
|
386
|
+
if not folder_name or not folder_name.strip():
|
387
|
+
raise ValueError("Folder name cannot be empty")
|
388
|
+
|
389
|
+
drive_service = DriveService()
|
390
|
+
escaped_folder_name = folder_name.strip().replace("'", "\\'")
|
391
|
+
|
392
|
+
# --- Step 1: Attempt Exact Match ---
|
393
|
+
logger.info(f"Step 1: Searching for exact folder name: '{escaped_folder_name}'")
|
394
|
+
exact_query = f"name = '{escaped_folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false"
|
395
|
+
folders = drive_service.search_files(
|
396
|
+
query=exact_query,
|
397
|
+
page_size=5,
|
398
|
+
shared_drive_id=shared_drive_id,
|
399
|
+
include_shared_drives=True,
|
400
|
+
)
|
401
|
+
|
402
|
+
# If no exact match, fall back to partial match
|
403
|
+
if not folders:
|
404
|
+
logger.info(
|
405
|
+
f"No exact match found. Step 2: Searching for folder name containing '{escaped_folder_name}'"
|
406
|
+
)
|
407
|
+
contains_query = f"name contains '{escaped_folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false"
|
408
|
+
folders = drive_service.search_files(
|
409
|
+
query=contains_query,
|
410
|
+
page_size=5,
|
411
|
+
shared_drive_id=shared_drive_id,
|
412
|
+
include_shared_drives=True,
|
413
|
+
)
|
414
|
+
|
415
|
+
if isinstance(folders, dict) and folders.get("error"):
|
416
|
+
raise ValueError(
|
417
|
+
f"Folder search failed: {folders.get('message', 'Unknown error')}"
|
418
|
+
)
|
419
|
+
|
420
|
+
result = {
|
421
|
+
"folder_name": folder_name,
|
422
|
+
"folders_found": folders,
|
423
|
+
"folder_count": len(folders) if folders else 0,
|
424
|
+
}
|
425
|
+
|
426
|
+
if not include_files:
|
427
|
+
return result
|
428
|
+
|
429
|
+
if not folders:
|
430
|
+
result["message"] = f"No folders found with name matching '{folder_name}'"
|
431
|
+
return result
|
432
|
+
|
433
|
+
target_folder = folders[0]
|
434
|
+
folder_id = target_folder["id"]
|
435
|
+
|
436
|
+
# Build the search query for files within the folder
|
437
|
+
folder_constraint = f"'{folder_id}' in parents and trashed=false"
|
438
|
+
|
439
|
+
if file_query and file_query.strip():
|
440
|
+
# Use the same smart query logic as drive_search_files
|
441
|
+
clean_file_query = file_query.strip()
|
442
|
+
if (
|
443
|
+
" " not in clean_file_query
|
444
|
+
and ":" not in clean_file_query
|
445
|
+
and "=" not in clean_file_query
|
446
|
+
):
|
447
|
+
wrapped_file_query = (
|
448
|
+
f"fullText contains '{clean_file_query.replace("'", "\\'")}'"
|
449
|
+
)
|
450
|
+
else:
|
451
|
+
wrapped_file_query = clean_file_query.replace("'", "\\'")
|
452
|
+
combined_query = f"{wrapped_file_query} and {folder_constraint}"
|
453
|
+
else:
|
454
|
+
combined_query = folder_constraint
|
455
|
+
|
456
|
+
files = drive_service.search_files(
|
457
|
+
query=combined_query, page_size=page_size, include_shared_drives=True
|
458
|
+
)
|
459
|
+
|
460
|
+
if isinstance(files, dict) and files.get("error"):
|
461
|
+
raise ValueError(
|
462
|
+
f"File search in folder failed: {files.get('message', 'Unknown error')}"
|
463
|
+
)
|
464
|
+
|
465
|
+
result["target_folder"] = target_folder
|
466
|
+
result["files"] = files
|
467
|
+
result["file_count"] = len(files) if files else 0
|
468
|
+
|
469
|
+
return result
|
@@ -14,10 +14,9 @@ logger = logging.getLogger(__name__)
|
|
14
14
|
# --- Gmail Tool Functions --- #
|
15
15
|
|
16
16
|
|
17
|
-
@mcp.tool(
|
18
|
-
|
19
|
-
|
20
|
-
)
|
17
|
+
# @mcp.tool(
|
18
|
+
# name="query_gmail_emails",
|
19
|
+
# )
|
21
20
|
async def query_gmail_emails(query: str, max_results: int = 100) -> dict[str, Any]:
|
22
21
|
"""
|
23
22
|
Searches for Gmail emails using Gmail query syntax.
|
@@ -45,10 +44,9 @@ async def query_gmail_emails(query: str, max_results: int = 100) -> dict[str, An
|
|
45
44
|
return {"count": len(emails), "emails": emails}
|
46
45
|
|
47
46
|
|
48
|
-
@mcp.tool(
|
49
|
-
|
50
|
-
|
51
|
-
)
|
47
|
+
# @mcp.tool(
|
48
|
+
# name="gmail_get_message_details",
|
49
|
+
# )
|
52
50
|
async def gmail_get_message_details(email_id: str) -> dict[str, Any]:
|
53
51
|
"""
|
54
52
|
Retrieves a complete Gmail email message by its ID.
|
@@ -77,10 +75,9 @@ async def gmail_get_message_details(email_id: str) -> dict[str, Any]:
|
|
77
75
|
return result
|
78
76
|
|
79
77
|
|
80
|
-
@mcp.tool(
|
81
|
-
|
82
|
-
|
83
|
-
)
|
78
|
+
# @mcp.tool(
|
79
|
+
# name="gmail_get_attachment_content",
|
80
|
+
# )
|
84
81
|
async def gmail_get_attachment_content(message_id: str, attachment_id: str) -> dict[str, Any]:
|
85
82
|
"""
|
86
83
|
Retrieves a specific attachment from a Gmail message.
|
@@ -109,10 +106,9 @@ async def gmail_get_attachment_content(message_id: str, attachment_id: str) -> d
|
|
109
106
|
return result
|
110
107
|
|
111
108
|
|
112
|
-
@mcp.tool(
|
113
|
-
|
114
|
-
|
115
|
-
)
|
109
|
+
# @mcp.tool(
|
110
|
+
# name="create_gmail_draft",
|
111
|
+
# )
|
116
112
|
async def create_gmail_draft(
|
117
113
|
to: str,
|
118
114
|
subject: str,
|
@@ -150,10 +146,9 @@ async def create_gmail_draft(
|
|
150
146
|
return result
|
151
147
|
|
152
148
|
|
153
|
-
@mcp.tool(
|
154
|
-
|
155
|
-
|
156
|
-
)
|
149
|
+
# @mcp.tool(
|
150
|
+
# name="delete_gmail_draft",
|
151
|
+
# )
|
157
152
|
async def delete_gmail_draft(
|
158
153
|
draft_id: str,
|
159
154
|
) -> dict[str, Any]:
|
@@ -189,10 +184,9 @@ async def delete_gmail_draft(
|
|
189
184
|
}
|
190
185
|
|
191
186
|
|
192
|
-
@mcp.tool(
|
193
|
-
|
194
|
-
|
195
|
-
)
|
187
|
+
# @mcp.tool(
|
188
|
+
# name="gmail_send_draft",
|
189
|
+
# )
|
196
190
|
async def gmail_send_draft(draft_id: str) -> dict[str, Any]:
|
197
191
|
"""
|
198
192
|
Sends a specific draft email.
|
@@ -219,10 +213,9 @@ async def gmail_send_draft(draft_id: str) -> dict[str, Any]:
|
|
219
213
|
return result
|
220
214
|
|
221
215
|
|
222
|
-
@mcp.tool(
|
223
|
-
|
224
|
-
|
225
|
-
)
|
216
|
+
# @mcp.tool(
|
217
|
+
# name="gmail_reply_to_email",
|
218
|
+
# )
|
226
219
|
async def gmail_reply_to_email(
|
227
220
|
email_id: str,
|
228
221
|
reply_body: str,
|
@@ -262,10 +255,9 @@ async def gmail_reply_to_email(
|
|
262
255
|
return result
|
263
256
|
|
264
257
|
|
265
|
-
@mcp.tool(
|
266
|
-
|
267
|
-
|
268
|
-
)
|
258
|
+
# @mcp.tool(
|
259
|
+
# name="gmail_bulk_delete_messages",
|
260
|
+
# )
|
269
261
|
async def gmail_bulk_delete_messages(
|
270
262
|
message_ids: list[str],
|
271
263
|
) -> dict[str, Any]:
|
@@ -300,10 +292,9 @@ async def gmail_bulk_delete_messages(
|
|
300
292
|
return result
|
301
293
|
|
302
294
|
|
303
|
-
@mcp.tool(
|
304
|
-
|
305
|
-
|
306
|
-
)
|
295
|
+
# @mcp.tool(
|
296
|
+
# name="gmail_send_email",
|
297
|
+
# )
|
307
298
|
async def gmail_send_email(
|
308
299
|
to: list[str],
|
309
300
|
subject: str,
|