google-workspace-mcp 1.1.5__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,9 +3,14 @@ Google Calendar tool handlers for Google Workspace MCP.
3
3
  """
4
4
 
5
5
  import logging
6
- from typing import Any
7
6
 
8
7
  from google_workspace_mcp.app import mcp # Import from central app module
8
+ from google_workspace_mcp.models import (
9
+ CalendarEventCreationOutput,
10
+ CalendarEventDeletionOutput,
11
+ CalendarEventDetailsOutput,
12
+ CalendarEventsOutput,
13
+ )
9
14
  from google_workspace_mcp.services.calendar import CalendarService
10
15
 
11
16
  logger = logging.getLogger(__name__)
@@ -16,6 +21,7 @@ logger = logging.getLogger(__name__)
16
21
 
17
22
  # @mcp.tool(
18
23
  # name="list_calendars",
24
+ # description="Lists all calendars accessible by the user.",
19
25
  # )
20
26
  # async def list_calendars() -> dict[str, Any]:
21
27
  # """
@@ -39,186 +45,196 @@ logger = logging.getLogger(__name__)
39
45
  # return {"count": len(calendars), "calendars": calendars}
40
46
 
41
47
 
42
- # @mcp.tool(
43
- # name="calendar_get_events",
44
- # )
48
+ @mcp.tool(
49
+ name="calendar_get_events",
50
+ description="Retrieve calendar events within a specified time range.",
51
+ )
45
52
  async def calendar_get_events(
46
53
  time_min: str,
47
54
  time_max: str,
48
55
  calendar_id: str = "primary",
49
56
  max_results: int = 250,
50
57
  show_deleted: bool = False,
51
- ) -> dict[str, Any]:
58
+ ) -> CalendarEventsOutput:
52
59
  """
53
60
  Retrieve calendar events within a specified time range.
54
61
 
55
62
  Args:
56
- time_min: Start time in RFC3339 format (e.g., "2024-01-01T00:00:00Z").
57
- time_max: End time in RFC3339 format (e.g., "2024-01-01T23:59:59Z").
58
- calendar_id: ID of the calendar (defaults to 'primary').
59
- max_results: Maximum number of events to return (default: 250).
60
- show_deleted: Whether to include deleted events (default: False).
63
+ time_min: Start of time range (ISO datetime string)
64
+ time_max: End of time range (ISO datetime string)
65
+ calendar_id: Calendar identifier (default: 'primary')
66
+ max_results: Maximum number of events to return
67
+ show_deleted: Whether to include deleted events
61
68
 
62
69
  Returns:
63
- A dictionary containing the list of events or an error message.
70
+ CalendarEventsOutput: Structured output containing the events data
64
71
  """
65
- logger.info(f"Executing calendar_get_events tool on calendar '{calendar_id}' between {time_min} and {time_max}")
66
-
67
- if not calendar_id:
68
- raise ValueError("calendar_id parameter is required")
69
- if not time_min:
70
- raise ValueError("time_min parameter is required")
71
- if not time_max:
72
- raise ValueError("time_max parameter is required")
72
+ logger.info(f"Executing calendar_get_events tool for calendar {calendar_id}")
73
73
 
74
74
  calendar_service = CalendarService()
