MCP Calendar Server
A production-quality Model Context Protocol (MCP) server for calendar management with robust timezone handling, recurrence support, and idempotency guarantees.
🎯 What is MCP?
The Model Context Protocol is a standardized way for AI agents (like Claude, ChatGPT, etc.) to interact with external tools and data sources. This server exposes calendar operations as MCP tools, allowing AI assistants to:
- Create events with timezone-aware scheduling
- List events with automatic recurrence expansion
- Cancel individual instances or entire event series
✨ Features
🌍 Timezone Correctness (Top Priority)
Timezone handling is notoriously difficult. Here's how we handle it:
- Storage: All events stored in UTC (source of truth)
- Input: Accept ISO 8601 datetime + IANA timezone (e.g.,
"Asia/Kolkata") - Output: Return events in their original timezone
- DST Handling: Recurrence expansion happens in local time to preserve wall-clock semantics (e.g., "9 AM daily" stays at 9 AM even across DST transitions)
- Library: Uses Luxon for robust timezone math
Why UTC Storage?
- Single source of truth
- No ambiguity during DST transitions
- Easy comparison and sorting
- Portable across systems
Why Original Timezone Matters?
- Preserves user intent ("9 AM in Kolkata" not "3:30 AM UTC")
- Correct recurrence expansion (daily meetings stay at same wall time)
🔁 Recurrence Support
Supports repeating events with:
- Frequencies:
daily,weekly,monthly - Intervals: Every N days/weeks/months (e.g., "every 2 weeks")
- End Dates:
untilparameter (ISO 8601)
Implementation Design:
- Recurrence rules stored as JSON in the database
- Virtual instances generated on-demand during
calendar.list - No duplicate rows for recurring events
- Cancel individual instances via
cancellationstable - Safety limits: Max 1000 instances per query to prevent DOS
🔒 Idempotency
Prevents duplicate events from retries:
- Optional
idempotency_keyparameter incalendar.create - If key exists, returns existing event (no duplicate created)
- Unique constraint enforced at database level
💾 Storage
Uses SQLite with better-sqlite3:
- Synchronous API: Simpler, more reliable than async
- WAL Mode: Better concurrency
- Schema:
events: Core event data (title, start_utc, duration, timezone, recurrence)cancellations: Tracks cancelled instances of recurring events- Indexes on
start_utcandidempotency_keyfor performance
📦 Installation
# Clone or navigate to project directory
cd calender-mcp
# Install dependencies
npm install
# Build TypeScript
npm run build
🚀 Usage
Running the Server
npm start
The server runs on stdio (standard input/output) as per MCP specification.
Configuring with Claude Desktop
Add to your Claude Desktop config (claude_desktop_config.json):
{
"mcpServers": {
"calendar": {
"command": "node",
"args": ["C:/path/to/calender-mcp/dist/index.js"]
}
}
}
Example Tool Calls
1. Create a Simple Event
{
"tool": "calendar.create",
"arguments": {
"title": "Team Standup",
"start": "2026-01-20T09:00:00",
"end": "2026-01-20T09:30:00",
"tz": "Asia/Kolkata"
}
}
2. Create a Recurring Event
{
"tool": "calendar.create",
"arguments": {
"title": "Weekly Review",
"start": "2026-01-20T15:00:00",
"end": "2026-01-20T16:00:00",
"tz": "America/New_York",
"recurrence": {
"freq": "weekly",
"interval": 1,
"until": "2026-06-30T23:59:59"
},
"idempotency_key": "weekly-review-2026"
}
}
3. List Events
{
"tool": "calendar.list",
"arguments": {
"range_start": "2026-01-20T00:00:00Z",
"range_end": "2026-01-27T00:00:00Z"
}
}
4. Cancel an Entire Event Series
{
"tool": "calendar.cancel",
"arguments": {
"event_id": "abc-123-def-456"
}
}
5. Cancel a Specific Instance
{
"tool": "calendar.cancel",
"arguments": {
"event_id": "abc-123-def-456",
"instance_start_utc": "2026-01-27T10:00:00.000Z"
}
}
🏗️ Architecture
src/
├── index.ts # MCP server setup & tool registration
├── storage/
│ └── db.ts # SQLite schema & connection
├── time/
│ └── timezone.ts # UTC conversion utilities (Luxon)
├── recurrence/
│ └── expand.ts # Recurrence expansion logic
└── tools/
├── create.ts # calendar.create handler
├── list.ts # calendar.list handler
└── cancel.ts # calendar.cancel handler
🧪 Testing
Create a test script (test.mjs):
import { spawn } from 'child_process';
const server = spawn('node', ['dist/index.js']);
// Send MCP request
const request = {
jsonrpc: '2.0',
id: 1,
method: 'tools/call',
params: {
name: 'calendar.create',
arguments: {
title: 'Test Event',
start: '2026-01-20T10:00:00',
end: '2026-01-20T11:00:00',
tz: 'UTC'
}
}
};
server.stdin.write(JSON.stringify(request) + '\n');
server.stdout.on('data', (data) => {
console.log('Response:', data.toString());
});
Run: node test.mjs
🚧 Known Limitations
- No Sync: Events are stored locally only. No Google Calendar / Outlook sync.
- No Notifications: Server doesn't send reminders or alerts.
- Recurrence Complexity: Only supports simple rules (no "2nd Tuesday of month").
- Performance: Recurrence expansion is brute-force iteration (acceptable for <1000 instances).
- No Conflict Detection: Doesn't prevent overlapping events.
🔮 Future Extensions
- Calendar Sync: Integrate with Google Calendar API, Microsoft Graph
- Notifications: Email/SMS reminders via Twilio, SendGrid
- Search: Full-text search on event titles/notes
- Attachments: Store files/links with events
- Attendees: Multi-user support with invitations
- Conflict Detection: Warn about overlapping events
- Advanced Recurrence: RRULE support (RFC 5545)
📚 Design Decisions
Why Luxon over Moment/Date-fns?
- Immutable: Safer API, no accidental mutations
- IANA Timezone Support: Built-in, no extra plugins
- Modern: Active development, ESM-first
Why SQLite over JSON Files?
- ACID Guarantees: Atomic writes, no corruption
- Indexes: Fast lookups on
start_utc,idempotency_key - Constraints: Unique keys enforced at DB level
- Scalability: Handles thousands of events efficiently
Why Virtual Instances for Recurrence?
- Storage Efficiency: One row instead of hundreds
- Flexibility: Change recurrence rule retroactively
- Cancellations: Track exceptions cleanly
📄 License
ISC
---
Built with ❤️ for the MCP ecosystem











