Cal.com Calendar MCP Server

by mumunha
Verified
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; import axios from "axios"; const ADD_APPOINTMENT_TOOL: Tool = { name: "calcom_add_appointment", description: "Creates a new appointment in Cal.com calendar. " + "Use this for scheduling new meetings or appointments. " + "Requires event type ID, start time, end time, name, email, and optional notes. ", inputSchema: { type: "object", properties: { eventTypeId: { type: "number", description: "The Cal.com event type ID" }, startTime: { type: "string", description: "Start time in ISO format (YYYY-MM-DDTHH:mm:ss.sssZ)" }, endTime: { type: "string", description: "End time in ISO format (YYYY-MM-DDTHH:mm:ss.sssZ)" }, name: { type: "string", description: "Attendee's name" }, email: { type: "string", description: "Attendee's email" }, notes: { type: "string", description: "Optional notes for the appointment", } }, required: ["eventTypeId", "startTime", "endTime", "name", "email"], }, }; const UPDATE_APPOINTMENT_TOOL: Tool = { name: "calcom_update_appointment", description: "Updates an existing appointment in Cal.com calendar. " + "Use this for rescheduling or modifying existing appointments. " + "Requires booking ID and the fields to update. ", inputSchema: { type: "object", properties: { bookingId: { type: "number", description: "The Cal.com booking ID to update" }, startTime: { type: "string", description: "New start time in ISO format (YYYY-MM-DDTHH:mm:ss.sssZ)" }, endTime: { type: "string", description: "New end time in ISO format (YYYY-MM-DDTHH:mm:ss.sssZ)" }, notes: { type: "string", description: "New notes for the appointment" } }, required: ["bookingId"], } }; const DELETE_APPOINTMENT_TOOL: Tool = { name: "calcom_delete_appointment", description: "Deletes an existing appointment from Cal.com calendar. " + "Use this for canceling appointments. " + "Requires booking ID. ", inputSchema: { type: "object", properties: { bookingId: { type: "number", description: "The Cal.com booking ID to delete" }, reason: { type: "string", description: "Optional reason for cancellation" } }, required: ["bookingId"], } }; const LIST_APPOINTMENTS_TOOL: Tool = { name: "calcom_list_appointments", description: "Lists appointments from Cal.com calendar. " + "Can be filtered by date range. " + "Returns a list of appointments with their details. ", inputSchema: { type: "object", properties: { startDate: { type: "string", description: "Start date in YYYY-MM-DD format" }, endDate: { type: "string", description: "End date in YYYY-MM-DD format" } }, required: ["startDate", "endDate"], } }; // Server implementation const server = new Server( { name: "example-servers/calcom-calendar", version: "0.1.0", }, { capabilities: { tools: {}, }, }, ); // Check for API key const CALCOM_API_KEY = process.env.CALCOM_API_KEY || ''; if (!CALCOM_API_KEY) { console.error("Error: CALCOM_API_KEY environment variable is required"); process.exit(1); } // Initialize Cal.com API client const calComApiClient = axios.create({ baseURL: 'https://api.cal.com/v1', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CALCOM_API_KEY}` } }); // Rate limiting const RATE_LIMIT = { perSecond: 5, perDay: 1000 }; let requestCount = { second: 0, day: 0, lastSecReset: Date.now(), lastDayReset: Date.now() }; function checkRateLimit() { const now = Date.now(); // Reset per-second counter if (now - requestCount.lastSecReset > 1000) { requestCount.second = 0; requestCount.lastSecReset = now; } // Reset per-day counter if (now - requestCount.lastDayReset > 86400000) { // 24 hours requestCount.day = 0; requestCount.lastDayReset = now; } if (requestCount.second >= RATE_LIMIT.perSecond || requestCount.day >= RATE_LIMIT.perDay) { throw new Error('Rate limit exceeded'); } requestCount.second++; requestCount.day++; } // Cal.com API functions interface Appointment { id: number; eventTypeId: number; startTime: string; endTime: string; status: string; attendees: { name: string; email: string; }[]; notes?: string; } function isCalComAddAppointmentArgs(args: unknown): args is { eventTypeId: number; startTime: string; endTime: string; name: string; email: string; notes?: string; } { return ( typeof args === "object" && args !== null && "eventTypeId" in args && "startTime" in args && "endTime" in args && "name" in args && "email" in args ); } function isCalComUpdateAppointmentArgs(args: unknown): args is { bookingId: number; startTime?: string; endTime?: string; notes?: string; } { return ( typeof args === "object" && args !== null && "bookingId" in args ); } function isCalComDeleteAppointmentArgs(args: unknown): args is { bookingId: number; reason?: string; } { return ( typeof args === "object" && args !== null && "bookingId" in args ); } function isCalComListAppointmentsArgs(args: unknown): args is { startDate: string; endDate: string; } { return ( typeof args === "object" && args !== null && "startDate" in args && "endDate" in args ); } async function addAppointment( eventTypeId: number, startTime: string, endTime: string, name: string, email: string, notes?: string ) { checkRateLimit(); try { const response = await calComApiClient.post('/bookings', { eventTypeId, start: new Date(startTime).toISOString(), end: new Date(endTime).toISOString(), name, email, notes, }); const booking = response.data; return `Appointment created successfully! Booking ID: ${booking.id} Event Type: ${booking.eventTypeId} Start Time: ${booking.startTime} End Time: ${booking.endTime} Attendee: ${name} (${email}) ${notes ? `Notes: ${notes}` : ""}`; } catch (error: any) { if (axios.isAxiosError(error)) { throw new Error(`Failed to create appointment: ${error.response?.data?.message || error.message}`); } throw new Error(`Failed to create appointment: ${String(error)}`); } } async function updateAppointment( bookingId: number, startTime?: string, endTime?: string, notes?: string ) { checkRateLimit(); try { const updateData: Record<string, any> = {}; if (startTime) updateData.start = new Date(startTime).toISOString(); if (endTime) updateData.end = new Date(endTime).toISOString(); if (notes !== undefined) updateData.notes = notes; const response = await calComApiClient.patch(`/bookings/${bookingId}`, updateData); const booking = response.data; return `Appointment updated successfully! Booking ID: ${booking.id} ${startTime ? `New Start Time: ${booking.startTime}` : ""} ${endTime ? `New End Time: ${booking.endTime}` : ""} ${notes !== undefined ? `New Notes: ${notes}` : ""}`; } catch (error: any) { if (axios.isAxiosError(error)) { throw new Error(`Failed to update appointment: ${error.response?.data?.message || error.message}`); } throw new Error(`Failed to update appointment: ${String(error)}`); } } async function deleteAppointment(bookingId: number, reason?: string) { checkRateLimit(); try { await calComApiClient.delete(`/bookings/${bookingId}`, { data: reason ? { reason } : undefined }); return `Appointment deleted successfully! Booking ID: ${bookingId} ${reason ? `Reason: ${reason}` : ""}`; } catch (error: any) { if (axios.isAxiosError(error)) { throw new Error(`Failed to delete appointment: ${error.response?.data?.message || error.message}`); } throw new Error(`Failed to delete appointment: ${String(error)}`); } } async function listAppointments(startDate: string, endDate: string) { checkRateLimit(); try { const response = await calComApiClient.get('/bookings', { params: { dateFrom: startDate, dateTo: endDate, } }); const bookings = response.data; if (bookings.length === 0) { return "No appointments found for the selected date range."; } return bookings.map((booking: any) => ` ID: ${booking.id} Event Type: ${booking.eventTypeId} Status: ${booking.status} Start Time: ${booking.startTime} End Time: ${booking.endTime} Attendees: ${booking.attendees.map((a: any) => `${a.name} (${a.email})`).join(", ")} ${booking.notes ? `Notes: ${booking.notes}` : ""} `).join("\n---\n"); } catch (error: any) { if (axios.isAxiosError(error)) { throw new Error(`Failed to list appointments: ${error.response?.data?.message || error.message}`); } throw new Error(`Failed to list appointments: ${String(error)}`); } } // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ ADD_APPOINTMENT_TOOL, UPDATE_APPOINTMENT_TOOL, DELETE_APPOINTMENT_TOOL, LIST_APPOINTMENTS_TOOL ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; if (!args) { throw new Error("No arguments provided"); } switch (name) { case "calcom_add_appointment": { if (!isCalComAddAppointmentArgs(args)) { throw new Error("Invalid arguments for calcom_add_appointment"); } const { eventTypeId, startTime, endTime, name, email, notes } = args; const result = await addAppointment(eventTypeId, startTime, endTime, name, email, notes); return { content: [{ type: "text", text: result }], isError: false, }; } case "calcom_update_appointment": { if (!isCalComUpdateAppointmentArgs(args)) { throw new Error("Invalid arguments for calcom_update_appointment"); } const { bookingId, startTime, endTime, notes } = args; const result = await updateAppointment(bookingId, startTime, endTime, notes); return { content: [{ type: "text", text: result }], isError: false, }; } case "calcom_delete_appointment": { if (!isCalComDeleteAppointmentArgs(args)) { throw new Error("Invalid arguments for calcom_delete_appointment"); } const { bookingId, reason } = args; const result = await deleteAppointment(bookingId, reason); return { content: [{ type: "text", text: result }], isError: false, }; } case "calcom_list_appointments": { if (!isCalComListAppointmentsArgs(args)) { throw new Error("Invalid arguments for calcom_list_appointments"); } const { startDate, endDate } = args; const result = await listAppointments(startDate, endDate); return { content: [{ type: "text", text: result }], isError: false, }; } default: return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true, }; } } catch (error: any) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Cal.com Calendar MCP Server running on stdio"); } runServer().catch((error: any) => { console.error("Fatal error running server:", error); process.exit(1); });