75
- events = calendar_service.get_events(
76
- calendar_id=calendar_id,
75
+ result = calendar_service.get_events(
77
76
  time_min=time_min,
78
77
  time_max=time_max,
78
+ calendar_id=calendar_id,
79
79
  max_results=max_results,
80
80
  show_deleted=show_deleted,
81
81
  )
82
82
 
83
- if isinstance(events, dict) and events.get("error"):
84
- raise ValueError(events.get("message", "Error getting calendar events"))
83
+ # Check for errors in the service result
84
+ if isinstance(result, dict) and result.get("error"):
85
+ raise ValueError(result.get("message", "Error retrieving calendar events"))
85
86
 
87
+ # Extract events list - the service returns a dict with 'items' key containing events
88
+ events = result.get("items", []) if isinstance(result, dict) else result
86
89
  if not events:
87
- return {"message": "No events found for the specified time range."}
90
+ events = []
88
91
 
89
- # Return raw service result
90
- return {"count": len(events), "events": events}
92
+ # Return the Pydantic model instance
93
+ return CalendarEventsOutput(count=len(events), events=events)
91
94
 
92
95
 
93
- # @mcp.tool(
94
- # name="calendar_get_event_details",
95
- # )
96
- async def calendar_get_event_details(event_id: str, calendar_id: str = "primary") -> dict[str, Any]:
96
+ @mcp.tool(
97
+ name="calendar_get_event_details",
98
+ description="Retrieves detailed information for a specific calendar event by its ID.",
99
+ )
100
+ async def calendar_get_event_details(
101
+ event_id: str, calendar_id: str = "primary"
102
+ ) -> CalendarEventDetailsOutput:
97
103
  """
98
- Retrieves details for a specific event in a Google Calendar.
104
+ Retrieves detailed information for a specific calendar event by its ID.
99
105
 
100
106
  Args:
101
- event_id: The ID of the event to retrieve.
102
- calendar_id: The ID of the calendar containing the event. Defaults to 'primary'.
107
+ event_id: The unique identifier of the event
108
+ calendar_id: Calendar identifier (default: 'primary')
103
109
 
104
110
  Returns:
105
- A dictionary containing the event details (summary, start, end, description, attendees, etc.),
106
- or an error message.
111
+ CalendarEventDetailsOutput: Structured output containing the event details
107
112
  """
108
- logger.info(f"Executing calendar_get_event_details tool for event_id: '{event_id}', calendar_id: '{calendar_id}'")
109
- if not event_id or not event_id.strip():
110
- raise ValueError("Event ID cannot be empty.")
111
- if not calendar_id or not calendar_id.strip():
112
- raise ValueError("Calendar ID cannot be empty.") # Ensure calendar_id is also validated
113
+ logger.info(f"Executing calendar_get_event_details tool for event {event_id}")
113
114
 
114
115
  calendar_service = CalendarService()
115
- event_details = calendar_service.get_event_details(event_id=event_id, calendar_id=calendar_id)
116
+ result = calendar_service.get_event_details(
117
+ event_id=event_id, calendar_id=calendar_id
118
+ )
116
119
 
117
- if isinstance(event_details, dict) and event_details.get("error"):
118
- raise ValueError(event_details.get("message", "Error retrieving event details"))
120
+ # Check for errors in the service result
121
+ if isinstance(result, dict) and result.get("error"):
122
+ raise ValueError(result.get("message", "Error retrieving event details"))
119
123
 
120
- if not event_details: # Should be caught by error dict check
124
+ if not result: # Should be caught by error dict check
121
125
  raise ValueError(f"Failed to retrieve details for event '{event_id}'")
122
126
 
123
- return event_details
127
+ # Return the Pydantic model instance
128
+ return CalendarEventDetailsOutput(
129
+ id=result["id"],
130
+ summary=result.get("summary", ""),
131
+ start=result.get("start", {}),
132
+ end=result.get("end", {}),
133
+ description=result.get("description"),
134
+ attendees=result.get("attendees"),
135
+ location=result.get("location"),
136
+ )
124
137
 
125
138
 
126
- # @mcp.tool(
127
- # name="create_calendar_event",
128
- # )
139
+ @mcp.tool(
140
+ name="create_calendar_event",
141
+ description="Creates a new event in a specified Google Calendar.",
142
+ )
129
143
  async def create_calendar_event(
130
144
  summary: str,
131
145
  start_time: str,
132
146
  end_time: str,
133
147
  calendar_id: str = "primary",
134
- location: str | None = None,
135
- description: str | None = None,
136
- attendees: list[str] | None = None,
148
+ location: str = None,
149
+ description: str = None,
150
+ attendees: list[str] = None,
137
151
  send_notifications: bool = True,
138
- timezone: str | None = None,
139
- ) -> dict[str, Any]:
152
+ timezone: str = None,
153
+ ) -> CalendarEventCreationOutput:
140
154
  """
141
- Create a new calendar event.
155
+ Creates a new event in a specified Google Calendar.
142
156
 
143
157
  Args:
144
- summary: Title of the event.
145
- start_time: Start time in RFC3339 format (e.g. 2024-12-01T10:00:00Z).
146
- end_time: End time in RFC3339 format (e.g. 2024-12-01T11:00:00Z).
147
- calendar_id: Calendar ID (defaults to "primary").
148
- location: Location of the event (optional).
149
- description: Description or notes (optional).
150
- attendees: List of attendee email addresses (optional).
151
- send_notifications: Whether to send notifications to attendees (default True).
152
- timezone: Timezone for the event (e.g., 'America/New_York', defaults to UTC).
158
+ summary: Event title/summary
159
+ start_time: Event start time (ISO datetime string)
160
+ end_time: Event end time (ISO datetime string)
161
+ calendar_id: Calendar identifier (default: 'primary')
162
+ location: Event location (optional)
163
+ description: Event description (optional)
164
+ attendees: List of attendee email addresses (optional)
165
+ send_notifications: Whether to send notifications (default: True)
166
+ timezone: Timezone for the event (optional)
153
167
 
154
168
  Returns:
155
- A dictionary containing the created event details.
169
+ CalendarEventCreationOutput: Structured output containing the created event data
156
170
  """
