calendar.ts•12.6 kB
import { Tool, TextContent, ImageContent, EmbeddedResource } from '@modelcontextprotocol/sdk/types.js';
import { GAuthService } from '../services/gauth.js';
import { google } from 'googleapis';
import { USER_ID_ARG } from '../types/tool-handler.js';
const CALENDAR_ID_ARG = 'calendar_id';
export class CalendarTools {
  private calendar: ReturnType<typeof google.calendar>;
  constructor(private gauth: GAuthService) {
    this.calendar = google.calendar({ version: 'v3', auth: this.gauth.getClient() });
  }
  getTools(): Tool[] {
    return [
      {
        name: 'calendar_list_accounts',
        description: 'Lists all configured Google accounts that can be used with the calendar tools. This tool does not require a user_id as it lists available accounts before selection.',
        inputSchema: {
          type: 'object',
          properties: {},
          additionalProperties: false,
          required: []
        }
      },
      {
        name: 'calendar_list',
        description: `Lists all calendars accessible by the user. 
        Call it before any other tool whenever the user specifies a particular agenda (Family, Holidays, etc.).
        Returns detailed calendar metadata including access roles and timezone information.`,
        inputSchema: {
          type: 'object',
          properties: {
            [USER_ID_ARG]: {
              type: 'string',
              description: 'Email address of the user'
            }
          },
          required: [USER_ID_ARG]
        }
      },
      {
        name: 'calendar_get_events',
        description: 'Retrieves calendar events from the user\'s Google Calendar within a specified time range.',
        inputSchema: {
          type: 'object',
          properties: {
            [USER_ID_ARG]: {
              type: 'string',
              description: 'Email address of the user'
            },
            [CALENDAR_ID_ARG]: {
              type: 'string',
              description: 'Calendar ID to fetch events from. Use "primary" for the primary calendar.',
              default: 'primary'
            },
            time_min: {
              type: 'string',
              description: 'Start time in RFC3339 format (e.g. 2024-12-01T00:00:00Z). Defaults to current time if not specified.'
            },
            time_max: {
              type: 'string',
              description: 'End time in RFC3339 format (e.g. 2024-12-31T23:59:59Z). Optional.'
            },
            max_results: {
              type: 'integer',
              description: 'Maximum number of events to return (1-2500)',
              minimum: 1,
              maximum: 2500,
              default: 250
            },
            show_deleted: {
              type: 'boolean',
              description: 'Whether to include deleted events',
              default: false
            },
            timezone: {
              type: 'string',
              description: 'Timezone for the events (e.g. \'America/New_York\'). Defaults to UTC.',
              default: 'UTC'
            }
          },
          required: [USER_ID_ARG]
        }
      },
      {
        name: 'calendar_create_event',
        description: 'Creates a new event in the specified Google Calendar.',
        inputSchema: {
          type: 'object',
          properties: {
            [USER_ID_ARG]: {
              type: 'string',
              description: 'Email address of the user'
            },
            [CALENDAR_ID_ARG]: {
              type: 'string',
              description: 'Calendar ID to create the event in. Use "primary" for the primary calendar.',
              default: 'primary'
            },
            summary: {
              type: 'string',
              description: 'Title of the event'
            },
            start_time: {
              type: 'string',
              description: 'Start time in RFC3339 format (e.g. 2024-12-01T10:00:00Z)'
            },
            end_time: {
              type: 'string',
              description: 'End time in RFC3339 format (e.g. 2024-12-01T11:00:00Z)'
            },
            location: {
              type: 'string',
              description: 'Location of the event (optional)'
            },
            description: {
              type: 'string',
              description: 'Description or notes for the event (optional)'
            },
            attendees: {
              type: 'array',
              items: {
                type: 'string'
              },
              description: 'List of attendee email addresses (optional)'
            },
            send_notifications: {
              type: 'boolean',
              description: 'Whether to send notifications to attendees',
              default: true
            },
            timezone: {
              type: 'string',
              description: 'Timezone for the event (e.g. \'America/New_York\'). Defaults to UTC.',
              default: 'UTC'
            }
          },
          required: [USER_ID_ARG, 'summary', 'start_time', 'end_time']
        }
      },
      {
        name: 'calendar_delete_event',
        description: 'Deletes an event from the specified Google Calendar.',
        inputSchema: {
          type: 'object',
          properties: {
            [USER_ID_ARG]: {
              type: 'string',
              description: 'Email address of the user'
            },
            [CALENDAR_ID_ARG]: {
              type: 'string',
              description: 'Calendar ID containing the event. Use "primary" for the primary calendar.',
              default: 'primary'
            },
            event_id: {
              type: 'string',
              description: 'The ID of the calendar event to delete'
            },
            send_notifications: {
              type: 'boolean',
              description: 'Whether to send cancellation notifications to attendees',
              default: true
            }
          },
          required: [USER_ID_ARG, 'event_id']
        }
      }
    ];
  }
  async handleTool(name: string, args: Record<string, any>): Promise<Array<TextContent | ImageContent | EmbeddedResource>> {
    switch (name) {
      case 'calendar_list_accounts':
        return this.listAccounts();
      case 'calendar_list':
        return this.listCalendars(args);
      case 'calendar_get_events':
        return this.getCalendarEvents(args);
      case 'calendar_create_event':
        return this.createCalendarEvent(args);
      case 'calendar_delete_event':
        return this.deleteCalendarEvent(args);
      default:
        throw new Error(`Unknown tool: ${name}`);
    }
  }
  private async listAccounts(): Promise<Array<TextContent>> {
    try {
      const accounts = await this.gauth.getAccountInfo();
      const accountList = accounts.map(account => ({
        email: account.email,
        accountType: account.accountType,
        extraInfo: account.extraInfo,
        description: account.toDescription()
      }));
      if (accountList.length === 0) {
        return [{
          type: 'text',
          text: JSON.stringify({
            message: 'No accounts configured. Please check your .accounts.json file.',
            accounts: []
          }, null, 2)
        }];
      }
      return [{
        type: 'text',
        text: JSON.stringify({
          message: `Found ${accountList.length} configured account(s)`,
          accounts: accountList
        }, null, 2)
      }];
    } catch (error) {
      console.error('Error listing accounts:', error);
      return [{
        type: 'text',
        text: JSON.stringify({
          error: `Failed to list accounts: ${(error as Error).message}`,
          accounts: []
        }, null, 2)
      }];
    }
  }
  private async listCalendars(args: Record<string, any>): Promise<Array<TextContent>> {
    const userId = args[USER_ID_ARG];
    if (!userId) {
      throw new Error(`Missing required argument: ${USER_ID_ARG}`);
    }
    try {
      console.error('Attempting to list calendars...');
      const response = await this.calendar.calendarList.list();
      const calendars = response.data.items?.map(calendar => ({
        id: calendar.id,
        summary: calendar.summary,
        primary: calendar.primary || false,
        timeZone: calendar.timeZone,
        etag: calendar.etag,
        accessRole: calendar.accessRole
      })) || [];
      console.error(`Successfully retrieved ${calendars.length} calendars`);
      return [{
        type: 'text',
        text: JSON.stringify(calendars, null, 2)
      }];
    } catch (error) {
      console.error('Error listing calendars:', error);
      throw error;
    }
  }
  private async getCalendarEvents(args: Record<string, any>): Promise<Array<TextContent>> {
    const userId = args[USER_ID_ARG];
    if (!userId) {
      throw new Error(`Missing required argument: ${USER_ID_ARG}`);
    }
    try {
      const timeMin = args.time_min || new Date().toISOString();
      const maxResults = Math.min(Math.max(1, args.max_results || 250), 2500);
      const calendarId = args[CALENDAR_ID_ARG] || 'primary';
      const timezone = args.timezone || 'UTC';
      const params = {
        calendarId,
        timeMin,
        maxResults,
        singleEvents: true,
        orderBy: 'startTime' as const,
        showDeleted: args.show_deleted || false
      };
      if (args.time_max) {
        Object.assign(params, { timeMax: args.time_max });
      }
      const response = await this.calendar.events.list(params);
      const events = response.data.items?.map(event => ({
        id: event.id,
        summary: event.summary,
        description: event.description,
        start: event.start,
        end: event.end,
        status: event.status,
        creator: event.creator,
        organizer: event.organizer,
        attendees: event.attendees,
        location: event.location,
        hangoutLink: event.hangoutLink,
        conferenceData: event.conferenceData,
        recurringEventId: event.recurringEventId
      })) || [];
      return [{
        type: 'text',
        text: JSON.stringify(events, null, 2)
      }];
    } catch (error) {
      console.error('Error getting calendar events:', error);
      throw error;
    }
  }
  private async createCalendarEvent(args: Record<string, any>): Promise<Array<TextContent>> {
    const userId = args[USER_ID_ARG];
    const required = ['summary', 'start_time', 'end_time'];
    
    if (!userId) {
      throw new Error(`Missing required argument: ${USER_ID_ARG}`);
    }
    if (!required.every(key => key in args)) {
      throw new Error(`Missing required arguments: ${required.filter(key => !(key in args)).join(', ')}`);
    }
    try {
      const timezone = args.timezone || 'UTC';
      const event = {
        summary: args.summary,
        location: args.location,
        description: args.description,
        start: {
          dateTime: args.start_time,
          timeZone: timezone
        },
        end: {
          dateTime: args.end_time,
          timeZone: timezone
        },
        attendees: args.attendees?.map((email: string) => ({ email }))
      };
      const response = await this.calendar.events.insert({
        calendarId: args[CALENDAR_ID_ARG] || 'primary',
        requestBody: event,
        sendUpdates: args.send_notifications ? 'all' : 'none'
      });
      return [{
        type: 'text',
        text: JSON.stringify(response.data, null, 2)
      }];
    } catch (error) {
      console.error('Error creating calendar event:', error);
      throw error;
    }
  }
  private async deleteCalendarEvent(args: Record<string, any>): Promise<Array<TextContent>> {
    const userId = args[USER_ID_ARG];
    const eventId = args.event_id;
    if (!userId) {
      throw new Error(`Missing required argument: ${USER_ID_ARG}`);
    }
    if (!eventId) {
      throw new Error('Missing required argument: event_id');
    }
    try {
      await this.calendar.events.delete({
        calendarId: args[CALENDAR_ID_ARG] || 'primary',
        eventId: eventId,
        sendUpdates: args.send_notifications ? 'all' : 'none'
      });
      return [{
        type: 'text',
        text: JSON.stringify({
          success: true,
          message: 'Event successfully deleted'
        }, null, 2)
      }];
    } catch (error) {
      console.error('Error deleting calendar event:', error);
      return [{
        type: 'text',
        text: JSON.stringify({
          success: false,
          message: 'Failed to delete event'
        }, null, 2)
      }];
    }
  }
}