Skip to content

feat(mcp-and-sync): add Crove Cal MCP server and realtime webhook sync test suite - #39

Merged
JOY (JOY) merged 1 commit into
mainfrom
dev
Aug 26, 2026
Merged

feat(mcp-and-sync): add Crove Cal MCP server and realtime webhook sync test suite#39
JOY (JOY) merged 1 commit into
mainfrom
dev

Conversation

@JOY

Copy link
Copy Markdown

Summary

  • Added automated unit & integration test suite for Realtime Webhook Sync (\�pps/web/app/api/webhooks/dos-org-sync/tests/route.test.ts) with 10 passing tests.
  • Created @calcom/mcp-server\ package implementing the Model Context Protocol (MCP) server for Crove Cal with 8 tools (\list_event_types, \get_event_type, \get_available_slots, \create_booking, \get_booking,
    eschedule_booking, \cancel_booking, \list_bookings).
  • Updated \docs/Architecture.md\ and .env.example\ with standard SMTP configuration guidelines for Amazon SES and Brevo.

Made with Cursor

…realtime sync webhook

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 658c1785-4f74-4f20-89df-7ef6d08d8449

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@JOY
JOY (JOY) merged commit cb9edc5 into main Aug 26, 2026
3 checks passed
@cursor

cursor Bot commented Aug 26, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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)

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +52 to +108
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of getAvailableSlotsHandler has two major timezone-related bugs:

  1. Hardcoded UTC Working Hours: It generates candidate slots between 09:00 and 17:00 UTC regardless of the host's timezone (eventType.timeZone) or the requested timezone (input.timeZone). For a host in Asia/Ho_Chi_Minh (UTC+7), this shifts their working hours to 16:00 - 00:00 local time, allowing bookings in the middle of the night and blocking their actual working day.
  2. Incorrect Date Boundaries: startDate and endDate are constructed using UTC midnight (00:00:00.000Z and 23:59:59.999Z). For negative timezone offsets (like America/New_York at UTC-4), a booking at 22:00 local time on the target day actually occurs at 02:00 UTC 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);
  }

Comment on lines +1 to +4
{
"name": "@calcom/mcp-server",
"version": "1.0.0",
"private": true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
{
"name": "@calcom/mcp-server",
"version": "1.0.0",
"private": true,
{
"name": "@calcom/mcp-server",
"version": "1.0.0",
"private": true,
"type": "module",

Comment on lines +17 to +22
if (require.main === module) {
startStdioServer().catch((error) => {
console.error("[crove-cal-mcp] Fatal error starting MCP server:", error);
process.exit(1);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
import { createCroveCalMcpServer } from "./server";
import { createCroveCalMcpServer } from "./server.js";

Comment on lines +4 to +12
import {
cancelBookingHandler,
createBookingHandler,
getBookingHandler,
listBookingsHandler,
rescheduleBookingHandler,
} from "./tools/bookings";
import { getEventTypeDetailsHandler, listEventTypesHandler } from "./tools/eventTypes";
import { getAvailableSlotsHandler } from "./tools/slots";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
import { startStdioServer } from "../src/index";
import { startStdioServer } from "../src/index.js";

Comment on lines +7 to +9
"bin": {
"crove-cal-mcp": "./bin/crove-cal-mcp.ts"
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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:

  1. Add a build step (e.g., using tsc or tsup) to compile the TypeScript files to JavaScript (e.g., into a dist directory).
  2. Point the bin field to the compiled JavaScript file (e.g., ./dist/bin/crove-cal-mcp.js).
  3. Update the main field to point to ./dist/index.js.
Suggested change
"bin": {
"crove-cal-mcp": "./bin/crove-cal-mcp.ts"
},
"bin": {
"crove-cal-mcp": "./dist/bin/crove-cal-mcp.js"
},

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant