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.
@@ -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(query: str, max_results: int = 100) -> dict[str, Any]:
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
- A dictionary containing the list of matching emails or an error message.
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
- return {"message": "No emails found for the query."}
55
+ emails = []
44
56
 
45
- return {"count": len(emails), "emails": emails}
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) -> dict[str, Any]:
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
- A dictionary containing the email details and attachments.
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 result
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(message_id: str, attachment_id: str) -> dict[str, Any]:
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
- A dictionary containing filename, mimeType, size, and base64 data.
116
+ GmailAttachmentOutput containing filename, mimeType, size, and base64 data.
94
117
  """
95
- logger.info(f"Executing gmail_get_attachment_content tool - Msg: {message_id}, Attach: {attachment_id}")
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(message_id=message_id, attachment_id=attachment_id)
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
- # FastMCP should handle this dict, recognizing 'data' as content blob.
109
- return result
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
- ) -> dict[str, Any]:
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
- A dictionary containing the created draft details.
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(to=to, subject=subject, body=body, cc=cc, bcc=bcc)
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
- ) -> dict[str, Any]:
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
- A dictionary confirming the deletion.
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(gmail_service, "last_error", None) # Hypothetical error capture
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
- "message": f"Draft with ID '{draft_id}' deleted successfully.",
188
- "success": True,
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) -> dict[str, Any]:
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
- A dictionary containing the details of the sent message or an error.
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 result
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
- ) -> dict[str, Any]:
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
- A dictionary containing the sent message or created draft details.
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 result
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
- ) -> dict[str, Any]:
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
- A dictionary summarizing the deletion result.
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 result
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
- ) -> dict[str, Any]:
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
- A dictionary containing the details of the sent message or an error.
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 not to or not isinstance(to, list) or not all(isinstance(email, str) and email.strip() for email in to):
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 body is None: # Allow empty string for body, but not None if it implies missing arg.
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 result
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) -> dict[str, Any]:
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
- A dictionary containing the 'spreadsheet_id', 'title', and 'spreadsheet_url'
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(f"Failed to create spreadsheet '{title}' or did not receive a spreadsheet ID.")
41
-
42
- return result
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) -> dict[str, Any]:
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
- A dictionary containing the range and a list of lists representing the cell values,
60
- or an error message.
73
+ SheetsReadOutput containing the range and cell values.
61
74
  """
62
- logger.info(f"Executing sheets_read_range tool for spreadsheet_id: '{spreadsheet_id}', range: '{range_a1}'")
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 not result or "values" not in result: # Check for 'values' as it's key for successful read
75
- raise ValueError(f"Failed to read range '{range_a1}' from spreadsheet '{spreadsheet_id}'.")
76
-
77
- return result
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
- ) -> dict[str, Any]:
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
- A dictionary detailing the update (updated range, number of cells, etc.),
103
- or an error message.
125
+ SheetsWriteOutput detailing the update.
104
126
  """
105
- logger.info(f"Executing sheets_write_range tool for spreadsheet_id: '{spreadsheet_id}', range: '{range_a1}'")
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(f"Failed to write to range '{range_a1}' in spreadsheet '{spreadsheet_id}'.")
128
-
129
- return result
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
- ) -> dict[str, Any]:
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
- A dictionary detailing the append operation (e.g., range of appended data),
156
- or an error message.
186
+ SheetsAppendOutput detailing the append operation.
157
187
  """
158
- logger.info(f"Executing sheets_append_rows tool for spreadsheet_id: '{spreadsheet_id}', range: '{range_a1}'")
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("insert_data_option must be either 'INSERT_ROWS' or 'OVERWRITE'.")
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(f"Failed to append rows to range '{range_a1}' in spreadsheet '{spreadsheet_id}'.")
219
+ raise ValueError(
220
+ f"Failed to append rows to range '{range_a1}' in spreadsheet '{spreadsheet_id}'."
221
+ )
186
222
 
187
- return result
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) -> dict[str, Any]:
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
- A dictionary confirming the cleared range, or an error message.
244
+ SheetsClearOutput confirming the cleared range.
205
245
  """
206
- logger.info(f"Executing sheets_clear_range tool for spreadsheet_id: '{spreadsheet_id}', range: '{range_a1}'")
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(spreadsheet_id=spreadsheet_id, range_a1=range_a1)
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(f"Failed to clear range '{range_a1}' in spreadsheet '{spreadsheet_id}'.")
263
+ raise ValueError(
264
+ f"Failed to clear range '{range_a1}' in spreadsheet '{spreadsheet_id}'."
265
+ )
220
266
 
221
- return result
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) -> dict[str, Any]:
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
- A dictionary containing properties of the newly created sheet (like sheetId, title, index),
238
- or an error message.
285
+ SheetsAddSheetOutput containing properties of the newly created sheet.
239
286
  """
240
- logger.info(f"Executing sheets_add_sheet tool for spreadsheet_id: '{spreadsheet_id}', title: '{title}'")
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(f"Failed to add sheet '{title}' to spreadsheet '{spreadsheet_id}'.")
302
+ raise ValueError(
303
+ f"Failed to add sheet '{title}' to spreadsheet '{spreadsheet_id}'."
304
+ )
254
305
 
255
- return result
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(spreadsheet_id: str, sheet_id: int) -> dict[str, Any]:
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
- A dictionary confirming the deletion or an error message.
326
+ SheetsDeleteSheetOutput confirming the deletion.
272
327
  """
273
- logger.info(f"Executing sheets_delete_sheet tool for spreadsheet_id: '{spreadsheet_id}', sheet_id: {sheet_id}")
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(spreadsheet_id=spreadsheet_id, sheet_id=sheet_id)
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(f"Failed to delete sheet ID '{sheet_id}' from spreadsheet '{spreadsheet_id}'.")
345
+ raise ValueError(
346
+ f"Failed to delete sheet ID '{sheet_id}' from spreadsheet '{spreadsheet_id}'."
347
+ )
287
348
 
288
- return result
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
+ )