Skip to main content
Glama
README.md
# 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

A3.6/5.0

Scored across 2 tools

Disambiguation4/5

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.

Naming Consistency5/5

Both tool names follow the 'get_' prefix with a clear noun phrase (todays_meetings, availability). The naming pattern is consistent and predictable.

Tool Count3/5

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.

Completeness2/5

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.

Maintenance

ActivityInactive
ResponsivenessNo issues