157
- logger.info(f"Executing create_calendar_event on calendar '{calendar_id}'")
158
- if not summary or not start_time or not end_time:
159
- raise ValueError("Summary, start_time, and end_time are required")
171
+ logger.info(f"Executing create_calendar_event tool for summary: {summary}")
160
172
 
161
173
  calendar_service = CalendarService()
162
174
  result = calendar_service.create_event(
163
175
  summary=summary,
164
176
  start_time=start_time,
165
177
  end_time=end_time,
178
+ calendar_id=calendar_id,
166
179
  location=location,
167
180
  description=description,
168
181
  attendees=attendees,
169
182
  send_notifications=send_notifications,
170
183
  timezone=timezone,
171
- calendar_id=calendar_id,
172
184
  )
173
185
 
174
- if not result or (isinstance(result, dict) and result.get("error")):
175
- error_msg = "Error creating calendar event"
176
- if isinstance(result, dict):
177
- error_msg = result.get("message", error_msg)
178
- raise ValueError(error_msg)
186
+ # Check for errors in the service result
187
+ if isinstance(result, dict) and result.get("error"):
188
+ raise ValueError(result.get("message", "Error creating calendar event"))
179
189
 
180
- return result
190
+ if not result:
191
+ raise ValueError("Error creating calendar event")
181
192
 
193
+ # Return the Pydantic model instance
194
+ return CalendarEventCreationOutput(
195
+ id=result["id"],
196
+ html_link=result.get("htmlLink", ""),
197
+ summary=result.get("summary", summary),
198
+ start=result.get("start", {}),
199
+ end=result.get("end", {}),
200
+ )
182
201
 
183
- # @mcp.tool(
184
- # name="delete_calendar_event",
185
- # )
202
+
203
+ @mcp.tool(
204
+ name="delete_calendar_event",
205
+ description="Deletes an event from Google Calendar by its event ID.",
206
+ )
186
207
  async def delete_calendar_event(
187
208
  event_id: str,
188
209
  calendar_id: str = "primary",
189
210
  send_notifications: bool = True,
190
- ) -> dict[str, Any]:
211
+ ) -> CalendarEventDeletionOutput:
191
212
  """
192
- Delete a calendar event by its ID.
213
+ Deletes an event from Google Calendar by its event ID.
193
214
 
194
215
  Args:
195
- event_id: The ID of the event to delete.
196
- calendar_id: Calendar ID containing the event (defaults to "primary").
197
- send_notifications: Whether to send cancellation notifications (default True).
216
+ event_id: The unique identifier of the event to delete
217
+ calendar_id: Calendar identifier (default: 'primary')
218
+ send_notifications: Whether to send notifications (default: True)
198
219
 
199
220
  Returns:
200
- A dictionary confirming the deletion.
221
+ CalendarEventDeletionOutput: Structured output containing the deletion result
201
222
  """
