Skip to main content
Glama

Google Calendar and Meet MCP Server

by INSIDE-HAIR
GoogleMeetAPI.ts66.9 kB
/** * Google Meet API client for Google Meet MCP Server v3.0 * * Comprehensive API client that integrates Google Calendar API v3 and Google Meet API v2 * with advanced monitoring, authentication, and debugging capabilities. * * Features: * - Direct Token Authentication (CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN) * - File-based Authentication (legacy G_OAUTH_CREDENTIALS support) * - Real-time API monitoring and performance tracking * - Comprehensive error handling with business logic validation * - Integration with v3.0 monitoring system (health checks, metrics) * - Debug mode support for troubleshooting Claude Desktop integration * * Authentication Methods: * 1. Direct Token (Recommended): Uses environment variables for tokens * 2. File-based (Legacy): Uses credentials.json file * * Monitoring Integration: * - All API calls are automatically monitored when ENABLE_HEALTH_CHECK=true * - Performance metrics are exposed via /metrics endpoint * - API health status available via /api-status endpoint * - Debug logging available when LOG_LEVEL=debug * * Supports 23 validated tools across: * - Calendar API v3: 6 tools (calendars, events, permissions) * - Meet API v2: 17 tools (spaces, records, recordings, transcripts, participants) */ import fs from "fs/promises"; import path from "path"; import { google, calendar_v3 } from "googleapis"; // Dynamic import for 'open' to avoid Smithery compatibility issues import http from "http"; import { URL } from "url"; import { GoogleApiErrorHandler } from "./errors/GoogleApiErrorHandler.js"; import type { GoogleOAuth2Client, GoogleCalendarClient, GoogleTokens, GoogleAuthOAuth2Instance, GoogleCalendarInstance, MeetSpace, SpaceConfig, ConferenceRecord, Recording, Transcript, TranscriptEntry, Participant, ParticipantSession, GuestPermissions, CreateMeetingOptions, EventUpdateData, SpaceUpdateData, RestClientOptions, RestResponse, GoogleApiError, NodeError, ProcessedCalendar, ProcessedEvent, GoogleCalendar } from './types/index.js'; /** * REST client for Google Meet API v2 * Provides direct access to Meet API endpoints not available in googleapis library */ class MeetRestClient { auth: GoogleAuthOAuth2Instance; baseUrls: { v2: string }; constructor(oAuth2Client: GoogleAuthOAuth2Instance) { this.auth = oAuth2Client; this.baseUrls = { v2: "https://meet.googleapis.com/v2", }; } /** * Get access token from OAuth2 client */ async getAccessToken() { try { const { token } = await this.auth.getAccessToken(); return token; } catch (error) { throw new Error(`Failed to get access token: ${error.message}`); } } /** * Make authenticated request to Meet API */ async makeRequest(endpoint: string, options: RestClientOptions = {}, apiVersion = "v2"): Promise<any> { const accessToken = await this.getAccessToken(); const baseUrl = this.baseUrls[apiVersion]; // Ensure GET method is explicitly set for read operations when not specified const method = options.method || "GET"; const requestOptions = { method: method, ...options, headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json", ...options.headers, }, }; // Only add Content-Type for requests with body (POST, PATCH, PUT) if (["POST", "PATCH", "PUT"].includes(method) && options.body) { requestOptions.headers["Content-Type"] = "application/json"; } const response = await fetch(`${baseUrl}${endpoint}`, requestOptions); if (!response.ok) { let errorData; try { errorData = await response.json(); } catch (parseError) { errorData = null; } // Create error object that matches GoogleApiError interface const apiError = new Error(`Meet API Error: ${response.statusText}`) as GoogleApiError; apiError.response = { status: response.status, statusText: response.statusText, data: errorData, }; throw apiError; } const contentType = response.headers.get("content-type"); if (contentType && contentType.includes("application/json")) { return response.json(); } return response.text(); } // ========== GOOGLE MEET API v2 METHODS (GA) ========== /** * Create a Google Meet space */ async createSpace(config: SpaceConfig): Promise<MeetSpace> { const requestBody = { config }; return this.makeRequest("/spaces", { method: "POST", body: JSON.stringify(requestBody), }); } /** * Get space details */ async getSpace(spaceName: string): Promise<MeetSpace> { // Validate and format space name if (!spaceName || typeof spaceName !== "string") { throw new Error("Invalid space name: must be a non-empty string"); } // Ensure spaceName is properly formatted (spaces/{space_id}) const formattedSpaceName = spaceName.startsWith("spaces/") ? spaceName : `spaces/${spaceName}`; // Validate the formatted name (spaces/{space_id} or spaces/{meetingCode}) if (!formattedSpaceName.match(/^spaces\/[a-zA-Z0-9_-]{1,128}$/)) { throw new Error( `Invalid space name format: ${formattedSpaceName}. Expected: spaces/{space_id} or spaces/{meetingCode}` ); } return this.makeRequest(`/${formattedSpaceName}`); } /** * Update space configuration */ async updateSpace(spaceName: string, config: SpaceConfig): Promise<MeetSpace> { const requestBody = { config }; return this.makeRequest(`/${spaceName}`, { method: "PATCH", body: JSON.stringify(requestBody), }); } /** * End active conference in space */ async endActiveConference(spaceName: string): Promise<void> { return this.makeRequest(`/${spaceName}:endActiveConference`, { method: "POST", body: JSON.stringify({}), }); } /** * List conference records */ async listConferenceRecords(filter?: string, pageSize = 10): Promise<{ conferenceRecords: ConferenceRecord[]; nextPageToken?: string }> { const params = new URLSearchParams(); if (filter) params.append("filter", filter); if (pageSize) params.append("pageSize", pageSize.toString()); const endpoint = `/conferenceRecords?${params.toString()}`; return this.makeRequest(endpoint); } /** * Get conference record */ async getConferenceRecord(conferenceRecordName: string): Promise<ConferenceRecord> { return this.makeRequest(`/${conferenceRecordName}`); } /** * List recordings for conference record */ async listRecordings(conferenceRecordName: string): Promise<{ recordings: Recording[]; nextPageToken?: string }> { return this.makeRequest(`/${conferenceRecordName}/recordings`); } /** * Get recording details */ async getRecording(recordingName: string): Promise<Recording> { return this.makeRequest(`/${recordingName}`); } /** * List transcripts for conference record */ async listTranscripts(conferenceRecordName: string): Promise<{ transcripts: Transcript[]; nextPageToken?: string }> { return this.makeRequest(`/${conferenceRecordName}/transcripts`); } /** * Get transcript details */ async getTranscript(transcriptName: string): Promise<Transcript> { return this.makeRequest(`/${transcriptName}`); } /** * List transcript entries */ async listTranscriptEntries(transcriptName: string, pageSize = 100): Promise<{ transcriptEntries: TranscriptEntry[]; nextPageToken?: string }> { const params = new URLSearchParams(); if (pageSize) params.append("pageSize", pageSize.toString()); const endpoint = `/${transcriptName}/entries?${params.toString()}`; return this.makeRequest(endpoint); } // ========== ADDITIONAL METHODS FROM OFFICIAL SPECS ========== /** * Delete a Google Meet space * Note: Google Meet API v2 does not support deleting spaces directly. * This method is not available in the official API specification. */ async deleteSpace(spaceName: string): Promise<never> { throw new Error( "Delete space operation is not supported by Google Meet API v2. Spaces cannot be deleted directly." ); } /** * Get participant details */ async getParticipant(participantName: string): Promise<Participant> { return this.makeRequest(`/${participantName}`); } /** * List participants for a conference record */ async listParticipants(conferenceRecordName: string, pageSize = 10): Promise<{ participants: Participant[]; nextPageToken?: string }> { const params = new URLSearchParams(); if (pageSize) params.append("pageSize", pageSize.toString()); const endpoint = `/${conferenceRecordName}/participants?${params.toString()}`; return this.makeRequest(endpoint); } /** * Get participant session details */ async getParticipantSession(participantSessionName: string): Promise<ParticipantSession> { return this.makeRequest(`/${participantSessionName}`); } /** * List participant sessions */ async listParticipantSessions(participantName: string, pageSize = 10): Promise<{ participantSessions: ParticipantSession[]; nextPageToken?: string }> { const params = new URLSearchParams(); if (pageSize) params.append("pageSize", pageSize.toString()); const endpoint = `/${participantName}/participantSessions?${params.toString()}`; return this.makeRequest(endpoint); } // getTranscriptEntry method removed - not available in Google Meet API v2 // Use listTranscriptEntries instead to get transcript entries } class GoogleMeetAPI { credentialsPath: string; tokenPath: string; calendar: GoogleCalendarInstance | null; meetRestClient: MeetRestClient | null; auth: GoogleAuthOAuth2Instance | null; meet: null; /** * Initialize the Google Meet API client. * @param {string} credentialsPath - Path to the client_secret.json file * @param {string} tokenPath - Path to save/load the token.json file */ constructor(credentialsPath: string, tokenPath: string) { this.credentialsPath = credentialsPath; this.tokenPath = tokenPath; this.calendar = null; this.meetRestClient = null; this.auth = null; } /** * Initialize with direct tokens (Smithery-style authentication) */ async initializeWithDirectTokens() { const CLIENT_ID = process.env.CLIENT_ID; const CLIENT_SECRET = process.env.CLIENT_SECRET; const REFRESH_TOKEN = process.env.REFRESH_TOKEN; // Create OAuth2 client with direct credentials const oAuth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET); oAuth2Client.setCredentials({ refresh_token: REFRESH_TOKEN }); // Store OAuth2 client for shared use this.auth = oAuth2Client; // Initialize API clients try { this.calendar = google.calendar({ version: "v3", auth: oAuth2Client }); // Initialize Meet REST client for direct API access this.meetRestClient = new MeetRestClient(oAuth2Client); this.meet = null; if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error("✅ Google Meet API v2 access enabled via REST client"); } // Test the authentication by making a simple API call await this.calendar.calendarList.list({ maxResults: 1 }); console.error("✅ Direct token authentication successful"); } catch (error) { throw new Error(`Direct token authentication failed: ${error.message}`); } } /** * Initialize the API client with OAuth2 credentials. */ async initialize() { // Method 1: Smithery-style direct token authentication (recommended for production) if (process.env.CLIENT_ID && process.env.CLIENT_SECRET && process.env.REFRESH_TOKEN) { console.error("🔑 Using direct token authentication (Smithery-style)"); await this.initializeWithDirectTokens(); return; } // Method 2: File-based OAuth credentials (existing method) console.error("📁 Using file-based OAuth credentials"); // Debug logging - only in development mode if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error(`=== GOOGLE API INITIALIZATION DEBUG ===`); console.error(`Attempting to read credentials from: ${this.credentialsPath}`); console.error(`Google import:`, typeof google); console.error(`Google.auth:`, typeof google.auth); console.error(`Google.calendar:`, typeof google.calendar); } let credentials; try { credentials = JSON.parse( await fs.readFile(this.credentialsPath, "utf8") ); } catch (error) { if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error(`Failed to read credentials file: ${error.message}`); } throw new Error(`Cannot read credentials file at ${this.credentialsPath}: ${error.message}`); } // Handle both 'web' and 'installed' credential types const credentialData = credentials.web || credentials.installed; if (!credentialData) { throw new Error( 'Invalid credentials file format. Expected "web" or "installed" key.' ); } const { client_id, client_secret, redirect_uris } = credentialData; const oAuth2Client = new google.auth.OAuth2( client_id, client_secret, redirect_uris[0] ); try { // Check if token exists and use it const token = JSON.parse(await fs.readFile(this.tokenPath, "utf8")); oAuth2Client.setCredentials(token); // Check if token is expired and needs refresh if (token.expiry_date && token.expiry_date < Date.now()) { // Token is expired, refresh it const { credentials } = await (oAuth2Client as any).refreshToken( token.refresh_token ); await fs.writeFile(this.tokenPath, JSON.stringify(credentials)); oAuth2Client.setCredentials(credentials); } } catch (error) { // Token doesn't exist or is invalid, try to create it automatically console.error(`No valid token found at ${this.tokenPath}`); console.error(`Attempting automatic authentication...`); try { await this.performAutoAuth(oAuth2Client); } catch (authError) { throw new Error( `Authentication failed. Please run setup manually: G_OAUTH_CREDENTIALS="${this.credentialsPath}" npm run setup` ); } } // Store OAuth2 client for shared use this.auth = oAuth2Client; // Initialize the calendar API with debugging if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error("Initializing Google Calendar API..."); console.error("Google object:", typeof google); console.error("Google.calendar function:", typeof google.calendar); } this.calendar = google.calendar({ version: "v3", auth: oAuth2Client }); if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error("Calendar object:", typeof this.calendar); console.error("Calendar.calendars:", typeof this.calendar?.calendars); console.error("Calendar.calendars.list:", typeof this.calendar?.calendars?.list); } // Initialize Meet REST client for direct API access this.meetRestClient = new MeetRestClient(oAuth2Client); // Note: Now using direct REST API calls for Google Meet API v2 this.meet = null; if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error("✅ Google Meet API v2 access enabled via REST client"); } } // ========== GOOGLE CALENDAR API v3 METHODS ========== /** * Lists calendars available to the user * @returns {Promise<Array>} - Array of calendar objects */ async listCalendars(): Promise<ProcessedCalendar[]> { if (!this.calendar) { throw new Error("Calendar API not initialized. Please call initialize() first."); } if (!this.calendar.calendarList || typeof this.calendar.calendarList.list !== 'function') { throw new Error("Calendar API calendarList.list method is not available. Check googleapis initialization."); } try { const response = await this.calendar.calendarList.list(); const calendars = response.data.items || []; return calendars.map((calendar: GoogleCalendar) => ({ id: calendar.id, summary: calendar.summary || "No title", primary: calendar.primary || false, backgroundColor: calendar.backgroundColor, foregroundColor: calendar.foregroundColor, accessRole: calendar.accessRole, })); } catch (error) { throw new Error(`Error listing calendars: ${error.message}`); } } /** * Check if a calendar is accessible to the current user * @param {string} calendarId - Calendar ID to check * @returns {Promise<boolean>} - True if accessible, false otherwise */ async isCalendarAccessible(calendarId: string): Promise<boolean> { try { // Try to get calendar metadata await this.calendar.calendars.get({ calendarId }); return true; } catch (error) { if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error(`🔍 Calendar ${calendarId} accessibility check failed:`, error.message); } return false; } } /** * Find which calendar contains a specific event * @param {string} eventId - Event ID to search for * @returns {Promise<string|null>} - Calendar ID containing the event, or null if not found */ async findEventCalendar(eventId: string): Promise<string | null> { try { // Get list of all accessible calendars const calendars = await this.listCalendars(); // Try to find the event in each calendar for (const calendar of calendars) { try { await this.calendar.events.get({ calendarId: calendar.id, eventId: eventId, }); // If we get here without error, we found the calendar if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error(`🔍 Found event ${eventId} in calendar: ${calendar.id} (${calendar.summary})`); } return calendar.id; } catch (error) { // Event not in this calendar, continue searching continue; } } if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error(`🔍 Event ${eventId} not found in any accessible calendar`); } return null; } catch (error) { if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error(`🔍 Error searching for event ${eventId}:`, error.message); } return null; } } /** * List upcoming calendar events (including those with Google Meet). * @param {number} maxResults - Maximum number of results to return * @param {string} timeMin - Start time in ISO format * @param {string} timeMax - End time in ISO format * @param {string} calendarId - Calendar ID (default: "primary") * @returns {Promise<Array>} - List of calendar events */ async listCalendarEvents( maxResults = 10, timeMin: string | null = null, timeMax: string | null = null, calendarId = "primary" ): Promise<ProcessedEvent[]> { // Verify calendar client is initialized if (!this.calendar) { throw new Error("Calendar API not initialized. Please ensure authentication is completed."); } // For non-primary calendars, check accessibility first if (calendarId !== "primary") { const accessible = await this.isCalendarAccessible(calendarId); if (!accessible) { throw new Error(`Calendar "${calendarId}" is not accessible. Please verify the calendar ID and your permissions.`); } } // Default timeMin to now if not provided if (!timeMin) { timeMin = new Date().toISOString(); } // Prepare parameters for the API call const params: any = { calendarId: calendarId, maxResults: maxResults, timeMin: timeMin, orderBy: "startTime", singleEvents: true, conferenceDataVersion: 1, }; if (timeMax) { params.timeMax = timeMax; } if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error("🔍 listCalendarEvents params:", JSON.stringify(params, null, 2)); console.error("🔍 Calendar client available:", !!this.calendar); console.error("🔍 Auth client available:", !!this.auth); } try { const response = await this.calendar.events.list(params); const events = response.data.items || []; // Return all events, format them properly const formattedEvents = []; for (const event of events) { const formattedEvent = this._formatCalendarEvent(event); if (formattedEvent) { formattedEvents.push(formattedEvent); } } return formattedEvents; } catch (error) { // Enhanced error handling for calendar access issues if (error.code === 404) { throw new Error(`Calendar not found: The calendar with ID "${calendarId}" does not exist or you don't have access to it. Please check the calendar ID and your permissions.`); } if (error.code === 403) { throw new Error(`Access denied: You don't have sufficient permissions to access calendar "${calendarId}". Required permission: reader access or higher.`); } if (error.code === 401) { throw new Error(`Authentication failed: Your credentials are invalid or expired. Please re-authenticate.`); } if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error("🔍 Full error details:", { code: error.code, message: error.message, status: error.status, calendarId: calendarId, params: params }); } throw new Error(`Error listing meetings: ${error.message} (Code: ${error.code || 'unknown'})`); } } /** * Get details of a specific calendar event. * @param {string} eventId - ID of the calendar event to retrieve * @param {string} calendarId - Calendar ID (defaults to "primary") * @returns {Promise<Object>} - Calendar event details */ async getCalendarEvent(eventId: string, calendarId?: string): Promise<ProcessedEvent> { // If no calendar ID is provided, try to find the event in available calendars if (!calendarId) { console.error("🔍 No calendar_id provided, searching for event in all calendars..."); const foundCalendarId = await this.findEventCalendar(eventId); if (foundCalendarId) { calendarId = foundCalendarId; console.error(`🔍 Found event in calendar: ${calendarId}`); } else { calendarId = "primary"; // Fallback to primary console.error("🔍 Event not found in any calendar, defaulting to primary"); } } try { const response = await this.calendar.events.get({ calendarId: calendarId, eventId: eventId, } as any); const event = response.data; const formattedEvent = this._formatCalendarEvent(event); if (!formattedEvent) { throw new Error(`Failed to format event data for event ID ${eventId}`); } return formattedEvent; } catch (error) { throw new Error(`Error getting meeting: ${error.message}`); } } /** * Create a new calendar event with optional Google Meet conference. * @param {Object} eventData - Event data * @param {string} eventData.summary - Title of the event * @param {string} eventData.startTime - Start time in ISO format * @param {string} eventData.endTime - End time in ISO format * @param {string} eventData.description - Description for the event * @param {string} eventData.location - Location for the event * @param {string} eventData.timeZone - Time zone * @param {Array<string>} eventData.attendees - List of email addresses for attendees * @param {boolean} eventData.createMeetConference - Whether to create Google Meet conference * @param {Object} eventData.guestPermissions - Guest permissions * @param {string} eventData.calendarId - Calendar ID (default: "primary") * @returns {Promise<Object>} - Created event details */ async createCalendarEvent({ summary, startTime, endTime, description = "", location = "", timeZone = "UTC", attendees = [], createMeetConference = false, guestPermissions = {} as GuestPermissions, calendarId = "primary", }) { // Prepare attendees list in the format required by the API const formattedAttendees = attendees.map((email) => ({ email })); // Create the event object const event: any = { summary: summary, description: description, location: location, start: { dateTime: startTime, timeZone: timeZone, }, end: { dateTime: endTime, timeZone: timeZone, }, attendees: formattedAttendees, // Guest permissions from Calendar API guestsCanInviteOthers: guestPermissions?.canInviteOthers !== undefined ? guestPermissions.canInviteOthers : true, guestsCanModify: guestPermissions?.canModify !== undefined ? guestPermissions.canModify : false, guestsCanSeeOtherGuests: guestPermissions?.canSeeOtherGuests !== undefined ? guestPermissions.canSeeOtherGuests : true, }; // Add Google Meet conference if requested if (createMeetConference) { event.conferenceData = { createRequest: { requestId: `meet-${Date.now()}`, conferenceSolutionKey: { type: "hangoutsMeet" as const, }, }, }; } try { const response = await this.calendar.events.insert({ calendarId: calendarId, conferenceDataVersion: createMeetConference ? 1 : 0, resource: event, }); const createdEvent = response.data; const formattedEvent = this._formatCalendarEvent(createdEvent); if (!formattedEvent) { throw new Error("Failed to format event data for newly created event"); } return formattedEvent; } catch (error) { throw new Error(`Error creating calendar event: ${error.message}`); } } /** * Create a new Google Meet meeting (legacy method for compatibility). * @param {string} summary - Title of the meeting * @param {string} startTime - Start time in ISO format * @param {string} endTime - End time in ISO format * @param {string} description - Description for the meeting * @param {Array<string>} attendees - List of email addresses for attendees * @param {boolean} enableRecording - Whether to enable recording (requires Google Workspace) * @param {Object} options - Additional options * @param {Array<string>} options.coHosts - List of email addresses for co-hosts * @param {boolean} options.enableTranscription - Enable transcription * @param {boolean} options.enableSmartNotes - Enable smart notes * @param {boolean} options.attendanceReport - Enable attendance report * @param {Object} options.spaceConfig - Space configuration options * @param {Object} options.guestPermissions - Guest permissions for Calendar API * @returns {Promise<Object>} - Created meeting details */ async createMeeting( summary: string, startTime: string, endTime: string, description = "", attendees: string[] = [], enableRecording = false, options: CreateMeetingOptions = {} ) { const { coHosts = [], enableTranscription = false, enableSmartNotes = false, attendanceReport = false, spaceConfig = {}, guestPermissions = {} as GuestPermissions, } = options; // Note: Using Calendar API with enhanced descriptions for all features // Google Meet link will be generated automatically by Calendar API if (coHosts.length > 0) { if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error( `Co-hosts will be documented in meeting description: ${coHosts.join( ", " )}` ); } } // Prepare attendees list in the format required by the API const formattedAttendees = attendees.map((email) => ({ email })); // Create the event with Google Meet conferencing and guest permissions const event = { summary: summary, description: description, start: { dateTime: startTime, timeZone: "UTC", }, end: { dateTime: endTime, timeZone: "UTC", }, attendees: formattedAttendees, conferenceData: { createRequest: { requestId: `meet-${Date.now()}`, conferenceSolutionKey: { type: "hangoutsMeet" as const, }, }, }, // Guest permissions from Calendar API guestsCanInviteOthers: guestPermissions?.canInviteOthers !== undefined ? guestPermissions.canInviteOthers : true, guestsCanModify: guestPermissions?.canModify !== undefined ? guestPermissions.canModify : false, guestsCanSeeOtherGuests: guestPermissions?.canSeeOtherGuests !== undefined ? guestPermissions.canSeeOtherGuests : true, }; // Add comprehensive feature notes to description let featureNotes = []; if (enableRecording) featureNotes.push( "📹 Auto-recording enabled (activate manually when meeting starts)" ); if (enableTranscription) featureNotes.push( "📝 Auto-transcription enabled (available post-meeting)" ); if (enableSmartNotes) featureNotes.push("🧠 Smart notes with AI summaries enabled"); if (coHosts.length > 0) featureNotes.push( `👥 Co-hosts assigned: ${coHosts.join( ", " )} (promote manually in meeting)` ); if (attendanceReport) featureNotes.push("📊 Attendance report will be generated"); // Add moderation settings if (options.spaceConfig?.moderation === "ON") { let moderationNotes = ["🛡️ Moderation enabled:"]; if (options.spaceConfig?.moderationRestrictions?.chatRestriction === "HOSTS_ONLY") moderationNotes.push(" • 💬 Chat restricted to hosts only"); if (options.spaceConfig?.moderationRestrictions?.presentRestriction === "HOSTS_ONLY") moderationNotes.push(" • 🖥️ Screen sharing restricted to hosts only"); if (options.spaceConfig?.moderationRestrictions?.defaultJoinAsViewerType === "ON") moderationNotes.push(" • 👀 Participants join as viewers by default"); featureNotes.push(moderationNotes.join("\n")); } if (featureNotes.length > 0) { event.description = (description ? description + "\n\n" : "") + "🚀 Enhanced Meeting Features:\n" + featureNotes.join("\n") + "\n\n⚠️ Note: Advanced features require Google Workspace Business Standard or higher and manual activation during the meeting."; } try { const response = await this.calendar.events.insert({ calendarId: "primary", conferenceDataVersion: 1, resource: event, }); const createdEvent = response.data; if (!createdEvent.conferenceData) { throw new Error("Failed to create Google Meet conferencing data"); } const meeting = this._formatMeetingData(createdEvent); if (!meeting) { throw new Error( "Failed to format meeting data for newly created event" ); } return meeting; } catch (error) { throw new Error(`Error creating meeting: ${error.message}`); } } /** * Update an existing calendar event. * @param {string} eventId - ID of the event to update * @param {Object} updateData - Fields to update * @param {string} calendarId - Calendar ID (defaults to "primary") * @returns {Promise<Object>} - Updated event details */ async updateCalendarEvent(eventId: string, updateData: EventUpdateData = {}, calendarId?: string): Promise<ProcessedEvent> { // Check if this is a recurring event instance const isRecurringInstance = eventId.includes('_') && /\d{8}T\d{6}Z$/.test(eventId); if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error("🔍 updateCalendarEvent called with:", { eventId, calendarId: calendarId || "undefined", isRecurringInstance, updateDataKeys: Object.keys(updateData), calendarInitialized: !!this.calendar }); } // Verify calendar client is initialized if (!this.calendar) { throw new Error("Calendar API not initialized. Please ensure authentication is completed."); } // If no calendar ID is provided, try to find the event in available calendars if (!calendarId) { console.error("🔍 No calendar_id provided, searching for event in all calendars..."); const foundCalendarId = await this.findEventCalendar(eventId); if (foundCalendarId) { calendarId = foundCalendarId; console.error(`🔍 Found event in calendar: ${calendarId}`); } else { calendarId = "primary"; // Fallback to primary console.error("🔍 Event not found in any calendar, defaulting to primary"); } } try { // First, get the existing event if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error("🔍 Getting existing event with params:", { calendarId, eventId }); } const response = await this.calendar.events.get({ calendarId: calendarId, eventId: eventId, }); // Log event details for debugging recurring event permissions if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { const event = response.data; console.error("🔍 Event details:", { id: event.id, summary: event.summary, status: event.status, creator: event.creator?.email, organizer: event.organizer?.email, isRecurring: !!event.recurringEventId, recurringEventId: event.recurringEventId, originalStartTime: event.originalStartTime, guestsCanModify: event.guestsCanModify, locked: event.locked, eventType: event.eventType }); } const event = response.data; // Check for event restrictions before attempting update if (event.locked) { throw new Error(`Cannot update event: This event is locked and cannot be modified.`); } if (isRecurringInstance && event.guestsCanModify === false) { console.error("⚠️ Warning: Event has guestsCanModify=false, update may fail for non-organizers"); } // For recurring instances, check if we have organizer permissions if (isRecurringInstance) { const currentUserEmail = process.env.CLIENT_ID?.split('@')[0]; // Approximate user identification const isOrganizer = event.organizer?.email === currentUserEmail; const isCreator = event.creator?.email === currentUserEmail; if (!isOrganizer && !isCreator) { console.error("⚠️ Warning: You are not the organizer/creator of this recurring event. Update may fail."); } // For recurring instances, consider updating the master event instead if (event.recurringEventId && Object.keys(updateData).length === 1 && updateData.summary) { console.error("🔄 Info: This is a recurring event instance. For title changes, consider updating the master event:", event.recurringEventId); } } // Update the fields that were provided if (updateData.summary !== undefined) { event.summary = updateData.summary; } if (updateData.description !== undefined) { event.description = updateData.description; } if (updateData.location !== undefined) { event.location = updateData.location; } if (updateData.startTime !== undefined) { event.start.dateTime = updateData.startTime; } if (updateData.endTime !== undefined) { event.end.dateTime = updateData.endTime; } if (updateData.timeZone !== undefined) { event.start.timeZone = updateData.timeZone; event.end.timeZone = updateData.timeZone; } if (updateData.attendees !== undefined) { event.attendees = updateData.attendees.map((email) => ({ email })); } // Update guest permissions if (updateData.guestCanInviteOthers !== undefined) { event.guestsCanInviteOthers = updateData.guestCanInviteOthers; } if (updateData.guestCanModify !== undefined) { event.guestsCanModify = updateData.guestCanModify; } if (updateData.guestCanSeeOtherGuests !== undefined) { event.guestsCanSeeOtherGuests = updateData.guestCanSeeOtherGuests; } // Make the API call to update the event // Using update (PUT) instead of patch because we're sending the complete event object // after fetching it first (GET + UPDATE pattern recommended by Google) const updateParams: any = { calendarId: calendarId, eventId: eventId, conferenceDataVersion: 1, requestBody: event, }; // Add sendUpdates parameter to control notifications // - 'all': Send notifications to all guests // - 'externalOnly': Send notifications only to external guests (not same domain) // - 'none': Don't send notifications updateParams.sendUpdates = updateData.sendUpdates || 'none'; if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error("🔍 Update params:", { calendarId, eventId, isRecurringInstance, sendUpdates: updateParams.sendUpdates, hasConferenceData: !!event.conferenceData, recurringEventInfo: isRecurringInstance ? { baseEventId: eventId.split('_')[0], instanceDate: eventId.split('_')[1] } : null }); } const updateResponse = await this.calendar.events.update(updateParams); const updatedEvent = updateResponse.data; const formattedEvent = this._formatCalendarEvent(updatedEvent); if (!formattedEvent) { throw new Error("Failed to format event data for updated event"); } return formattedEvent; } catch (error) { // Enhanced error handling for recurring events and permissions if (error.code === 403) { let errorMessage = `Access denied: `; if (isRecurringInstance) { errorMessage += `Cannot modify recurring event instance "${eventId}". This may be due to: - Insufficient permissions to modify recurring events - The event is managed by another user or organization - Recurring event restrictions (some fields may be immutable) - The event may be locked or have special restrictions`; } else { errorMessage += `You don't have sufficient permissions to modify this event. Check your calendar permissions.`; } throw new Error(errorMessage); } if (error.code === 404) { throw new Error(`Event not found: The event "${eventId}" does not exist in the specified calendar.`); } if (error.code === 401) { throw new Error(`Authentication failed: Your credentials are invalid or expired.`); } if (process.env.NODE_ENV === 'development' || process.env.MCP_DEBUG === 'true') { console.error("🔍 Full update error details:", { code: error.code, message: error.message, status: error.status, eventId, calendarId, isRecurringInstance, updateDataKeys: Object.keys(updateData) }); } throw new Error(`Error updating calendar event: ${error.message} (Code: ${error.code || 'unknown'})`); } } /** * Delete a calendar event. * @param {string} eventId - ID of the event to delete * @param {string} calendarId - Calendar ID (defaults to "primary") * @returns {Promise<boolean>} - True if deleted successfully */ async deleteCalendarEvent(eventId: string, calendarId?: string): Promise<boolean> { // If no calendar ID is provided, try to find the event in available calendars if (!calendarId) { console.error("🔍 No calendar_id provided, searching for event in all calendars..."); const foundCalendarId = await this.findEventCalendar(eventId); if (foundCalendarId) { calendarId = foundCalendarId; console.error(`🔍 Found event in calendar: ${calendarId}`); } else { calendarId = "primary"; // Fallback to primary console.error("🔍 Event not found in any calendar, defaulting to primary"); } } try { await this.calendar.events.delete({ calendarId: calendarId, eventId: eventId, }); return true; } catch (error) { throw new Error(`Error deleting calendar event: ${error.message}`); } } /** * Move a calendar event from one calendar to another. * @param {string} eventId - ID of the event to move * @param {string} sourceCalendarId - ID of the source calendar (where the event currently is) * @param {string} destinationCalendarId - ID of the destination calendar * @returns {Promise<ProcessedEvent>} - The moved event details */ async moveCalendarEvent(eventId: string, sourceCalendarId: string, destinationCalendarId: string): Promise<ProcessedEvent> { try { const response = await this.calendar.events.move({ calendarId: sourceCalendarId, eventId: eventId, destination: destinationCalendarId, }); const movedEvent = response.data; const formattedEvent = this._formatCalendarEvent(movedEvent); if (!formattedEvent) { throw new Error(`Failed to format event data for moved event ${eventId}`); } return formattedEvent; } catch (error) { throw new Error(`Error moving calendar event: ${error.message}`); } } /** * Update an existing Google Meet meeting (legacy method for compatibility). * @param {string} meetingId - ID of the meeting to update * @param {Object} updateData - Fields to update * @returns {Promise<Object>} - Updated meeting details */ async updateMeeting( meetingId: string, { summary, description, startTime, endTime, attendees }: { summary?: string; description?: string; startTime?: string; endTime?: string; attendees?: string[]; } = {} ): Promise<any> { try { // First, get the existing event const response = await this.calendar.events.get({ calendarId: "primary", eventId: meetingId, }); const event = response.data; // Update the fields that were provided if (summary !== undefined) { event.summary = summary; } if (description !== undefined) { event.description = description; } if (startTime !== undefined) { event.start.dateTime = startTime; } if (endTime !== undefined) { event.end.dateTime = endTime; } if (attendees !== undefined) { event.attendees = attendees.map((email) => ({ email })); } // Make the API call to update the event // Using update (PUT) instead of patch because we're sending the complete event object const updateResponse = await this.calendar.events.update({ calendarId: "primary", eventId: meetingId, conferenceDataVersion: 1, requestBody: event, }); const updatedEvent = updateResponse.data; if (!updatedEvent.conferenceData) { throw new Error( "Updated event does not have Google Meet conferencing data" ); } const meeting = this._formatMeetingData(updatedEvent); if (!meeting) { throw new Error("Failed to format meeting data for updated event"); } return meeting; } catch (error) { throw new Error(`Error updating meeting: ${error.message}`); } } /** * Delete a Google Meet meeting. * @param {string} meetingId - ID of the meeting to delete * @returns {Promise<boolean>} - True if deleted successfully */ async deleteMeeting(meetingId: string): Promise<boolean> { try { await this.calendar.events.delete({ calendarId: "primary", eventId: meetingId, }); return true; } catch (error) { throw new Error(`Error deleting meeting: ${error.message}`); } } /** * Perform automatic OAuth authentication flow * @param {Object} oAuth2Client - The OAuth2 client */ async performAutoAuth(oAuth2Client) { const SCOPES = [ "https://www.googleapis.com/auth/calendar", "https://www.googleapis.com/auth/meetings.space.created", "https://www.googleapis.com/auth/meetings.space.readonly", "https://www.googleapis.com/auth/meetings.space.settings", ]; console.error("\n🔐 Google Authentication Required"); console.error( "📝 To use Google Meet MCP Server, you need to authenticate with Google." ); return new Promise((resolve, reject) => { // Create a temporary HTTP server to handle the OAuth callback const server = http.createServer((req, res) => { const url = new URL(req.url, "http://localhost:3000"); if (url.pathname === "/oauth2callback") { const code = url.searchParams.get("code"); const error = url.searchParams.get("error"); if (error) { res.writeHead(400, { "Content-Type": "text/html" }); res.end(` <html> <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;"> <h1>❌ Authentication Failed</h1> <p>Error: ${error}</p> <p>You can close this window.</p> </body> </html> `); server.close(); reject(new Error(`Authentication failed: ${error}`)); return; } if (code) { res.writeHead(200, { "Content-Type": "text/html" }); res.end(` <html> <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;"> <h1>✅ Authentication Successful!</h1> <p>You can now close this window and return to Claude.</p> <p>Google Meet MCP Server is now configured.</p> </body> </html> `); // Exchange code for tokens oAuth2Client .getToken(code) .then(({ tokens }) => { return fs .writeFile(this.tokenPath, JSON.stringify(tokens, null, 2)) .then(() => tokens); }) .then((tokens) => { oAuth2Client.setCredentials(tokens); console.error("✅ Authentication successful! Token saved."); server.close(); resolve(undefined); }) .catch((err) => { console.error("❌ Error saving token:", err); server.close(); reject(err); }); } } else { res.writeHead(404); res.end("Not found"); } }); server.listen(3000, async () => { const authUrl = oAuth2Client.generateAuthUrl({ access_type: "offline", scope: SCOPES, prompt: "consent", redirect_uri: "http://localhost:3000/oauth2callback", }); console.error("🌐 Opening browser for authentication..."); console.error(`🔗 If browser doesn't open, visit: ${authUrl}`); // Try to open browser (dynamic import for compatibility) try { const { default: open } = await import("open"); open(authUrl).catch(() => { console.error("❌ Could not open browser automatically"); console.error(`📋 Please manually visit: ${authUrl}`); }); } catch { console.error("⚠️ Browser opening not available in this environment"); console.error(`📋 Please manually visit: ${authUrl}`); } // Timeout after 5 minutes setTimeout(() => { server.close(); reject(new Error("Authentication timeout. Please try again.")); }, 300000); }); server.on("error", (err) => { if ((err as NodeError).code === "EADDRINUSE") { console.error( "❌ Port 3000 is in use. Please try again or run setup script manually." ); } reject(err); }); }); } // ========== GOOGLE MEET API v2 METHODS (GA - Generally Available) ========== /** * Create a Google Meet space with advanced configuration. * @param {Object} config - Space configuration * @returns {Promise<Object>} - Created space data */ async createMeetSpace(config: { accessType?: 'OPEN' | 'TRUSTED' | 'RESTRICTED'; moderationMode?: 'ON' | 'OFF'; chatRestriction?: 'HOSTS_ONLY' | 'NO_RESTRICTION'; presentRestriction?: 'HOSTS_ONLY' | 'NO_RESTRICTION'; defaultJoinAsViewer?: boolean; enableRecording?: boolean; enableTranscription?: boolean; enableSmartNotes?: boolean; attendanceReport?: boolean; }): Promise<MeetSpace> { // Use REST client for direct Google Meet API v2 access const spaceConfig: SpaceConfig = { accessType: config.accessType || "TRUSTED", entryPointAccess: "ALL", }; // Add moderation configuration if specified if (config.moderationMode) { spaceConfig.moderation = config.moderationMode; // Only add moderationRestrictions if moderation is ON if ( config.moderationMode === "ON" && (config.chatRestriction || config.presentRestriction || config.defaultJoinAsViewer) ) { spaceConfig.moderationRestrictions = {}; if (config.chatRestriction) { spaceConfig.moderationRestrictions.chatRestriction = config.chatRestriction; } if (config.presentRestriction) { spaceConfig.moderationRestrictions.presentRestriction = config.presentRestriction; } if (config.defaultJoinAsViewer) { spaceConfig.moderationRestrictions.defaultJoinAsViewerType = "ON"; } } } // Add artifact configuration (recording, transcription, smart notes) if ( config.enableRecording || config.enableTranscription || config.enableSmartNotes ) { spaceConfig.artifactConfig = {}; if (config.enableRecording) { spaceConfig.artifactConfig.recordingConfig = { autoRecordingGeneration: "ON", }; } if (config.enableTranscription) { spaceConfig.artifactConfig.transcriptionConfig = { autoTranscriptionGeneration: "ON", }; } if (config.enableSmartNotes) { spaceConfig.artifactConfig.smartNotesConfig = { autoSmartNotesGeneration: "ON", }; } } // Add attendance report configuration if (config.attendanceReport) { spaceConfig.attendanceReportGenerationType = "GENERATE_REPORT"; } return await this.meetRestClient.createSpace(spaceConfig); } /** * Get details of a Google Meet space. * @param {string} spaceName - Name of the space (spaces/{space_id}) * @returns {Promise<Object>} - Space data from Google Meet API v2 */ async getMeetSpace(spaceName) { // Use REST client for direct Google Meet API v2 access return await this.meetRestClient.getSpace(spaceName); } /** * Update configuration of a Google Meet space. * @param {string} spaceName - Name of the space (spaces/{space_id}) * @param {Object} updateData - Configuration to update * @returns {Promise<Object>} - Updated space data */ async updateMeetSpace(spaceName: string, updateData: SpaceUpdateData): Promise<MeetSpace> { // Use REST client for direct Google Meet API v2 access const spaceConfig: SpaceConfig = {}; if (updateData.accessType) { spaceConfig.accessType = updateData.accessType; } if ( updateData.moderationMode || updateData.chatRestriction || updateData.presentRestriction ) { spaceConfig.moderation = updateData.moderationMode || "OFF"; spaceConfig.moderationRestrictions = {}; if (updateData.chatRestriction) { spaceConfig.moderationRestrictions.chatRestriction = updateData.chatRestriction; } if (updateData.presentRestriction) { spaceConfig.moderationRestrictions.presentRestriction = updateData.presentRestriction; } } return await this.meetRestClient.updateSpace(spaceName, spaceConfig); } /** * End the active conference in a Google Meet space. * @param {string} spaceName - Name of the space (spaces/{space_id}) * @returns {Promise<boolean>} - Success status */ async endActiveConference(spaceName: string): Promise<boolean> { // Use REST client for direct Google Meet API v2 access await this.meetRestClient.endActiveConference(spaceName); return true; } /** * List conference records for historical meetings. * @param {string} filter - Filter for conference records * @param {number} pageSize - Maximum number of results to return * @returns {Promise<Array>} - Conference records */ async listConferenceRecords(filter: string | null = null, pageSize = 10): Promise<{ conferenceRecords: ConferenceRecord[]; nextPageToken?: string }> { // Use REST client for direct Google Meet API v2 access return await this.meetRestClient.listConferenceRecords(filter, pageSize); } /** * Get details of a specific conference record. * @param {string} conferenceRecordName - Name of the conference record * @returns {Promise<Object>} - Conference record details */ async getConferenceRecord(conferenceRecordName: string): Promise<ConferenceRecord> { // Use REST client for direct Google Meet API v2 access return await this.meetRestClient.getConferenceRecord(conferenceRecordName); } /** * List recordings for a conference record. * @param {string} conferenceRecordName - Name of the conference record * @returns {Promise<Array>} - Recordings list */ async listRecordings(conferenceRecordName: string): Promise<{ recordings: Recording[]; nextPageToken?: string }> { // Use REST client for direct Google Meet API v2 access return await this.meetRestClient.listRecordings(conferenceRecordName); } /** * Get details of a specific recording. * @param {string} recordingName - Name of the recording * @returns {Promise<Object>} - Recording details */ async getRecording(recordingName: string): Promise<Recording> { // Use REST client for direct Google Meet API v2 access return await this.meetRestClient.getRecording(recordingName); } /** * List transcripts for a conference record. * @param {string} conferenceRecordName - Name of the conference record * @returns {Promise<Array>} - Transcripts list */ async listTranscripts(conferenceRecordName: string): Promise<{ transcripts: Transcript[]; nextPageToken?: string }> { // Use REST client for direct Google Meet API v2 access return await this.meetRestClient.listTranscripts(conferenceRecordName); } /** * Get details of a specific transcript. * @param {string} transcriptName - Name of the transcript * @returns {Promise<Object>} - Transcript details */ async getTranscript(transcriptName: string): Promise<Transcript> { // Use REST client for direct Google Meet API v2 access return await this.meetRestClient.getTranscript(transcriptName); } /** * List transcript entries (individual speech segments). * @param {string} transcriptName - Name of the transcript * @param {number} pageSize - Maximum number of entries to return * @returns {Promise<Array>} - Transcript entries */ async listTranscriptEntries(transcriptName: string, pageSize = 100): Promise<{ transcriptEntries: TranscriptEntry[]; nextPageToken?: string }> { // Use REST client for direct Google Meet API v2 access return await this.meetRestClient.listTranscriptEntries( transcriptName, pageSize ); } // ========== ADDITIONAL METHODS FROM OFFICIAL SPECS ========== /** * Delete a Google Meet space. * Note: This operation is not supported by Google Meet API v2. * @param {string} spaceName - Name of the space (spaces/{space_id}) * @returns {Promise<boolean>} - Always false (operation not supported) */ async deleteSpace(spaceName: string): Promise<never> { console.warn( `deleteSpace: Operation not supported by Google Meet API v2. Spaces cannot be deleted directly.` ); throw new Error( `Delete space operation is not supported by Google Meet API v2. Space: ${spaceName}` ); } /** * Get participant details. * @param {string} participantName - Name of the participant * @returns {Promise<Object>} - Participant details */ async getParticipant(participantName: string): Promise<Participant> { try { // Use REST client for direct Google Meet API v2 access return await this.meetRestClient.getParticipant(participantName); } catch (error) { // Fallback response if REST API fails console.warn( `Meet API v2 failed for getParticipant, using fallback: ${error.message}` ); throw new Error(`Participant not found: ${error.message}`); } } /** * List participants for a conference record. * @param {string} conferenceRecordName - Name of the conference record * @param {number} pageSize - Maximum number of participants to return * @returns {Promise<Object>} - List of participants */ async listParticipants(conferenceRecordName: string, pageSize = 10): Promise<{ participants: Participant[]; nextPageToken?: string }> { try { // Use REST client for direct Google Meet API v2 access return await this.meetRestClient.listParticipants( conferenceRecordName, pageSize ); } catch (error) { // Fallback response if REST API fails console.warn( `Meet API v2 failed for listParticipants, using fallback: ${error.message}` ); return { participants: [], nextPageToken: null }; } } /** * Get participant session details. * @param {string} participantSessionName - Name of the participant session * @returns {Promise<Object>} - Participant session details */ async getParticipantSession(participantSessionName: string): Promise<ParticipantSession> { try { // Use REST client for direct Google Meet API v2 access return await this.meetRestClient.getParticipantSession( participantSessionName ); } catch (error) { // Fallback response if REST API fails console.warn( `Meet API v2 failed for getParticipantSession, using fallback: ${error.message}` ); throw new Error(`Participant session not found: ${error.message}`); } } /** * List participant sessions. * @param {string} participantName - Name of the participant * @param {number} pageSize - Maximum number of sessions to return * @returns {Promise<Object>} - List of participant sessions */ async listParticipantSessions(participantName: string, pageSize = 10): Promise<{ participantSessions: ParticipantSession[]; nextPageToken?: string }> { try { // Use REST client for direct Google Meet API v2 access return await this.meetRestClient.listParticipantSessions( participantName, pageSize ); } catch (error) { // Fallback response if REST API fails console.warn( `Meet API v2 failed for listParticipantSessions, using fallback: ${error.message}` ); return { participantSessions: [], nextPageToken: null }; } } // getTranscriptEntry method removed - not available in Google Meet API v2 // Use listTranscriptEntries instead to get individual transcript entries /** * Get recordings for a meeting. * @param {string} meetingCode - The meeting code from the Meet URL * @returns {Promise<Object>} - Recording information */ async getMeetingRecordings(meetingCode: string): Promise<{ message: string; meeting_code: string; recordings: any[] }> { try { // Try to find the conference record by meeting code // This is a simplified implementation - in practice, you'd need to search through conference records return { message: "Recording retrieval requires Google Workspace Business Standard or higher", meeting_code: meetingCode, recordings: [], }; } catch (error) { throw new Error(`Error getting recordings: ${error.message}`); } } /** * Format event data to calendar event format. * @param {Object} event - Event data from Google Calendar API * @returns {Object|null} - Formatted calendar event data or null */ _formatCalendarEvent(event: any): ProcessedEvent | null { // Format attendees const attendees = (event.attendees || []).map((attendee) => ({ email: attendee.email, response_status: attendee.responseStatus, })); // Check for Google Meet conference data let meetLink = null; let hasMeetConference = false; if (event.conferenceData) { hasMeetConference = true; for (const entryPoint of event.conferenceData.entryPoints || []) { if (entryPoint.entryPointType === "video") { meetLink = entryPoint.uri; break; } } } // Build the formatted calendar event data const calendarEvent = { id: event.id, summary: event.summary || "", description: event.description || "", location: event.location || "", start_time: event.start?.dateTime || event.start?.date, end_time: event.end?.dateTime || event.end?.date, time_zone: event.start?.timeZone || event.end?.timeZone || "UTC", attendees: attendees, creator: event.creator?.email, organizer: event.organizer?.email, created: event.created, updated: event.updated, has_meet_conference: hasMeetConference, meet_link: meetLink, guest_can_invite_others: event.guestsCanInviteOthers, guest_can_modify: event.guestsCanModify, guest_can_see_other_guests: event.guestsCanSeeOtherGuests, }; return calendarEvent; } /** * Format event data to meeting format (legacy method for compatibility). * @param {Object} event - Event data from Google Calendar API * @returns {Object|null} - Formatted meeting data or null */ _formatMeetingData(event: any): any | null { if (!event.conferenceData) { return null; } // Extract the Google Meet link let meetLink = null; for (const entryPoint of event.conferenceData.entryPoints || []) { if (entryPoint.entryPointType === "video") { meetLink = entryPoint.uri; break; } } if (!meetLink) { return null; } // Format attendees const attendees = (event.attendees || []).map((attendee) => ({ email: attendee.email, response_status: attendee.responseStatus, })); // Check if recording is mentioned in description const recordingEnabled = event.description?.includes("📹 Note: Recording will be enabled") || false; // Build the formatted meeting data const meeting = { id: event.id, summary: event.summary || "", description: event.description || "", meet_link: meetLink, recording_enabled: recordingEnabled, start_time: event.start?.dateTime || event.start?.date, end_time: event.end?.dateTime || event.end?.date, attendees: attendees, creator: event.creator?.email, organizer: event.organizer?.email, created: event.created, updated: event.updated, }; return meeting; } // ========== GOOGLE CALENDAR API v3 - ADDITIONAL METHODS ========== /** * Query free/busy information for calendars * @param {string[]} calendarIds - Array of calendar IDs to query * @param {string} timeMin - Start time (ISO 8601) * @param {string} timeMax - End time (ISO 8601) * @returns {Promise<Object>} - Free/busy information */ async queryFreeBusy(calendarIds: string[], timeMin: string, timeMax: string): Promise<any> { try { const response = await this.calendar.freebusy.query({ requestBody: { timeMin, timeMax, items: calendarIds.map(id => ({ id })) } }); return { kind: response.data.kind, timeMin: response.data.timeMin, timeMax: response.data.timeMax, calendars: response.data.calendars || {} }; } catch (error) { throw new Error(`Error querying free/busy: ${error.message}`); } } /** * Create event using natural language (Quick Add) * @param {string} calendarId - Calendar ID to add event to * @param {string} text - Natural language text describing the event * @returns {Promise<Object>} - Created event details */ async quickAddEvent(calendarId: string, text: string): Promise<ProcessedEvent> { try { const response = await this.calendar.events.quickAdd({ calendarId, text, sendNotifications: true }); const event = response.data; return this._formatCalendarEvent(event); } catch (error) { throw new Error(`Error creating quick add event: ${error.message}`); } } } export default GoogleMeetAPI;

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/INSIDE-HAIR/mcp-google-calendar-and-meet'

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