Google Calendar MCP Server

by am2rican5
Verified
import { authenticate } from "@google-cloud/local-auth"; import type { Credentials, OAuth2Client } from "google-auth-library"; import { google } from "googleapis"; import * as fs from "node:fs"; import * as path from "node:path"; import { OAuth2Client as OAuth2 } from "google-auth-library"; import type { CalendarEvent, GoogleCalendarEvent, GoogleCalendarEventList, GoogleCalendarList, } from "../types/index.js"; interface GoogleCredentials { installed?: { client_id: string; client_secret: string; redirect_uris: string[]; }; web?: { client_id: string; client_secret: string; redirect_uris: string[]; }; } /** * Service for interacting with Google Calendar API */ export class GoogleCalendarService { private static instance: GoogleCalendarService; private authClient: OAuth2Client | null = null; private credentials: Credentials | null = null; private readonly CREDENTIALS_PATH = process.env.CREDENTIALS_PATH ?? "./credentials.json"; private readonly TOKEN_PATH = path.join( path.dirname(this.CREDENTIALS_PATH), "mcp-google-calendar-token.json", ); // Scopes for Google Calendar API private readonly SCOPES = [ "https://www.googleapis.com/auth/calendar.readonly", "https://www.googleapis.com/auth/calendar.events", ]; private constructor() {} /** * Get the singleton instance of GoogleCalendarService */ public static getInstance(): GoogleCalendarService { if (!GoogleCalendarService.instance) { GoogleCalendarService.instance = new GoogleCalendarService(); } return GoogleCalendarService.instance; } /** * Load credentials from environment variable */ private loadCredentials(): GoogleCredentials { if (!this.CREDENTIALS_PATH) { throw new Error("CREDENTIALS_PATH environment variable is not set"); } if (!fs.existsSync(this.CREDENTIALS_PATH)) { throw new Error(`Credentials file not found at ${this.CREDENTIALS_PATH}`); } const content = fs.readFileSync(this.CREDENTIALS_PATH, "utf8"); return JSON.parse(content) as GoogleCredentials; } /** * Load saved token from disk */ private loadSavedToken(): Credentials | null { try { if (fs.existsSync(this.TOKEN_PATH)) { const token = fs.readFileSync(this.TOKEN_PATH, "utf8"); return JSON.parse(token); } } catch (error) { console.error("Error loading saved token:", error); } return null; } /** * Save token to disk */ private saveToken(token: Credentials): void { try { fs.writeFileSync(this.TOKEN_PATH, JSON.stringify(token)); } catch (error) { console.error("Error saving token:", error); } } /** * Authorize with Google Calendar API */ public async authorize(): Promise<OAuth2Client> { // If we already have an authorized client, return it if (this.authClient && this.credentials) { this.authClient.setCredentials(this.credentials); return this.authClient; } try { // Load credentials from environment variable if (!fs.existsSync(this.CREDENTIALS_PATH)) { throw new Error( `Credentials file not found at: ${this.CREDENTIALS_PATH}`, ); } // Load credentials from file const credentials: GoogleCredentials = JSON.parse( fs.readFileSync(this.CREDENTIALS_PATH, "utf-8"), ); // Extract client ID and secret const clientId = credentials.web?.client_id || credentials.installed?.client_id; const clientSecret = credentials.web?.client_secret || credentials.installed?.client_secret; const redirectUri = (credentials.web?.redirect_uris || credentials.installed?.redirect_uris || [])[0]; if (!clientId || !clientSecret) { throw new Error("Invalid credentials file format"); } // Create OAuth2 client const authClient = new OAuth2(clientId, clientSecret, redirectUri); this.authClient = authClient; // Try to load saved token const savedToken = this.loadSavedToken(); if (savedToken) { this.credentials = savedToken; this.authClient.setCredentials(savedToken); return this.authClient; } // If no saved token, start new authentication flow this.authClient = (await authenticate({ scopes: this.SCOPES, keyfilePath: this.CREDENTIALS_PATH, })) as OAuth2Client; // Store credentials in memory and save to disk this.credentials = this.authClient.credentials; this.saveToken(this.credentials); return this.authClient; } catch (error) { console.error("Error authorizing with Google:", error); throw error; } } /** * Get list of available calendars */ public async getCalendars(pageToken?: string): Promise<GoogleCalendarList> { const auth = await this.authorize(); const calendar = google.calendar({ version: "v3", auth }); const res = await calendar.calendarList.list({ pageToken, }); return res.data; } /** * Get calendar events between two dates */ public async getEvents( calendarId: string, startDate: string, endDate: string, pageToken?: string, ): Promise<GoogleCalendarEventList> { const auth = await this.authorize(); const calendar = google.calendar({ version: "v3", auth }); const res = await calendar.events.list({ calendarId, timeMin: new Date(startDate).toISOString(), timeMax: new Date(endDate).toISOString(), singleEvents: true, orderBy: "startTime", pageToken, }); return res.data; } /** * Create a new calendar event */ public async createEvent( calendarId: string, event: CalendarEvent, ): Promise<GoogleCalendarEvent> { const auth = await this.authorize(); const calendar = google.calendar({ version: "v3", auth }); const res = await calendar.events.insert({ calendarId, requestBody: { summary: event.summary, description: event.description, start: { date: event.start }, end: { date: event.end }, anyoneCanAddSelf: event.anyoneCanAddSelf, colorId: event.colorId, }, }); return res.data; } /** * Get a calendar event */ public async getEvent( calendarId: string, eventId: string, ): Promise<GoogleCalendarEvent> { const auth = await this.authorize(); const calendar = google.calendar({ version: "v3", auth }); const res = await calendar.events.get({ calendarId, eventId, }); return res.data; } /** * Edit a calendar event */ public async updateEvent( calendarId: string, eventId: string, event: CalendarEvent, ): Promise<GoogleCalendarEvent> { const auth = await this.authorize(); const calendar = google.calendar({ version: "v3", auth }); const res = await calendar.events.update({ calendarId, eventId, requestBody: { summary: event.summary, description: event.description, start: { date: event.start }, end: { date: event.end }, anyoneCanAddSelf: event.anyoneCanAddSelf, colorId: event.colorId, }, }); return res.data; } /** * Delete a calendar event */ public async deleteEvent(calendarId: string, eventId: string): Promise<void> { const auth = await this.authorize(); const calendar = google.calendar({ version: "v3", auth }); await calendar.events.delete({ calendarId, eventId, }); } }