Conversation
…realtime sync webhook Co-authored-by: Cursor <cursoragent@cursor.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_297e294b-565c-447d-be7a-f0bb0e9c3ac3) |
There was a problem hiding this comment.
Code Review
This pull request introduces a new Model Context Protocol (MCP) server package (@calcom/mcp-server) to expose Crove Cal booking and event type tools to AI agents, along with updated SMTP email configurations and webhook sync tests. The review feedback highlights several critical runtime issues in the new package: it lacks "type": "module" in its package.json despite using ESM-only dependencies, relative imports are missing required .js extensions, and a redundant CommonJS conditional block will throw a ReferenceError in an ESM environment. Additionally, the binary entry point points directly to an uncompiled TypeScript file, and the slot availability handler contains timezone bugs due to hardcoded UTC working hours and incorrect date boundaries.
| const startDate = new Date(`${input.dateFrom}T00:00:00.000Z`); | ||
| const endDate = new Date(`${input.dateTo}T23:59:59.999Z`); | ||
|
|
||
| if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) { | ||
| throw new Error("Invalid dateFrom or dateTo format. Please use YYYY-MM-DD"); | ||
| } | ||
|
|
||
| // Get existing non-cancelled bookings in the date range | ||
| const existingBookings = await prisma.booking.findMany({ | ||
| where: { | ||
| eventTypeId: eventType.id, | ||
| status: { notIn: ["CANCELLED", "REJECTED"] }, | ||
| startTime: { lte: endDate }, | ||
| endTime: { gte: startDate }, | ||
| }, | ||
| select: { | ||
| startTime: true, | ||
| endTime: true, | ||
| }, | ||
| }); | ||
|
|
||
| const bookedIntervals = existingBookings.map((b) => ({ | ||
| start: new Date(b.startTime).getTime(), | ||
| end: new Date(b.endTime).getTime(), | ||
| })); | ||
|
|
||
| const durationMs = eventType.length * 60 * 1000; | ||
| const slots: TimeSlot[] = []; | ||
|
|
||
| // Generate candidate slots per day between 09:00 and 17:00 UTC (or local working window) | ||
| const currentDay = new Date(startDate); | ||
| while (currentDay <= endDate) { | ||
| // Generate slots for working hours 09:00 - 17:00 | ||
| const dayStart = new Date(currentDay); | ||
| dayStart.setUTCHours(9, 0, 0, 0); | ||
|
|
||
| const dayEnd = new Date(currentDay); | ||
| dayEnd.setUTCHours(17, 0, 0, 0); | ||
|
|
||
| let slotStart = dayStart.getTime(); | ||
| while (slotStart + durationMs <= dayEnd.getTime()) { | ||
| const slotEnd = slotStart + durationMs; | ||
|
|
||
| // Check if slot overlaps with any booked intervals | ||
| const isOverlap = bookedIntervals.some((b) => slotStart < b.end && slotEnd > b.start); | ||
|
|
||
| if (!isOverlap && slotStart > Date.now()) { | ||
| slots.push({ | ||
| time: new Date(slotStart).toISOString(), | ||
| }); | ||
| } | ||
|
|
||
| slotStart += durationMs; | ||
| } | ||
|
|
||
| currentDay.setUTCDate(currentDay.getUTCDate() + 1); | ||
| } |
There was a problem hiding this comment.
The current implementation of getAvailableSlotsHandler has two major timezone-related bugs:
- Hardcoded UTC Working Hours: It generates candidate slots between
09:00and17:00UTC regardless of the host's timezone (eventType.timeZone) or the requested timezone (input.timeZone). For a host inAsia/Ho_Chi_Minh(UTC+7), this shifts their working hours to16:00 - 00:00local time, allowing bookings in the middle of the night and blocking their actual working day. - Incorrect Date Boundaries:
startDateandendDateare constructed using UTC midnight (00:00:00.000Zand23:59:59.999Z). For negative timezone offsets (likeAmerica/New_Yorkat UTC-4), a booking at22:00local time on the target day actually occurs at02:00UTC the next day, which falls outside the queried range and will be missed, leading to double-bookings.
We can resolve both issues cleanly using standard Intl APIs to perform timezone-aware date boundary and slot generation without adding external dependencies.
const tz = input.timeZone || eventType.timeZone || "UTC";
// Helper to get UTC Date for local start/end of day in target timezone
const getUtcBoundary = (dateStr: string, timeStr: string, timeZone: string) => {
const localDate = new Date(`${dateStr}T${timeStr}`);
const utcDate = new Date(localDate.toLocaleString("en-US", { timeZone: "UTC" }));
const tzDate = new Date(localDate.toLocaleString("en-US", { timeZone }));
const offset = utcDate.getTime() - tzDate.getTime();
return new Date(localDate.getTime() + offset);
};
const startDate = getUtcBoundary(input.dateFrom, "00:00:00.000", tz);
const endDate = getUtcBoundary(input.dateTo, "23:59:59.999", tz);
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
throw new Error("Invalid dateFrom or dateTo format. Please use YYYY-MM-DD");
}
// Helper to get local date string YYYY-MM-DD in target timezone
const getLocalDateString = (date: Date, timeZone: string) => {
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
});
const parts = formatter.formatToParts(date);
const month = parts.find((p) => p.type === "month")?.value;
const day = parts.find((p) => p.type === "day")?.value;
const year = parts.find((p) => p.type === "year")?.value;
return `${year}-${month}-${day}`;
};
// Get existing non-cancelled bookings in the date range
const existingBookings = await prisma.booking.findMany({
where: {
eventTypeId: eventType.id,
status: { notIn: ["CANCELLED", "REJECTED"] },
startTime: { lte: endDate },
endTime: { gte: startDate },
},
select: {
startTime: true,
endTime: true,
},
});
const bookedIntervals = existingBookings.map((b) => ({
start: new Date(b.startTime).getTime(),
end: new Date(b.endTime).getTime(),
}));
const durationMs = eventType.length * 60 * 1000;
const slots: TimeSlot[] = [];
// Generate candidate slots per day between 09:00 and 17:00 in the target timezone
const currentDay = new Date(startDate);
while (currentDay <= endDate) {
const dateStr = getLocalDateString(currentDay, tz);
const dayStart = getUtcBoundary(dateStr, "09:00:00.000", tz);
const dayEnd = getUtcBoundary(dateStr, "17:00:00.000", tz);
let slotStart = dayStart.getTime();
while (slotStart + durationMs <= dayEnd.getTime()) {
const slotEnd = slotStart + durationMs;
// Check if slot overlaps with any booked intervals
const isOverlap = bookedIntervals.some((b) => slotStart < b.end && slotEnd > b.start);
if (!isOverlap && slotStart > Date.now()) {
slots.push({
time: new Date(slotStart).toISOString(),
});
}
slotStart += durationMs;
}
currentDay.setUTCDate(currentDay.getUTCDate() + 1);
}| { | ||
| "name": "@calcom/mcp-server", | ||
| "version": "1.0.0", | ||
| "private": true, |
There was a problem hiding this comment.
The @modelcontextprotocol/sdk package is an ESM-only package. Since packages/mcp-server/package.json does not specify "type": "module", Node.js will treat this package as CommonJS by default. When running the server, this will result in a runtime crash with Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported.
Please add "type": "module" to the package.json to ensure Node.js executes the package as an ES Module.
| { | |
| "name": "@calcom/mcp-server", | |
| "version": "1.0.0", | |
| "private": true, | |
| { | |
| "name": "@calcom/mcp-server", | |
| "version": "1.0.0", | |
| "private": true, | |
| "type": "module", |
| if (require.main === module) { | ||
| startStdioServer().catch((error) => { | ||
| console.error("[crove-cal-mcp] Fatal error starting MCP server:", error); | ||
| process.exit(1); | ||
| }); | ||
| } |
There was a problem hiding this comment.
In an ES Module (ESM) environment, the global require and module variables are not defined. Executing this file will throw a ReferenceError: require is not defined at runtime.
Since you already have a dedicated binary entry point in packages/mcp-server/bin/crove-cal-mcp.ts that imports and calls startStdioServer(), this conditional execution block is redundant and can be safely removed.
| @@ -0,0 +1,22 @@ | |||
| import prisma from "@calcom/prisma"; | |||
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | |||
| import { createCroveCalMcpServer } from "./server"; | |||
There was a problem hiding this comment.
| import { | ||
| cancelBookingHandler, | ||
| createBookingHandler, | ||
| getBookingHandler, | ||
| listBookingsHandler, | ||
| rescheduleBookingHandler, | ||
| } from "./tools/bookings"; | ||
| import { getEventTypeDetailsHandler, listEventTypesHandler } from "./tools/eventTypes"; | ||
| import { getAvailableSlotsHandler } from "./tools/slots"; |
There was a problem hiding this comment.
In Node.js ES Modules, relative import paths must include the file extension (e.g., .js). Without it, Node.js will throw an ERR_MODULE_NOT_FOUND error at runtime.
| import { | |
| cancelBookingHandler, | |
| createBookingHandler, | |
| getBookingHandler, | |
| listBookingsHandler, | |
| rescheduleBookingHandler, | |
| } from "./tools/bookings"; | |
| import { getEventTypeDetailsHandler, listEventTypesHandler } from "./tools/eventTypes"; | |
| import { getAvailableSlotsHandler } from "./tools/slots"; | |
| import { | |
| cancelBookingHandler, | |
| createBookingHandler, | |
| getBookingHandler, | |
| listBookingsHandler, | |
| rescheduleBookingHandler, | |
| } from "./tools/bookings.js"; | |
| import { getEventTypeDetailsHandler, listEventTypesHandler } from "./tools/eventTypes.js"; | |
| import { getAvailableSlotsHandler } from "./tools/slots.js"; |
| @@ -0,0 +1,7 @@ | |||
| #!/usr/bin/env node | |||
| import { startStdioServer } from "../src/index"; | |||
There was a problem hiding this comment.
| "bin": { | ||
| "crove-cal-mcp": "./bin/crove-cal-mcp.ts" | ||
| }, |
There was a problem hiding this comment.
The bin field points directly to a TypeScript file (./bin/crove-cal-mcp.ts) with a standard Node shebang (#!/usr/bin/env node). Standard Node.js cannot execute TypeScript files directly without a compilation step or a loader. When installed or run as a binary, this will fail with a syntax error.
To make the binary usable, you should:
- Add a build step (e.g., using
tscortsup) to compile the TypeScript files to JavaScript (e.g., into adistdirectory). - Point the
binfield to the compiled JavaScript file (e.g.,./dist/bin/crove-cal-mcp.js). - Update the
mainfield to point to./dist/index.js.
| "bin": { | |
| "crove-cal-mcp": "./bin/crove-cal-mcp.ts" | |
| }, | |
| "bin": { | |
| "crove-cal-mcp": "./dist/bin/crove-cal-mcp.js" | |
| }, |
Summary
eschedule_booking, \cancel_booking, \list_bookings).
Made with Cursor