google-workspace-mcp 1.0.2__py3-none-any.whl → 1.0.4__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/services/docs_service.py +283 -32
- google_workspace_mcp/services/drive.py +74 -48
- google_workspace_mcp/tools/docs_tools.py +100 -13
- google_workspace_mcp/tools/drive.py +20 -11
- {google_workspace_mcp-1.0.2.dist-info → google_workspace_mcp-1.0.4.dist-info}/METADATA +2 -2
- {google_workspace_mcp-1.0.2.dist-info → google_workspace_mcp-1.0.4.dist-info}/RECORD +8 -8
- {google_workspace_mcp-1.0.2.dist-info → google_workspace_mcp-1.0.4.dist-info}/WHEEL +0 -0
- {google_workspace_mcp-1.0.2.dist-info → google_workspace_mcp-1.0.4.dist-info}/entry_points.txt +0 -0
@@ -4,6 +4,8 @@ Google Docs service implementation.
|
|
4
4
|
|
5
5
|
import io
|
6
6
|
import logging
|
7
|
+
import urllib.parse
|
8
|
+
import urllib.request
|
7
9
|
from typing import Any
|
8
10
|
|
9
11
|
from googleapiclient.discovery import build
|
@@ -42,7 +44,9 @@ class DocsService(BaseGoogleService):
|
|
42
44
|
body = {"title": title}
|
43
45
|
document = self.service.documents().create(body=body).execute()
|
44
46
|
# The response includes documentId, title, and other fields.
|
45
|
-
logger.info(
|
47
|
+
logger.info(
|
48
|
+
f"Successfully created document: {document.get('title')} (ID: {document.get('documentId')})"
|
49
|
+
)
|
46
50
|
return {
|
47
51
|
"document_id": document.get("documentId"),
|
48
52
|
"title": document.get("title"),
|
@@ -74,7 +78,11 @@ class DocsService(BaseGoogleService):
|
|
74
78
|
logger.info(f"Fetching metadata for document ID: {document_id}")
|
75
79
|
# The 'fields' parameter can be used to specify which fields to return.
|
76
80
|
# e.g., "documentId,title,body,revisionId,suggestionsViewMode"
|
77
|
-
document =
|
81
|
+
document = (
|
82
|
+
self.service.documents()
|
83
|
+
.get(documentId=document_id, fields="documentId,title")
|
84
|
+
.execute()
|
85
|
+
)
|
78
86
|
logger.info(
|
79
87
|
f"Successfully fetched metadata for document: {document.get('title')} (ID: {document.get('documentId')})"
|
80
88
|
)
|
@@ -84,10 +92,14 @@ class DocsService(BaseGoogleService):
|
|
84
92
|
"document_link": f"https://docs.google.com/document/d/{document.get('documentId')}/edit",
|
85
93
|
}
|
86
94
|
except HttpError as error:
|
87
|
-
logger.error(
|
95
|
+
logger.error(
|
96
|
+
f"Error fetching document metadata for ID {document_id}: {error}"
|
97
|
+
)
|
88
98
|
return self.handle_api_error("get_document_metadata", error)
|
89
99
|
except Exception as e:
|
90
|
-
logger.exception(
|
100
|
+
logger.exception(
|
101
|
+
f"Unexpected error fetching document metadata for ID {document_id}"
|
102
|
+
)
|
91
103
|
return {
|
92
104
|
"error": True,
|
93
105
|
"error_type": "unexpected_service_error",
|
@@ -95,7 +107,9 @@ class DocsService(BaseGoogleService):
|
|
95
107
|
"operation": "get_document_metadata",
|
96
108
|
}
|
97
109
|
|
98
|
-
def get_document_content_as_markdown(
|
110
|
+
def get_document_content_as_markdown(
|
111
|
+
self, document_id: str
|
112
|
+
) -> dict[str, Any] | None:
|
99
113
|
"""
|
100
114
|
Retrieves the content of a Google Document as Markdown using Drive API export.
|
101
115
|
|
@@ -106,7 +120,9 @@ class DocsService(BaseGoogleService):
|
|
106
120
|
A dictionary with 'document_id' and 'markdown_content', or an error dictionary.
|
107
121
|
"""
|
108
122
|
try:
|
109
|
-
logger.info(
|
123
|
+
logger.info(
|
124
|
+
f"Attempting to export document ID: {document_id} as Markdown via Drive API."
|
125
|
+
)
|
110
126
|
|
111
127
|
# Obtain credentials
|
112
128
|
credentials = gauth.get_credentials()
|
@@ -125,7 +141,9 @@ class DocsService(BaseGoogleService):
|
|
125
141
|
# Attempt to export as 'text/markdown'
|
126
142
|
# Not all environments or GDoc content might support 'text/markdown' perfectly.
|
127
143
|
# 'text/plain' is a safer fallback.
|
128
|
-
request = drive_service_client.files().export_media(
|
144
|
+
request = drive_service_client.files().export_media(
|
145
|
+
fileId=document_id, mimeType="text/markdown"
|
146
|
+
)
|
129
147
|
fh = io.BytesIO()
|
130
148
|
downloader = MediaIoBaseDownload(fh, request)
|
131
149
|
done = False
|
@@ -136,10 +154,14 @@ class DocsService(BaseGoogleService):
|
|
136
154
|
content_bytes = fh.getvalue()
|
137
155
|
|
138
156
|
if content_bytes is None:
|
139
|
-
raise Exception(
|
157
|
+
raise Exception(
|
158
|
+
"Failed to download exported content (bytes object is None)."
|
159
|
+
)
|
140
160
|
|
141
161
|
markdown_content = content_bytes.decode("utf-8")
|
142
|
-
logger.info(
|
162
|
+
logger.info(
|
163
|
+
f"Successfully exported document ID: {document_id} to Markdown."
|
164
|
+
)
|
143
165
|
return {"document_id": document_id, "markdown_content": markdown_content}
|
144
166
|
|
145
167
|
except HttpError as error:
|
@@ -148,9 +170,13 @@ class DocsService(BaseGoogleService):
|
|
148
170
|
f"HTTPError exporting document {document_id} as Markdown: {error}. Falling back to text/plain might be an option if this is a common issue for certain docs."
|
149
171
|
)
|
150
172
|
# For now, just return the error from the attempt.
|
151
|
-
return self.handle_api_error(
|
173
|
+
return self.handle_api_error(
|
174
|
+
"get_document_content_as_markdown_drive_export", error
|
175
|
+
)
|
152
176
|
except Exception as e:
|
153
|
-
logger.exception(
|
177
|
+
logger.exception(
|
178
|
+
f"Unexpected error exporting document {document_id} as Markdown: {e}"
|
179
|
+
)
|
154
180
|
return {
|
155
181
|
"error": True,
|
156
182
|
"error_type": "export_error",
|
@@ -158,7 +184,9 @@ class DocsService(BaseGoogleService):
|
|
158
184
|
"operation": "get_document_content_as_markdown",
|
159
185
|
}
|
160
186
|
|
161
|
-
def append_text(
|
187
|
+
def append_text(
|
188
|
+
self, document_id: str, text: str, ensure_newline: bool = True
|
189
|
+
) -> dict[str, Any] | None:
|
162
190
|
"""
|
163
191
|
Appends text to the end of a Google Document.
|
164
192
|
|
@@ -171,7 +199,9 @@ class DocsService(BaseGoogleService):
|
|
171
199
|
A dictionary indicating success or an error dictionary.
|
172
200
|
"""
|
173
201
|
try:
|
174
|
-
logger.info(
|
202
|
+
logger.info(
|
203
|
+
f"Appending text to document ID: {document_id}. ensure_newline={ensure_newline}"
|
204
|
+
)
|
175
205
|
|
176
206
|
# To append at the end, we need to find the current end of the body segment.
|
177
207
|
# Get document to find end index. Fields "body(content(endIndex))" might be enough.
|
@@ -180,7 +210,9 @@ class DocsService(BaseGoogleService):
|
|
180
210
|
# If ensure_newline, and doc is not empty, prepend "\n" to text.
|
181
211
|
|
182
212
|
document = (
|
183
|
-
self.service.documents()
|
213
|
+
self.service.documents()
|
214
|
+
.get(documentId=document_id, fields="body(content(endIndex,paragraph))")
|
215
|
+
.execute()
|
184
216
|
)
|
185
217
|
end_index = (
|
186
218
|
document.get("body", {}).get("content", [])[-1].get("endIndex")
|
@@ -189,21 +221,26 @@ class DocsService(BaseGoogleService):
|
|
189
221
|
)
|
190
222
|
|
191
223
|
text_to_insert = text
|
192
|
-
if
|
224
|
+
if (
|
225
|
+
ensure_newline and end_index > 1
|
226
|
+
): # A new doc might have an end_index of 1 for the initial implicit paragraph
|
193
227
|
text_to_insert = "\n" + text
|
194
228
|
|
195
229
|
requests = [
|
196
230
|
{
|
197
231
|
"insertText": {
|
198
232
|
"location": {
|
199
|
-
"index": end_index
|
233
|
+
"index": end_index
|
234
|
+
- 1 # Insert before the final newline of the document/body
|
200
235
|
},
|
201
236
|
"text": text_to_insert,
|
202
237
|
}
|
203
238
|
}
|
204
239
|
]
|
205
240
|
|
206
|
-
self.service.documents().batchUpdate(
|
241
|
+
self.service.documents().batchUpdate(
|
242
|
+
documentId=document_id, body={"requests": requests}
|
243
|
+
).execute()
|
207
244
|
logger.info(f"Successfully appended text to document ID: {document_id}")
|
208
245
|
return {
|
209
246
|
"document_id": document_id,
|
@@ -214,7 +251,9 @@ class DocsService(BaseGoogleService):
|
|
214
251
|
logger.error(f"Error appending text to document {document_id}: {error}")
|
215
252
|
return self.handle_api_error("append_text", error)
|
216
253
|
except Exception as e:
|
217
|
-
logger.exception(
|
254
|
+
logger.exception(
|
255
|
+
f"Unexpected error appending text to document {document_id}"
|
256
|
+
)
|
218
257
|
return {
|
219
258
|
"error": True,
|
220
259
|
"error_type": "unexpected_service_error",
|
@@ -222,7 +261,9 @@ class DocsService(BaseGoogleService):
|
|
222
261
|
"operation": "append_text",
|
223
262
|
}
|
224
263
|
|
225
|
-
def prepend_text(
|
264
|
+
def prepend_text(
|
265
|
+
self, document_id: str, text: str, ensure_newline: bool = True
|
266
|
+
) -> dict[str, Any] | None:
|
226
267
|
"""
|
227
268
|
Prepends text to the beginning of a Google Document.
|
228
269
|
|
@@ -235,14 +276,20 @@ class DocsService(BaseGoogleService):
|
|
235
276
|
A dictionary indicating success or an error dictionary.
|
236
277
|
"""
|
237
278
|
try:
|
238
|
-
logger.info(
|
279
|
+
logger.info(
|
280
|
+
f"Prepending text to document ID: {document_id}. ensure_newline={ensure_newline}"
|
281
|
+
)
|
239
282
|
|
240
283
|
text_to_insert = text
|
241
284
|
# To prepend, we generally insert at index 1 (after the initial Body segment start).
|
242
285
|
# If ensure_newline is true, and the document isn't empty, add a newline *after* the prepended text.
|
243
286
|
# This requires checking if the document has existing content.
|
244
287
|
if ensure_newline:
|
245
|
-
document =
|
288
|
+
document = (
|
289
|
+
self.service.documents()
|
290
|
+
.get(documentId=document_id, fields="body(content(endIndex))")
|
291
|
+
.execute()
|
292
|
+
)
|
246
293
|
current_content_exists = bool(document.get("body", {}).get("content"))
|
247
294
|
if current_content_exists:
|
248
295
|
text_to_insert = text + "\n"
|
@@ -259,7 +306,9 @@ class DocsService(BaseGoogleService):
|
|
259
306
|
}
|
260
307
|
]
|
261
308
|
|
262
|
-
self.service.documents().batchUpdate(
|
309
|
+
self.service.documents().batchUpdate(
|
310
|
+
documentId=document_id, body={"requests": requests}
|
311
|
+
).execute()
|
263
312
|
logger.info(f"Successfully prepended text to document ID: {document_id}")
|
264
313
|
return {
|
265
314
|
"document_id": document_id,
|
@@ -270,7 +319,9 @@ class DocsService(BaseGoogleService):
|
|
270
319
|
logger.error(f"Error prepending text to document {document_id}: {error}")
|
271
320
|
return self.handle_api_error("prepend_text", error)
|
272
321
|
except Exception as e:
|
273
|
-
logger.exception(
|
322
|
+
logger.exception(
|
323
|
+
f"Unexpected error prepending text to document {document_id}"
|
324
|
+
)
|
274
325
|
return {
|
275
326
|
"error": True,
|
276
327
|
"error_type": "unexpected_service_error",
|
@@ -302,7 +353,9 @@ class DocsService(BaseGoogleService):
|
|
302
353
|
A dictionary indicating success or an error dictionary.
|
303
354
|
"""
|
304
355
|
try:
|
305
|
-
logger.info(
|
356
|
+
logger.info(
|
357
|
+
f"Inserting text into document ID: {document_id} at index: {index}, segment: {segment_id}"
|
358
|
+
)
|
306
359
|
|
307
360
|
# Default to index 1 if not provided, which usually targets the start of the body content.
|
308
361
|
# For an empty document, this effectively adds text.
|
@@ -321,7 +374,9 @@ class DocsService(BaseGoogleService):
|
|
321
374
|
|
322
375
|
requests = [request]
|
323
376
|
|
324
|
-
self.service.documents().batchUpdate(
|
377
|
+
self.service.documents().batchUpdate(
|
378
|
+
documentId=document_id, body={"requests": requests}
|
379
|
+
).execute()
|
325
380
|
logger.info(f"Successfully inserted text into document ID: {document_id}")
|
326
381
|
return {
|
327
382
|
"document_id": document_id,
|
@@ -332,7 +387,9 @@ class DocsService(BaseGoogleService):
|
|
332
387
|
logger.error(f"Error inserting text into document {document_id}: {error}")
|
333
388
|
return self.handle_api_error("insert_text", error)
|
334
389
|
except Exception as e:
|
335
|
-
logger.exception(
|
390
|
+
logger.exception(
|
391
|
+
f"Unexpected error inserting text into document {document_id}"
|
392
|
+
)
|
336
393
|
return {
|
337
394
|
"error": True,
|
338
395
|
"error_type": "unexpected_service_error",
|
@@ -340,7 +397,9 @@ class DocsService(BaseGoogleService):
|
|
340
397
|
"operation": "insert_text",
|
341
398
|
}
|
342
399
|
|
343
|
-
def batch_update(
|
400
|
+
def batch_update(
|
401
|
+
self, document_id: str, requests: list[dict]
|
402
|
+
) -> dict[str, Any] | None:
|
344
403
|
"""
|
345
404
|
Applies a list of update requests to the specified Google Document.
|
346
405
|
|
@@ -354,16 +413,24 @@ class DocsService(BaseGoogleService):
|
|
354
413
|
or an error dictionary.
|
355
414
|
"""
|
356
415
|
try:
|
357
|
-
logger.info(
|
416
|
+
logger.info(
|
417
|
+
f"Executing batchUpdate for document ID: {document_id} with {len(requests)} requests."
|
418
|
+
)
|
358
419
|
if not requests:
|
359
|
-
logger.warning(
|
420
|
+
logger.warning(
|
421
|
+
f"batchUpdate called with no requests for document ID: {document_id}"
|
422
|
+
)
|
360
423
|
return {
|
361
424
|
"document_id": document_id,
|
362
425
|
"replies": [],
|
363
426
|
"message": "No requests provided.",
|
364
427
|
}
|
365
428
|
|
366
|
-
response =
|
429
|
+
response = (
|
430
|
+
self.service.documents()
|
431
|
+
.batchUpdate(documentId=document_id, body={"requests": requests})
|
432
|
+
.execute()
|
433
|
+
)
|
367
434
|
# The response object contains a list of 'replies', one for each request,
|
368
435
|
# or it might be empty if all requests were successful without specific replies.
|
369
436
|
# It also contains 'writeControl' and 'documentId'.
|
@@ -376,13 +443,197 @@ class DocsService(BaseGoogleService):
|
|
376
443
|
"write_control": response.get("writeControl"),
|
377
444
|
}
|
378
445
|
except HttpError as error:
|
379
|
-
logger.error(
|
446
|
+
logger.error(
|
447
|
+
f"Error during batchUpdate for document {document_id}: {error}"
|
448
|
+
)
|
380
449
|
return self.handle_api_error("batch_update", error)
|
381
450
|
except Exception as e:
|
382
|
-
logger.exception(
|
451
|
+
logger.exception(
|
452
|
+
f"Unexpected error during batchUpdate for document {document_id}"
|
453
|
+
)
|
383
454
|
return {
|
384
455
|
"error": True,
|
385
456
|
"error_type": "unexpected_service_error",
|
386
457
|
"message": str(e),
|
387
458
|
"operation": "batch_update",
|
388
459
|
}
|
460
|
+
|
461
|
+
def _validate_image_url(self, image_url: str) -> dict[str, Any] | None:
|
462
|
+
"""
|
463
|
+
Validates an image URL for Google Docs API requirements.
|
464
|
+
|
465
|
+
Returns None if valid, or error dict if invalid.
|
466
|
+
"""
|
467
|
+
if not image_url or not image_url.strip():
|
468
|
+
return {
|
469
|
+
"error": True,
|
470
|
+
"error_type": "validation_error",
|
471
|
+
"message": "Image URL cannot be empty",
|
472
|
+
}
|
473
|
+
|
474
|
+
# Check URL length (≤ 2kB)
|
475
|
+
if len(image_url) > 2048:
|
476
|
+
return {
|
477
|
+
"error": True,
|
478
|
+
"error_type": "validation_error",
|
479
|
+
"message": f"Image URL too long: {len(image_url)} chars (max: 2048)",
|
480
|
+
}
|
481
|
+
|
482
|
+
try:
|
483
|
+
# Make HEAD request to check accessibility and get metadata
|
484
|
+
req = urllib.request.Request(image_url, method="HEAD")
|
485
|
+
req.add_header("User-Agent", "Google-Workspace-MCP/1.0")
|
486
|
+
|
487
|
+
with urllib.request.urlopen(req, timeout=10) as response:
|
488
|
+
content_type = response.headers.get("Content-Type", "").lower()
|
489
|
+
content_length = response.headers.get("Content-Length")
|
490
|
+
|
491
|
+
# Check MIME type (PNG, JPEG, GIF only)
|
492
|
+
allowed_types = ["image/png", "image/jpeg", "image/jpg", "image/gif"]
|
493
|
+
if not any(
|
494
|
+
allowed_type in content_type for allowed_type in allowed_types
|
495
|
+
):
|
496
|
+
return {
|
497
|
+
"error": True,
|
498
|
+
"error_type": "validation_error",
|
499
|
+
"message": f"Unsupported image format: {content_type}. Must be PNG, JPEG, or GIF",
|
500
|
+
}
|
501
|
+
|
502
|
+
# Check file size (< 50MB)
|
503
|
+
if content_length:
|
504
|
+
size_mb = int(content_length) / (1024 * 1024)
|
505
|
+
if size_mb >= 50:
|
506
|
+
return {
|
507
|
+
"error": True,
|
508
|
+
"error_type": "validation_error",
|
509
|
+
"message": f"Image too large: {size_mb:.1f}MB (max: 50MB)",
|
510
|
+
}
|
511
|
+
|
512
|
+
return None # Valid
|
513
|
+
|
514
|
+
except urllib.error.HTTPError as e:
|
515
|
+
return {
|
516
|
+
"error": True,
|
517
|
+
"error_type": "validation_error",
|
518
|
+
"message": f"Image URL not accessible: HTTP {e.code} {e.reason}",
|
519
|
+
}
|
520
|
+
except urllib.error.URLError as e:
|
521
|
+
return {
|
522
|
+
"error": True,
|
523
|
+
"error_type": "validation_error",
|
524
|
+
"message": f"Image URL not reachable: {e.reason}",
|
525
|
+
}
|
526
|
+
except Exception as e:
|
527
|
+
return {
|
528
|
+
"error": True,
|
529
|
+
"error_type": "validation_error",
|
530
|
+
"message": f"Failed to validate image URL: {str(e)}",
|
531
|
+
}
|
532
|
+
|
533
|
+
def insert_image(
|
534
|
+
self,
|
535
|
+
document_id: str,
|
536
|
+
image_url: str,
|
537
|
+
index: int,
|
538
|
+
width: float | None = None,
|
539
|
+
height: float | None = None,
|
540
|
+
) -> dict[str, Any] | None:
|
541
|
+
"""
|
542
|
+
Inserts an image into a Google Document from a URL at a specific index.
|
543
|
+
|
544
|
+
Args:
|
545
|
+
document_id: The ID of the Google Document.
|
546
|
+
image_url: The publicly accessible URL of the image to insert.
|
547
|
+
index: The 1-based index in the document where the image will be inserted.
|
548
|
+
width: Optional width of the image in points (PT).
|
549
|
+
height: Optional height of the image in points (PT).
|
550
|
+
|
551
|
+
Returns:
|
552
|
+
A dictionary containing the inserted image ID and success status, or an error dictionary.
|
553
|
+
"""
|
554
|
+
try:
|
555
|
+
logger.info(f"Inserting image into document {document_id} at index {index}")
|
556
|
+
|
557
|
+
# Validate inputs
|
558
|
+
if not document_id or not document_id.strip():
|
559
|
+
raise ValueError("Document ID cannot be empty")
|
560
|
+
|
561
|
+
if not isinstance(index, int) or index < 1:
|
562
|
+
raise ValueError("Index must be a positive integer (1-based)")
|
563
|
+
|
564
|
+
# Validate image URL
|
565
|
+
validation_error = self._validate_image_url(image_url)
|
566
|
+
if validation_error:
|
567
|
+
return validation_error
|
568
|
+
|
569
|
+
# Build the insert image request
|
570
|
+
insert_request = {
|
571
|
+
"insertInlineImage": {
|
572
|
+
"location": {
|
573
|
+
"index": index - 1 # Convert to 0-based index for API
|
574
|
+
},
|
575
|
+
"uri": image_url,
|
576
|
+
}
|
577
|
+
}
|
578
|
+
|
579
|
+
# Add optional dimensions
|
580
|
+
if width is not None or height is not None:
|
581
|
+
insert_request["insertInlineImage"]["objectSize"] = {}
|
582
|
+
if width is not None:
|
583
|
+
insert_request["insertInlineImage"]["objectSize"]["width"] = {
|
584
|
+
"magnitude": width,
|
585
|
+
"unit": "PT",
|
586
|
+
}
|
587
|
+
if height is not None:
|
588
|
+
insert_request["insertInlineImage"]["objectSize"]["height"] = {
|
589
|
+
"magnitude": height,
|
590
|
+
"unit": "PT",
|
591
|
+
}
|
592
|
+
|
593
|
+
# Execute the batch update
|
594
|
+
result = (
|
595
|
+
self.service.documents()
|
596
|
+
.batchUpdate(
|
597
|
+
documentId=document_id, body={"requests": [insert_request]}
|
598
|
+
)
|
599
|
+
.execute()
|
600
|
+
)
|
601
|
+
|
602
|
+
# Extract the inserted image ID from the reply
|
603
|
+
inserted_image_id = None
|
604
|
+
if result.get("replies") and len(result["replies"]) > 0:
|
605
|
+
reply = result["replies"][0]
|
606
|
+
if "insertInlineImage" in reply:
|
607
|
+
inserted_image_id = reply["insertInlineImage"].get("objectId")
|
608
|
+
|
609
|
+
logger.info(
|
610
|
+
f"Successfully inserted image {inserted_image_id} into document {document_id}"
|
611
|
+
)
|
612
|
+
|
613
|
+
return {
|
614
|
+
"document_id": document_id,
|
615
|
+
"inserted_image_id": inserted_image_id,
|
616
|
+
"success": True,
|
617
|
+
}
|
618
|
+
|
619
|
+
except ValueError as e:
|
620
|
+
logger.error(f"Validation error inserting image: {e}")
|
621
|
+
return {
|
622
|
+
"error": True,
|
623
|
+
"error_type": "validation_error",
|
624
|
+
"message": str(e),
|
625
|
+
"operation": "insert_image",
|
626
|
+
}
|
627
|
+
except HttpError as error:
|
628
|
+
logger.error(f"Google Docs API error inserting image: {error}")
|
629
|
+
return self.handle_api_error("insert_image", error)
|
630
|
+
except Exception as e:
|
631
|
+
logger.exception(
|
632
|
+
f"Unexpected error inserting image into document {document_id}"
|
633
|
+
)
|
634
|
+
return {
|
635
|
+
"error": True,
|
636
|
+
"error_type": "unexpected_service_error",
|
637
|
+
"message": str(e),
|
638
|
+
"operation": "insert_image",
|
639
|
+
}
|
@@ -4,14 +4,14 @@ Provides comprehensive file management capabilities through Google Drive API.
|
|
4
4
|
"""
|
5
5
|
|
6
6
|
import base64
|
7
|
+
import binascii
|
7
8
|
import io
|
8
9
|
import logging
|
9
10
|
import mimetypes
|
10
|
-
import os
|
11
11
|
from typing import Any
|
12
12
|
|
13
13
|
from googleapiclient.errors import HttpError
|
14
|
-
from googleapiclient.http import
|
14
|
+
from googleapiclient.http import MediaIoBaseDownload, MediaIoBaseUpload
|
15
15
|
|
16
16
|
from google_workspace_mcp.services.base import BaseGoogleService
|
17
17
|
|
@@ -27,7 +27,9 @@ class DriveService(BaseGoogleService):
|
|
27
27
|
"""Initialize the Drive service."""
|
28
28
|
super().__init__("drive", "v3")
|
29
29
|
|
30
|
-
def search_files(
|
30
|
+
def search_files(
|
31
|
+
self, query: str, page_size: int = 10, shared_drive_id: str | None = None
|
32
|
+
) -> list[dict[str, Any]]:
|
31
33
|
"""
|
32
34
|
Search for files in Google Drive.
|
33
35
|
|
@@ -40,17 +42,16 @@ class DriveService(BaseGoogleService):
|
|
40
42
|
List of file metadata dictionaries (id, name, mimeType, etc.) or an error dictionary
|
41
43
|
"""
|
42
44
|
try:
|
43
|
-
logger.info(
|
45
|
+
logger.info(
|
46
|
+
f"Searching files with query: '{query}', page_size: {page_size}, shared_drive_id: {shared_drive_id}"
|
47
|
+
)
|
44
48
|
|
45
49
|
# Validate and constrain page_size
|
46
50
|
page_size = max(1, min(page_size, 1000))
|
47
51
|
|
48
|
-
# Format query with proper escaping
|
49
|
-
formatted_query = query.replace("'", "\\'")
|
50
|
-
|
51
52
|
# Build list parameters with shared drive support
|
52
53
|
list_params = {
|
53
|
-
"q":
|
54
|
+
"q": query, # Use the query directly without modification
|
54
55
|
"pageSize": page_size,
|
55
56
|
"fields": "files(id, name, mimeType, modifiedTime, size, webViewLink, iconLink)",
|
56
57
|
"supportsAllDrives": True,
|
@@ -59,9 +60,13 @@ class DriveService(BaseGoogleService):
|
|
59
60
|
|
60
61
|
if shared_drive_id:
|
61
62
|
list_params["driveId"] = shared_drive_id
|
62
|
-
list_params["corpora"] =
|
63
|
+
list_params["corpora"] = (
|
64
|
+
"drive" # Search within the specified shared drive
|
65
|
+
)
|
63
66
|
else:
|
64
|
-
list_params["corpora"] =
|
67
|
+
list_params["corpora"] = (
|
68
|
+
"user" # Default to user's files if no specific shared drive ID
|
69
|
+
)
|
65
70
|
|
66
71
|
results = self.service.files().list(**list_params).execute()
|
67
72
|
files = results.get("files", [])
|
@@ -84,12 +89,18 @@ class DriveService(BaseGoogleService):
|
|
84
89
|
"""
|
85
90
|
try:
|
86
91
|
# Get file metadata
|
87
|
-
file_metadata =
|
92
|
+
file_metadata = (
|
93
|
+
self.service.files()
|
94
|
+
.get(fileId=file_id, fields="mimeType, name")
|
95
|
+
.execute()
|
96
|
+
)
|
88
97
|
|
89
98
|
original_mime_type = file_metadata.get("mimeType")
|
90
99
|
file_name = file_metadata.get("name", "Unknown")
|
91
100
|
|
92
|
-
logger.info(
|
101
|
+
logger.info(
|
102
|
+
f"Reading file '{file_name}' ({file_id}) with mimeType: {original_mime_type}"
|
103
|
+
)
|
93
104
|
|
94
105
|
# Handle Google Workspace files by exporting
|
95
106
|
if original_mime_type.startswith("application/vnd.google-apps."):
|
@@ -129,7 +140,9 @@ class DriveService(BaseGoogleService):
|
|
129
140
|
.execute()
|
130
141
|
)
|
131
142
|
|
132
|
-
logger.info(
|
143
|
+
logger.info(
|
144
|
+
f"Successfully retrieved metadata for file: {file_metadata.get('name', 'Unknown')}"
|
145
|
+
)
|
133
146
|
return file_metadata
|
134
147
|
|
135
148
|
except Exception as e:
|
@@ -185,13 +198,17 @@ class DriveService(BaseGoogleService):
|
|
185
198
|
|
186
199
|
created_folder = self.service.files().create(**create_params).execute()
|
187
200
|
|
188
|
-
logger.info(
|
201
|
+
logger.info(
|
202
|
+
f"Successfully created folder '{folder_name}' with ID: {created_folder.get('id')}"
|
203
|
+
)
|
189
204
|
return created_folder
|
190
205
|
|
191
206
|
except Exception as e:
|
192
207
|
return self.handle_api_error("create_folder", e)
|
193
208
|
|
194
|
-
def _export_google_file(
|
209
|
+
def _export_google_file(
|
210
|
+
self, file_id: str, file_name: str, mime_type: str
|
211
|
+
) -> dict[str, Any]:
|
195
212
|
"""Export a Google Workspace file in an appropriate format."""
|
196
213
|
# Determine export format
|
197
214
|
export_mime_type = None
|
@@ -216,7 +233,9 @@ class DriveService(BaseGoogleService):
|
|
216
233
|
|
217
234
|
# Export the file
|
218
235
|
try:
|
219
|
-
request = self.service.files().export_media(
|
236
|
+
request = self.service.files().export_media(
|
237
|
+
fileId=file_id, mimeType=export_mime_type
|
238
|
+
)
|
220
239
|
|
221
240
|
content_bytes = self._download_content(request)
|
222
241
|
if isinstance(content_bytes, dict) and content_bytes.get("error"):
|
@@ -248,7 +267,9 @@ class DriveService(BaseGoogleService):
|
|
248
267
|
except Exception as e:
|
249
268
|
return self.handle_api_error("_export_google_file", e)
|
250
269
|
|
251
|
-
def _download_regular_file(
|
270
|
+
def _download_regular_file(
|
271
|
+
self, file_id: str, file_name: str, mime_type: str
|
272
|
+
) -> dict[str, Any]:
|
252
273
|
"""Download a regular (non-Google Workspace) file."""
|
253
274
|
request = self.service.files().get_media(fileId=file_id, supportsAllDrives=True)
|
254
275
|
|
@@ -262,7 +283,9 @@ class DriveService(BaseGoogleService):
|
|
262
283
|
content = content_bytes.decode("utf-8")
|
263
284
|
return {"mimeType": mime_type, "content": content, "encoding": "utf-8"}
|
264
285
|
except UnicodeDecodeError:
|
265
|
-
logger.warning(
|
286
|
+
logger.warning(
|
287
|
+
f"UTF-8 decoding failed for file {file_id} ('{file_name}', {mime_type}). Using base64."
|
288
|
+
)
|
266
289
|
content = base64.b64encode(content_bytes).decode("utf-8")
|
267
290
|
return {
|
268
291
|
"mimeType": mime_type,
|
@@ -289,61 +312,60 @@ class DriveService(BaseGoogleService):
|
|
289
312
|
except Exception as e:
|
290
313
|
return self.handle_api_error("download_content", e)
|
291
314
|
|
292
|
-
def
|
315
|
+
def upload_file_content(
|
293
316
|
self,
|
294
|
-
|
317
|
+
filename: str,
|
318
|
+
content_base64: str,
|
295
319
|
parent_folder_id: str | None = None,
|
296
320
|
shared_drive_id: str | None = None,
|
297
321
|
) -> dict[str, Any]:
|
298
322
|
"""
|
299
|
-
Upload a file to Google Drive.
|
323
|
+
Upload a file to Google Drive using its content.
|
300
324
|
|
301
325
|
Args:
|
302
|
-
|
303
|
-
|
304
|
-
|
326
|
+
filename: The name for the file in Google Drive.
|
327
|
+
content_base64: Base64 encoded content of the file.
|
328
|
+
parent_folder_id: Optional parent folder ID.
|
329
|
+
shared_drive_id: Optional shared drive ID.
|
305
330
|
|
306
331
|
Returns:
|
307
|
-
Dict containing file metadata on success, or error information on failure
|
332
|
+
Dict containing file metadata on success, or error information on failure.
|
308
333
|
"""
|
309
334
|
try:
|
310
|
-
|
311
|
-
|
312
|
-
|
335
|
+
logger.info(f"Uploading file '{filename}' from content.")
|
336
|
+
|
337
|
+
# Decode the base64 content
|
338
|
+
try:
|
339
|
+
content_bytes = base64.b64decode(content_base64, validate=True)
|
340
|
+
except (ValueError, TypeError, binascii.Error) as e:
|
341
|
+
logger.error(f"Invalid base64 content for file '{filename}': {e}")
|
313
342
|
return {
|
314
343
|
"error": True,
|
315
|
-
"error_type": "
|
316
|
-
"message":
|
317
|
-
"operation": "
|
344
|
+
"error_type": "invalid_content",
|
345
|
+
"message": "Invalid base64 encoded content provided.",
|
346
|
+
"operation": "upload_file_content",
|
318
347
|
}
|
319
348
|
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
# Get file MIME type
|
324
|
-
mime_type, _ = mimetypes.guess_type(file_path)
|
349
|
+
# Get file MIME type from filename
|
350
|
+
mime_type, _ = mimetypes.guess_type(filename)
|
325
351
|
if mime_type is None:
|
326
352
|
mime_type = "application/octet-stream"
|
327
353
|
|
328
|
-
file_metadata = {"name":
|
329
|
-
|
330
|
-
# Set parent folder if specified
|
354
|
+
file_metadata = {"name": filename}
|
331
355
|
if parent_folder_id:
|
332
356
|
file_metadata["parents"] = [parent_folder_id]
|
333
357
|
elif shared_drive_id:
|
334
|
-
# If shared drive is specified but no parent, set shared drive as parent
|
335
358
|
file_metadata["parents"] = [shared_drive_id]
|
336
359
|
|
337
|
-
|
360
|
+
# Use MediaIoBaseUpload for in-memory content
|
361
|
+
media = MediaIoBaseUpload(io.BytesIO(content_bytes), mimetype=mime_type)
|
338
362
|
|
339
|
-
# Prepare create parameters
|
340
363
|
create_params = {
|
341
364
|
"body": file_metadata,
|
342
365
|
"media_body": media,
|
343
366
|
"fields": "id,name,mimeType,modifiedTime,size,webViewLink",
|
344
367
|
"supportsAllDrives": True,
|
345
368
|
}
|
346
|
-
|
347
369
|
if shared_drive_id:
|
348
370
|
create_params["driveId"] = shared_drive_id
|
349
371
|
|
@@ -353,14 +375,14 @@ class DriveService(BaseGoogleService):
|
|
353
375
|
return file
|
354
376
|
|
355
377
|
except HttpError as e:
|
356
|
-
return self.handle_api_error("
|
378
|
+
return self.handle_api_error("upload_file_content", e)
|
357
379
|
except Exception as e:
|
358
|
-
logger.error(f"Non-API error in
|
380
|
+
logger.error(f"Non-API error in upload_file_content: {str(e)}")
|
359
381
|
return {
|
360
382
|
"error": True,
|
361
383
|
"error_type": "local_error",
|
362
|
-
"message": f"Error uploading file: {str(e)}",
|
363
|
-
"operation": "
|
384
|
+
"message": f"Error uploading file from content: {str(e)}",
|
385
|
+
"operation": "upload_file_content",
|
364
386
|
}
|
365
387
|
|
366
388
|
def delete_file(self, file_id: str) -> dict[str, Any]:
|
@@ -400,7 +422,11 @@ class DriveService(BaseGoogleService):
|
|
400
422
|
# API allows pageSize up to 100 for drives.list
|
401
423
|
actual_page_size = min(max(1, page_size), 100)
|
402
424
|
|
403
|
-
results =
|
425
|
+
results = (
|
426
|
+
self.service.drives()
|
427
|
+
.list(pageSize=actual_page_size, fields="drives(id, name, kind)")
|
428
|
+
.execute()
|
429
|
+
)
|
404
430
|
drives = results.get("drives", [])
|
405
431
|
|
406
432
|
# Filter for kind='drive#drive' just to be sure, though API should only return these
|
@@ -37,7 +37,9 @@ async def docs_create_document(title: str) -> dict[str, Any]:
|
|
37
37
|
raise ValueError(result.get("message", "Error creating document"))
|
38
38
|
|
39
39
|
if not result or not result.get("document_id"):
|
40
|
-
raise ValueError(
|
40
|
+
raise ValueError(
|
41
|
+
f"Failed to create document '{title}' or did not receive a document ID."
|
42
|
+
)
|
41
43
|
|
42
44
|
return result
|
43
45
|
|
@@ -57,7 +59,9 @@ async def docs_get_document_metadata(document_id: str) -> dict[str, Any]:
|
|
57
59
|
A dictionary containing the document's 'document_id', 'title', and 'document_link',
|
58
60
|
or an error message.
|
59
61
|
"""
|
60
|
-
logger.info(
|
62
|
+
logger.info(
|
63
|
+
f"Executing docs_get_document_metadata tool for document_id: '{document_id}'"
|
64
|
+
)
|
61
65
|
if not document_id or not document_id.strip():
|
62
66
|
raise ValueError("Document ID cannot be empty.")
|
63
67
|
|
@@ -88,7 +92,9 @@ async def docs_get_content_as_markdown(document_id: str) -> dict[str, Any]:
|
|
88
92
|
A dictionary containing the 'document_id' and 'markdown_content',
|
89
93
|
or an error message.
|
90
94
|
"""
|
91
|
-
logger.info(
|
95
|
+
logger.info(
|
96
|
+
f"Executing docs_get_content_as_markdown tool for document_id: '{document_id}'"
|
97
|
+
)
|
92
98
|
if not document_id or not document_id.strip():
|
93
99
|
raise ValueError("Document ID cannot be empty.")
|
94
100
|
|
@@ -96,10 +102,14 @@ async def docs_get_content_as_markdown(document_id: str) -> dict[str, Any]:
|
|
96
102
|
result = docs_service.get_document_content_as_markdown(document_id=document_id)
|
97
103
|
|
98
104
|
if isinstance(result, dict) and result.get("error"):
|
99
|
-
raise ValueError(
|
105
|
+
raise ValueError(
|
106
|
+
result.get("message", "Error retrieving document content as Markdown")
|
107
|
+
)
|
100
108
|
|
101
109
|
if not result or "markdown_content" not in result:
|
102
|
-
raise ValueError(
|
110
|
+
raise ValueError(
|
111
|
+
f"Failed to retrieve Markdown content for document '{document_id}'."
|
112
|
+
)
|
103
113
|
|
104
114
|
return result
|
105
115
|
|
@@ -108,7 +118,9 @@ async def docs_get_content_as_markdown(document_id: str) -> dict[str, Any]:
|
|
108
118
|
name="docs_append_text",
|
109
119
|
description="Appends text to the end of a specified Google Document.",
|
110
120
|
)
|
111
|
-
async def docs_append_text(
|
121
|
+
async def docs_append_text(
|
122
|
+
document_id: str, text: str, ensure_newline: bool = True
|
123
|
+
) -> dict[str, Any]:
|
112
124
|
"""
|
113
125
|
Appends the given text to the end of the specified Google Document.
|
114
126
|
|
@@ -127,7 +139,9 @@ async def docs_append_text(document_id: str, text: str, ensure_newline: bool = T
|
|
127
139
|
# Text can be empty, that's fine.
|
128
140
|
|
129
141
|
docs_service = DocsService()
|
130
|
-
result = docs_service.append_text(
|
142
|
+
result = docs_service.append_text(
|
143
|
+
document_id=document_id, text=text, ensure_newline=ensure_newline
|
144
|
+
)
|
131
145
|
|
132
146
|
if isinstance(result, dict) and result.get("error"):
|
133
147
|
raise ValueError(result.get("message", "Error appending text to document"))
|
@@ -142,7 +156,9 @@ async def docs_append_text(document_id: str, text: str, ensure_newline: bool = T
|
|
142
156
|
name="docs_prepend_text",
|
143
157
|
description="Prepends text to the beginning of a specified Google Document.",
|
144
158
|
)
|
145
|
-
async def docs_prepend_text(
|
159
|
+
async def docs_prepend_text(
|
160
|
+
document_id: str, text: str, ensure_newline: bool = True
|
161
|
+
) -> dict[str, Any]:
|
146
162
|
"""
|
147
163
|
Prepends the given text to the beginning of the specified Google Document.
|
148
164
|
|
@@ -161,7 +177,9 @@ async def docs_prepend_text(document_id: str, text: str, ensure_newline: bool =
|
|
161
177
|
# Text can be empty, that's fine.
|
162
178
|
|
163
179
|
docs_service = DocsService()
|
164
|
-
result = docs_service.prepend_text(
|
180
|
+
result = docs_service.prepend_text(
|
181
|
+
document_id=document_id, text=text, ensure_newline=ensure_newline
|
182
|
+
)
|
165
183
|
|
166
184
|
if isinstance(result, dict) and result.get("error"):
|
167
185
|
raise ValueError(result.get("message", "Error prepending text to document"))
|
@@ -195,13 +213,17 @@ async def docs_insert_text(
|
|
195
213
|
Returns:
|
196
214
|
A dictionary indicating success or an error message.
|
197
215
|
"""
|
198
|
-
logger.info(
|
216
|
+
logger.info(
|
217
|
+
f"Executing docs_insert_text tool for document_id: '{document_id}' at index: {index}"
|
218
|
+
)
|
199
219
|
if not document_id or not document_id.strip():
|
200
220
|
raise ValueError("Document ID cannot be empty.")
|
201
221
|
# Text can be empty, that's a valid insertion (though perhaps not useful).
|
202
222
|
|
203
223
|
docs_service = DocsService()
|
204
|
-
result = docs_service.insert_text(
|
224
|
+
result = docs_service.insert_text(
|
225
|
+
document_id=document_id, text=text, index=index, segment_id=segment_id
|
226
|
+
)
|
205
227
|
|
206
228
|
if isinstance(result, dict) and result.get("error"):
|
207
229
|
raise ValueError(result.get("message", "Error inserting text into document"))
|
@@ -231,7 +253,9 @@ async def docs_batch_update(document_id: str, requests: list[dict]) -> dict[str,
|
|
231
253
|
A dictionary containing the API response from the batchUpdate call (includes replies for each request),
|
232
254
|
or an error message.
|
233
255
|
"""
|
234
|
-
logger.info(
|
256
|
+
logger.info(
|
257
|
+
f"Executing docs_batch_update tool for document_id: '{document_id}' with {len(requests)} requests."
|
258
|
+
)
|
235
259
|
if not document_id or not document_id.strip():
|
236
260
|
raise ValueError("Document ID cannot be empty.")
|
237
261
|
if not isinstance(requests, list):
|
@@ -243,9 +267,72 @@ async def docs_batch_update(document_id: str, requests: list[dict]) -> dict[str,
|
|
243
267
|
result = docs_service.batch_update(document_id=document_id, requests=requests)
|
244
268
|
|
245
269
|
if isinstance(result, dict) and result.get("error"):
|
246
|
-
raise ValueError(
|
270
|
+
raise ValueError(
|
271
|
+
result.get("message", "Error executing batch update on document")
|
272
|
+
)
|
247
273
|
|
248
274
|
if not result: # Should be caught by error dict check
|
249
275
|
raise ValueError(f"Failed to execute batch update on document '{document_id}'.")
|
250
276
|
|
251
277
|
return result # Return the full response which includes documentId and replies
|
278
|
+
|
279
|
+
|
280
|
+
@mcp.tool(
|
281
|
+
name="docs_insert_image",
|
282
|
+
description="Inserts an image into a Google Document from a URL at a specific index. The image URL must be publicly accessible and in PNG, JPEG, or GIF format.",
|
283
|
+
)
|
284
|
+
async def docs_insert_image(
|
285
|
+
document_id: str,
|
286
|
+
image_url: str,
|
287
|
+
index: int,
|
288
|
+
width: float | None = None,
|
289
|
+
height: float | None = None,
|
290
|
+
) -> dict[str, Any]:
|
291
|
+
"""
|
292
|
+
Inserts an image into a Google Document from a URL at a specific index.
|
293
|
+
|
294
|
+
Args:
|
295
|
+
document_id: The ID of the Google Document.
|
296
|
+
image_url: The publicly accessible URL of the image to insert.
|
297
|
+
index: The 1-based index in the document where the image will be inserted.
|
298
|
+
width: Optional width of the image in points (PT).
|
299
|
+
height: Optional height of the image in points (PT).
|
300
|
+
|
301
|
+
Returns:
|
302
|
+
A dictionary containing the inserted image ID and success status, or an error message.
|
303
|
+
"""
|
304
|
+
logger.info(
|
305
|
+
f"Executing docs_insert_image tool for document_id: '{document_id}' at index: {index}"
|
306
|
+
)
|
307
|
+
|
308
|
+
if not document_id or not document_id.strip():
|
309
|
+
raise ValueError("Document ID cannot be empty.")
|
310
|
+
|
311
|
+
if not image_url or not image_url.strip():
|
312
|
+
raise ValueError("Image URL cannot be empty.")
|
313
|
+
|
314
|
+
if not isinstance(index, int) or index < 1:
|
315
|
+
raise ValueError("Index must be a positive integer (1-based).")
|
316
|
+
|
317
|
+
if width is not None and (not isinstance(width, int | float) or width <= 0):
|
318
|
+
raise ValueError("Width must be a positive number.")
|
319
|
+
|
320
|
+
if height is not None and (not isinstance(height, int | float) or height <= 0):
|
321
|
+
raise ValueError("Height must be a positive number.")
|
322
|
+
|
323
|
+
docs_service = DocsService()
|
324
|
+
result = docs_service.insert_image(
|
325
|
+
document_id=document_id,
|
326
|
+
image_url=image_url,
|
327
|
+
index=index,
|
328
|
+
width=width,
|
329
|
+
height=height,
|
330
|
+
)
|
331
|
+
|
332
|
+
if isinstance(result, dict) and result.get("error"):
|
333
|
+
raise ValueError(result.get("message", "Error inserting image into document"))
|
334
|
+
|
335
|
+
if not result or not result.get("success"):
|
336
|
+
raise ValueError(f"Failed to insert image into document '{document_id}'.")
|
337
|
+
|
338
|
+
return result
|
@@ -42,7 +42,9 @@ async def drive_search_files(
|
|
42
42
|
raise ValueError("Query cannot be empty")
|
43
43
|
|
44
44
|
drive_service = DriveService()
|
45
|
-
files = drive_service.search_files(
|
45
|
+
files = drive_service.search_files(
|
46
|
+
query=query, page_size=page_size, shared_drive_id=shared_drive_id
|
47
|
+
)
|
46
48
|
|
47
49
|
if isinstance(files, dict) and files.get("error"):
|
48
50
|
raise ValueError(f"Search failed: {files.get('message', 'Unknown error')}")
|
@@ -82,18 +84,20 @@ async def drive_read_file_content(file_id: str) -> dict[str, Any]:
|
|
82
84
|
|
83
85
|
@mcp.tool(
|
84
86
|
name="drive_upload_file",
|
85
|
-
description="
|
87
|
+
description="Uploads a file to Google Drive by providing its content directly.",
|
86
88
|
)
|
87
89
|
async def drive_upload_file(
|
88
|
-
|
90
|
+
filename: str,
|
91
|
+
content_base64: str,
|
89
92
|
parent_folder_id: str | None = None,
|
90
93
|
shared_drive_id: str | None = None,
|
91
94
|
) -> dict[str, Any]:
|
92
95
|
"""
|
93
|
-
|
96
|
+
Uploads a file to Google Drive using its base64 encoded content.
|
94
97
|
|
95
98
|
Args:
|
96
|
-
|
99
|
+
filename: The desired name for the file in Google Drive (e.g., "report.pdf").
|
100
|
+
content_base64: The content of the file, encoded in base64.
|
97
101
|
parent_folder_id: Optional parent folder ID to upload the file to.
|
98
102
|
shared_drive_id: Optional shared drive ID to upload the file to a shared drive.
|
99
103
|
|
@@ -101,14 +105,17 @@ async def drive_upload_file(
|
|
101
105
|
A dictionary containing the uploaded file metadata or an error.
|
102
106
|
"""
|
103
107
|
logger.info(
|
104
|
-
f"Executing drive_upload_file with
|
108
|
+
f"Executing drive_upload_file with filename: '{filename}', parent_folder_id: {parent_folder_id}, shared_drive_id: {shared_drive_id}"
|
105
109
|
)
|
106
|
-
if not
|
107
|
-
raise ValueError("
|
110
|
+
if not filename or not filename.strip():
|
111
|
+
raise ValueError("Filename cannot be empty")
|
112
|
+
if not content_base64 or not content_base64.strip():
|
113
|
+
raise ValueError("File content (content_base64) cannot be empty")
|
108
114
|
|
109
115
|
drive_service = DriveService()
|
110
|
-
result = drive_service.
|
111
|
-
|
116
|
+
result = drive_service.upload_file_content(
|
117
|
+
filename=filename,
|
118
|
+
content_base64=content_base64,
|
112
119
|
parent_folder_id=parent_folder_id,
|
113
120
|
shared_drive_id=shared_drive_id,
|
114
121
|
)
|
@@ -154,7 +161,9 @@ async def drive_create_folder(
|
|
154
161
|
)
|
155
162
|
|
156
163
|
if isinstance(result, dict) and result.get("error"):
|
157
|
-
raise ValueError(
|
164
|
+
raise ValueError(
|
165
|
+
f"Folder creation failed: {result.get('message', 'Unknown error')}"
|
166
|
+
)
|
158
167
|
|
159
168
|
return result
|
160
169
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: google-workspace-mcp
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.4
|
4
4
|
Summary: MCP server for Google Workspace integration
|
5
5
|
Author-email: Arclio Team <info@arclio.com>
|
6
6
|
License: MIT
|
@@ -11,7 +11,7 @@ Requires-Dist: google-auth-httplib2>=0.1.0
|
|
11
11
|
Requires-Dist: google-auth-oauthlib>=1.0.0
|
12
12
|
Requires-Dist: google-auth>=2.22.0
|
13
13
|
Requires-Dist: markdown>=3.5.0
|
14
|
-
Requires-Dist: markdowndeck>=0.1.
|
14
|
+
Requires-Dist: markdowndeck>=0.1.3
|
15
15
|
Requires-Dist: mcp>=1.7.0
|
16
16
|
Requires-Dist: python-dotenv>=1.0.0
|
17
17
|
Requires-Dist: pytz>=2023.3
|
@@ -18,21 +18,21 @@ google_workspace_mcp/resources/slides.py,sha256=m2KVCYUOJ9uKMVWXXw9lXNem3OFT438X
|
|
18
18
|
google_workspace_mcp/services/__init__.py,sha256=crw4YinYFi7QDfJZUCcUqPliRtlViSE7QSqAX8j33eE,473
|
19
19
|
google_workspace_mcp/services/base.py,sha256=uLtFB158kaY_n3JWKgW2kxQ2tx9SP9zxX-PWuPxydFI,2378
|
20
20
|
google_workspace_mcp/services/calendar.py,sha256=_FvgDKfMvFWIamDHOimY3cVK2zPk0o7iUbbKeJehaHY,8370
|
21
|
-
google_workspace_mcp/services/docs_service.py,sha256=
|
22
|
-
google_workspace_mcp/services/drive.py,sha256=
|
21
|
+
google_workspace_mcp/services/docs_service.py,sha256=e67bvDb0lAmDbSXYRxPAIH93rXPbHS0Fq07c-eBcy08,25286
|
22
|
+
google_workspace_mcp/services/drive.py,sha256=9ReL9XkRLpK7ZS1KngHVv2ZGykeOo4N5ncp-gSYcAEA,16804
|
23
23
|
google_workspace_mcp/services/gmail.py,sha256=M6trjL9uFOe5WnBORyic4Y7lU4Z0X9VnEq-yU7RY-No,24846
|
24
24
|
google_workspace_mcp/services/sheets_service.py,sha256=67glYCTuQynka_vAXxCsM2U5SMuRNXXpTff2G-X-fT0,18068
|
25
25
|
google_workspace_mcp/services/slides.py,sha256=qfXh6GBr2JkCGPgVmwvV4NapLlzF7A1_WlYQR8EXMEE,37310
|
26
26
|
google_workspace_mcp/tools/__init__.py,sha256=vwa7hV8HwrLqs3Sf7RTrt1MlVZ-KjptXuFDSJt5tWzA,107
|
27
27
|
google_workspace_mcp/tools/calendar.py,sha256=KX42ZHNMcO69GuU-Re-37HY1Kk0jGM0dwUSrkexi_So,7890
|
28
|
-
google_workspace_mcp/tools/docs_tools.py,sha256=
|
29
|
-
google_workspace_mcp/tools/drive.py,sha256=
|
28
|
+
google_workspace_mcp/tools/docs_tools.py,sha256=L4zgjBAZxGYEnvNzUj_-leNXc1tIghzmDBinng2uMjI,12677
|
29
|
+
google_workspace_mcp/tools/drive.py,sha256=HUL_ozcTvWOIXKM0eQpLLYY6Yu_V2aFyhPVWW8GTpyw,7132
|
30
30
|
google_workspace_mcp/tools/gmail.py,sha256=nkbN6IC8yhSxrNwxUVRh9qAakomKJjGP4-MEKJoTnpA,11269
|
31
31
|
google_workspace_mcp/tools/sheets_tools.py,sha256=1lC_6oeXCZQDFI4f_hHrOZOnu-t6iVeQ2ErYwhS172Q,11760
|
32
32
|
google_workspace_mcp/tools/slides.py,sha256=GTtkIaQf6GMtzaxu8wlAemWcY5n5Y6RMEwWILPoiMFo,15382
|
33
33
|
google_workspace_mcp/utils/__init__.py,sha256=jKEfO2DhR_lwsRSoUgceixDwzkGVlp_vmbrz_6AHOk0,50
|
34
34
|
google_workspace_mcp/utils/markdown_slides.py,sha256=G7EbK3DDki90hNilGzbczs2e345v-HjYB5GWhFbEtw4,21015
|
35
|
-
google_workspace_mcp-1.0.
|
36
|
-
google_workspace_mcp-1.0.
|
37
|
-
google_workspace_mcp-1.0.
|
38
|
-
google_workspace_mcp-1.0.
|
35
|
+
google_workspace_mcp-1.0.4.dist-info/METADATA,sha256=wvx5j35AYP6PIi55dZYiokXZCU-t-xWjmK5XQjOVaD0,25104
|
36
|
+
google_workspace_mcp-1.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
37
|
+
google_workspace_mcp-1.0.4.dist-info/entry_points.txt,sha256=t9eBYTnGzEBpiiRx_SCTqforubwM6Ebf5mszIj-MjeA,79
|
38
|
+
google_workspace_mcp-1.0.4.dist-info/RECORD,,
|
File without changes
|
{google_workspace_mcp-1.0.2.dist-info → google_workspace_mcp-1.0.4.dist-info}/entry_points.txt
RENAMED
File without changes
|