Skip to main content
Glama

Zoom Recordings No-Auth

by peakmojo
server.js13.6 kB
#!/usr/bin/env node import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import axios from 'axios'; import { Buffer } from 'buffer'; import { DateTime } from 'luxon'; // Initialize logging const logLevels = { ERROR: 0, WARN: 1, INFO: 2, DEBUG: 3 }; class Logger { constructor(name) { this.name = name; this.logLevel = process.env.LOG_LEVEL ? logLevels[(process.env.LOG_LEVEL || 'INFO').toUpperCase()] : logLevels.INFO; } log(level, message) { if (logLevels[level] <= this.logLevel) { const timestamp = new Date().toISOString(); console.error(`${timestamp} - ${this.name} - ${level} - ${message}`); } } info(message) { this.log('INFO', message); } warn(message) { this.log('WARN', message); } error(message) { this.log('ERROR', message); } debug(message) { this.log('DEBUG', message); } } const logger = new Logger('zoom-mcp'); class ZoomClient { constructor({ accessToken, refreshToken, clientId, clientSecret } = {}) { if (!accessToken && !refreshToken) { throw new Error("Either accessToken or refreshToken must be provided"); } this.accessToken = accessToken; this.refreshToken = refreshToken; this.clientId = clientId; this.clientSecret = clientSecret; this.baseUrl = "https://api.zoom.us/v2"; } _getHeaders() { return { "Authorization": `Bearer ${this.accessToken}`, "Content-Type": "application/json" }; } async _handleTokenRefresh(operation) { try { return await operation(); } catch (error) { logger.error(`Request error: ${error.message}`); if (error.response) { const statusCode = error.response.status; if (statusCode === 401) { return JSON.stringify({ error: "Unauthorized. Token might be expired. Try refreshing your token.", details: error.message }); } else { return JSON.stringify({ error: `Zoom API error: ${statusCode}`, details: error.message }); } } return JSON.stringify({ error: "Request to Zoom API failed", details: error.message }); } } async refreshAccessToken(clientId, clientSecret) { logger.debug(`Starting refreshAccessToken with clientId=${clientId?.slice(0, 5)}...`); if (!this.refreshToken) { return JSON.stringify({ error: "No refresh token provided", status: "error" }); } try { this.clientId = clientId; this.clientSecret = clientSecret; const authHeader = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); const headers = { "Authorization": `Basic ${authHeader}`, "Content-Type": "application/x-www-form-urlencoded" }; const data = { grant_type: "refresh_token", refresh_token: this.refreshToken }; logger.debug(`Making POST request to Zoom OAuth token endpoint with refreshToken=${this.refreshToken.slice(0, 10)}...`); const response = await axios.post( "https://zoom.us/oauth/token", data, { headers } ); logger.debug(`Received response with status code: ${response.status}`); if (response.status === 200) { const result = response.data; logger.debug("Successfully refreshed token"); this.accessToken = result.access_token; this.refreshToken = result.refresh_token || this.refreshToken; const expiresIn = result.expires_in || 3600; const expiry = DateTime.now().plus({ seconds: expiresIn }); return JSON.stringify({ access_token: this.accessToken, refresh_token: this.refreshToken, expires_at: expiry.toISO(), expires_in: expiresIn, status: "success" }); } else { logger.error(`Failed to refresh token: ${response.status}, Response: ${response.data}`); return JSON.stringify({ error: `Failed to refresh token. Status code: ${response.status}`, details: response.data, raw_response: response.data, status: "error" }); } } catch (error) { logger.error(`Exception in refreshAccessToken: ${error.message}`); return JSON.stringify({ error: error.message, status: "error" }); } } async listRecordings({ from_date, to_date, page_size = 30, page_number = 1 } = {}) { try { const operation = async () => { logger.debug(`Fetching recordings with page_size=${page_size}, page_number=${page_number}`); const params = { page_size: Math.min(page_size, 300), page_number: page_number }; if (from_date) params.from = from_date; if (to_date) params.to = to_date; const response = await axios.get( `${this.baseUrl}/users/me/recordings`, { headers: this._getHeaders(), params } ); if (response.status !== 200) { return JSON.stringify({ error: `Failed to retrieve recordings. Status code: ${response.status}`, details: response.data, status: "error" }); } return JSON.stringify(response.data); }; return await this._handleTokenRefresh(operation); } catch (error) { logger.error(`Exception in listRecordings: ${error.message}`); return JSON.stringify({ error: error.message }); } } async getRecordingDetails(meeting_id) { try { logger.debug(`Fetching recording details for meeting_id=${meeting_id}`); const response = await axios.get( `${this.baseUrl}/meetings/${meeting_id}/recordings`, { headers: this._getHeaders() } ); if (response.status !== 200) { return JSON.stringify({ error: `Failed to retrieve recording details. Status code: ${response.status}`, details: response.data, status: "error" }); } return JSON.stringify(response.data); } catch (error) { logger.error(`Exception in getRecordingDetails: ${error.message}`); if (error.response?.status === 401) { return JSON.stringify({ error: "Unauthorized. Token might be expired. Please refresh your token.", status: "error" }); } return JSON.stringify({ error: error.message }); } } async getMeetingTranscript(meetingId) { try { const operation = async () => { logger.debug(`Fetching transcript for meetingId=${meetingId}`); const recordingsResponse = await axios.get( `${this.baseUrl}/meetings/${meetingId}/recordings`, { headers: this._getHeaders() } ); if (recordingsResponse.status !== 200) { return JSON.stringify({ error: `Failed to retrieve recording information. Status code: ${recordingsResponse.status}`, details: recordingsResponse.data, status: "error" }); } const recordingsData = recordingsResponse.data; const transcriptFiles = recordingsData.recording_files?.filter( file => file.file_type === "TRANSCRIPT" ) || []; if (!transcriptFiles.length) { return JSON.stringify({ error: "No transcript files found for this meeting", status: "error" }); } const transcripts = []; for (const file of transcriptFiles) { if (file.download_url) { const transcriptResponse = await axios.get( file.download_url, { headers: this._getHeaders() } ); if (transcriptResponse.status === 200) { transcripts.push({ file_id: file.id || "", file_name: file.file_name || "", recording_start: file.recording_start || "", recording_end: file.recording_end || "", content: transcriptResponse.data }); } } } return JSON.stringify({ meeting_id: meetingId, topic: recordingsData.topic || "", meeting_duration: recordingsData.duration || 0, transcripts, status: "success" }); }; return await this._handleTokenRefresh(operation); } catch (error) { logger.error(`Exception in getMeetingTranscript: ${error.message}`); return JSON.stringify({ error: error.message }); } } } async function main() { logger.info('Starting Zoom MCP server'); try { const server = new McpServer({ name: 'Zoom MCP', version: '0.1.0' }); // Define tools server.tool( 'zoom_refresh_token', 'Refresh the Zoom OAuth2 access token using the refresh token and client credentials for API access', { zoom_access_token: z.string().optional().describe('Zoom OAuth2 access token (optional if expired)'), zoom_refresh_token: z.string().describe('Zoom OAuth2 refresh token'), zoom_client_id: z.string().describe('Zoom OAuth2 client ID for token refresh'), zoom_client_secret: z.string().describe('Zoom OAuth2 client secret for token refresh') }, async ({ zoom_access_token, zoom_refresh_token, zoom_client_id, zoom_client_secret }) => { try { const zoom = new ZoomClient({ accessToken: zoom_access_token, refreshToken: zoom_refresh_token }); const result = await zoom.refreshAccessToken(zoom_client_id, zoom_client_secret); return { content: [{ type: 'text', text: result }] }; } catch (error) { return { content: [{ type: 'text', text: JSON.stringify({ error: error.message, status: 'error' }) }] }; } } ); server.tool( 'zoom_list_recordings', 'List Zoom cloud recordings from a user\'s Zoom account with pagination support', { zoom_access_token: z.string().describe('Zoom OAuth2 access token'), from_date: z.string().optional().describe('Start date for Zoom recording search in \'YYYY-MM-DD\' format'), to_date: z.string().optional().describe('End date for Zoom recording search in \'YYYY-MM-DD\' format'), page_size: z.number().optional().describe('Number of Zoom recordings to return per page (default: 30, max: 300)'), page_number: z.number().optional().describe('Page number of Zoom recordings to return (default: 1)') }, async ({ zoom_access_token, from_date, to_date, page_size = 30, page_number = 1 }) => { try { const zoom = new ZoomClient({ accessToken: zoom_access_token }); logger.error(`listing recordings with from_date=${from_date}, to_date=${to_date}, page_size=${page_size}, page_number=${page_number}`); const result = await zoom.listRecordings({ from_date, to_date, page_size, page_number }); return { content: [{ type: 'text', text: result }] }; } catch (error) { return { content: [{ type: 'text', text: `Error: ${error.message}` }] }; } } ); server.tool( 'zoom_get_recording_details', 'Get detailed information about a specific Zoom meeting recording including recording files and metadata', { zoom_access_token: z.string().describe('Zoom OAuth2 access token'), meeting_id: z.string().describe('The Zoom meeting ID to retrieve recording details for') }, async ({ zoom_access_token, meeting_id }) => { try { const zoom = new ZoomClient({ accessToken: zoom_access_token }); const result = await zoom.getRecordingDetails(meeting_id); return { content: [{ type: 'text', text: result }] }; } catch (error) { return { content: [{ type: 'text', text: `Error: ${error.message}` }] }; } } ); server.tool( 'zoom_get_meeting_transcript', 'Get transcript files and content from a specific Zoom meeting recording if available', { zoom_access_token: z.string().describe('Zoom OAuth2 access token'), meeting_id: z.string().describe('The Zoom meeting ID to retrieve transcript for') }, async ({ zoom_access_token, meeting_id }) => { try { const zoom = new ZoomClient({ accessToken: zoom_access_token }); const result = await zoom.getMeetingTranscript(meeting_id); return { content: [{ type: 'text', text: result }] }; } catch (error) { return { content: [{ type: 'text', text: `Error: ${error.message}` }] }; } } ); // Use STDIO transport const transport = new StdioServerTransport(); await server.connect(transport); logger.info('MCP server started and ready to receive requests'); } catch (error) { logger.error(`Error starting server: ${error.message}`); process.exit(1); } } // Start the server main();

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/peakmojo/mcp-server-zoom-noauth'

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