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
@@ -3,9 +3,19 @@ Gmail tools for Google Workspace MCP operations.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import logging
|
6
|
-
from typing import Any
|
7
6
|
|
8
7
|
from google_workspace_mcp.app import mcp
|
8
|
+
from google_workspace_mcp.models import (
|
9
|
+
GmailAttachmentOutput,
|
10
|
+
GmailBulkDeleteOutput,
|
11
|
+
GmailDraftCreationOutput,
|
12
|
+
GmailDraftDeletionOutput,
|
13
|
+
GmailDraftSendOutput,
|
14
|
+
GmailEmailSearchOutput,
|
15
|
+
GmailMessageDetailsOutput,
|
16
|
+
GmailReplyOutput,
|
17
|
+
GmailSendOutput,
|
18
|
+
)
|
9
19
|
from google_workspace_mcp.services.gmail import GmailService
|
10
20
|
|
11
21
|
logger = logging.getLogger(__name__)
|
@@ -18,7 +28,9 @@ logger = logging.getLogger(__name__)
|
|
18
28
|
name="query_gmail_emails",
|
19
29
|
description="Query Gmail emails based on a search query.",
|
20
30
|
)
|
21
|
-
async def query_gmail_emails(
|
31
|
+
async def query_gmail_emails(
|
32
|
+
query: str, max_results: int = 100
|
33
|
+
) -> GmailEmailSearchOutput:
|
22
34
|
"""
|
23
35
|
Searches for Gmail emails using Gmail query syntax.
|
24
36
|
|
@@ -27,7 +39,7 @@ async def query_gmail_emails(query: str, max_results: int = 100) -> dict[str, An
|
|
27
39
|
max_results: Maximum number of emails to return.
|
28
40
|
|
29
41
|
Returns:
|
30
|
-
|
42
|
+
GmailEmailSearchOutput containing the list of matching emails.
|
31
43
|
"""
|
32
44
|
logger.info(f"Executing query_gmail_emails tool with query: '{query}'")
|
33
45
|
|
@@ -40,16 +52,16 @@ async def query_gmail_emails(query: str, max_results: int = 100) -> dict[str, An
|
|
40
52
|
|
41
53
|
# Return appropriate message if no results
|
42
54
|
if not emails:
|
43
|
-
|
55
|
+
emails = []
|
44
56
|
|
45
|
-
return
|
57
|
+
return GmailEmailSearchOutput(count=len(emails), emails=emails)
|
46
58
|
|
47
59
|
|
48
60
|
@mcp.tool(
|
49
61
|
name="gmail_get_message_details",
|
50
62
|
description="Retrieves a complete Gmail email message by its ID.",
|
51
63
|
)
|
52
|
-
async def gmail_get_message_details(email_id: str) ->
|
64
|
+
async def gmail_get_message_details(email_id: str) -> GmailMessageDetailsOutput:
|
53
65
|
"""
|
54
66
|
Retrieves a complete Gmail email message by its ID.
|
55
67
|
|
@@ -57,7 +69,7 @@ async def gmail_get_message_details(email_id: str) -> dict[str, Any]:
|
|
57
69
|
email_id: The ID of the Gmail message to retrieve.
|
58
70
|
|
59
71
|
Returns:
|
60
|
-
|
72
|
+
GmailMessageDetailsOutput containing the email details and attachments.
|
61
73
|
"""
|
62
74
|
logger.info(f"Executing gmail_get_message_details tool with email_id: '{email_id}'")
|
63
75
|
if not email_id or not email_id.strip():
|
@@ -74,14 +86,25 @@ async def gmail_get_message_details(email_id: str) -> dict[str, Any]:
|
|
74
86
|
if not result:
|
75
87
|
raise ValueError(f"Failed to retrieve email with ID: {email_id}")
|
76
88
|
|
77
|
-
return
|
89
|
+
return GmailMessageDetailsOutput(
|
90
|
+
id=result["id"],
|
91
|
+
thread_id=result.get("thread_id", ""),
|
92
|
+
subject=result.get("subject", ""),
|
93
|
+
from_email=result.get("from_email", ""),
|
94
|
+
to_email=result.get("to_email", []),
|
95
|
+
date=result.get("date", ""),
|
96
|
+
body=result.get("body", ""),
|
97
|
+
attachments=result.get("attachments"),
|
98
|
+
)
|
78
99
|
|
79
100
|
|
80
101
|
@mcp.tool(
|
81
102
|
name="gmail_get_attachment_content",
|
82
103
|
description="Retrieves a specific attachment from a Gmail message.",
|
83
104
|
)
|
84
|
-
async def gmail_get_attachment_content(
|
105
|
+
async def gmail_get_attachment_content(
|
106
|
+
message_id: str, attachment_id: str
|
107
|
+
) -> GmailAttachmentOutput:
|
85
108
|
"""
|
86
109
|
Retrieves a specific attachment from a Gmail message.
|
87
110
|
|
@@ -90,14 +113,18 @@ async def gmail_get_attachment_content(message_id: str, attachment_id: str) -> d
|
|
90
113
|
attachment_id: The ID of the attachment to retrieve.
|
91
114
|
|
92
115
|
Returns:
|
93
|
-
|
116
|
+
GmailAttachmentOutput containing filename, mimeType, size, and base64 data.
|
94
117
|
"""
|
95
|
-
logger.info(
|
118
|
+
logger.info(
|
119
|
+
f"Executing gmail_get_attachment_content tool - Msg: {message_id}, Attach: {attachment_id}"
|
120
|
+
)
|
96
121
|
if not message_id or not attachment_id:
|
97
122
|
raise ValueError("Message ID and attachment ID are required")
|
98
123
|
|
99
124
|
gmail_service = GmailService()
|
100
|
-
result = gmail_service.get_attachment_content(
|
125
|
+
result = gmail_service.get_attachment_content(
|
126
|
+
message_id=message_id, attachment_id=attachment_id
|
127
|
+
)
|
101
128
|
|
102
129
|
if not result or (isinstance(result, dict) and result.get("error")):
|
103
130
|
error_msg = "Error getting attachment"
|
@@ -105,8 +132,12 @@ async def gmail_get_attachment_content(message_id: str, attachment_id: str) -> d
|
|
105
132
|
error_msg = result.get("message", error_msg)
|
106
133
|
raise ValueError(error_msg)
|
107
134
|
|
108
|
-
|
109
|
-
|
135
|
+
return GmailAttachmentOutput(
|
136
|
+
filename=result["filename"],
|
137
|
+
mime_type=result["mime_type"],
|
138
|
+
size=result["size"],
|
139
|
+
data=result["data"],
|
140
|
+
)
|
110
141
|
|
111
142
|
|
112
143
|
@mcp.tool(
|
@@ -119,7 +150,7 @@ async def create_gmail_draft(
|
|
119
150
|
body: str,
|
120
151
|
cc: list[str] | None = None,
|
121
152
|
bcc: list[str] | None = None,
|
122
|
-
) ->
|
153
|
+
) -> GmailDraftCreationOutput:
|
123
154
|
"""
|
124
155
|
Creates a draft email message in Gmail.
|
125
156
|
|
@@ -131,7 +162,7 @@ async def create_gmail_draft(
|
|
131
162
|
bcc: Optional list of email addresses to BCC.
|
132
163
|
|
133
164
|
Returns:
|
134
|
-
|
165
|
+
GmailDraftCreationOutput containing the created draft details.
|
135
166
|
"""
|
136
167
|
logger.info("Executing create_gmail_draft")
|
137
168
|
if not to or not subject or not body: # Check for empty strings
|
@@ -139,7 +170,9 @@ async def create_gmail_draft(
|
|
139
170
|
|
140
171
|
gmail_service = GmailService()
|
141
172
|
# Pass bcc parameter even though service may not use it (for test compatibility)
|
142
|
-
result = gmail_service.create_draft(
|
173
|
+
result = gmail_service.create_draft(
|
174
|
+
to=to, subject=subject, body=body, cc=cc, bcc=bcc
|
175
|
+
)
|
143
176
|
|
144
177
|
if not result or (isinstance(result, dict) and result.get("error")):
|
145
178
|
error_msg = "Error creating draft"
|
@@ -147,7 +180,7 @@ async def create_gmail_draft(
|
|
147
180
|
error_msg = result.get("message", error_msg)
|
148
181
|
raise ValueError(error_msg)
|
149
182
|
|
150
|
-
return result
|
183
|
+
return GmailDraftCreationOutput(id=result["id"], message=result.get("message", {}))
|
151
184
|
|
152
185
|
|
153
186
|
@mcp.tool(
|
@@ -156,7 +189,7 @@ async def create_gmail_draft(
|
|
156
189
|
)
|
157
190
|
async def delete_gmail_draft(
|
158
191
|
draft_id: str,
|
159
|
-
) ->
|
192
|
+
) -> GmailDraftDeletionOutput:
|
160
193
|
"""
|
161
194
|
Deletes a specific draft email from Gmail.
|
162
195
|
|
@@ -164,7 +197,7 @@ async def delete_gmail_draft(
|
|
164
197
|
draft_id: The ID of the draft to delete.
|
165
198
|
|
166
199
|
Returns:
|
167
|
-
|
200
|
+
GmailDraftDeletionOutput confirming the deletion.
|
168
201
|
"""
|
169
202
|
logger.info(f"Executing delete_gmail_draft with draft_id: '{draft_id}'")
|
170
203
|
if not draft_id or not draft_id.strip():
|
@@ -177,23 +210,24 @@ async def delete_gmail_draft(
|
|
177
210
|
# Attempt to check if the service returned an error dict
|
178
211
|
# (Assuming handle_api_error might return dict or False/None)
|
179
212
|
# This part might need adjustment based on actual service error handling
|
180
|
-
error_info = getattr(
|
213
|
+
error_info = getattr(
|
214
|
+
gmail_service, "last_error", None
|
215
|
+
) # Hypothetical error capture
|
181
216
|
error_msg = "Failed to delete draft"
|
182
217
|
if isinstance(error_info, dict) and error_info.get("error"):
|
183
218
|
error_msg = error_info.get("message", error_msg)
|
184
219
|
raise ValueError(error_msg)
|
185
220
|
|
186
|
-
return
|
187
|
-
|
188
|
-
|
189
|
-
}
|
221
|
+
return GmailDraftDeletionOutput(
|
222
|
+
message=f"Draft with ID '{draft_id}' deleted successfully.", success=True
|
223
|
+
)
|
190
224
|
|
191
225
|
|
192
226
|
@mcp.tool(
|
193
227
|
name="gmail_send_draft",
|
194
228
|
description="Sends an existing draft email from Gmail.",
|
195
229
|
)
|
196
|
-
async def gmail_send_draft(draft_id: str) ->
|
230
|
+
async def gmail_send_draft(draft_id: str) -> GmailDraftSendOutput:
|
197
231
|
"""
|
198
232
|
Sends a specific draft email.
|
199
233
|
|
@@ -201,7 +235,7 @@ async def gmail_send_draft(draft_id: str) -> dict[str, Any]:
|
|
201
235
|
draft_id: The ID of the draft to send.
|
202
236
|
|
203
237
|
Returns:
|
204
|
-
|
238
|
+
GmailDraftSendOutput containing the details of the sent message.
|
205
239
|
"""
|
206
240
|
logger.info(f"Executing gmail_send_draft tool for draft_id: '{draft_id}'")
|
207
241
|
if not draft_id or not draft_id.strip():
|
@@ -216,7 +250,11 @@ async def gmail_send_draft(draft_id: str) -> dict[str, Any]:
|
|
216
250
|
if not result: # Should be caught by error dict check
|
217
251
|
raise ValueError(f"Failed to send draft '{draft_id}'")
|
218
252
|
|
219
|
-
return
|
253
|
+
return GmailDraftSendOutput(
|
254
|
+
id=result["id"],
|
255
|
+
thread_id=result.get("thread_id", ""),
|
256
|
+
label_ids=result.get("label_ids", []),
|
257
|
+
)
|
220
258
|
|
221
259
|
|
222
260
|
@mcp.tool(
|
@@ -228,7 +266,7 @@ async def gmail_reply_to_email(
|
|
228
266
|
reply_body: str,
|
229
267
|
send: bool = False,
|
230
268
|
reply_all: bool = False,
|
231
|
-
) ->
|
269
|
+
) -> GmailReplyOutput:
|
232
270
|
"""
|
233
271
|
Creates a reply to an existing email thread.
|
234
272
|
|
@@ -239,7 +277,7 @@ async def gmail_reply_to_email(
|
|
239
277
|
reply_all: If True, reply to all recipients. If False, reply to sender only.
|
240
278
|
|
241
279
|
Returns:
|
242
|
-
|
280
|
+
GmailReplyOutput containing the sent message or created draft details.
|
243
281
|
"""
|
244
282
|
logger.info(f"Executing gmail_reply_to_email to message: '{email_id}'")
|
245
283
|
if not email_id or not reply_body:
|
@@ -259,7 +297,9 @@ async def gmail_reply_to_email(
|
|
259
297
|
error_msg = result.get("message", error_msg)
|
260
298
|
raise ValueError(error_msg)
|
261
299
|
|
262
|
-
return
|
300
|
+
return GmailReplyOutput(
|
301
|
+
id=result["id"], thread_id=result.get("thread_id", ""), in_reply_to=email_id
|
302
|
+
)
|
263
303
|
|
264
304
|
|
265
305
|
@mcp.tool(
|
@@ -268,7 +308,7 @@ async def gmail_reply_to_email(
|
|
268
308
|
)
|
269
309
|
async def gmail_bulk_delete_messages(
|
270
310
|
message_ids: list[str],
|
271
|
-
) ->
|
311
|
+
) -> GmailBulkDeleteOutput:
|
272
312
|
"""
|
273
313
|
Deletes multiple Gmail emails using a list of message IDs.
|
274
314
|
|
@@ -276,7 +316,7 @@ async def gmail_bulk_delete_messages(
|
|
276
316
|
message_ids: A list of email message IDs to delete.
|
277
317
|
|
278
318
|
Returns:
|
279
|
-
|
319
|
+
GmailBulkDeleteOutput summarizing the deletion result.
|
280
320
|
"""
|
281
321
|
# Validation first - check if it's a list
|
282
322
|
if not isinstance(message_ids, list):
|
@@ -297,7 +337,13 @@ async def gmail_bulk_delete_messages(
|
|
297
337
|
error_msg = result.get("message", error_msg)
|
298
338
|
raise ValueError(error_msg)
|
299
339
|
|
300
|
-
return
|
340
|
+
return GmailBulkDeleteOutput(
|
341
|
+
deleted_count=result.get("deleted_count", len(message_ids)),
|
342
|
+
success=result.get("success", True),
|
343
|
+
message=result.get(
|
344
|
+
"message", f"Successfully deleted {len(message_ids)} messages"
|
345
|
+
),
|
346
|
+
)
|
301
347
|
|
302
348
|
|
303
349
|
@mcp.tool(
|
@@ -310,7 +356,7 @@ async def gmail_send_email(
|
|
310
356
|
body: str,
|
311
357
|
cc: list[str] | None = None,
|
312
358
|
bcc: list[str] | None = None,
|
313
|
-
) ->
|
359
|
+
) -> GmailSendOutput:
|
314
360
|
"""
|
315
361
|
Composes and sends an email message.
|
316
362
|
|
@@ -322,14 +368,20 @@ async def gmail_send_email(
|
|
322
368
|
bcc: Optional. A list of BCC recipient email addresses.
|
323
369
|
|
324
370
|
Returns:
|
325
|
-
|
371
|
+
GmailSendOutput containing the details of the sent message.
|
326
372
|
"""
|
327
373
|
logger.info(f"Executing gmail_send_email tool to: {to}, subject: '{subject}'")
|
328
|
-
if
|
374
|
+
if (
|
375
|
+
not to
|
376
|
+
or not isinstance(to, list)
|
377
|
+
or not all(isinstance(email, str) and email.strip() for email in to)
|
378
|
+
):
|
329
379
|
raise ValueError("Recipients 'to' must be a non-empty list of email strings.")
|
330
380
|
if not subject or not subject.strip():
|
331
381
|
raise ValueError("Subject cannot be empty.")
|
332
|
-
if
|
382
|
+
if (
|
383
|
+
body is None
|
384
|
+
): # Allow empty string for body, but not None if it implies missing arg.
|
333
385
|
raise ValueError("Body cannot be None (can be an empty string).")
|
334
386
|
|
335
387
|
gmail_service = GmailService()
|
@@ -341,4 +393,8 @@ async def gmail_send_email(
|
|
341
393
|
if not result:
|
342
394
|
raise ValueError("Failed to send email")
|
343
395
|
|
344
|
-
return
|
396
|
+
return GmailSendOutput(
|
397
|
+
id=result["id"],
|
398
|
+
thread_id=result.get("thread_id", ""),
|
399
|
+
label_ids=result.get("label_ids", []),
|
400
|
+
)
|
@@ -6,6 +6,15 @@ import logging
|
|
6
6
|
from typing import Any
|
7
7
|
|
8
8
|
from google_workspace_mcp.app import mcp
|
9
|
+
from google_workspace_mcp.models import (
|
10
|
+
SheetsAddSheetOutput,
|
11
|
+
SheetsAppendOutput,
|
12
|
+
SheetsClearOutput,
|
13
|
+
SheetsCreationOutput,
|
14
|
+
SheetsDeleteSheetOutput,
|
15
|
+
SheetsReadOutput,
|
16
|
+
SheetsWriteOutput,
|
17
|
+
)
|
9
18
|
from google_workspace_mcp.services.sheets_service import SheetsService
|
10
19
|
|
11
20
|
logger = logging.getLogger(__name__)
|
@@ -15,7 +24,7 @@ logger = logging.getLogger(__name__)
|
|
15
24
|
name="sheets_create_spreadsheet",
|
16
25
|
description="Creates a new Google Spreadsheet with a specified title.",
|
17
26
|
)
|
18
|
-
async def sheets_create_spreadsheet(title: str) ->
|
27
|
+
async def sheets_create_spreadsheet(title: str) -> SheetsCreationOutput:
|
19
28
|
"""
|
20
29
|
Creates a new, empty Google Spreadsheet.
|
21
30
|
|
@@ -23,8 +32,7 @@ async def sheets_create_spreadsheet(title: str) -> dict[str, Any]:
|
|
23
32
|
title: The title for the new Google Spreadsheet.
|
24
33
|
|
25
34
|
Returns:
|
26
|
-
|
27
|
-
of the created spreadsheet, or an error message.
|
35
|
+
SheetsCreationOutput containing the spreadsheet_id, title, and spreadsheet_url.
|
28
36
|
"""
|
29
37
|
logger.info(f"Executing sheets_create_spreadsheet tool with title: '{title}'")
|
30
38
|
if not title or not title.strip():
|
@@ -37,16 +45,22 @@ async def sheets_create_spreadsheet(title: str) -> dict[str, Any]:
|
|
37
45
|
raise ValueError(result.get("message", "Error creating spreadsheet"))
|
38
46
|
|
39
47
|
if not result or not result.get("spreadsheet_id"):
|
40
|
-
raise ValueError(
|
41
|
-
|
42
|
-
|
48
|
+
raise ValueError(
|
49
|
+
f"Failed to create spreadsheet '{title}' or did not receive a spreadsheet ID."
|
50
|
+
)
|
51
|
+
|
52
|
+
return SheetsCreationOutput(
|
53
|
+
spreadsheet_id=result["spreadsheet_id"],
|
54
|
+
title=result["title"],
|
55
|
+
spreadsheet_url=result["spreadsheet_url"],
|
56
|
+
)
|
43
57
|
|
44
58
|
|
45
59
|
@mcp.tool(
|
46
60
|
name="sheets_read_range",
|
47
61
|
description="Reads data from a specified range in a Google Spreadsheet (e.g., 'Sheet1!A1:B5').",
|
48
62
|
)
|
49
|
-
async def sheets_read_range(spreadsheet_id: str, range_a1: str) ->
|
63
|
+
async def sheets_read_range(spreadsheet_id: str, range_a1: str) -> SheetsReadOutput:
|
50
64
|
"""
|
51
65
|
Reads data from a given A1 notation range in a Google Spreadsheet.
|
52
66
|
|
@@ -56,10 +70,11 @@ async def sheets_read_range(spreadsheet_id: str, range_a1: str) -> dict[str, Any
|
|
56
70
|
to the first visible sheet or if sheet name is part of it).
|
57
71
|
|
58
72
|
Returns:
|
59
|
-
|
60
|
-
or an error message.
|
73
|
+
SheetsReadOutput containing the range and cell values.
|
61
74
|
"""
|
62
|
-
logger.info(
|
75
|
+
logger.info(
|
76
|
+
f"Executing sheets_read_range tool for spreadsheet_id: '{spreadsheet_id}', range: '{range_a1}'"
|
77
|
+
)
|
63
78
|
if not spreadsheet_id or not spreadsheet_id.strip():
|
64
79
|
raise ValueError("Spreadsheet ID cannot be empty.")
|
65
80
|
if not range_a1 or not range_a1.strip():
|
@@ -71,10 +86,18 @@ async def sheets_read_range(spreadsheet_id: str, range_a1: str) -> dict[str, Any
|
|
71
86
|
if isinstance(result, dict) and result.get("error"):
|
72
87
|
raise ValueError(result.get("message", "Error reading range from spreadsheet"))
|
73
88
|
|
74
|
-
if
|
75
|
-
|
76
|
-
|
77
|
-
|
89
|
+
if (
|
90
|
+
not result or "values" not in result
|
91
|
+
): # Check for 'values' as it's key for successful read
|
92
|
+
raise ValueError(
|
93
|
+
f"Failed to read range '{range_a1}' from spreadsheet '{spreadsheet_id}'."
|
94
|
+
)
|
95
|
+
|
96
|
+
return SheetsReadOutput(
|
97
|
+
range=result.get("range", range_a1),
|
98
|
+
values=result.get("values", []),
|
99
|
+
major_dimension=result.get("major_dimension", "ROWS"),
|
100
|
+
)
|
78
101
|
|
79
102
|
|
80
103
|
@mcp.tool(
|
@@ -86,7 +109,7 @@ async def sheets_write_range(
|
|
86
109
|
range_a1: str,
|
87
110
|
values: list[list[Any]],
|
88
111
|
value_input_option: str = "USER_ENTERED",
|
89
|
-
) ->
|
112
|
+
) -> SheetsWriteOutput:
|
90
113
|
"""
|
91
114
|
Writes data (list of lists) to a given A1 notation range in a Google Spreadsheet.
|
92
115
|
|
@@ -99,10 +122,11 @@ async def sheets_write_range(
|
|
99
122
|
"USER_ENTERED": Values parsed as if typed by user (e.g., formulas).
|
100
123
|
"RAW": Values taken literally. (Default: "USER_ENTERED")
|
101
124
|
Returns:
|
102
|
-
|
103
|
-
or an error message.
|
125
|
+
SheetsWriteOutput detailing the update.
|
104
126
|
"""
|
105
|
-
logger.info(
|
127
|
+
logger.info(
|
128
|
+
f"Executing sheets_write_range tool for spreadsheet_id: '{spreadsheet_id}', range: '{range_a1}'"
|
129
|
+
)
|
106
130
|
if not spreadsheet_id or not spreadsheet_id.strip():
|
107
131
|
raise ValueError("Spreadsheet ID cannot be empty.")
|
108
132
|
if not range_a1 or not range_a1.strip():
|
@@ -124,9 +148,16 @@ async def sheets_write_range(
|
|
124
148
|
raise ValueError(result.get("message", "Error writing to range in spreadsheet"))
|
125
149
|
|
126
150
|
if not result or not result.get("updated_range"):
|
127
|
-
raise ValueError(
|
128
|
-
|
129
|
-
|
151
|
+
raise ValueError(
|
152
|
+
f"Failed to write to range '{range_a1}' in spreadsheet '{spreadsheet_id}'."
|
153
|
+
)
|
154
|
+
|
155
|
+
return SheetsWriteOutput(
|
156
|
+
updated_range=result["updated_range"],
|
157
|
+
updated_rows=result.get("updated_rows", 0),
|
158
|
+
updated_columns=result.get("updated_columns", 0),
|
159
|
+
updated_cells=result.get("updated_cells", 0),
|
160
|
+
)
|
130
161
|
|
131
162
|
|
132
163
|
@mcp.tool(
|
@@ -139,7 +170,7 @@ async def sheets_append_rows(
|
|
139
170
|
values: list[list[Any]],
|
140
171
|
value_input_option: str = "USER_ENTERED",
|
141
172
|
insert_data_option: str = "INSERT_ROWS",
|
142
|
-
) ->
|
173
|
+
) -> SheetsAppendOutput:
|
143
174
|
"""
|
144
175
|
Appends rows of data to a sheet or table in a Google Spreadsheet.
|
145
176
|
|
@@ -152,10 +183,11 @@ async def sheets_append_rows(
|
|
152
183
|
insert_data_option: How new data should be inserted ("INSERT_ROWS" or "OVERWRITE"). Default: "INSERT_ROWS".
|
153
184
|
|
154
185
|
Returns:
|
155
|
-
|
156
|
-
or an error message.
|
186
|
+
SheetsAppendOutput detailing the append operation.
|
157
187
|
"""
|
158
|
-
logger.info(
|
188
|
+
logger.info(
|
189
|
+
f"Executing sheets_append_rows tool for spreadsheet_id: '{spreadsheet_id}', range: '{range_a1}'"
|
190
|
+
)
|
159
191
|
if not spreadsheet_id or not spreadsheet_id.strip():
|
160
192
|
raise ValueError("Spreadsheet ID cannot be empty.")
|
161
193
|
if not range_a1 or not range_a1.strip():
|
@@ -167,7 +199,9 @@ async def sheets_append_rows(
|
|
167
199
|
if value_input_option not in ["USER_ENTERED", "RAW"]:
|
168
200
|
raise ValueError("value_input_option must be either 'USER_ENTERED' or 'RAW'.")
|
169
201
|
if insert_data_option not in ["INSERT_ROWS", "OVERWRITE"]:
|
170
|
-
raise ValueError(
|
202
|
+
raise ValueError(
|
203
|
+
"insert_data_option must be either 'INSERT_ROWS' or 'OVERWRITE'."
|
204
|
+
)
|
171
205
|
|
172
206
|
sheets_service = SheetsService()
|
173
207
|
result = sheets_service.append_rows(
|
@@ -182,16 +216,22 @@ async def sheets_append_rows(
|
|
182
216
|
raise ValueError(result.get("message", "Error appending rows to spreadsheet"))
|
183
217
|
|
184
218
|
if not result: # Check for empty or None result as well
|
185
|
-
raise ValueError(
|
219
|
+
raise ValueError(
|
220
|
+
f"Failed to append rows to range '{range_a1}' in spreadsheet '{spreadsheet_id}'."
|
221
|
+
)
|
186
222
|
|
187
|
-
return
|
223
|
+
return SheetsAppendOutput(
|
224
|
+
spreadsheet_id=spreadsheet_id,
|
225
|
+
table_range=result.get("table_range", range_a1),
|
226
|
+
updates=result.get("updates", {}),
|
227
|
+
)
|
188
228
|
|
189
229
|
|
190
230
|
@mcp.tool(
|
191
231
|
name="sheets_clear_range",
|
192
232
|
description="Clears values from a specified range in a Google Spreadsheet (e.g., 'Sheet1!A1:B5').",
|
193
233
|
)
|
194
|
-
async def sheets_clear_range(spreadsheet_id: str, range_a1: str) ->
|
234
|
+
async def sheets_clear_range(spreadsheet_id: str, range_a1: str) -> SheetsClearOutput:
|
195
235
|
"""
|
196
236
|
Clears all values from a given A1 notation range in a Google Spreadsheet.
|
197
237
|
Note: This usually clears only the values, not formatting.
|
@@ -201,31 +241,39 @@ async def sheets_clear_range(spreadsheet_id: str, range_a1: str) -> dict[str, An
|
|
201
241
|
range_a1: The A1 notation of the range to clear (e.g., "Sheet1!A1:B5").
|
202
242
|
|
203
243
|
Returns:
|
204
|
-
|
244
|
+
SheetsClearOutput confirming the cleared range.
|
205
245
|
"""
|
206
|
-
logger.info(
|
246
|
+
logger.info(
|
247
|
+
f"Executing sheets_clear_range tool for spreadsheet_id: '{spreadsheet_id}', range: '{range_a1}'"
|
248
|
+
)
|
207
249
|
if not spreadsheet_id or not spreadsheet_id.strip():
|
208
250
|
raise ValueError("Spreadsheet ID cannot be empty.")
|
209
251
|
if not range_a1 or not range_a1.strip():
|
210
252
|
raise ValueError("Range (A1 notation) cannot be empty.")
|
211
253
|
|
212
254
|
sheets_service = SheetsService()
|
213
|
-
result = sheets_service.clear_range(
|
255
|
+
result = sheets_service.clear_range(
|
256
|
+
spreadsheet_id=spreadsheet_id, range_a1=range_a1
|
257
|
+
)
|
214
258
|
|
215
259
|
if isinstance(result, dict) and result.get("error"):
|
216
260
|
raise ValueError(result.get("message", "Error clearing range in spreadsheet"))
|
217
261
|
|
218
262
|
if not result or not result.get("cleared_range"):
|
219
|
-
raise ValueError(
|
263
|
+
raise ValueError(
|
264
|
+
f"Failed to clear range '{range_a1}' in spreadsheet '{spreadsheet_id}'."
|
265
|
+
)
|
220
266
|
|
221
|
-
return
|
267
|
+
return SheetsClearOutput(
|
268
|
+
cleared_range=result["cleared_range"], spreadsheet_id=spreadsheet_id
|
269
|
+
)
|
222
270
|
|
223
271
|
|
224
272
|
@mcp.tool(
|
225
273
|
name="sheets_add_sheet",
|
226
274
|
description="Adds a new sheet (tab) to an existing Google Spreadsheet.",
|
227
275
|
)
|
228
|
-
async def sheets_add_sheet(spreadsheet_id: str, title: str) ->
|
276
|
+
async def sheets_add_sheet(spreadsheet_id: str, title: str) -> SheetsAddSheetOutput:
|
229
277
|
"""
|
230
278
|
Adds a new sheet with the given title to the specified spreadsheet.
|
231
279
|
|
@@ -234,10 +282,11 @@ async def sheets_add_sheet(spreadsheet_id: str, title: str) -> dict[str, Any]:
|
|
234
282
|
title: The title for the new sheet.
|
235
283
|
|
236
284
|
Returns:
|
237
|
-
|
238
|
-
or an error message.
|
285
|
+
SheetsAddSheetOutput containing properties of the newly created sheet.
|
239
286
|
"""
|
240
|
-
logger.info(
|
287
|
+
logger.info(
|
288
|
+
f"Executing sheets_add_sheet tool for spreadsheet_id: '{spreadsheet_id}', title: '{title}'"
|
289
|
+
)
|
241
290
|
if not spreadsheet_id or not spreadsheet_id.strip():
|
242
291
|
raise ValueError("Spreadsheet ID cannot be empty.")
|
243
292
|
if not title or not title.strip():
|
@@ -250,16 +299,22 @@ async def sheets_add_sheet(spreadsheet_id: str, title: str) -> dict[str, Any]:
|
|
250
299
|
raise ValueError(result.get("message", "Error adding sheet to spreadsheet"))
|
251
300
|
|
252
301
|
if not result or not result.get("sheet_properties"):
|
253
|
-
raise ValueError(
|
302
|
+
raise ValueError(
|
303
|
+
f"Failed to add sheet '{title}' to spreadsheet '{spreadsheet_id}'."
|
304
|
+
)
|
254
305
|
|
255
|
-
return
|
306
|
+
return SheetsAddSheetOutput(
|
307
|
+
sheet_properties=result["sheet_properties"], spreadsheet_id=spreadsheet_id
|
308
|
+
)
|
256
309
|
|
257
310
|
|
258
311
|
@mcp.tool(
|
259
312
|
name="sheets_delete_sheet",
|
260
313
|
description="Deletes a specific sheet (tab) from a Google Spreadsheet using its numeric sheet ID.",
|
261
314
|
)
|
262
|
-
async def sheets_delete_sheet(
|
315
|
+
async def sheets_delete_sheet(
|
316
|
+
spreadsheet_id: str, sheet_id: int
|
317
|
+
) -> SheetsDeleteSheetOutput:
|
263
318
|
"""
|
264
319
|
Deletes a sheet from the specified spreadsheet using its numeric ID.
|
265
320
|
|
@@ -268,21 +323,32 @@ async def sheets_delete_sheet(spreadsheet_id: str, sheet_id: int) -> dict[str, A
|
|
268
323
|
sheet_id: The numeric ID of the sheet to delete.
|
269
324
|
|
270
325
|
Returns:
|
271
|
-
|
326
|
+
SheetsDeleteSheetOutput confirming the deletion.
|
272
327
|
"""
|
273
|
-
logger.info(
|
328
|
+
logger.info(
|
329
|
+
f"Executing sheets_delete_sheet tool for spreadsheet_id: '{spreadsheet_id}', sheet_id: {sheet_id}"
|
330
|
+
)
|
274
331
|
if not spreadsheet_id or not spreadsheet_id.strip():
|
275
332
|
raise ValueError("Spreadsheet ID cannot be empty.")
|
276
333
|
if not isinstance(sheet_id, int):
|
277
334
|
raise ValueError("Sheet ID must be an integer.")
|
278
335
|
|
279
336
|
sheets_service = SheetsService()
|
280
|
-
result = sheets_service.delete_sheet(
|
337
|
+
result = sheets_service.delete_sheet(
|
338
|
+
spreadsheet_id=spreadsheet_id, sheet_id=sheet_id
|
339
|
+
)
|
281
340
|
|
282
341
|
if isinstance(result, dict) and result.get("error"):
|
283
342
|
raise ValueError(result.get("message", "Error deleting sheet from spreadsheet"))
|
284
343
|
|
285
344
|
if not result or not result.get("success"):
|
286
|
-
raise ValueError(
|
345
|
+
raise ValueError(
|
346
|
+
f"Failed to delete sheet ID '{sheet_id}' from spreadsheet '{spreadsheet_id}'."
|
347
|
+
)
|
287
348
|
|
288
|
-
return
|
349
|
+
return SheetsDeleteSheetOutput(
|
350
|
+
success=result["success"],
|
351
|
+
message=result.get("message", f"Sheet ID '{sheet_id}' deleted successfully"),
|
352
|
+
spreadsheet_id=spreadsheet_id,
|
353
|
+
deleted_sheet_id=sheet_id,
|
354
|
+
)
|