202
- logger.info(f"Executing delete_calendar_event on calendar '{calendar_id}', event '{event_id}'")
203
- if not event_id:
204
- raise ValueError("Event ID is required")
223
+ logger.info(f"Executing delete_calendar_event tool for event {event_id}")
205
224
 
206
225
  calendar_service = CalendarService()
207
- success = calendar_service.delete_event(
226
+ result = calendar_service.delete_event(
208
227
  event_id=event_id,
209
- send_notifications=send_notifications,
210
228
  calendar_id=calendar_id,
229
+ send_notifications=send_notifications,
211
230
  )
212
231
 
213
- if not success:
214
- # Attempt to check if the service returned an error dict
215
- error_info = getattr(calendar_service, "last_error", None) # Hypothetical
216
- error_msg = "Failed to delete calendar event"
217
- if isinstance(error_info, dict) and error_info.get("error"):
218
- error_msg = error_info.get("message", error_msg)
219
- raise ValueError(error_msg)
220
-
221
- return {
222
- "message": f"Event with ID '{event_id}' deleted successfully from calendar '{calendar_id}'.",
223
- "success": True,
224
- }
232
+ # Check for errors in the service result
233
+ if isinstance(result, dict) and result.get("error"):
234
+ raise ValueError(result.get("message", "Error deleting calendar event"))
235
+
236
+ # Return the Pydantic model instance
237
+ return CalendarEventDeletionOutput(
238
+ message=f"Event with ID '{event_id}' deleted successfully from calendar '{calendar_id}'.",
239
+ success=True,
240
+ )
@@ -3,18 +3,26 @@ Google Docs tool handlers for Google Workspace MCP.
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
+ DocumentBatchUpdateOutput,
10
+ DocumentContentOutput,
11
+ DocumentCreationOutput,
12
+ DocumentImageInsertOutput,
13
+ DocumentMetadataOutput,
14
+ DocumentUpdateOutput,
15
+ )
9
16
  from google_workspace_mcp.services.docs_service import DocsService
10
17
 
11
18
  logger = logging.getLogger(__name__)
12
19
 
13
20
 
14
- # @mcp.tool(
15
- # name="docs_create_document",
16
- # )
17
- async def docs_create_document(title: str) -> dict[str, Any]:
21
+ @mcp.tool(
22
+ name="docs_create_document",
23
+ description="Creates a new Google Document with a specified title.",
24
+ )
25
+ async def docs_create_document(title: str) -> DocumentCreationOutput:
18
26
  """
19
27
  Creates a new, empty Google Document.
20
28
 
@@ -22,8 +30,7 @@ async def docs_create_document(title: str) -> dict[str, Any]:
22
30
  title: The title for the new Google Document.
23
31
 
24
32
  Returns:
25
- A dictionary containing the 'document_id', 'title', and 'document_link' of the created document,
26
- or an error message.
33
+ DocumentCreationOutput containing document_id, title, and document_link.
27
34
  """
28
35
  logger.info(f"Executing docs_create_document tool with title: '{title}'")
29
36
  if not title or not title.strip():
@@ -40,13 +47,18 @@ async def docs_create_document(title: str) -> dict[str, Any]:
40
47
  f"Failed to create document '{title}' or did not receive a document ID."
41
48
  )
42
49
 
43
- return result
50
+ return DocumentCreationOutput(
51
+ document_id=result["document_id"],
52
+ title=result["title"],
53
+ document_link=result["document_link"],
54
+ )
44
55
 
45
56
 
