GoogleMeetAPI.ts•66.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;