calendar-mcp
# calendar-mcp
An MCP (Model Context Protocol) server that reads your **Microsoft 365 calendar**
and answers two questions for any day:
- **What meetings do I have?** (`get_todays_meetings`)
- **When am I free?** (`get_availability` — busy blocks + free gaps within working hours)
It authenticates as *you* using the Microsoft **device code flow** (no admin
consent, no app secrets) and reads your calendar via the **Microsoft Graph API**.
This README doubles as a learning guide structured around four topics:
1. [Understand MCP Fundamentals & Architecture](#1-understand-mcp-fundamentals--architecture)
2. [Practical MCP Server Implementation & Deployment](#2-practical-mcp-server-implementation--deployment)
3. [Integrate MCP with a Real Workflow](#3-integrate-mcp-with-a-real-workflow)
4. [Reinforcement & Validation](#4-reinforcement--validation)
---
## 1. Understand MCP Fundamentals & Architecture
**What MCP is.** The Model Context Protocol is an open standard that lets an AI
client (Claude Desktop, Claude Code, etc.) call external **tools**, read
**resources**, and use **prompts** through a uniform JSON-RPC interface. Instead
of every integration being bespoke, an MCP *server* exposes capabilities and any
MCP *client* can use them.
**The three roles.**
| Role | In this project |
|------|-----------------|
| **Host / Client** | Claude Desktop or Claude Code — starts the server, sends requests |
| **Server** | `calendar-mcp` — exposes the two calendar tools |
| **Transport** | `stdio` — JSON-RPC messages over stdin/stdout |
**Message flow (this server):**
```
Client calendar-mcp (server) Microsoft Graph
│ initialize ──────────────────▶ │
│ ◀────────────── capabilities │ │
│ tools/list ──────────────────▶ │
│ ◀──────── [get_todays_meetings, get_availability] │
│ tools/call get_availability ─▶ │
│ │ acquireTokenSilent (MSAL cache) │
│ │ GET /me/calendarView ──────────────▶│
│ │ ◀──────────────── events │
│ ◀──── text: free/busy slots │ │
```
**Architecture of this codebase** (one responsibility per file — see team
standards):
```
src/
├── index.ts MCP server: registers tools, formats output
├── login.ts one-time device-code sign-in CLI
├── config.ts env validation (Zod) → Config
├── logger.ts structured logger → STDERR (stdout is the protocol!)
├── time.ts timezone math (wall-clock ⇄ UTC instants)
├── types/calendar.ts Zod schemas + domain types + Result<T>
└── services/
├── auth-service.ts MSAL device-code flow + token cache persistence
├── graph-service.ts calls Microsoft Graph, normalizes events
└── availability-service.ts merges busy blocks, computes free gaps (pure)
```
**Two architectural rules worth internalizing:**
1. **stdout is sacred.** A stdio MCP server speaks JSON-RPC on stdout. Any stray
`console.log` corrupts the stream. That's why `logger.ts` writes only to
stderr.
2. **Auth is separated from serving.** The interactive sign-in lives in a
separate `login` script. The server itself *only* refreshes tokens silently,
so it never needs to prompt a human mid-request.
---
## 2. Practical MCP Server Implementation & Deployment
### Prerequisites
- Node.js ≥ 18 (you have v22 ✓)
- A Microsoft 365 / Outlook account with a calendar
- An Azure App Registration (free — guide below)
### Step A — Create an Azure App Registration
You need a **Client ID** and **Tenant ID**. This app uses *delegated* permissions
(it acts as you), so no client secret and no admin consent are required.
1. Go to <https://portal.azure.com> → search **App registrations** → **New registration**.
2. **Name:** `calendar-mcp` (anything).
3. **Supported account types:**
- Just your work/school account → *Accounts in this organizational directory only*.
- Personal Microsoft accounts too → *Accounts in any org directory and personal Microsoft accounts*.
4. **Redirect URI:** leave blank. Click **Register**.
5. On the **Overview** page, copy:
- **Application (client) ID** → this is `AZURE_CLIENT_ID`
- **Directory (tenant) ID** → this is `AZURE_TENANT_ID`
(or use `common` if you chose a multi-tenant/personal account type).
6. Left menu → **Authentication** → **Advanced settings** → set
**Allow public client flows** to **Yes**. *(Required for device code flow.)* Save.
7. Left menu → **API permissions** → **Add a permission** → **Microsoft Graph**
→ **Delegated permissions** → search and add **`Calendars.Read`** → **Add permissions**.
- Personal accounts consent on first sign-in; org accounts may need an admin to
**Grant admin consent** depending on tenant policy.
### Step B — Configure the project
```bash
cp .env.example .env
# edit .env and set AZURE_CLIENT_ID (and AZURE_TENANT_ID if not "common")
```
| Variable | Required | Default | Notes |
|----------|----------|---------|-------|
| `AZURE_CLIENT_ID` | ✅ | — | Application (client) ID GUID |
| `AZURE_TENANT_ID` | | `common` | Tenant GUID, or `common` for personal/multi-tenant |
| `TOKEN_CACHE_PATH` | | `~/.calendar-mcp/token-cache.json` | Where tokens are stored (chmod 600) |
| `TIMEZONE` | | system tz | IANA name, e.g. `Asia/Kolkata` |
| `WORKING_HOURS_START` | | `9` | Hour 0–23 for availability window |
| `WORKING_HOURS_END` | | `18` | Hour 1–24 for availability window |
### Step C — Install, sign in, build
```bash
npm install
npm run login # device-code flow: opens a URL, you paste a code, sign in ONCE
npm run build # compile TypeScript → dist/
```
`npm run login` prints something like:
```
To sign in, use a web browser to open https://microsoft.com/devicelogin
and enter the code ABCD-EFGH to authenticate.
```
After success, a token cache is written to `TOKEN_CACHE_PATH`. The server uses it
silently from then on.
### Step D — Run it
```bash
npm start # runs dist/index.js over stdio
# or during development:
npm run dev # runs src/index.ts via tsx (no build step)
```
### Deployment: wire it into an MCP client
**Claude Desktop** — edit `claude_desktop_config.json`
(macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):
```json
{
"mcpServers": {
"calendar": {
"command": "node",
"args": ["/Users/khushal/Documents/practice project/calendar-mcp/dist/index.js"],
"env": {
"AZURE_CLIENT_ID": "your-client-id-guid",
"AZURE_TENANT_ID": "common",
"TIMEZONE": "Asia/Kolkata"
}
}
}
}
```
**Claude Code** (CLI):
```bash
claude mcp add calendar -- node "/Users/khushal/Documents/practice project/calendar-mcp/dist/index.js"
```
Restart the client. The `calendar` server's tools will appear.
> Note: run `npm run login` first so the token cache exists before the client
> launches the server. The server never prompts for sign-in itself.
---
## 3. Integrate MCP with a Real Workflow
Once connected, you talk to your calendar in natural language and the model picks
the right tool:
| You ask… | Tool called | Result |
|----------|-------------|--------|
| "What meetings do I have today?" | `get_todays_meetings` | Ordered list with times, locations, organizers |
| "What's on my calendar on 2026-06-25?" | `get_todays_meetings {date}` | Same, for that date |
| "When am I free today?" | `get_availability` | Free gaps + busy blocks within 9–18 |
| "Do I have a 2-hour block this afternoon?" | `get_availability` | Model reads the free slots and reasons over them |
| "Find me 30 min between meetings before 2pm" | `get_availability {workingHoursEnd:14}` | Narrowed window |
**Why this composes well:** `get_availability` returns *structured* free/busy
spans, so the model can chain reasoning ("schedule the review in your longest free
block") without you doing the arithmetic. This is the real value of MCP — the tool
provides facts; the model provides judgment.
Example tool output:
```
Availability for 2026-06-23 (Asia/Kolkata), working hours 09:00–18:00:
Total free: 5h 30m
Free slots:
• 9:00 AM – 11:00 AM
• 11:30 AM – 1:00 PM
• 2:00 PM – 4:00 PM
Busy blocks:
• 11:00 AM – 11:30 AM
• 1:00 PM – 2:00 PM
• 4:00 PM – 6:00 PM
```
---
## 4. Reinforcement & Validation
### Validate the protocol without a calendar
The server answers `initialize` and `tools/list` before any auth. Smoke-test with
raw JSON-RPC (a well-formed dummy client ID is enough):
```bash
printf '%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
| AZURE_CLIENT_ID=11111111-1111-1111-1111-111111111111 node dist/index.js 2>/dev/null
```
You should see the server info and both tool schemas.
### Validate the auth boundary
Calling a tool with no cached login returns a friendly, non-crashing error:
```bash
printf '%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_todays_meetings","arguments":{}}}' \
| AZURE_CLIENT_ID=11111111-1111-1111-1111-111111111111 TOKEN_CACHE_PATH=/tmp/none.json node dist/index.js 2>/dev/null
# → "Not signed in. Run `npm run login` ..."
```
### Validate against your real calendar
```bash
npm run login # if you haven't already
npx @modelcontextprotocol/inspector node dist/index.js
```
The **MCP Inspector** opens a UI where you can call `get_todays_meetings` and
`get_availability` and see live results from your calendar.
### Checklist
- [ ] `npm run build` compiles with no errors
- [ ] `tools/list` returns both tools
- [ ] `npm run login` completes and writes the token cache
- [ ] `get_todays_meetings` matches what you see in Outlook
- [ ] `get_availability` free + busy spans add up to the working window
- [ ] All-day events do not block availability; tentative meetings show as busy
- [ ] Timezone is correct (compare a meeting time vs Outlook)
### Things to try next (stretch goals)
- Add a `find_slot` tool that takes a duration and returns the earliest free block.
- Expose the calendar as an MCP **resource** (read-only data) in addition to tools.
- Support multiple calendars or a `freeBusy` query across attendees
(`/me/calendar/getSchedule`).
- Cache Graph responses briefly to cut latency on repeated calls.
---
## Security notes
- The token cache (`TOKEN_CACHE_PATH`) holds refresh/access tokens. It's written
`chmod 600` and is git-ignored. Treat it like a password.
- Only `Calendars.Read` is requested — the server cannot modify your calendar.
- No secrets are stored in code; the Client ID is not a secret but lives in env.
## Project scripts
| Command | Does |
|---------|------|
| `npm run dev` | Run the server from source (tsx) |
| `npm run build` | Compile to `dist/` |
| `npm start` | Run the compiled server |
| `npm run login` | One-time device-code sign-in (source) |
| `npm run login:prod` | Same, from compiled `dist/` |
| `npm run typecheck` | Type-check without emitting |
TDQS
Scored across 2 tools
The two tools are distinct: one lists meetings, the other shows free/busy availability. While both relate to the calendar for a given day, their descriptions clearly differentiate the outputs.
Both tool names follow the 'get_' prefix with a clear noun phrase (todays_meetings, availability). The naming pattern is consistent and predictable.
With only two tools, the server feels under-scoped for a calendar integration. However, the narrow focus on read-only queries makes the count somewhat acceptable, though it is on the thin side.
The tool surface lacks any CRUD operations for meetings (create, update, delete) and only provides read-only queries. This is a significant gap for a calendar MCP server, as agents cannot manage calendar events.