workspace-mcp 1.0.1__py3-none-any.whl → 1.0.2__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.
- auth/service_decorator.py +31 -32
- core/utils.py +36 -0
- gcalendar/calendar_tools.py +308 -258
- gchat/chat_tools.py +131 -158
- gdocs/docs_tools.py +121 -149
- gdrive/drive_tools.py +168 -171
- gforms/forms_tools.py +118 -157
- gmail/gmail_tools.py +319 -400
- gsheets/sheets_tools.py +144 -197
- gslides/slides_tools.py +113 -157
- main.py +30 -24
- {workspace_mcp-1.0.1.dist-info → workspace_mcp-1.0.2.dist-info}/METADATA +6 -3
- {workspace_mcp-1.0.1.dist-info → workspace_mcp-1.0.2.dist-info}/RECORD +17 -17
- {workspace_mcp-1.0.1.dist-info → workspace_mcp-1.0.2.dist-info}/WHEEL +0 -0
- {workspace_mcp-1.0.1.dist-info → workspace_mcp-1.0.2.dist-info}/entry_points.txt +0 -0
- {workspace_mcp-1.0.1.dist-info → workspace_mcp-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {workspace_mcp-1.0.1.dist-info → workspace_mcp-1.0.2.dist-info}/top_level.txt +0 -0
gcalendar/calendar_tools.py
CHANGED
@@ -7,14 +7,14 @@ This module provides MCP tools for interacting with Google Calendar API.
|
|
7
7
|
import datetime
|
8
8
|
import logging
|
9
9
|
import asyncio
|
10
|
-
import
|
11
|
-
import sys
|
10
|
+
import re
|
12
11
|
from typing import List, Optional, Dict, Any
|
13
12
|
|
14
13
|
from mcp import types
|
15
14
|
from googleapiclient.errors import HttpError
|
16
15
|
|
17
16
|
from auth.service_decorator import require_google_service
|
17
|
+
from core.utils import handle_http_errors
|
18
18
|
|
19
19
|
from core.server import server
|
20
20
|
|
@@ -30,7 +30,6 @@ def _correct_time_format_for_api(
|
|
30
30
|
if not time_str:
|
31
31
|
return None
|
32
32
|
|
33
|
-
# Log the incoming time string for debugging
|
34
33
|
logger.info(
|
35
34
|
f"_correct_time_format_for_api: Processing {param_name} with value '{time_str}'"
|
36
35
|
)
|
@@ -81,6 +80,7 @@ def _correct_time_format_for_api(
|
|
81
80
|
|
82
81
|
@server.tool()
|
83
82
|
@require_google_service("calendar", "calendar_read")
|
83
|
+
@handle_http_errors("list_calendars")
|
84
84
|
async def list_calendars(service, user_google_email: str) -> str:
|
85
85
|
"""
|
86
86
|
Retrieves a list of calendars accessible to the authenticated user.
|
@@ -93,36 +93,28 @@ async def list_calendars(service, user_google_email: str) -> str:
|
|
93
93
|
"""
|
94
94
|
logger.info(f"[list_calendars] Invoked. Email: '{user_google_email}'")
|
95
95
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
return text_output
|
114
|
-
except HttpError as error:
|
115
|
-
message = f"API error listing calendars: {error}. You might need to re-authenticate. LLM: Try 'start_google_auth' with user's email and service_name='Google Calendar'."
|
116
|
-
logger.error(message, exc_info=True)
|
117
|
-
raise Exception(message)
|
118
|
-
except Exception as e:
|
119
|
-
message = f"Unexpected error listing calendars: {e}."
|
120
|
-
logger.exception(message)
|
121
|
-
raise Exception(message)
|
96
|
+
calendar_list_response = await asyncio.to_thread(
|
97
|
+
lambda: service.calendarList().list().execute()
|
98
|
+
)
|
99
|
+
items = calendar_list_response.get("items", [])
|
100
|
+
if not items:
|
101
|
+
return f"No calendars found for {user_google_email}."
|
102
|
+
|
103
|
+
calendars_summary_list = [
|
104
|
+
f"- \"{cal.get('summary', 'No Summary')}\"{' (Primary)' if cal.get('primary') else ''} (ID: {cal['id']})"
|
105
|
+
for cal in items
|
106
|
+
]
|
107
|
+
text_output = (
|
108
|
+
f"Successfully listed {len(items)} calendars for {user_google_email}:\n"
|
109
|
+
+ "\n".join(calendars_summary_list)
|
110
|
+
)
|
111
|
+
logger.info(f"Successfully listed {len(items)} calendars for {user_google_email}.")
|
112
|
+
return text_output
|
122
113
|
|
123
114
|
|
124
115
|
@server.tool()
|
125
116
|
@require_google_service("calendar", "calendar_read")
|
117
|
+
@handle_http_errors("get_events")
|
126
118
|
async def get_events(
|
127
119
|
service,
|
128
120
|
user_google_email: str,
|
@@ -142,83 +134,75 @@ async def get_events(
|
|
142
134
|
max_results (int): The maximum number of events to return. Defaults to 25.
|
143
135
|
|
144
136
|
Returns:
|
145
|
-
str: A formatted list of events (summary, start
|
137
|
+
str: A formatted list of events (summary, start and end times, link) within the specified range.
|
146
138
|
"""
|
147
|
-
|
139
|
+
logger.info(
|
140
|
+
f"[get_events] Raw time parameters - time_min: '{time_min}', time_max: '{time_max}'"
|
141
|
+
)
|
142
|
+
|
143
|
+
# Ensure time_min and time_max are correctly formatted for the API
|
144
|
+
formatted_time_min = _correct_time_format_for_api(time_min, "time_min")
|
145
|
+
effective_time_min = formatted_time_min or (
|
146
|
+
datetime.datetime.utcnow().isoformat() + "Z"
|
147
|
+
)
|
148
|
+
if time_min is None:
|
148
149
|
logger.info(
|
149
|
-
f"
|
150
|
+
f"time_min not provided, defaulting to current UTC time: {effective_time_min}"
|
150
151
|
)
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
effective_time_min = formatted_time_min or (
|
155
|
-
datetime.datetime.utcnow().isoformat() + "Z"
|
152
|
+
else:
|
153
|
+
logger.info(
|
154
|
+
f"time_min processing: original='{time_min}', formatted='{formatted_time_min}', effective='{effective_time_min}'"
|
156
155
|
)
|
157
|
-
if time_min is None:
|
158
|
-
logger.info(
|
159
|
-
f"time_min not provided, defaulting to current UTC time: {effective_time_min}"
|
160
|
-
)
|
161
|
-
else:
|
162
|
-
logger.info(
|
163
|
-
f"time_min processing: original='{time_min}', formatted='{formatted_time_min}', effective='{effective_time_min}'"
|
164
|
-
)
|
165
|
-
|
166
|
-
effective_time_max = _correct_time_format_for_api(time_max, "time_max")
|
167
|
-
if time_max:
|
168
|
-
logger.info(
|
169
|
-
f"time_max processing: original='{time_max}', formatted='{effective_time_max}'"
|
170
|
-
)
|
171
156
|
|
172
|
-
|
157
|
+
effective_time_max = _correct_time_format_for_api(time_max, "time_max")
|
158
|
+
if time_max:
|
173
159
|
logger.info(
|
174
|
-
f"
|
160
|
+
f"time_max processing: original='{time_max}', formatted='{effective_time_max}'"
|
175
161
|
)
|
176
162
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
calendarId=calendar_id,
|
181
|
-
timeMin=effective_time_min,
|
182
|
-
timeMax=effective_time_max,
|
183
|
-
maxResults=max_results,
|
184
|
-
singleEvents=True,
|
185
|
-
orderBy="startTime",
|
186
|
-
)
|
187
|
-
.execute
|
188
|
-
)
|
189
|
-
items = events_result.get("items", [])
|
190
|
-
if not items:
|
191
|
-
return f"No events found in calendar '{calendar_id}' for {user_google_email} for the specified time range."
|
192
|
-
|
193
|
-
event_details_list = []
|
194
|
-
for item in items:
|
195
|
-
summary = item.get("summary", "No Title")
|
196
|
-
start = item["start"].get("dateTime", item["start"].get("date"))
|
197
|
-
link = item.get("htmlLink", "No Link")
|
198
|
-
event_id = item.get("id", "No ID")
|
199
|
-
# Include the event ID in the output so users can copy it for modify/delete operations
|
200
|
-
event_details_list.append(
|
201
|
-
f'- "{summary}" (Starts: {start}) ID: {event_id} | Link: {link}'
|
202
|
-
)
|
163
|
+
logger.info(
|
164
|
+
f"[get_events] Final API parameters - calendarId: '{calendar_id}', timeMin: '{effective_time_min}', timeMax: '{effective_time_max}', maxResults: {max_results}"
|
165
|
+
)
|
203
166
|
|
204
|
-
|
205
|
-
|
206
|
-
|
167
|
+
events_result = await asyncio.to_thread(
|
168
|
+
lambda: service.events()
|
169
|
+
.list(
|
170
|
+
calendarId=calendar_id,
|
171
|
+
timeMin=effective_time_min,
|
172
|
+
timeMax=effective_time_max,
|
173
|
+
maxResults=max_results,
|
174
|
+
singleEvents=True,
|
175
|
+
orderBy="startTime",
|
207
176
|
)
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
177
|
+
.execute()
|
178
|
+
)
|
179
|
+
items = events_result.get("items", [])
|
180
|
+
if not items:
|
181
|
+
return f"No events found in calendar '{calendar_id}' for {user_google_email} for the specified time range."
|
182
|
+
|
183
|
+
event_details_list = []
|
184
|
+
for item in items:
|
185
|
+
summary = item.get("summary", "No Title")
|
186
|
+
start_time = item["start"].get("dateTime", item["start"].get("date"))
|
187
|
+
end_time = item["end"].get("dateTime", item["end"].get("date"))
|
188
|
+
link = item.get("htmlLink", "No Link")
|
189
|
+
event_id = item.get("id", "No ID")
|
190
|
+
# Include the start/end date, and event ID in the output so users can copy it for modify/delete operations
|
191
|
+
event_details_list.append(
|
192
|
+
f'- "{summary}" (Starts: {start_time}, Ends: {end_time}) ID: {event_id} | Link: {link}'
|
193
|
+
)
|
194
|
+
|
195
|
+
text_output = (
|
196
|
+
f"Successfully retrieved {len(items)} events from calendar '{calendar_id}' for {user_google_email}:\n"
|
197
|
+
+ "\n".join(event_details_list)
|
198
|
+
)
|
199
|
+
logger.info(f"Successfully retrieved {len(items)} events for {user_google_email}.")
|
200
|
+
return text_output
|
218
201
|
|
219
202
|
|
220
203
|
@server.tool()
|
221
204
|
@require_google_service("calendar", "calendar_events")
|
205
|
+
@handle_http_errors("create_event")
|
222
206
|
async def create_event(
|
223
207
|
service,
|
224
208
|
user_google_email: str,
|
@@ -230,6 +214,7 @@ async def create_event(
|
|
230
214
|
location: Optional[str] = None,
|
231
215
|
attendees: Optional[List[str]] = None,
|
232
216
|
timezone: Optional[str] = None,
|
217
|
+
attachments: Optional[List[str]] = None,
|
233
218
|
) -> str:
|
234
219
|
"""
|
235
220
|
Creates a new event.
|
@@ -244,6 +229,7 @@ async def create_event(
|
|
244
229
|
location (Optional[str]): Event location.
|
245
230
|
attendees (Optional[List[str]]): Attendee email addresses.
|
246
231
|
timezone (Optional[str]): Timezone (e.g., "America/New_York").
|
232
|
+
attachments (Optional[List[str]]): List of Google Drive file URLs or IDs to attach to the event.
|
247
233
|
|
248
234
|
Returns:
|
249
235
|
str: Confirmation message of the successful event creation with event link.
|
@@ -251,53 +237,97 @@ async def create_event(
|
|
251
237
|
logger.info(
|
252
238
|
f"[create_event] Invoked. Email: '{user_google_email}', Summary: {summary}"
|
253
239
|
)
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
"
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
240
|
+
logger.info(f"[create_event] Incoming attachments param: {attachments}")
|
241
|
+
# If attachments value is a string, split by comma and strip whitespace
|
242
|
+
if attachments and isinstance(attachments, str):
|
243
|
+
attachments = [a.strip() for a in attachments.split(',') if a.strip()]
|
244
|
+
logger.info(f"[create_event] Parsed attachments list from string: {attachments}")
|
245
|
+
event_body: Dict[str, Any] = {
|
246
|
+
"summary": summary,
|
247
|
+
"start": (
|
248
|
+
{"date": start_time}
|
249
|
+
if "T" not in start_time
|
250
|
+
else {"dateTime": start_time}
|
251
|
+
),
|
252
|
+
"end": (
|
253
|
+
{"date": end_time} if "T" not in end_time else {"dateTime": end_time}
|
254
|
+
),
|
255
|
+
}
|
256
|
+
if location:
|
257
|
+
event_body["location"] = location
|
258
|
+
if description:
|
259
|
+
event_body["description"] = description
|
260
|
+
if timezone:
|
261
|
+
if "dateTime" in event_body["start"]:
|
262
|
+
event_body["start"]["timeZone"] = timezone
|
263
|
+
if "dateTime" in event_body["end"]:
|
264
|
+
event_body["end"]["timeZone"] = timezone
|
265
|
+
if attendees:
|
266
|
+
event_body["attendees"] = [{"email": email} for email in attendees]
|
267
|
+
|
268
|
+
if attachments:
|
269
|
+
# Accept both file URLs and file IDs. If a URL, extract the fileId.
|
270
|
+
event_body["attachments"] = []
|
271
|
+
from googleapiclient.discovery import build
|
272
|
+
drive_service = None
|
273
|
+
try:
|
274
|
+
drive_service = service._http and build("drive", "v3", http=service._http)
|
275
|
+
except Exception as e:
|
276
|
+
logger.warning(f"Could not build Drive service for MIME type lookup: {e}")
|
277
|
+
for att in attachments:
|
278
|
+
file_id = None
|
279
|
+
if att.startswith("https://"):
|
280
|
+
# Match /d/<id>, /file/d/<id>, ?id=<id>
|
281
|
+
match = re.search(r"(?:/d/|/file/d/|id=)([\w-]+)", att)
|
282
|
+
file_id = match.group(1) if match else None
|
283
|
+
logger.info(f"[create_event] Extracted file_id '{file_id}' from attachment URL '{att}'")
|
284
|
+
else:
|
285
|
+
file_id = att
|
286
|
+
logger.info(f"[create_event] Using direct file_id '{file_id}' for attachment")
|
287
|
+
if file_id:
|
288
|
+
file_url = f"https://drive.google.com/open?id={file_id}"
|
289
|
+
mime_type = "application/vnd.google-apps.drive-sdk"
|
290
|
+
title = "Drive Attachment"
|
291
|
+
# Try to get the actual MIME type and filename from Drive
|
292
|
+
if drive_service:
|
293
|
+
try:
|
294
|
+
file_metadata = await asyncio.to_thread(
|
295
|
+
lambda: drive_service.files().get(fileId=file_id, fields="mimeType,name").execute()
|
296
|
+
)
|
297
|
+
mime_type = file_metadata.get("mimeType", mime_type)
|
298
|
+
filename = file_metadata.get("name")
|
299
|
+
if filename:
|
300
|
+
title = filename
|
301
|
+
logger.info(f"[create_event] Using filename '{filename}' as attachment title")
|
302
|
+
else:
|
303
|
+
logger.info(f"[create_event] No filename found, using generic title")
|
304
|
+
except Exception as e:
|
305
|
+
logger.warning(f"Could not fetch metadata for file {file_id}: {e}")
|
306
|
+
event_body["attachments"].append({
|
307
|
+
"fileUrl": file_url,
|
308
|
+
"title": title,
|
309
|
+
"mimeType": mime_type,
|
310
|
+
})
|
279
311
|
created_event = await asyncio.to_thread(
|
280
|
-
service.events().insert(
|
312
|
+
lambda: service.events().insert(
|
313
|
+
calendarId=calendar_id, body=event_body, supportsAttachments=True
|
314
|
+
).execute()
|
281
315
|
)
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
316
|
+
else:
|
317
|
+
created_event = await asyncio.to_thread(
|
318
|
+
lambda: service.events().insert(calendarId=calendar_id, body=event_body).execute()
|
319
|
+
)
|
320
|
+
link = created_event.get("htmlLink", "No link available")
|
321
|
+
confirmation_message = f"Successfully created event '{created_event.get('summary', summary)}' for {user_google_email}. Link: {link}"
|
322
|
+
logger.info(
|
286
323
|
f"Event created successfully for {user_google_email}. ID: {created_event.get('id')}, Link: {link}"
|
287
324
|
)
|
288
|
-
|
289
|
-
except HttpError as error:
|
290
|
-
message = f"API error creating event: {error}. You might need to re-authenticate. LLM: Try 'start_google_auth' with the user's email ({user_google_email}) and service_name='Google Calendar'."
|
291
|
-
logger.error(message, exc_info=True)
|
292
|
-
raise Exception(message)
|
293
|
-
except Exception as e:
|
294
|
-
message = f"Unexpected error creating event: {e}."
|
295
|
-
logger.exception(message)
|
296
|
-
raise Exception(message)
|
325
|
+
return confirmation_message
|
297
326
|
|
298
327
|
|
299
328
|
@server.tool()
|
300
329
|
@require_google_service("calendar", "calendar_events")
|
330
|
+
@handle_http_errors("modify_event")
|
301
331
|
async def modify_event(
|
302
332
|
service,
|
303
333
|
user_google_email: str,
|
@@ -333,104 +363,91 @@ async def modify_event(
|
|
333
363
|
f"[modify_event] Invoked. Email: '{user_google_email}', Event ID: {event_id}"
|
334
364
|
)
|
335
365
|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
)
|
366
|
+
# Build the event body with only the fields that are provided
|
367
|
+
event_body: Dict[str, Any] = {}
|
368
|
+
if summary is not None:
|
369
|
+
event_body["summary"] = summary
|
370
|
+
if start_time is not None:
|
371
|
+
event_body["start"] = (
|
372
|
+
{"date": start_time}
|
373
|
+
if "T" not in start_time
|
374
|
+
else {"dateTime": start_time}
|
375
|
+
)
|
376
|
+
if timezone is not None and "dateTime" in event_body["start"]:
|
377
|
+
event_body["start"]["timeZone"] = timezone
|
378
|
+
if end_time is not None:
|
379
|
+
event_body["end"] = (
|
380
|
+
{"date": end_time} if "T" not in end_time else {"dateTime": end_time}
|
381
|
+
)
|
382
|
+
if timezone is not None and "dateTime" in event_body["end"]:
|
383
|
+
event_body["end"]["timeZone"] = timezone
|
384
|
+
if description is not None:
|
385
|
+
event_body["description"] = description
|
386
|
+
if location is not None:
|
387
|
+
event_body["location"] = location
|
388
|
+
if attendees is not None:
|
389
|
+
event_body["attendees"] = [{"email": email} for email in attendees]
|
390
|
+
if (
|
391
|
+
timezone is not None
|
392
|
+
and "start" not in event_body
|
393
|
+
and "end" not in event_body
|
394
|
+
):
|
395
|
+
# If timezone is provided but start/end times are not, we need to fetch the existing event
|
396
|
+
# to apply the timezone correctly. This is a simplification; a full implementation
|
397
|
+
# might handle this more robustly or require start/end with timezone.
|
398
|
+
# For now, we'll log a warning and skip applying timezone if start/end are missing.
|
399
|
+
logger.warning(
|
400
|
+
f"[modify_event] Timezone provided but start_time and end_time are missing. Timezone will not be applied unless start/end times are also provided."
|
401
|
+
)
|
373
402
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
403
|
+
if not event_body:
|
404
|
+
message = "No fields provided to modify the event."
|
405
|
+
logger.warning(f"[modify_event] {message}")
|
406
|
+
raise Exception(message)
|
378
407
|
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
408
|
+
# Log the event ID for debugging
|
409
|
+
logger.info(
|
410
|
+
f"[modify_event] Attempting to update event with ID: '{event_id}' in calendar '{calendar_id}'"
|
411
|
+
)
|
383
412
|
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
)
|
389
|
-
logger.info(
|
390
|
-
f"[modify_event] Successfully verified event exists before update"
|
391
|
-
)
|
392
|
-
except HttpError as get_error:
|
393
|
-
if get_error.resp.status == 404:
|
394
|
-
logger.error(
|
395
|
-
f"[modify_event] Event not found during pre-update verification: {get_error}"
|
396
|
-
)
|
397
|
-
message = f"Event not found during verification. The event with ID '{event_id}' could not be found in calendar '{calendar_id}'. This may be due to incorrect ID format or the event no longer exists."
|
398
|
-
raise Exception(message)
|
399
|
-
else:
|
400
|
-
logger.warning(
|
401
|
-
f"[modify_event] Error during pre-update verification, but proceeding with update: {get_error}"
|
402
|
-
)
|
403
|
-
|
404
|
-
# Proceed with the update
|
405
|
-
updated_event = await asyncio.to_thread(
|
406
|
-
service.events()
|
407
|
-
.update(calendarId=calendar_id, eventId=event_id, body=event_body)
|
408
|
-
.execute
|
413
|
+
# Try to get the event first to verify it exists
|
414
|
+
try:
|
415
|
+
await asyncio.to_thread(
|
416
|
+
lambda: service.events().get(calendarId=calendar_id, eventId=event_id).execute()
|
409
417
|
)
|
410
|
-
|
411
|
-
link = updated_event.get("htmlLink", "No link available")
|
412
|
-
confirmation_message = f"Successfully modified event '{updated_event.get('summary', summary)}' (ID: {event_id}) for {user_google_email}. Link: {link}"
|
413
418
|
logger.info(
|
414
|
-
f"
|
419
|
+
f"[modify_event] Successfully verified event exists before update"
|
415
420
|
)
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
421
|
+
except HttpError as get_error:
|
422
|
+
if get_error.resp.status == 404:
|
423
|
+
logger.error(
|
424
|
+
f"[modify_event] Event not found during pre-update verification: {get_error}"
|
425
|
+
)
|
426
|
+
message = f"Event not found during verification. The event with ID '{event_id}' could not be found in calendar '{calendar_id}'. This may be due to incorrect ID format or the event no longer exists."
|
427
|
+
raise Exception(message)
|
422
428
|
else:
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
429
|
+
logger.warning(
|
430
|
+
f"[modify_event] Error during pre-update verification, but proceeding with update: {get_error}"
|
431
|
+
)
|
432
|
+
|
433
|
+
# Proceed with the update
|
434
|
+
updated_event = await asyncio.to_thread(
|
435
|
+
lambda: service.events()
|
436
|
+
.update(calendarId=calendar_id, eventId=event_id, body=event_body)
|
437
|
+
.execute()
|
438
|
+
)
|
439
|
+
|
440
|
+
link = updated_event.get("htmlLink", "No link available")
|
441
|
+
confirmation_message = f"Successfully modified event '{updated_event.get('summary', summary)}' (ID: {event_id}) for {user_google_email}. Link: {link}"
|
442
|
+
logger.info(
|
443
|
+
f"Event modified successfully for {user_google_email}. ID: {updated_event.get('id')}, Link: {link}"
|
444
|
+
)
|
445
|
+
return confirmation_message
|
430
446
|
|
431
447
|
|
432
448
|
@server.tool()
|
433
449
|
@require_google_service("calendar", "calendar_events")
|
450
|
+
@handle_http_errors("delete_event")
|
434
451
|
async def delete_event(service, user_google_email: str, event_id: str, calendar_id: str = "primary") -> str:
|
435
452
|
"""
|
436
453
|
Deletes an existing event.
|
@@ -447,50 +464,83 @@ async def delete_event(service, user_google_email: str, event_id: str, calendar_
|
|
447
464
|
f"[delete_event] Invoked. Email: '{user_google_email}', Event ID: {event_id}"
|
448
465
|
)
|
449
466
|
|
467
|
+
# Log the event ID for debugging
|
468
|
+
logger.info(
|
469
|
+
f"[delete_event] Attempting to delete event with ID: '{event_id}' in calendar '{calendar_id}'"
|
470
|
+
)
|
471
|
+
|
472
|
+
# Try to get the event first to verify it exists
|
450
473
|
try:
|
451
|
-
|
474
|
+
await asyncio.to_thread(
|
475
|
+
lambda: service.events().get(calendarId=calendar_id, eventId=event_id).execute()
|
476
|
+
)
|
452
477
|
logger.info(
|
453
|
-
f"[delete_event]
|
478
|
+
f"[delete_event] Successfully verified event exists before deletion"
|
454
479
|
)
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
service.events().get(calendarId=calendar_id, eventId=event_id).execute
|
480
|
+
except HttpError as get_error:
|
481
|
+
if get_error.resp.status == 404:
|
482
|
+
logger.error(
|
483
|
+
f"[delete_event] Event not found during pre-delete verification: {get_error}"
|
460
484
|
)
|
461
|
-
|
462
|
-
|
485
|
+
message = f"Event not found during verification. The event with ID '{event_id}' could not be found in calendar '{calendar_id}'. This may be due to incorrect ID format or the event no longer exists."
|
486
|
+
raise Exception(message)
|
487
|
+
else:
|
488
|
+
logger.warning(
|
489
|
+
f"[delete_event] Error during pre-delete verification, but proceeding with deletion: {get_error}"
|
463
490
|
)
|
464
|
-
except HttpError as get_error:
|
465
|
-
if get_error.resp.status == 404:
|
466
|
-
logger.error(
|
467
|
-
f"[delete_event] Event not found during pre-delete verification: {get_error}"
|
468
|
-
)
|
469
|
-
message = f"Event not found during verification. The event with ID '{event_id}' could not be found in calendar '{calendar_id}'. This may be due to incorrect ID format or the event no longer exists."
|
470
|
-
raise Exception(message)
|
471
|
-
else:
|
472
|
-
logger.warning(
|
473
|
-
f"[delete_event] Error during pre-delete verification, but proceeding with deletion: {get_error}"
|
474
|
-
)
|
475
491
|
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
492
|
+
# Proceed with the deletion
|
493
|
+
await asyncio.to_thread(
|
494
|
+
lambda: service.events().delete(calendarId=calendar_id, eventId=event_id).execute()
|
495
|
+
)
|
480
496
|
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
+
confirmation_message = f"Successfully deleted event (ID: {event_id}) from calendar '{calendar_id}' for {user_google_email}."
|
498
|
+
logger.info(f"Event deleted successfully for {user_google_email}. ID: {event_id}")
|
499
|
+
return confirmation_message
|
500
|
+
|
501
|
+
|
502
|
+
@server.tool()
|
503
|
+
@require_google_service("calendar", "calendar_read")
|
504
|
+
@handle_http_errors("get_event")
|
505
|
+
async def get_event(
|
506
|
+
service,
|
507
|
+
user_google_email: str,
|
508
|
+
event_id: str,
|
509
|
+
calendar_id: str = "primary"
|
510
|
+
) -> str:
|
511
|
+
"""
|
512
|
+
Retrieves the details of a single event by its ID from a specified Google Calendar.
|
513
|
+
|
514
|
+
Args:
|
515
|
+
user_google_email (str): The user's Google email address. Required.
|
516
|
+
event_id (str): The ID of the event to retrieve. Required.
|
517
|
+
calendar_id (str): The ID of the calendar to query. Defaults to 'primary'.
|
518
|
+
|
519
|
+
Returns:
|
520
|
+
str: A formatted string with the event's details.
|
521
|
+
"""
|
522
|
+
logger.info(f"[get_event] Invoked. Email: '{user_google_email}', Event ID: {event_id}")
|
523
|
+
event = await asyncio.to_thread(
|
524
|
+
lambda: service.events().get(calendarId=calendar_id, eventId=event_id).execute()
|
525
|
+
)
|
526
|
+
summary = event.get("summary", "No Title")
|
527
|
+
start = event["start"].get("dateTime", event["start"].get("date"))
|
528
|
+
end = event["end"].get("dateTime", event["end"].get("date"))
|
529
|
+
link = event.get("htmlLink", "No Link")
|
530
|
+
description = event.get("description", "No Description")
|
531
|
+
location = event.get("location", "No Location")
|
532
|
+
attendees = event.get("attendees", [])
|
533
|
+
attendee_emails = ", ".join([a.get("email", "") for a in attendees]) if attendees else "None"
|
534
|
+
event_details = (
|
535
|
+
f'Event Details:\n'
|
536
|
+
f'- Title: {summary}\n'
|
537
|
+
f'- Starts: {start}\n'
|
538
|
+
f'- Ends: {end}\n'
|
539
|
+
f'- Description: {description}\n'
|
540
|
+
f'- Location: {location}\n'
|
541
|
+
f'- Attendees: {attendee_emails}\n'
|
542
|
+
f'- Event ID: {event_id}\n'
|
543
|
+
f'- Link: {link}'
|
544
|
+
)
|
545
|
+
logger.info(f"[get_event] Successfully retrieved event {event_id} for {user_google_email}.")
|
546
|
+
return event_details
|