Skip to main content
Glama

Google Calendar MCP Server

by bezael
server.ts11.4 kB
#!/usr/bin/env node /** * Servidor HTTP REST API para Google Calendar * Expone los endpoints del MCP como API REST para despliegue en Railway */ // Mensaje de inicio inmediato para diagnóstico console.log('🚀 Iniciando servidor MCP Calendar API...'); console.log(`📦 Node version: ${process.version}`); console.log(`📁 Working directory: ${process.cwd()}`); import cors from 'cors'; import 'dotenv/config'; import express, { type Express, type Request, type Response } from 'express'; import { createEvent } from './tools/createEvent.js'; import { deleteEvent } from './tools/deleteEvent.js'; import { getEvent } from './tools/getEvent.js'; import { listEvents } from './tools/listEvents.js'; import { updateEvent } from './tools/updateEvent.js'; import { MCPCalendarError } from './utils/errors.js'; import { logger } from './utils/logger.js'; const app: Express = express(); const PORT = Number(process.env.PORT) || 3000; // Middleware app.use(cors()); app.use(express.json()); // Root endpoint app.get('/', (_req: Request, res: Response): void => { res.json({ status: 'ok', service: 'mcp-gcal-api', version: '0.1.0', endpoints: { health: '/health', createEvent: 'POST /api/events', getEvent: 'GET /api/events/:eventId', listEvents: 'GET /api/events', updateEvent: 'PUT /api/events/:eventId', deleteEvent: 'DELETE /api/events/:eventId' } }); }); // Health check endpoint - Railway usa esto para verificar que el servidor está listo app.get('/health', (_req: Request, res: Response): void => { res.status(200).json({ status: 'ok', service: 'mcp-gcal-api', timestamp: new Date().toISOString(), uptime: process.uptime() }); }); // Readiness probe - Endpoint adicional para verificar que el servidor está completamente listo app.get('/ready', (_req: Request, res: Response): void => { res.status(200).json({ status: 'ready', service: 'mcp-gcal-api', timestamp: new Date().toISOString() }); }); // POST /api/events - Crear evento app.post('/api/events', async (req: Request, res: Response): Promise<void> => { try { const { summary, description, location, start, end, calendarId, timeZone, attendees } = req.body; if (!summary || !start || !end) { res.status(400).json({ error: 'validation_error', message: 'Los campos summary, start y end son requeridos' }); return; } const result = await createEvent({ summary, description, location, start, end, calendarId, timeZone, attendees }); res.status(201).json(result); } catch (error) { if (error instanceof MCPCalendarError) { const mcpError = error.toMCPError(); res.status(mcpError.type === 'auth_error' ? 401 : 400).json(mcpError); } else { logger.error('Error al crear evento', { error: String(error) }); res.status(500).json({ error: 'unknown_error', message: error instanceof Error ? error.message : String(error) }); } } }); // GET /api/events/:eventId - Obtener evento app.get('/api/events/:eventId', async (req: Request, res: Response): Promise<void> => { try { const eventId = req.params.eventId; const { calendarId } = req.query; if (!eventId) { res.status(400).json({ error: 'validation_error', message: 'El parámetro eventId es requerido' }); return; } const result = await getEvent({ eventId, calendarId: calendarId as string | undefined }); res.json(result); } catch (error) { if (error instanceof MCPCalendarError) { const mcpError = error.toMCPError(); const statusCode = mcpError.type === 'not_found_error' ? 404 : mcpError.type === 'auth_error' ? 401 : 400; res.status(statusCode).json(mcpError); } else { logger.error('Error al obtener evento', { error: String(error) }); res.status(500).json({ error: 'unknown_error', message: error instanceof Error ? error.message : String(error) }); } } }); // GET /api/events - Listar eventos app.get('/api/events', async (req: Request, res: Response): Promise<void> => { try { const { timeMin, timeMax, maxResults, calendarId, q } = req.query; if (!timeMin || !timeMax) { res.status(400).json({ error: 'validation_error', message: 'Los parámetros timeMin y timeMax son requeridos' }); return; } const result = await listEvents({ timeMin: timeMin as string, timeMax: timeMax as string, maxResults: maxResults ? Number(maxResults) : undefined, calendarId: calendarId as string | undefined, q: q as string | undefined }); res.json(result); } catch (error) { if (error instanceof MCPCalendarError) { const mcpError = error.toMCPError(); res.status(mcpError.type === 'auth_error' ? 401 : 400).json(mcpError); } else { logger.error('Error al listar eventos', { error: String(error) }); res.status(500).json({ error: 'unknown_error', message: error instanceof Error ? error.message : String(error) }); } } }); // PUT /api/events/:eventId - Actualizar evento app.put('/api/events/:eventId', async (req: Request, res: Response): Promise<void> => { try { const eventId = req.params.eventId; const { summary, description, location, start, end, calendarId, timeZone, attendees } = req.body; if (!eventId) { res.status(400).json({ error: 'validation_error', message: 'El parámetro eventId es requerido' }); return; } const result = await updateEvent({ eventId, calendarId, summary, description, location, start, end, timeZone, attendees }); res.json(result); } catch (error) { if (error instanceof MCPCalendarError) { const mcpError = error.toMCPError(); const statusCode = mcpError.type === 'not_found_error' ? 404 : mcpError.type === 'auth_error' ? 401 : 400; res.status(statusCode).json(mcpError); } else { logger.error('Error al actualizar evento', { error: String(error) }); res.status(500).json({ error: 'unknown_error', message: error instanceof Error ? error.message : String(error) }); } } }); // DELETE /api/events/:eventId - Eliminar evento app.delete('/api/events/:eventId', async (req: Request, res: Response): Promise<void> => { try { const eventId = req.params.eventId; const { calendarId } = req.query; if (!eventId) { res.status(400).json({ error: 'validation_error', message: 'El parámetro eventId es requerido' }); return; } await deleteEvent({ eventId, calendarId: calendarId as string | undefined }); res.status(204).send(); } catch (error) { if (error instanceof MCPCalendarError) { const mcpError = error.toMCPError(); const statusCode = mcpError.type === 'not_found_error' ? 404 : mcpError.type === 'auth_error' ? 401 : 400; res.status(statusCode).json(mcpError); } else { logger.error('Error al eliminar evento', { error: String(error) }); res.status(500).json({ error: 'unknown_error', message: error instanceof Error ? error.message : String(error) }); } } }); // Manejo de rutas no encontradas app.use((_req: Request, res: Response) => { res.status(404).json({ error: 'not_found', message: 'Ruta no encontrada' }); }); // Manejo de errores global app.use((err: Error, _req: Request, res: Response, _next: express.NextFunction) => { logger.error('Error no manejado', { error: err.message, stack: err.stack }); res.status(500).json({ error: 'internal_server_error', message: 'Error interno del servidor' }); }); // Iniciar servidor // Escuchar en 0.0.0.0 para que Railway pueda enrutar el tráfico const HOST = process.env.HOST || '0.0.0.0'; // Validar que el puerto sea válido if (isNaN(PORT) || PORT <= 0 || PORT > 65535) { logger.error('Puerto inválido', { port: PORT }); console.error(`❌ Puerto inválido: ${PORT}`); process.exit(1); } logger.info('Iniciando servidor HTTP...', { host: HOST, port: PORT }); console.log(`🚀 Iniciando servidor en ${HOST}:${PORT}...`); let server: ReturnType<typeof app.listen>; try { server = app.listen(PORT, HOST, () => { logger.info(`Servidor HTTP iniciado exitosamente`, { host: HOST, port: PORT }); logger.info(`Health check disponible en /health`); console.log(`✅ Servidor escuchando en http://${HOST}:${PORT}`); console.log(`✅ Health check: http://${HOST}:${PORT}/health`); }); } catch (error) { logger.error('Error al crear el servidor', { error: error instanceof Error ? error.message : String(error) }); console.error('❌ Error al crear el servidor:', error); process.exit(1); } // Manejo de errores del servidor server.on('error', (error: NodeJS.ErrnoException) => { logger.error('Error al iniciar el servidor', { error: error.message, code: error.code }); console.error('❌ Error al iniciar el servidor:', error.message); if (error.code === 'EADDRINUSE') { console.error(`⚠️ El puerto ${PORT} ya está en uso`); } process.exit(1); }); // Manejo de cierre graceful server.on('close', () => { logger.info('Servidor cerrado'); console.log('👋 Servidor cerrado'); }); // Manejo de errores no capturados process.on('uncaughtException', (error: Error) => { logger.error('Excepción no capturada', { error: error.message, stack: error.stack }); console.error('❌ Excepción no capturada:', error); if (server) { server.close(() => { process.exit(1); }); } else { process.exit(1); } }); process.on('unhandledRejection', (reason: unknown) => { logger.error('Promesa rechazada no manejada', { reason: String(reason) }); console.error('❌ Promesa rechazada no manejada:', reason); if (server) { server.close(() => { process.exit(1); }); } else { process.exit(1); } }); // Manejo de señales para cierre graceful (importante para Railway) process.on('SIGTERM', () => { logger.info('Recibida señal SIGTERM, cerrando servidor...'); console.log('🛑 Recibida señal SIGTERM, cerrando servidor...'); if (server) { server.close(() => { logger.info('Servidor cerrado correctamente'); console.log('✅ Servidor cerrado correctamente'); process.exit(0); }); } else { process.exit(0); } }); process.on('SIGINT', () => { logger.info('Recibida señal SIGINT, cerrando servidor...'); console.log('🛑 Recibida señal SIGINT, cerrando servidor...'); if (server) { server.close(() => { logger.info('Servidor cerrado correctamente'); console.log('✅ Servidor cerrado correctamente'); process.exit(0); }); } else { process.exit(0); } });

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

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