Skip to main content
Glama

Google Calendar MCP Server

import { serve, ServerType } from '@hono/node-server'; import { Hono } from 'hono'; import { OAuth2Client } from 'google-auth-library'; import readline from 'readline'; import { OAuthHandler } from './oauth-handler'; import { tokenManager } from './token-manager'; import config from '../config/config'; import logger, { LoggerMeta } from '../utils/logger'; import { sanitizeErrorForLogging, sanitizeText } from '../utils/security-sanitizer'; /** * Authentication server management class * Handles OAuth server lifecycle and authentication flows */ export class AuthServer { private honoApp: Hono; private oauthHandler: OAuthHandler; private server: ServerType | null = null; private isServerRunning: boolean = false; private authorizationPromise: Promise<OAuth2Client> | null = null; constructor() { // Initialize OAuthHandler this.oauthHandler = new OAuthHandler(); // Get Hono app from OAuthHandler this.honoApp = this.oauthHandler.getApp(); logger.info('AuthServer initialized - OAuth server will be started when authentication is needed'); } /** * Check if server is currently running */ public isRunning(): boolean { return this.isServerRunning; } /** * Start the OAuth server */ public startServer(): void { if (!this.isServerRunning) { try { this.server = serve({ fetch: this.honoApp.fetch, port: config.auth.port, hostname: config.auth.host }); logger.info(`OAuth server started on ${config.auth.host}:${config.auth.port}`); this.isServerRunning = true; } catch (err: unknown) { const error = err as { code?: string }; if (error.code === 'EADDRINUSE') { logger.warn(`Port ${config.auth.port} is already in use, assuming OAuth server is already running`); // Set server object to null to indicate that we're using an existing server this.server = null; this.isServerRunning = false; } else { logger.error('OAuth server error:', { error } as LoggerMeta); this.isServerRunning = false; } } } } /** * Shut down the authentication server */ public shutdownServer(): void { if (this.server && this.isServerRunning) { logger.info('Shutting down OAuth server'); this.server.close(() => { logger.info('OAuth server has been shut down'); this.isServerRunning = false; }); } } /** * Start authentication flow for OAuth2Client */ public async initiateAuthentication(oauth2Client: OAuth2Client): Promise<OAuth2Client> { if (this.authorizationPromise) { return this.authorizationPromise; } const userId = 'default-user'; // Check if manual authentication is enabled if (config.auth.useManualAuth) { logger.info('Using manual authentication flow'); this.authorizationPromise = this.handleManualAuth(oauth2Client, userId); return this.authorizationPromise; } // Regular authentication flow with local server this.startServer(); this.authorizationPromise = this.startAuthenticationFlow(oauth2Client, userId); return this.authorizationPromise; } /** * Handle manual authentication flow */ private async handleManualAuth(oauth2Client: OAuth2Client, userId: string): Promise<OAuth2Client> { try { // Generate authentication URL for manual auth const redirectUri = `http://${config.auth.host}:${config.auth.port}/auth-success`; const authUrlResult = this.oauthHandler.generateAuthUrl(userId, redirectUri, true); const { authUrl, state } = typeof authUrlResult === 'string' ? { authUrl: authUrlResult, state: 'manual-auth' } : authUrlResult; const sanitizedUrl = sanitizeText(authUrl); logger.info(`Please authorize this app by visiting this URL: ${sanitizedUrl}`); // Try to open the browser automatically await this.openAuthUrl(authUrl); // Create readline interface for manual code input const rl = this.createReadlineInterface(); let authCode: string; try { // Prompt for authorization code authCode = await new Promise<string>((resolve, reject) => { rl.question('\\nAfter authorizing, please enter the authorization code shown by Google: ', (code) => { resolve(code.trim()); }); // Set timeout for user input setTimeout(() => { reject(new Error('Authentication input timeout (5 minutes)')); }, 5 * 60 * 1000); }); } finally { // Ensure readline interface is always closed rl.close(); } if (!authCode) { throw new Error('No authorization code provided'); } // Exchange code for tokens const result = await this.oauthHandler.exchangeCodeForTokens(authCode, state); if (!result.success) { throw new Error(result.message); } logger.info('Manual authentication successful'); // Get tokens and set credentials return this.setCredentialsFromTokens(oauth2Client, userId); } catch (error) { logger.error('Error in manual authentication:', { error: sanitizeErrorForLogging(error) } as LoggerMeta); throw error; } } /** * Start the authentication flow with token monitoring */ private startAuthenticationFlow(oauth2Client: OAuth2Client, userId: string): Promise<OAuth2Client> { return new Promise((resolve, reject) => { try { logger.debug('Starting authentication flow for user:', { userId } as LoggerMeta); this.generateAndOpenAuthUrl(userId); this.setupTokenMonitoring(oauth2Client, userId, resolve, reject); } catch (error) { this.handleAuthenticationError(error, reject); } }); } /** * Generate authentication URL and open in browser */ private generateAndOpenAuthUrl(userId: string): void { const redirectUri = `http://${config.auth.host}:${config.auth.port}/auth-success`; const authUrlResult = this.oauthHandler.generateAuthUrl(userId, redirectUri); const authUrl = typeof authUrlResult === 'string' ? authUrlResult : authUrlResult.authUrl; const sanitizedUrl = sanitizeText(authUrl); logger.info(`Please authorize this app by visiting this URL: ${sanitizedUrl}`); this.openAuthUrl(authUrl); } /** * Open authentication URL in browser */ private async openAuthUrl(authUrl: string): Promise<void> { try { const openModule = await import('open'); await openModule.default(authUrl); logger.info('Opening browser for authorization...'); } catch (error) { logger.warn('Failed to open browser automatically:', { error } as LoggerMeta); logger.info(`Please open this URL manually: ${authUrl}`); } } /** * Set up token monitoring with periodic checks */ private setupTokenMonitoring( oauth2Client: OAuth2Client, userId: string, resolve: (client: OAuth2Client) => void, reject: (error: Error) => void ): void { const checkToken = async () => { try { const accessToken = tokenManager.getToken(`${userId}_access`); if (accessToken) { logger.debug('Access token found, setting credentials'); const result = await this.setCredentialsFromTokens(oauth2Client, userId); this.processAuthenticationSuccess(resolve, result, intervalId, timeoutId); return result; } } catch (error) { logger.error('Error checking token:', { error } as LoggerMeta); } }; const intervalId = setInterval(checkToken, 1000); const timeoutId = setTimeout(() => { this.handleAuthenticationTimeout(intervalId, timeoutId, reject); }, 5 * 60 * 1000); } /** * Set credentials from stored tokens */ private async setCredentialsFromTokens(oauth2Client: OAuth2Client, userId: string): Promise<OAuth2Client> { const refreshToken = tokenManager.getToken(userId); const accessToken = tokenManager.getToken(`${userId}_access`); if (!accessToken) { throw new Error('Failed to obtain access token'); } const credentials = { access_token: accessToken, ...(refreshToken && { refresh_token: refreshToken }) }; if (refreshToken) { logger.info('Using stored refresh token for authentication'); } else { logger.warn('No refresh token available, proceeding with access token only'); } oauth2Client.setCredentials(credentials); return oauth2Client; } /** * Process successful authentication */ private processAuthenticationSuccess( resolve: (client: OAuth2Client) => void, oauth2Client: OAuth2Client, intervalId?: NodeJS.Timeout, timeoutId?: NodeJS.Timeout ): void { logger.debug('Processing successful authentication'); // Clean up timers if (intervalId) clearInterval(intervalId); if (timeoutId) clearTimeout(timeoutId); this.authorizationPromise = null; this.shutdownServer(); logger.info('Authentication completed successfully, resolving with OAuth2Client'); resolve(oauth2Client); } /** * Handle authentication timeout */ private handleAuthenticationTimeout( intervalId: NodeJS.Timeout, timeoutId: NodeJS.Timeout, reject: (error: Error) => void ): void { clearInterval(intervalId); clearTimeout(timeoutId); this.authorizationPromise = null; this.shutdownServer(); reject(new Error('Authorization timed out after 5 minutes')); } /** * Handle authentication errors */ private handleAuthenticationError(error: unknown, reject: (error: Error) => void): void { logger.error('Error in authorization:', { error: sanitizeErrorForLogging(error) } as LoggerMeta); this.authorizationPromise = null; this.shutdownServer(); reject(error instanceof Error ? error : new Error('Authentication error occurred')); } /** * Create a readline interface for manual code input */ private createReadlineInterface(): readline.Interface { return readline.createInterface({ input: process.stdin, output: process.stdout }); } /** * Get authentication server statistics */ public getStatistics(): { isRunning: boolean; hasActivePromise: boolean; serverHost: string; serverPort: number; } { return { isRunning: this.isServerRunning, hasActivePromise: this.authorizationPromise !== null, serverHost: config.auth.host, serverPort: config.auth.port }; } }

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/takumi0706/google-calendar-mcp'

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