46
- # @mcp.tool(
47
- # name="docs_get_document_metadata",
48
- # )
49
- async def docs_get_document_metadata(document_id: str) -> dict[str, Any]:
57
+ @mcp.tool(
58
+ name="docs_get_document_metadata",
59
+ description="Retrieves metadata (like title and ID) for a Google Document.",
60
+ )
61
+ async def docs_get_document_metadata(document_id: str) -> DocumentMetadataOutput:
50
62
  """
51
63
  Retrieves metadata for a specific Google Document.
52
64
 
@@ -54,8 +66,7 @@ async def docs_get_document_metadata(document_id: str) -> dict[str, Any]:
54
66
  document_id: The ID of the Google Document.
55
67
 
56
68
  Returns:
57
- A dictionary containing the document's 'document_id', 'title', and 'document_link',
58
- or an error message.
69
+ DocumentMetadataOutput containing document_id, title, and document_link.
59
70
  """
60
71
  logger.info(
61
72
  f"Executing docs_get_document_metadata tool for document_id: '{document_id}'"
@@ -72,13 +83,18 @@ async def docs_get_document_metadata(document_id: str) -> dict[str, Any]:
72
83
  if not result or not result.get("document_id"):
73
84
  raise ValueError(f"Failed to retrieve metadata for document '{document_id}'.")
74
85
 
75
- return result
86
+ return DocumentMetadataOutput(
87
+ document_id=result["document_id"],
88
+ title=result["title"],
89
+ document_link=result["document_link"],
90
+ )
76
91
 
77
92
 
78
- # @mcp.tool(
79
- # name="docs_get_content_as_markdown",
80
- # )
81
- async def docs_get_content_as_markdown(document_id: str) -> dict[str, Any]:
93
+ @mcp.tool(
94
+ name="docs_get_content_as_markdown",
95
+ description="Retrieves the content of a Google Document, attempting to convert it to Markdown. Note: Direct Markdown export from Google Docs via Drive API is not officially guaranteed for all document complexities and may result in errors or suboptimal formatting. For critical conversions, consider exporting as HTML and using a dedicated Markdown conversion library.",
96
+ )
97
+ async def docs_get_content_as_markdown(document_id: str) -> DocumentContentOutput:
82
98
  """
83
99
  Retrieves the main textual content of a Google Document, converted to Markdown.
84
100
 
@@ -86,8 +102,7 @@ async def docs_get_content_as_markdown(document_id: str) -> dict[str, Any]:
86
102
  document_id: The ID of the Google Document.
87
103
 
88
104
  Returns:
89
- A dictionary containing the 'document_id' and 'markdown_content',
90
- or an error message.
105
+ DocumentContentOutput containing document_id and markdown_content.
91
106
  """
92
107
  logger.info(
93
108
  f"Executing docs_get_content_as_markdown tool for document_id: '{document_id}'"
@@ -108,15 +123,18 @@ async def docs_get_content_as_markdown(document_id: str) -> dict[str, Any]:
108
123
  f"Failed to retrieve Markdown content for document '{document_id}'."
109
124
  )
110
125
 
111
- return result
126
+ return DocumentContentOutput(
127
+ document_id=result["document_id"], markdown_content=result["markdown_content"]
128
+ )
112
129
 
113
130
 
114
- # @mcp.tool(
115
- # name="docs_append_text",
116
- # )
131
+ @mcp.tool(
132
+ name="docs_append_text",
133
+ description="Appends text to the end of a specified Google Document.",
134
+ )
117
135
  async def docs_append_text(
118
136
  document_id: str, text: str, ensure_newline: bool = True
119
- ) -> dict[str, Any]:
137
+ ) -> DocumentUpdateOutput:
120
138
  """
121
139
  Appends the given text to the end of the specified Google Document.
122
140
 
@@ -127,7 +145,7 @@ async def docs_append_text(
127
145
  if the document is not empty, to ensure the new text starts on a new line. (Default: True)
128
146
 
129
147
  Returns:
130
- A dictionary indicating success or an error message.
148
+ DocumentUpdateOutput indicating success or failure.
131
149
  """
