Skip to main content
Glama

Google Calendar MCP

utils.ts11 kB
import { calendar_v3 } from "googleapis"; import { ConflictCheckResult } from "../services/conflict-detection/types.js"; /** * Generates a Google Calendar event view URL */ export function generateEventUrl(calendarId: string, eventId: string): string { const encodedCalendarId = encodeURIComponent(calendarId); const encodedEventId = encodeURIComponent(eventId); return `https://calendar.google.com/calendar/event?eid=${encodedEventId}&cid=${encodedCalendarId}`; } /** * Gets the URL for a calendar event */ export function getEventUrl(event: calendar_v3.Schema$Event, calendarId?: string): string | null { if (event.htmlLink) { return event.htmlLink; } else if (calendarId && event.id) { return generateEventUrl(calendarId, event.id); } return null; } /** * Formats a date/time with timezone abbreviation */ function formatDateTime(dateTime?: string | null, date?: string | null, timeZone?: string): string { if (!dateTime && !date) return "unspecified"; try { const dt = dateTime || date; if (!dt) return "unspecified"; // If it's a date-only event (all-day), handle it specially if (date && !dateTime) { // For all-day events, just format the date string directly // Date-only strings like "2025-03-15" should be displayed as-is const [year, month, day] = date.split('-').map(Number); // Create a date string without any timezone conversion const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; // Calculate day of week using Zeller's congruence (timezone-independent) const q = day; const m = month <= 2 ? month + 12 : month; const y = month <= 2 ? year - 1 : year; const k = y % 100; const j = Math.floor(y / 100); const h = (q + Math.floor((13 * (m + 1)) / 5) + k + Math.floor(k / 4) + Math.floor(j / 4) - 2 * j) % 7; const dayOfWeek = (h + 6) % 7; // Convert to 0=Sunday format return `${dayNames[dayOfWeek]}, ${monthNames[month - 1]} ${day}, ${year}`; } const parsedDate = new Date(dt); if (isNaN(parsedDate.getTime())) return dt; // For timed events, include timezone const options: Intl.DateTimeFormatOptions = { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZoneName: 'short' }; if (timeZone) { options.timeZone = timeZone; } return parsedDate.toLocaleString('en-US', options); } catch (error) { return dateTime || date || "unspecified"; } } /** * Formats attendees with their response status */ function formatAttendees(attendees?: calendar_v3.Schema$EventAttendee[]): string { if (!attendees || attendees.length === 0) return ""; const formatted = attendees.map(attendee => { const email = attendee.email || "unknown"; const name = attendee.displayName || email; const status = attendee.responseStatus || "unknown"; const statusText = { 'accepted': 'accepted', 'declined': 'declined', 'tentative': 'tentative', 'needsAction': 'pending' }[status] || 'unknown'; return `${name} (${statusText})`; }).join(", "); return `\nGuests: ${formatted}`; } /** * Formats a single event with rich details */ export function formatEventWithDetails(event: calendar_v3.Schema$Event, calendarId?: string): string { const title = event.summary ? `Event: ${event.summary}` : "Untitled Event"; const eventId = event.id ? `\nEvent ID: ${event.id}` : ""; const description = event.description ? `\nDescription: ${event.description}` : ""; const location = event.location ? `\nLocation: ${event.location}` : ""; const colorId = event.colorId ? `\nColor ID: ${event.colorId}` : ""; // Format start and end times with timezone const startTime = formatDateTime(event.start?.dateTime, event.start?.date, event.start?.timeZone || undefined); const endTime = formatDateTime(event.end?.dateTime, event.end?.date, event.end?.timeZone || undefined); let timeInfo: string; if (event.start?.date) { // All-day event if (event.start.date === event.end?.date) { // Single day all-day event timeInfo = `\nDate: ${startTime}`; } else { // Multi-day all-day event - end date is exclusive, so subtract 1 day for display if (event.end?.date) { // Parse the end date properly without timezone conversion const [year, month, day] = event.end.date.split('-').map(Number); // Subtract 1 day since end is exclusive, handling month/year boundaries let adjustedDay = day - 1; let adjustedMonth = month; let adjustedYear = year; if (adjustedDay < 1) { adjustedMonth--; if (adjustedMonth < 1) { adjustedMonth = 12; adjustedYear--; } // Get days in the previous month const daysInMonth = new Date(adjustedYear, adjustedMonth, 0).getDate(); adjustedDay = daysInMonth; } // Format without using Date object to avoid timezone issues const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; // Calculate day of week using Zeller's congruence const q = adjustedDay; const m = adjustedMonth <= 2 ? adjustedMonth + 12 : adjustedMonth; const y = adjustedMonth <= 2 ? adjustedYear - 1 : adjustedYear; const k = y % 100; const j = Math.floor(y / 100); const h = (q + Math.floor((13 * (m + 1)) / 5) + k + Math.floor(k / 4) + Math.floor(j / 4) - 2 * j) % 7; const dayOfWeek = (h + 6) % 7; // Convert to 0=Sunday format const adjustedEndTime = `${dayNames[dayOfWeek]}, ${monthNames[adjustedMonth - 1]} ${adjustedDay}, ${adjustedYear}`; timeInfo = `\nStart Date: ${startTime}\nEnd Date: ${adjustedEndTime}`; } else { timeInfo = `\nStart Date: ${startTime}`; } } } else { // Timed event timeInfo = `\nStart: ${startTime}\nEnd: ${endTime}`; } const attendeeInfo = formatAttendees(event.attendees); const eventUrl = getEventUrl(event, calendarId); const urlInfo = eventUrl ? `\nView: ${eventUrl}` : ""; return `${title}${eventId}${description}${timeInfo}${location}${colorId}${attendeeInfo}${urlInfo}`; } /** * Formats conflict check results for display */ export function formatConflictWarnings(conflicts: ConflictCheckResult): string { if (!conflicts.hasConflicts) return ""; let warnings = ""; // Format duplicate warnings if (conflicts.duplicates.length > 0) { warnings += "\n\n⚠️ POTENTIAL DUPLICATES DETECTED:"; for (const dup of conflicts.duplicates) { warnings += `\n\n━━━ Duplicate Event (${Math.round(dup.event.similarity * 100)}% similar) ━━━`; warnings += `\n${dup.suggestion}`; // Show full event details if available if (dup.fullEvent) { warnings += `\n\nExisting event details:`; warnings += `\n${formatEventWithDetails(dup.fullEvent, dup.calendarId)}`; } else { // Fallback to basic info warnings += `\n• "${dup.event.title}"`; if (dup.event.url) { warnings += `\n View existing event: ${dup.event.url}`; } } } } // Format conflict warnings if (conflicts.conflicts.length > 0) { warnings += "\n\n⚠️ SCHEDULING CONFLICTS DETECTED:"; const conflictsByCalendar = conflicts.conflicts.reduce((acc, conflict) => { if (!acc[conflict.calendar]) acc[conflict.calendar] = []; acc[conflict.calendar].push(conflict); return acc; }, {} as Record<string, typeof conflicts.conflicts>); for (const [calendar, calendarConflicts] of Object.entries(conflictsByCalendar)) { warnings += `\n\nCalendar: ${calendar}`; for (const conflict of calendarConflicts) { warnings += `\n\n━━━ Conflicting Event ━━━`; if (conflict.overlap) { warnings += `\n⚠️ Overlap: ${conflict.overlap.duration} (${conflict.overlap.percentage}% of your event)`; } // Show full event details if available if (conflict.fullEvent) { warnings += `\n\nConflicting event details:`; warnings += `\n${formatEventWithDetails(conflict.fullEvent, calendar)}`; } else { // Fallback to basic info warnings += `\n• Conflicts with "${conflict.event.title}"`; if (conflict.event.start && conflict.event.end) { const start = formatDateTime(conflict.event.start); const end = formatDateTime(conflict.event.end); warnings += `\n Time: ${start} - ${end}`; } if (conflict.event.url) { warnings += `\n View event: ${conflict.event.url}`; } } } } } return warnings; } /** * Creates a response with event details and optional conflict warnings */ export function createEventResponseWithConflicts( event: calendar_v3.Schema$Event, calendarId: string, conflicts?: ConflictCheckResult, actionVerb: string = "created" ): string { const eventDetails = formatEventWithDetails(event, calendarId); const conflictWarnings = conflicts ? formatConflictWarnings(conflicts) : ""; const successMessage = conflicts?.hasConflicts ? `Event ${actionVerb} with warnings!` : `Event ${actionVerb} successfully!`; return `${successMessage}\n\n${eventDetails}${conflictWarnings}`; }

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