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.
@@ -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 os
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
- try:
97
- calendar_list_response = await asyncio.to_thread(
98
- service.calendarList().list().execute
99
- )
100
- items = calendar_list_response.get("items", [])
101
- if not items:
102
- return f"No calendars found for {user_google_email}."
103
-
104
- calendars_summary_list = [
105
- f"- \"{cal.get('summary', 'No Summary')}\"{' (Primary)' if cal.get('primary') else ''} (ID: {cal['id']})"
106
- for cal in items
107
- ]
108
- text_output = (
109
- f"Successfully listed {len(items)} calendars for {user_google_email}:\n"
110
- + "\n".join(calendars_summary_list)
111
- )
112
- logger.info(f"Successfully listed {len(items)} calendars for {user_google_email}.")
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 time, link) within the specified range.
137
+ str: A formatted list of events (summary, start and end times, link) within the specified range.
146
138
  """
147
- try:
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"[get_events] Raw time parameters - time_min: '{time_min}', time_max: '{time_max}'"
150
+ f"time_min not provided, defaulting to current UTC time: {effective_time_min}"
150
151
  )
151
-
152
- # Ensure time_min and time_max are correctly formatted for the API
153
- formatted_time_min = _correct_time_format_for_api(time_min, "time_min")
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
- # Log the final API call parameters
157
+ effective_time_max = _correct_time_format_for_api(time_max, "time_max")
158
+ if time_max:
173
159
  logger.info(
174
- f"[get_events] Final API parameters - calendarId: '{calendar_id}', timeMin: '{effective_time_min}', timeMax: '{effective_time_max}', maxResults: {max_results}"
160
+ f"time_max processing: original='{time_max}', formatted='{effective_time_max}'"
175
161
  )
176
162
 
177
- events_result = await asyncio.to_thread(
178
- service.events()
179
- .list(
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
- text_output = (
205
- f"Successfully retrieved {len(items)} events from calendar '{calendar_id}' for {user_google_email}:\n"
206
- + "\n".join(event_details_list)
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
- logger.info(f"Successfully retrieved {len(items)} events for {user_google_email}.")
209
- return text_output
210
- except HttpError as error:
211
- message = f"API error getting events: {error}. You might need to re-authenticate. LLM: Try 'start_google_auth' with user's email and service_name='Google Calendar'."
212
- logger.error(message, exc_info=True)
213
- raise Exception(message)
214
- except Exception as e:
215
- message = f"Unexpected error getting events: {e}"
216
- logger.exception(message)
217
- raise Exception(message)
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
- try:
256
- event_body: Dict[str, Any] = {
257
- "summary": summary,
258
- "start": (
259
- {"date": start_time}
260
- if "T" not in start_time
261
- else {"dateTime": start_time}
262
- ),
263
- "end": (
264
- {"date": end_time} if "T" not in end_time else {"dateTime": end_time}
265
- ),
266
- }
267
- if location:
268
- event_body["location"] = location
269
- if description:
270
- event_body["description"] = description
271
- if timezone:
272
- if "dateTime" in event_body["start"]:
273
- event_body["start"]["timeZone"] = timezone
274
- if "dateTime" in event_body["end"]:
275
- event_body["end"]["timeZone"] = timezone
276
- if attendees:
277
- event_body["attendees"] = [{"email": email} for email in attendees]
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(calendarId=calendar_id, body=event_body).execute
312
+ lambda: service.events().insert(
313
+ calendarId=calendar_id, body=event_body, supportsAttachments=True
314
+ ).execute()
281
315
  )
282
-
283
- link = created_event.get("htmlLink", "No link available")
284
- confirmation_message = f"Successfully created event '{created_event.get('summary', summary)}' for {user_google_email}. Link: {link}"
285
- logger.info(
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
- return confirmation_message
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
- try:
337
- # Build the event body with only the fields that are provided
338
- event_body: Dict[str, Any] = {}
339
- if summary is not None:
340
- event_body["summary"] = summary
341
- if start_time is not None:
342
- event_body["start"] = (
343
- {"date": start_time}
344
- if "T" not in start_time
345
- else {"dateTime": start_time}
346
- )
347
- if timezone is not None and "dateTime" in event_body["start"]:
348
- event_body["start"]["timeZone"] = timezone
349
- if end_time is not None:
350
- event_body["end"] = (
351
- {"date": end_time} if "T" not in end_time else {"dateTime": end_time}
352
- )
353
- if timezone is not None and "dateTime" in event_body["end"]:
354
- event_body["end"]["timeZone"] = timezone
355
- if description is not None:
356
- event_body["description"] = description
357
- if location is not None:
358
- event_body["location"] = location
359
- if attendees is not None:
360
- event_body["attendees"] = [{"email": email} for email in attendees]
361
- if (
362
- timezone is not None
363
- and "start" not in event_body
364
- and "end" not in event_body
365
- ):
366
- # If timezone is provided but start/end times are not, we need to fetch the existing event
367
- # to apply the timezone correctly. This is a simplification; a full implementation
368
- # might handle this more robustly or require start/end with timezone.
369
- # For now, we'll log a warning and skip applying timezone if start/end are missing.
370
- logger.warning(
371
- 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."
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
- if not event_body:
375
- message = "No fields provided to modify the event."
376
- logger.warning(f"[modify_event] {message}")
377
- raise Exception(message)
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
- # Log the event ID for debugging
380
- logger.info(
381
- f"[modify_event] Attempting to update event with ID: '{event_id}' in calendar '{calendar_id}'"
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
- # Try to get the event first to verify it exists
385
- try:
386
- await asyncio.to_thread(
387
- service.events().get(calendarId=calendar_id, eventId=event_id).execute
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"Event modified successfully for {user_google_email}. ID: {updated_event.get('id')}, Link: {link}"
419
+ f"[modify_event] Successfully verified event exists before update"
415
420
  )
416
- return confirmation_message
417
- except HttpError as error:
418
- # Check for 404 Not Found error specifically
419
- if error.resp.status == 404:
420
- message = f"Event not found. The event with ID '{event_id}' could not be found in calendar '{calendar_id}'. LLM: The event may have been deleted, or the event ID might be incorrect. Verify the event exists using 'get_events' before attempting to modify it."
421
- logger.error(f"[modify_event] {message}")
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
- message = f"API error modifying event (ID: {event_id}): {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'."
424
- logger.error(message, exc_info=True)
425
- raise Exception(message)
426
- except Exception as e:
427
- message = f"Unexpected error modifying event (ID: {event_id}): {e}."
428
- logger.exception(message)
429
- raise Exception(message)
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
- # Log the event ID for debugging
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] Attempting to delete event with ID: '{event_id}' in calendar '{calendar_id}'"
478
+ f"[delete_event] Successfully verified event exists before deletion"
454
479
  )
455
-
456
- # Try to get the event first to verify it exists
457
- try:
458
- await asyncio.to_thread(
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
- logger.info(
462
- f"[delete_event] Successfully verified event exists before deletion"
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
- # Proceed with the deletion
477
- await asyncio.to_thread(
478
- service.events().delete(calendarId=calendar_id, eventId=event_id).execute
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
- confirmation_message = f"Successfully deleted event (ID: {event_id}) from calendar '{calendar_id}' for {user_google_email}."
482
- logger.info(f"Event deleted successfully for {user_google_email}. ID: {event_id}")
483
- return confirmation_message
484
- except HttpError as error:
485
- # Check for 404 Not Found error specifically
486
- if error.resp.status == 404:
487
- message = f"Event not found. The event with ID '{event_id}' could not be found in calendar '{calendar_id}'. LLM: The event may have been deleted already, or the event ID might be incorrect."
488
- logger.error(f"[delete_event] {message}")
489
- else:
490
- message = f"API error deleting event (ID: {event_id}): {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'."
491
- logger.error(message, exc_info=True)
492
- raise Exception(message)
493
- except Exception as e:
494
- message = f"Unexpected error deleting event (ID: {event_id}): {e}."
495
- logger.exception(message)
496
- raise Exception(message)
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