132
150
  logger.info(f"Executing docs_append_text tool for document_id: '{document_id}'")
133
151
  if not document_id or not document_id.strip():
@@ -145,15 +163,20 @@ async def docs_append_text(
145
163
  if not result or not result.get("success"):
146
164
  raise ValueError(f"Failed to append text to document '{document_id}'.")
147
165
 
148
- return result
166
+ return DocumentUpdateOutput(
167
+ success=result["success"],
168
+ message=result.get("message", "Text appended successfully"),
169
+ updated_range=result.get("updated_range"),
170
+ )
149
171
 
150
172
 
151
- # @mcp.tool(
152
- # name="docs_prepend_text",
153
- # )
173
+ @mcp.tool(
174
+ name="docs_prepend_text",
175
+ description="Prepends text to the beginning of a specified Google Document.",
176
+ )
154
177
  async def docs_prepend_text(
155
178
  document_id: str, text: str, ensure_newline: bool = True
156
- ) -> dict[str, Any]:
179
+ ) -> DocumentUpdateOutput:
157
180
  """
158
181
  Prepends the given text to the beginning of the specified Google Document.
159
182
 
@@ -164,7 +187,7 @@ async def docs_prepend_text(
164
187
  if the document was not empty, to ensure existing content starts on a new line. (Default: True)
165
188
 
166
189
  Returns:
167
- A dictionary indicating success or an error message.
190
+ DocumentUpdateOutput indicating success or failure.
168
191
  """
169
192
  logger.info(f"Executing docs_prepend_text tool for document_id: '{document_id}'")
170
193
  if not document_id or not document_id.strip():
@@ -182,15 +205,20 @@ async def docs_prepend_text(
182
205
  if not result or not result.get("success"):
183
206
  raise ValueError(f"Failed to prepend text to document '{document_id}'.")
184
207
 
185
- return result
208
+ return DocumentUpdateOutput(
209
+ success=result["success"],
210
+ message=result.get("message", "Text prepended successfully"),
211
+ updated_range=result.get("updated_range"),
212
+ )
186
213
 
187
214
 
188
- # @mcp.tool(
189
- # name="docs_insert_text",
190
- # )
215
+ @mcp.tool(
216
+ name="docs_insert_text",
217
+ description="Inserts text at a specified location in a Google Document. For simple appends or prepends, use 'docs_append_text' or 'docs_prepend_text'.",
218
+ )
191
219
  async def docs_insert_text(
192
220
  document_id: str, text: str, index: int | None = None, segment_id: str | None = None
193
- ) -> dict[str, Any]:
221
+ ) -> DocumentUpdateOutput:
194
222
  """
195
223
  Inserts the given text at a specific location within the specified Google Document.
196
224
 
@@ -198,14 +226,14 @@ async def docs_insert_text(
198
226
  document_id: The ID of the Google Document.
199
227
  text: The text to insert.
200
228
  index: Optional. The 0-based index where the text should be inserted within the segment.
201
- For the main body, an index of 1 typically refers to the beginning of the content.
202
- Consult Google Docs API documentation for details on indexing if precise placement is needed.
203
- If omitted, defaults to a sensible starting position (e.g., beginning of the body).
229
+ For the main body, an index of 1 typically refers to the beginning of the content.
230
+ Consult Google Docs API documentation for details on indexing if precise placement is needed.
231
+ If omitted, defaults to a sensible starting position (e.g., beginning of the body).
204
232
  segment_id: Optional. The ID of a specific document segment (e.g., header, footer).
205
233
  If omitted, the text is inserted into the main document body.
206
234
 
207
235
  Returns:
208
- A dictionary indicating success or an error message.
236
+ DocumentUpdateOutput indicating success or failure.
209
237
  """
