Skip to main content
Glama
calendar.ts17.5 kB
import { google } from "googleapis"; import { z } from "zod"; // Dynamic calendar data that gets populated on server start let availableCalendars: Array<{ id: string; summary: string }> = []; let systemTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; // Markdown formatting helpers function formatEventToMarkdown(event: any): string { let markdown = `# ${event.summary || 'Untitled Event'}\n\n`; if (event.description) markdown += `${event.description}\n\n`; const startDate = event.start?.dateTime ? new Date(event.start.dateTime) : null; const endDate = event.end?.dateTime ? new Date(event.end.dateTime) : null; if (startDate) { markdown += `Start: ${startDate.toLocaleString()} \n`; } if (endDate) { markdown += `End: ${endDate.toLocaleString()} \n`; } if (event.location) markdown += `Location: ${event.location} \n`; if (event.attendees && event.attendees.length > 0) { markdown += `Attendees: ${event.attendees.map((a: any) => { let attendee = a.email || a; if (a.responseStatus) { const status = a.responseStatus === 'accepted' ? '✅' : a.responseStatus === 'declined' ? '❌' : a.responseStatus === 'tentative' ? '❓' : '⏳'; attendee += ` ${status}`; } return attendee; }).join(', ')} \n`; } if (event.htmlLink) markdown += `Calendar Link: [View Event](${event.htmlLink}) \n`; if (event.id) markdown += `Event ID: \`${event.id}\` \n`; return markdown; } function formatEventListToMarkdown(events: any[], totalResults: number): string { if (!events.length) return "# No Upcoming Events\n\nNo events found in the specified time range."; let markdown = `# Upcoming Events (${totalResults})\n\n`; events.forEach((event, index) => { const startDate = event.start?.dateTime ? new Date(event.start.dateTime) : null; const endDate = event.end?.dateTime ? new Date(event.end.dateTime) : null; markdown += `## ${index + 1}. ${event.summary || 'Untitled Event'}\n`; if (startDate) { if (endDate && startDate.toDateString() === endDate.toDateString()) { // Same day event markdown += `When: ${startDate.toLocaleDateString()} ${startDate.toLocaleTimeString()} - ${endDate.toLocaleTimeString()} \n`; } else { markdown += `Start: ${startDate.toLocaleString()} \n`; if (endDate) markdown += `End: ${endDate.toLocaleString()} \n`; } } if (event.location) markdown += `Location: ${event.location} \n`; if (event.description) markdown += `Description: ${event.description} \n`; if (event.attendees && event.attendees.length > 0) { markdown += `Attendees: ${event.attendees.length} people \n`; } if (event.id) markdown += `ID: \`${event.id}\` \n`; markdown += `\n---\n\n`; }); return markdown; } function formatCalendarsToMarkdown(calendars: any[]): string { if (!calendars.length) return "No calendars found."; let markdown = `# Available Calendars (${calendars.length})\n\n`; calendars.forEach((cal, index) => { markdown += `## ${index + 1}. ${cal.summary || cal.id}\n`; markdown += `ID: \`${cal.id}\` \n`; if (cal.description) markdown += `Description: ${cal.description} \n`; if (cal.timeZone) markdown += `Time Zone: ${cal.timeZone} \n`; if (cal.accessRole) markdown += `Access: ${cal.accessRole} \n`; if (cal.primary) markdown += `Primary: Yes ⭐ \n`; markdown += `\n`; }); return markdown; } // Function to initialize calendar data export async function initializeCalendarData() { try { const auth = createCalendarAuth(); const calendar = google.calendar({ version: "v3", auth }); const response = await calendar.calendarList.list({ maxResults: 50 }); availableCalendars = response.data.items?.map((cal) => ({ id: cal.id || "", summary: cal.summary || cal.id || "", })) || []; // Loaded ${availableCalendars.length} calendars } catch (error) { console.warn( "Could not load calendars:", error instanceof Error ? error.message : String(error) ); // Fallback to primary calendar availableCalendars = [{ id: "primary", summary: "Primary Calendar" }]; } } // Helper function to get calendar description function getCalendarDescription() { if (availableCalendars.length === 0) { return "Calendar ID - Options: 'primary' (your main calendar), a specific calendar ID like 'john.doe@gmail.com', or a shared calendar ID"; } const calendarOptions = availableCalendars .map((cal) => `'${cal.id}' (${cal.summary})`) .join(", "); return `Calendar ID - Available options: ${calendarOptions}`; } // Calendar schemas - these are created as functions to capture dynamic data export const createEventSchema = () => z.object({ summary: z.string().describe("Event title/summary"), description: z.string().optional().describe("Event description"), location: z.string().optional().describe("Event location"), startDateTime: z .string() .describe( "Start date/time in ISO format (e.g., '2025-01-15T09:00:00-07:00')" ), endDateTime: z .string() .describe( "End date/time in ISO format (e.g., '2025-01-15T10:00:00-07:00')" ), attendees: z .array(z.string()) .optional() .describe("Array of attendee email addresses"), calendarId: z .string() .default("primary") .describe(getCalendarDescription()), timeZone: z .string() .optional() .describe( `Time zone - defaults to local time (${systemTimezone}). Examples: 'America/New_York', 'Europe/London', 'Asia/Tokyo'` ), }); export const listEventsSchema = () => z.object({ calendarId: z .string() .default("primary") .describe(getCalendarDescription()), timeMin: z .string() .optional() .describe("Lower bound for event start time (ISO format)"), timeMax: z .string() .optional() .describe("Upper bound for event start time (ISO format)"), maxResults: z .number() .min(1) .max(250) .default(10) .describe("Maximum number of events to return"), singleEvents: z .boolean() .default(true) .describe("Whether to expand recurring events"), orderBy: z .enum(["startTime", "updated"]) .default("startTime") .describe("Order of events"), }); export const getEventSchema = () => z.object({ eventId: z.string().describe("Event ID"), calendarId: z .string() .default("primary") .describe(getCalendarDescription()), }); export const updateEventSchema = () => z.object({ eventId: z.string().describe("Event ID"), calendarId: z .string() .default("primary") .describe(getCalendarDescription()), summary: z.string().optional().describe("Event title/summary"), description: z.string().optional().describe("Event description"), location: z.string().optional().describe("Event location"), startDateTime: z .string() .optional() .describe("Start date/time in ISO format"), endDateTime: z.string().optional().describe("End date/time in ISO format"), attendees: z .array(z.string()) .optional() .describe("Array of attendee email addresses"), }); export const deleteEventSchema = () => z.object({ eventId: z.string().describe("Event ID"), calendarId: z .string() .default("primary") .describe(getCalendarDescription()), }); export const listCalendarsSchema = () => z.object({ maxResults: z .number() .min(1) .max(250) .default(10) .describe("Maximum number of calendars to return"), }); // OAuth2 Authentication helper function createCalendarAuth() { const clientId = process.env.GOOGLE_CLIENT_ID; const clientSecret = process.env.GOOGLE_CLIENT_SECRET; const redirectUri = process.env.GOOGLE_REDIRECT_URI || "http://localhost:3000/oauth2callback"; if (!clientId || !clientSecret) { throw new Error( "GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are required. Run oauth-setup.js to configure." ); } const oauth2Client = new google.auth.OAuth2( clientId, clientSecret, redirectUri ); const accessToken = process.env.GOOGLE_ACCESS_TOKEN; const refreshToken = process.env.GOOGLE_REFRESH_TOKEN; if (!accessToken || !refreshToken) { throw new Error("OAuth2 tokens missing. Run oauth-setup.js to get tokens."); } oauth2Client.setCredentials({ access_token: accessToken, refresh_token: refreshToken, }); return oauth2Client; } // Create event function export async function createEvent( params: z.infer<ReturnType<typeof createEventSchema>> ) { try { const auth = createCalendarAuth(); const calendar = google.calendar({ version: "v3", auth }); const event: any = { summary: params.summary, description: params.description, location: params.location, start: { dateTime: params.startDateTime, timeZone: params.timeZone, }, end: { dateTime: params.endDateTime, timeZone: params.timeZone, }, }; if (params.attendees && params.attendees.length > 0) { event.attendees = params.attendees.map((email) => ({ email })); } const response = await calendar.events.insert({ calendarId: params.calendarId, requestBody: event, sendUpdates: "all", // Send invitations to attendees }); const eventData = { id: response.data.id, summary: response.data.summary, start: response.data.start, end: response.data.end, location: response.data.location, description: response.data.description, attendees: response.data.attendees, htmlLink: response.data.htmlLink, }; return { content: [ { type: "text" as const, text: `# Event Created Successfully ✅\n\n${formatEventToMarkdown(eventData)}`, }, ], }; } catch (error) { return { content: [ { type: "text" as const, text: `Error creating event: ${ error instanceof Error ? error.message : String(error) }`, }, ], }; } } // List events function export async function listEvents( params: z.infer<ReturnType<typeof listEventsSchema>> ) { try { const auth = createCalendarAuth(); const calendar = google.calendar({ version: "v3", auth }); const listParams: any = { calendarId: params.calendarId, maxResults: params.maxResults, singleEvents: params.singleEvents, orderBy: params.orderBy, }; if (params.timeMin) listParams.timeMin = params.timeMin; if (params.timeMax) listParams.timeMax = params.timeMax; // If no timeMin is specified, default to current time if (!params.timeMin) { listParams.timeMin = new Date().toISOString(); } const response = await calendar.events.list(listParams); const events = response.data.items?.map((event) => ({ id: event.id, summary: event.summary, description: event.description, location: event.location, start: event.start, end: event.end, attendees: event.attendees?.map((a) => ({ email: a.email, responseStatus: a.responseStatus, })), htmlLink: event.htmlLink, status: event.status, created: event.created, updated: event.updated, })); return { content: [ { type: "text" as const, text: formatEventListToMarkdown(events || [], events?.length || 0), }, ], }; } catch (error) { return { content: [ { type: "text" as const, text: `Error listing events: ${ error instanceof Error ? error.message : String(error) }`, }, ], }; } } // Get specific event function export async function getEvent( params: z.infer<ReturnType<typeof getEventSchema>> ) { try { const auth = createCalendarAuth(); const calendar = google.calendar({ version: "v3", auth }); const response = await calendar.events.get({ calendarId: params.calendarId, eventId: params.eventId, }); const event = response.data; const eventDetail = { id: event.id, summary: event.summary, description: event.description, location: event.location, start: event.start, end: event.end, attendees: event.attendees?.map((a) => ({ email: a.email, displayName: a.displayName, responseStatus: a.responseStatus, })), creator: event.creator, organizer: event.organizer, htmlLink: event.htmlLink, status: event.status, created: event.created, updated: event.updated, recurrence: event.recurrence, }; return { content: [ { type: "text" as const, text: formatEventToMarkdown(eventDetail), }, ], }; } catch (error) { return { content: [ { type: "text" as const, text: `Error getting event: ${ error instanceof Error ? error.message : String(error) }`, }, ], }; } } // Update event function export async function updateEvent( params: z.infer<ReturnType<typeof updateEventSchema>> ) { try { const auth = createCalendarAuth(); const calendar = google.calendar({ version: "v3", auth }); // First get the existing event const existingEvent = await calendar.events.get({ calendarId: params.calendarId, eventId: params.eventId, }); const updatedEvent: any = { ...existingEvent.data }; // Update only the provided fields if (params.summary !== undefined) updatedEvent.summary = params.summary; if (params.description !== undefined) updatedEvent.description = params.description; if (params.location !== undefined) updatedEvent.location = params.location; if (params.startDateTime !== undefined) { updatedEvent.start = { ...updatedEvent.start, dateTime: params.startDateTime, }; } if (params.endDateTime !== undefined) { updatedEvent.end = { ...updatedEvent.end, dateTime: params.endDateTime }; } if (params.attendees !== undefined) { updatedEvent.attendees = params.attendees.map((email) => ({ email })); } const response = await calendar.events.update({ calendarId: params.calendarId, eventId: params.eventId, requestBody: updatedEvent, sendUpdates: "all", }); const updatedEventData = { id: response.data.id, summary: response.data.summary, start: response.data.start, end: response.data.end, location: response.data.location, description: response.data.description, attendees: response.data.attendees, htmlLink: response.data.htmlLink, updated: response.data.updated, }; return { content: [ { type: "text" as const, text: `# Event Updated Successfully ✅\n\n${formatEventToMarkdown(updatedEventData)}`, }, ], }; } catch (error) { return { content: [ { type: "text" as const, text: `Error updating event: ${ error instanceof Error ? error.message : String(error) }`, }, ], }; } } // Delete event function export async function deleteEvent( params: z.infer<ReturnType<typeof deleteEventSchema>> ) { try { const auth = createCalendarAuth(); const calendar = google.calendar({ version: "v3", auth }); await calendar.events.delete({ calendarId: params.calendarId, eventId: params.eventId, sendUpdates: "all", }); return { content: [ { type: "text" as const, text: `# Event Deleted Successfully ✅\n\nEvent \`${params.eventId}\` has been deleted from your calendar.`, }, ], }; } catch (error) { return { content: [ { type: "text" as const, text: `Error deleting event: ${ error instanceof Error ? error.message : String(error) }`, }, ], }; } } // List calendars function export async function listCalendars( params: z.infer<ReturnType<typeof listCalendarsSchema>> ) { try { const auth = createCalendarAuth(); const calendar = google.calendar({ version: "v3", auth }); const response = await calendar.calendarList.list({ maxResults: params.maxResults, }); const calendars = response.data.items?.map((cal) => ({ id: cal.id, summary: cal.summary, description: cal.description, timeZone: cal.timeZone, accessRole: cal.accessRole, primary: cal.primary, backgroundColor: cal.backgroundColor, foregroundColor: cal.foregroundColor, })); return { content: [ { type: "text" as const, text: formatCalendarsToMarkdown(calendars || []), }, ], }; } catch (error) { return { content: [ { type: "text" as const, text: `Error listing calendars: ${ error instanceof Error ? error.message : String(error) }`, }, ], }; } }

Implementation Reference

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/CaptainCrouton89/maps-mcp'

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