Skip to main content
Glama
CalendarRegistry.ts14.5 kB
import { OAuth2Client } from 'google-auth-library'; import { calendar_v3, google } from 'googleapis'; import { getCredentialsProjectId } from '../auth/utils.js'; /** * Represents a calendar accessible from a specific account */ export interface CalendarAccess { accountId: string; accessRole: 'owner' | 'writer' | 'reader' | 'freeBusyReader'; primary: boolean; summary: string; summaryOverride?: string; } /** * Represents a unified view of a calendar across multiple accounts */ export interface UnifiedCalendar { calendarId: string; accounts: CalendarAccess[]; preferredAccount: string; // Account with highest permission displayName: string; // Primary account's name (summaryOverride > summary) } /** * Permission ranking for calendar access */ const PERMISSION_RANK: Record<string, number> = { 'owner': 4, 'writer': 3, 'reader': 2, 'freeBusyReader': 1, }; /** * CalendarRegistry service for managing calendar deduplication and permission-based account selection. * Implemented as a singleton to ensure cache is shared across all handlers * and can be properly invalidated when accounts change. */ export class CalendarRegistry { private static instance: CalendarRegistry | null = null; private cache: Map<string, { data: UnifiedCalendar[]; timestamp: number }> = new Map(); private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes // Track in-flight requests to prevent duplicate API calls during concurrent access private inFlightRequests: Map<string, Promise<UnifiedCalendar[]>> = new Map(); /** * Get the singleton instance of CalendarRegistry */ static getInstance(): CalendarRegistry { if (!CalendarRegistry.instance) { CalendarRegistry.instance = new CalendarRegistry(); } return CalendarRegistry.instance; } /** * Reset the singleton instance (useful for testing or when accounts change) * Clears the cache and resets the instance */ static resetInstance(): void { if (CalendarRegistry.instance) { CalendarRegistry.instance.clearCache(); } CalendarRegistry.instance = null; } /** * Get calendar client for a specific account */ private getCalendar(auth: OAuth2Client): calendar_v3.Calendar { const quotaProjectId = getCredentialsProjectId(); const config: any = { version: 'v3', auth, timeout: 3000 }; if (quotaProjectId) { config.quotaProjectId = quotaProjectId; } return google.calendar(config); } /** * Fetch all calendars from all accounts and build unified registry. * Uses in-flight request tracking to prevent duplicate API calls during concurrent access. */ async getUnifiedCalendars(accounts: Map<string, OAuth2Client>): Promise<UnifiedCalendar[]> { const cacheKey = Array.from(accounts.keys()).sort().join(','); // Check if there's already an in-flight request for this cache key const inFlight = this.inFlightRequests.get(cacheKey); if (inFlight) { return inFlight; } // Check cache const cached = this.cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) { return cached.data; } // Create new request and track it const requestPromise = this.fetchAndBuildUnifiedCalendars(accounts, cacheKey); this.inFlightRequests.set(cacheKey, requestPromise); try { return await requestPromise; } finally { // Remove from in-flight tracking once complete this.inFlightRequests.delete(cacheKey); } } /** * Internal method to fetch calendars and build the unified registry */ private async fetchAndBuildUnifiedCalendars( accounts: Map<string, OAuth2Client>, cacheKey: string ): Promise<UnifiedCalendar[]> { // Fetch calendars from all accounts in parallel const calendarsByAccount = await Promise.all( Array.from(accounts.entries()).map(async ([accountId, client]) => { try { const calendar = this.getCalendar(client); const response = await calendar.calendarList.list(); return { accountId, calendars: response.data.items || [] }; } catch (error) { // If one account fails, continue with others return { accountId, calendars: [] as calendar_v3.Schema$CalendarListEntry[] }; } }) ); // Build calendar map: calendarId -> CalendarAccess[] const calendarMap = new Map<string, CalendarAccess[]>(); for (const { accountId, calendars } of calendarsByAccount) { for (const cal of calendars) { if (!cal.id) continue; const access: CalendarAccess = { accountId, accessRole: (cal.accessRole as CalendarAccess['accessRole']) || 'reader', primary: cal.primary || false, summary: cal.summary || cal.id, summaryOverride: cal.summaryOverride ?? undefined }; const existing = calendarMap.get(cal.id) || []; existing.push(access); calendarMap.set(cal.id, existing); } } // Convert to UnifiedCalendar[] const unified: UnifiedCalendar[] = Array.from(calendarMap.entries()).map(([calendarId, accounts]) => { // Find preferred account (highest permission) const sortedAccounts = [...accounts].sort((a, b) => { const rankA = PERMISSION_RANK[a.accessRole] || 0; const rankB = PERMISSION_RANK[b.accessRole] || 0; return rankB - rankA; // Descending order }); const preferredAccount = sortedAccounts[0].accountId; // Determine display name (prefer primary account's override, then summary) const primaryAccess = accounts.find(a => a.primary); const preferredAccess = sortedAccounts[0]; const displayName = primaryAccess?.summaryOverride || preferredAccess.summaryOverride || preferredAccess.summary; return { calendarId, accounts, preferredAccount, displayName }; }); // Cache results this.cache.set(cacheKey, { data: unified, timestamp: Date.now() }); return unified; } /** * Find which account to use for a specific calendar * For write operations, returns account with highest permission * For read operations, returns any account with access (prefers higher permission) */ async getAccountForCalendar( calendarId: string, accounts: Map<string, OAuth2Client>, operationType: 'read' | 'write' = 'read' ): Promise<{ accountId: string; accessRole: string } | null> { const unified = await this.getUnifiedCalendars(accounts); const calendar = unified.find(c => c.calendarId === calendarId); if (!calendar) { return null; } if (operationType === 'write') { // For write operations, use account with highest permission const preferredAccess = calendar.accounts.find(a => a.accountId === calendar.preferredAccount); if (!preferredAccess) return null; // Check if account has write permission if (preferredAccess.accessRole === 'owner' || preferredAccess.accessRole === 'writer') { return { accountId: preferredAccess.accountId, accessRole: preferredAccess.accessRole }; } return null; // No write access available } // For read operations, use preferred account (highest permission) const preferredAccess = calendar.accounts.find(a => a.accountId === calendar.preferredAccount); if (!preferredAccess) return null; return { accountId: preferredAccess.accountId, accessRole: preferredAccess.accessRole }; } /** * Get all accounts that have access to a specific calendar */ async getAccountsForCalendar( calendarId: string, accounts: Map<string, OAuth2Client> ): Promise<CalendarAccess[]> { const unified = await this.getUnifiedCalendars(accounts); const calendar = unified.find(c => c.calendarId === calendarId); return calendar?.accounts || []; } /** * Clear cache and in-flight requests (useful for testing or when accounts change) */ clearCache(): void { this.cache.clear(); this.inFlightRequests.clear(); } /** * Resolve a calendar name or ID to a calendar ID and preferred account * Searches across all accounts for matching calendars by name * Returns the account with highest permissions for the matched calendar * * @param nameOrId Calendar name (summary/summaryOverride) or ID * @param accounts Map of available accounts * @param operationType 'read' or 'write' operation type * @returns Calendar ID and account info, or null if not found */ async resolveCalendarNameToId( nameOrId: string, accounts: Map<string, OAuth2Client>, operationType: 'read' | 'write' = 'read' ): Promise<{ calendarId: string; accountId: string; accessRole: string } | null> { // Special case: "primary" is an alias for each account's primary calendar // When only one account exists, use it directly without registry lookup if (nameOrId === 'primary') { if (accounts.size === 1) { const [accountId] = accounts.keys(); // Primary calendar always has owner access for the account owner return { calendarId: 'primary', accountId, accessRole: 'owner' }; } // Multiple accounts: try to find best match via registry // Each account's primary calendar ID is typically the account email const result = await this.getAccountForCalendar(nameOrId, accounts, operationType); if (result) { return { calendarId: nameOrId, ...result }; } // If registry lookup fails with multiple accounts, use first account as fallback // This maintains backwards compatibility while still working const [firstAccountId] = accounts.keys(); return { calendarId: 'primary', accountId: firstAccountId, accessRole: 'owner' }; } // If it looks like an ID (contains @), use getAccountForCalendar if (nameOrId.includes('@')) { const result = await this.getAccountForCalendar(nameOrId, accounts, operationType); if (result) { return { calendarId: nameOrId, ...result }; } return null; } // It's a name - search across all calendars const unified = await this.getUnifiedCalendars(accounts); const lowerName = nameOrId.toLowerCase(); // Search for matching calendar by name // Priority: exact summaryOverride > case-insensitive summaryOverride > exact summary > case-insensitive summary let match: UnifiedCalendar | undefined; // Priority 1: Exact match on any account's summaryOverride match = unified.find(cal => cal.accounts.some(a => a.summaryOverride === nameOrId) ); // Priority 2: Case-insensitive match on summaryOverride if (!match) { match = unified.find(cal => cal.accounts.some(a => a.summaryOverride?.toLowerCase() === lowerName) ); } // Priority 3: Exact match on displayName (primary account's name) if (!match) { match = unified.find(cal => cal.displayName === nameOrId); } // Priority 4: Case-insensitive match on displayName if (!match) { match = unified.find(cal => cal.displayName.toLowerCase() === lowerName); } // Priority 5: Exact match on any account's summary if (!match) { match = unified.find(cal => cal.accounts.some(a => a.summary === nameOrId) ); } // Priority 6: Case-insensitive match on summary if (!match) { match = unified.find(cal => cal.accounts.some(a => a.summary.toLowerCase() === lowerName) ); } if (!match) { return null; } // Check write access if needed if (operationType === 'write') { const preferredAccess = match.accounts.find(a => a.accountId === match!.preferredAccount); if (!preferredAccess || (preferredAccess.accessRole !== 'owner' && preferredAccess.accessRole !== 'writer')) { return null; // No write access available } return { calendarId: match.calendarId, accountId: preferredAccess.accountId, accessRole: preferredAccess.accessRole }; } // For read operations, return preferred account const preferredAccess = match.accounts.find(a => a.accountId === match!.preferredAccount); if (!preferredAccess) { return null; } return { calendarId: match.calendarId, accountId: preferredAccess.accountId, accessRole: preferredAccess.accessRole }; } /** * Resolve multiple calendar names/IDs to their owning accounts. * For each calendar, determines which account has access (using highest permission). * Returns a routing map of accountId -> calendarIds for efficient multi-account queries. * * @param namesOrIds Array of calendar names or IDs to resolve * @param accounts Map of available accounts * @param options.restrictToAccounts Only resolve on these specific accounts (for strict mode) * @returns Routing map and warnings for calendars not found */ async resolveCalendarsToAccounts( namesOrIds: string[], accounts: Map<string, OAuth2Client>, options?: { restrictToAccounts?: string[] } ): Promise<{ resolved: Map<string, string[]>; // accountId -> calendarIds for that account warnings: string[]; // calendars not found on any account }> { const resolved = new Map<string, string[]>(); const warnings: string[] = []; // Filter accounts if restricted const availableAccounts = options?.restrictToAccounts ? new Map(Array.from(accounts.entries()).filter(([id]) => options.restrictToAccounts!.includes(id))) : accounts; for (const nameOrId of namesOrIds) { // Use existing resolution logic which finds the preferred (highest permission) account const resolution = await this.resolveCalendarNameToId(nameOrId, availableAccounts, 'read'); if (!resolution) { warnings.push(`Calendar "${nameOrId}" not found on any account`); continue; } const { calendarId, accountId } = resolution; // Add to routing map const accountCalendars = resolved.get(accountId) || []; if (!accountCalendars.includes(calendarId)) { accountCalendars.push(calendarId); } resolved.set(accountId, accountCalendars); } return { resolved, warnings }; } }

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