210
238
  logger.info(
211
239
  f"Executing docs_insert_text tool for document_id: '{document_id}' at index: {index}"
@@ -225,13 +253,20 @@ async def docs_insert_text(
225
253
  if not result or not result.get("success"):
226
254
  raise ValueError(f"Failed to insert text into document '{document_id}'.")
227
255
 
228
- return result
256
+ return DocumentUpdateOutput(
257
+ success=result["success"],
258
+ message=result.get("message", "Text inserted successfully"),
259
+ updated_range=result.get("updated_range"),
260
+ )
229
261
 
230
262
 
231
- # @mcp.tool(
232
- # name="docs_batch_update",
233
- # )
234
- async def docs_batch_update(document_id: str, requests: list[dict]) -> dict[str, Any]:
263
+ @mcp.tool(
264
+ name="docs_batch_update",
265
+ description="Applies a list of raw Google Docs API update requests to a document. For advanced users familiar with Docs API request structures.",
266
+ )
267
+ async def docs_batch_update(
268
+ document_id: str, requests: list[dict]
269
+ ) -> DocumentBatchUpdateOutput:
235
270
  """
236
271
  Applies a list of Google Docs API update requests to the specified document.
237
272
  This is an advanced tool; requests must conform to the Google Docs API format.
@@ -240,11 +275,10 @@ async def docs_batch_update(document_id: str, requests: list[dict]) -> dict[str,
240
275
  Args:
241
276
  document_id: The ID of the Google Document.
242
277
  requests: A list of request objects (as dictionaries) to apply.
243
- Example request: {"insertText": {"location": {"index": 1}, "text": "Hello"}}
278
+ Example request: {"insertText": {"location": {"index": 1}, "text": "Hello"}}
244
279
 
245
280
  Returns:
246
- A dictionary containing the API response from the batchUpdate call (includes replies for each request),
247
- or an error message.
281
+ DocumentBatchUpdateOutput containing the API response from the batchUpdate call.
248
282
  """
249
283
  logger.info(
250
284
  f"Executing docs_batch_update tool for document_id: '{document_id}' with {len(requests)} requests."
@@ -267,19 +301,23 @@ async def docs_batch_update(document_id: str, requests: list[dict]) -> dict[str,
267
301
  if not result: # Should be caught by error dict check
268
302
  raise ValueError(f"Failed to execute batch update on document '{document_id}'.")
269
303
 
270
- return result # Return the full response which includes documentId and replies
304
+ return DocumentBatchUpdateOutput(
305
+ document_id=result.get("documentId", document_id),
306
+ replies=result.get("replies", []),
307
+ )
271
308
 
272
309
 
273
- # @mcp.tool(
274
- # name="docs_insert_image",
275
- # )
310
+ @mcp.tool(
311
+ name="docs_insert_image",
312
+ 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.",
313
+ )
276
314
  async def docs_insert_image(
277
315
  document_id: str,
278
316
  image_url: str,
279
317
  index: int,
280
318
  width: float | None = None,
281
319
  height: float | None = None,
282
- ) -> dict[str, Any]:
320
+ ) -> DocumentImageInsertOutput:
283
321
  """
284
322
  Inserts an image into a Google Document from a URL at a specific index.
285
323
 
@@ -291,7 +329,7 @@ async def docs_insert_image(
291
329
  height: Optional height of the image in points (PT).
292
330
 
293
331
  Returns:
294
- A dictionary containing the inserted image ID and success status, or an error message.
332
+ DocumentImageInsertOutput containing the inserted image details.
295
333
  """
296
334
  logger.info(
297
335
  f"Executing docs_insert_image tool for document_id: '{document_id}' at index: {index}"
@@ -327,4 +365,8 @@ async def docs_insert_image(
327
365
  if not result or not result.get("success"):
328
366
  raise ValueError(f"Failed to insert image into document '{document_id}'.")
329
367
 
330
- return result
368
+ return DocumentImageInsertOutput(
369
+ success=result["success"],
370
+ image_id=result.get("image_id"),
371
+ message=result.get("message", "Image inserted successfully"),
372
+ )