Skip to main content
Glama
RespondToEventHandler.ts6.51 kB
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { OAuth2Client } from "google-auth-library"; import { BaseToolHandler } from "./BaseToolHandler.js"; import { calendar_v3 } from 'googleapis'; import { createStructuredResponse } from "../../utils/response-builder.js"; import { RespondToEventResponse, convertGoogleEventToStructured } from "../../types/structured-responses.js"; import { RecurringEventHelpers, RecurringEventError, RECURRING_EVENT_ERRORS } from './RecurringEventHelpers.js'; export type RespondToEventInput = { calendarId: string; eventId: string; response: "accepted" | "declined" | "tentative" | "needsAction"; comment?: string; modificationScope?: "thisEventOnly" | "all"; originalStartTime?: string; sendUpdates?: "all" | "externalOnly" | "none"; account?: string; }; export class RespondToEventHandler extends BaseToolHandler { async runTool(args: RespondToEventInput, accounts: Map<string, OAuth2Client>): Promise<CallToolResult> { const validArgs = args; // Get OAuth2Client with automatic account selection for write operations // Also resolves calendar name to ID if a name was provided const { client: oauth2Client, accountId: selectedAccountId, calendarId: resolvedCalendarId } = await this.getClientWithAutoSelection( args.account, validArgs.calendarId, accounts, 'write' ); try { const calendar = this.getCalendar(oauth2Client); const helpers = new RecurringEventHelpers(calendar); // 1. Determine the target event ID (may be instance-specific for recurring events) let targetEventId = validArgs.eventId; // Handle recurring event scopes if (validArgs.modificationScope === 'thisEventOnly') { if (!validArgs.originalStartTime) { throw new RecurringEventError( 'originalStartTime is required when modificationScope is "thisEventOnly"', RECURRING_EVENT_ERRORS.MISSING_ORIGINAL_TIME ); } // Detect if event is recurring const eventType = await helpers.detectEventType(validArgs.eventId, resolvedCalendarId); if (eventType !== 'recurring') { throw new RecurringEventError( 'modificationScope "thisEventOnly" can only be used with recurring events', RECURRING_EVENT_ERRORS.NON_RECURRING_SCOPE ); } // Format instance ID for single instance response targetEventId = helpers.formatInstanceId(validArgs.eventId, validArgs.originalStartTime); } else if (validArgs.modificationScope === 'all') { // Extract base event ID by removing instance suffix if present // Instance IDs have format: baseId_YYYYMMDDTHHMMSSZ // Base IDs have no underscore, so split is safe for both cases targetEventId = validArgs.eventId.split('_')[0]; } // If no scope specified, default to 'all' behavior (use base event ID) // 2. Get the event to find the current user's attendee entry const eventResponse = await calendar.events.get({ calendarId: resolvedCalendarId, eventId: targetEventId }); const event = eventResponse.data; if (!event) { throw new Error('Event not found'); } // 3. Find the authenticated user's attendee entry (marked with self: true) const attendees = event.attendees || []; const selfAttendeeIndex = attendees.findIndex(a => a.self === true); if (selfAttendeeIndex === -1) { throw new Error( 'You are not an attendee of this event. Only attendees can respond to event invitations.' ); } const selfAttendee = attendees[selfAttendeeIndex]; // 4. Check if user is the organizer (organizers don't respond to their own events) if (selfAttendee.organizer === true) { throw new Error( 'You are the organizer of this event. Organizers do not respond to their own event invitations.' ); } // 5. Update the response status and optionally comment for the authenticated user const updatedAttendees = [...attendees]; updatedAttendees[selfAttendeeIndex] = { ...selfAttendee, responseStatus: validArgs.response, ...(validArgs.comment !== undefined && { comment: validArgs.comment }) }; // 6. Patch the event with the updated attendee list const actualSendUpdates = validArgs.sendUpdates || "none"; const updateResponse = await calendar.events.patch({ calendarId: resolvedCalendarId, eventId: targetEventId, requestBody: { attendees: updatedAttendees }, sendUpdates: actualSendUpdates }); if (!updateResponse.data) { throw new Error('Failed to update event response'); } // 7. Create structured response let message = `Your response has been set to "${validArgs.response}"`; if (validArgs.modificationScope === 'thisEventOnly') { message += ' for this instance only'; } else if (validArgs.modificationScope === 'all') { message += ' for all instances'; } if (validArgs.comment) { message += ` with note: "${validArgs.comment}"`; } const response: RespondToEventResponse = { event: convertGoogleEventToStructured(updateResponse.data, resolvedCalendarId, selectedAccountId), responseStatus: validArgs.response, sendUpdates: actualSendUpdates, message: message }; return createStructuredResponse(response); } catch (error: any) { if (error instanceof RecurringEventError) { throw error; } throw this.handleGoogleApiError(error); } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nspady/google-calendar-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server