calendar-mcp
# calendar-mcp
An MCP server that gives an LLM client read and write access to your Google Calendar. It runs as a single process — stdio by default, streamable HTTP optionally — and exposes 23 tools with structured output: listing and searching calendars and events, creating, updating, moving, RSVPing to and deleting events, free/busy queries, busyness analysis, recurring-event projection, and finding a mutual slot and booking it. On top of that it has a scheduling brain that knows your working hours: finding and booking focus time, detecting double-bookings across several accounts at once, proposing better times for a meeting, and auditing where your week actually went. Authentication is Google OAuth 2.0 (Desktop app flow); tokens are cached locally, per account, and refreshed automatically.
## Quick start
**1. Create Google OAuth credentials.** In the [Google Cloud console](https://console.cloud.google.com/), enable the Google Calendar API, then create an OAuth client ID of type **Desktop app**. Copy the client ID and secret. (Details in [Google Cloud setup](#google-cloud-setup).)
**2. Set them in your environment**, or in a `.env` file in the directory you run from (see `example.env`):
```dotenv
GOOGLE_CLIENT_ID='...'
GOOGLE_CLIENT_SECRET='...'
```
**3. Sign in once, then add the server to your client:**
```bash
uvx calendar-mcp-server auth
```
This opens a browser and saves a token in the config directory (see [Accounts](#accounts)); if you already have a `.gcp-saved-tokens.json` or set `TOKEN_FILE_PATH`, that file is used instead. Verify it with `calendar-mcp check`. Add more Google accounts with `calendar-mcp auth --account work`. After that the server runs non-interactively — it never opens a browser on its own unless you set `CALENDAR_MCP_ALLOW_BROWSER_AUTH=1`.
> The PyPI distribution is `calendar-mcp-server`. It installs two identical console scripts, `calendar-mcp` and `calendar-mcp-server`, so `uvx calendar-mcp-server` and a local `calendar-mcp` are the same command.
## Client configuration
**Claude Code**
```bash
claude mcp add calendar \
--env GOOGLE_CLIENT_ID=... \
--env GOOGLE_CLIENT_SECRET=... \
-- uvx calendar-mcp-server
```
**Claude Desktop** — `claude_desktop_config.json`:
```json
{
"mcpServers": {
"calendar": {
"command": "uvx",
"args": ["calendar-mcp-server"],
"env": {
"GOOGLE_CLIENT_ID": "...",
"GOOGLE_CLIENT_SECRET": "...",
"TOKEN_FILE_PATH": "/absolute/path/to/.gcp-saved-tokens.json"
}
}
}
}
```
**Cursor** — `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global): same `mcpServers` block as above.
**Any other client** that speaks the standard `mcpServers` JSON takes the same entry. `TOKEN_FILE_PATH` is optional now that tokens default to the OS config directory, which does not depend on the working directory the client picks — but if you do set it, make it an absolute path.
## Remote / HTTP mode
```bash
calendar-mcp --transport http --host 127.0.0.1 --port 8000
```
The MCP endpoint is then `http://127.0.0.1:8000/mcp` (change the path with `--path`).
**There is no authentication layer on the HTTP transport yet.** Anyone who can reach the endpoint gets full access to the calendar the saved token belongs to. Bind it to loopback, or expose it only behind a trusted reverse proxy that authenticates, or on a private network such as a tailnet. Do not put it on a public interface.
## Tools
Read-only tools never change anything. "Writes" tools create or modify events;
`delete_event` is the only one that destroys data.
**Calendars and events**
| Tool | | Description |
| --- | --- | --- |
| `list_calendars` | read | List the calendars the user can see, with IDs and timezones. |
| `find_events` | read | Search a calendar for events, expanding recurring series into instances. |
| `check_attendee_status` | read | Report who accepted, declined or has not answered an invitation. |
| `query_free_busy` | read | Busy intervals for one or more calendars, without event details. |
| `analyze_busyness` | read | Per-day event count and total scheduled minutes over a range. |
| `project_recurring_events` | read | Compute future occurrences from recurrence rules. |
| `create_calendar` | writes | Create a new secondary calendar. |
| `create_event` | writes | Create an event with explicit start/end times and optional attendees. |
| `quick_add_event` | writes | Create an event from a plain-English phrase, parsed by Google. |
| `update_event` | writes | Change fields on an event; omitted fields are left untouched. |
| `move_event` | writes | Reschedule an event, and/or move it to another calendar. |
| `add_attendee` | writes | Invite one or more people to an existing event. |
| `respond_to_event` | writes | Set your own RSVP (`accepted`/`declined`/`tentative`/`needsAction`). |
| `schedule_mutual` | writes | Find the first slot where everyone is free, then book it. |
| **`delete_event`** | **destroys** | Permanently delete an event. Asks the client to confirm via elicitation when supported. |
**Scheduling brain** — these read your saved preferences (working hours, lunch, buffer, minimum focus block).
| Tool | | Description |
| --- | --- | --- |
| `find_focus_time` | read | Uninterrupted blocks in a window that could be used for deep work, longest first. |
| `detect_conflicts` | read | Double-bookings and too-tight transitions, across every signed-in account at once. |
| `time_audit` | read | Where the time went: meeting hours, by size, domain, recurrence and person. |
| `suggest_reschedule` | writes *(opt-in)* | Ranked better times for an existing meeting. Suggests only, unless `apply: true`. |
| `block_focus_time` | writes | Book the best free blocks as focus time. `dry_run: true` to preview. |
**Local configuration** — no Google call, no `account` argument.
| Tool | | Description |
| --- | --- | --- |
| `list_accounts` | read | The accounts you have signed in, and which is the default. |
| `get_preferences` | read | Working hours, lunch, buffer, minimum focus block, focus calendar. |
| `set_preferences` | writes | Update and save those preferences. Local file only. |
Every calendar tool takes an optional trailing `account` argument (`detect_conflicts`
takes `accounts`, a list, because checking several at once is the point). All times
are ISO 8601 strings — a naive timestamp is interpreted in the target calendar's own
timezone.
### Scheduling brain
The five scheduling tools share one idea: your calendar is not the same as your
availability. They start from your working hours, subtract lunch, subtract what is
already booked, and subtract the buffer you want around meetings — then reason about
what is left.
Prompts that exercise them:
- "Find me six hours of focus time next week and block it out — show me the times first."
- "Is anything double-booked between my work and personal calendars this week?"
- "My Thursday is back-to-back. Suggest better times for the design review."
- "Where did my time go last month? Who am I spending it with?"
- "I need a 90-minute deep work block before Friday. Is there one?"
`find_focus_time` reports what exists; `block_focus_time` defends it by creating
events (busy, reminders off, nobody notified), trimming the last block so it books
exactly the hours you asked for rather than a whole afternoon. `detect_conflicts`
separates genuine overlaps from *tight* transitions that merely break your buffer,
and ignores events you declined, events marked free, and (by default) all-day
entries. `suggest_reschedule` keeps the meeting's duration, ranks candidate slots by
fewest attendee conflicts and prefers the event's current day, and moves nothing
unless you pass `apply: true`.
### Time audit
`time_audit` answers "how much of my week is meetings?" in one pass, grouped by
`day` or `week`:
```
2026-08-01 .. 2026-09-01, Europe/Berlin, grouped by week
Meetings: 41.5 h across 63 meetings
Working hours: 152.0 h available (lunch removed)
In working hours: 38.0 h -> 25% of the week
Busiest week: 2026-W34, 14.0 h
Heaviest day: 2026-08-20, 6.5 h
By size: 1:1 18.0 h | small 15.5 h | large 8.0 h
By recurrence: recurring 26.0 h | one-off 15.5 h
Top people: a.schmidt@example.com 9.0 h | j.lee@example.com 7.5 h
```
Declined meetings, events marked free and all-day entries are left out by default;
`include_declined` and `include_all_day` bring them back.
### Safety
- **`delete_event` is the only destructive tool.** It asks the client to confirm
through MCP elicitation when the client supports it, and proceeds normally when
it does not.
- **`block_focus_time` takes `dry_run`.** Run it with `dry_run: true` to see the
exact blocks it would book before anything is written.
- **`suggest_reschedule` does not move anything by default.** `apply` is `false`
and has to be set explicitly, once the user has agreed to a time.
- **Everything else that writes is additive** — creating or editing an event —
and `update_event` leaves fields you omit untouched.
- **`list_accounts`, `get_preferences` and `set_preferences` never touch Google.**
They read and write local files in the config directory.
## Accounts
You can sign in more than one Google account and pick between them per call.
```bash
calendar-mcp auth # the default account
calendar-mcp auth --account work # a second, named account
calendar-mcp accounts # list them, with token paths and sign-in state
```
Every calendar tool takes an optional `account` argument naming one of these
("what's on my work calendar tomorrow?"). Omit it and the server uses the
default: `CALENDAR_MCP_DEFAULT_ACCOUNT` if set, otherwise the account named
`default`, otherwise the only account you have signed in. `list_accounts`
returns the same list the CLI prints, so the model can discover the names
itself.
Account names must match `[A-Za-z0-9][A-Za-z0-9_-]{0,63}`.
**Where things live.** Tokens and preferences are stored in the OS user config
directory — `%LOCALAPPDATA%\calendar-mcp` on Windows, `~/.config/calendar-mcp`
on Linux, `~/Library/Application Support/calendar-mcp` on macOS — with one
token file per account under `accounts/`. Override the whole directory with
`CALENDAR_MCP_CONFIG_DIR`.
**Back-compat.** `TOKEN_FILE_PATH` still works and now means *the default
account's token*. If it is set, or if a `.gcp-saved-tokens.json` exists in the
working directory, that file is used for the `default` account and nothing
moves. Named accounts always live in the config directory.
## Preferences
The server remembers how you like your week to be shaped, so the scheduling
tools do not have to guess. Read them with `get_preferences` and change them
with `set_preferences` ("I start at 8 and I want 15 minutes between meetings").
Preferences are global — they describe you, not one account — and are stored as
`preferences.json` in the config directory.
| Field | Default | Meaning |
| --- | --- | --- |
| `timezone` | unset | IANA zone the working hours are expressed in, e.g. `Europe/Berlin`. |
| `working_hours` | Mon–Fri 09:00–17:00 | Per weekday (`mon`…`sun`), a list of `["HH:MM", "HH:MM"]` spans. |
| `buffer_minutes` | `0` | Gap to leave either side of a meeting when proposing times. |
| `min_focus_block_minutes` | `60` | Shortest free stretch that still counts as usable focus time. |
| `lunch` | unset | A daily break carved out of the working hours. |
| `focus_calendar_id` | `primary` | Calendar that focus blocks are booked on. |
```json
{
"timezone": "Europe/Berlin",
"working_hours": {
"mon": [["09:00", "12:00"], ["13:00", "18:00"]],
"tue": [["09:00", "17:00"]],
"wed": [["09:00", "17:00"]],
"thu": [["09:00", "17:00"]],
"fri": [["09:00", "15:00"]],
"sat": [],
"sun": []
},
"buffer_minutes": 15,
"min_focus_block_minutes": 90,
"lunch": ["12:30", "13:15"],
"focus_calendar_id": "primary"
}
```
`set_preferences` merges: only the arguments you pass change, and the merged
result is validated before it is written, so a rejected change leaves the saved
file untouched. The one exception is `working_hours`, which is a **whole-schedule
replacement** — weekdays you leave out of the dict become non-working days. Pass
`clear_lunch: true` to remove a lunch break.
## Configuration
| Variable | Default | Purpose |
| --- | --- | --- |
| `GOOGLE_CLIENT_ID` | — | OAuth client ID (required). |
| `GOOGLE_CLIENT_SECRET` | — | OAuth client secret (required). |
| `TOKEN_FILE_PATH` | `.gcp-saved-tokens.json` | Where the **default** account's OAuth token is cached. |
| `CALENDAR_MCP_CONFIG_DIR` | OS user config dir | Directory holding per-account tokens (`accounts/`) and `preferences.json`. |
| `CALENDAR_MCP_DEFAULT_ACCOUNT` | `default` | Account used when a tool's `account` argument is omitted. |
| `CALENDAR_SCOPES` | `https://www.googleapis.com/auth/calendar` | Scope requested. Use `.../auth/calendar.readonly` for read-only. |
| `OAUTH_CALLBACK_PORT` | `8080` | Local port for the OAuth callback during `calendar-mcp auth`. |
| `CALENDAR_MCP_ALLOW_BROWSER_AUTH` | unset | Set to `1` to let the server itself open a browser when no token exists. Off by default so a stdio server never hangs. |
| `CALENDAR_MCP_LOG_FILE` | unset | Mirror the stderr log to this file. |
| `CALENDAR_MCP_LOG_LEVEL` | `INFO` | `DEBUG`/`INFO`/`WARNING`/`ERROR`/`CRITICAL`. Overrides `--log-level`. |
| `HOST` | `127.0.0.1` | Default for `--host` in HTTP mode. |
| `PORT` | `8000` | Default for `--port` in HTTP mode. |
A `.env` file in the working directory is loaded on startup. Logs never go to stdout — stdout is the MCP protocol channel in stdio mode.
### Commands
```
calendar-mcp [--transport {stdio,http}] [--host H] [--port P] [--path /mcp] [--log-level L]
calendar-mcp serve ... # explicit form of the default
calendar-mcp auth [--account NAME] [--no-browser]
calendar-mcp accounts # list known accounts; exit 1 if none is signed in
calendar-mcp check [--account NAME] # token status + calendar list; exit 1 if no valid token
calendar-mcp --version
```
`python -m calendar_mcp` accepts the same arguments.
## Google Cloud setup
1. Create or select a project and **enable the Google Calendar API**.
2. **APIs & Services → Credentials → Create credentials → OAuth client ID → Application type: Desktop app.** Copy the client ID and secret. There is no JSON download to keep.
3. A Desktop app client has **no "Authorized redirect URIs" field** — Google permits `http://localhost` on any port for this client type, which is what the local callback on `OAUTH_CALLBACK_PORT` uses. Nothing to configure there.
4. On the **OAuth consent screen**: User Type *External*, fill in the app name and contact emails, add the `https://www.googleapis.com/auth/calendar` scope, and **add your own Google account as a test user**. Without that last step the sign-in is rejected.
## Development
```bash
git clone https://github.com/deciduus/calendar-mcp
cd calendar-mcp
uv venv
uv pip install -e ".[dev]"
pytest
```
Layout: `calendar_mcp/server.py` (the `MCPServer`, shared helpers and the credential provider), `tools/` (one module per tool area — the tool functions themselves), `calendar_actions.py` (Google API calls), `analysis.py`, `timeutil.py` (pure interval maths), `accounts.py` (multi-account token paths), `preferences.py` (the saved schedule), `models.py` (pydantic input/output models), `auth.py` (OAuth), `cli.py` (the `calendar-mcp` command). `scripts/smoke_stdio.py` spawns a real stdio server and checks the handshake and tool list.
## Upgrading from 1.0
Nothing breaks. All 15 original tools keep their names and their existing
parameters; each simply gained an optional trailing `account`. Your existing
`TOKEN_FILE_PATH` keeps working, and now names the default account's token.
What is new: multiple accounts, saved scheduling preferences, and eight new
tools (`find_focus_time`, `block_focus_time`, `detect_conflicts`,
`suggest_reschedule`, `time_audit`, `list_accounts`, `get_preferences`,
`set_preferences`).
## Upgrading from 0.x
- **Package and command renamed.** The distribution is now `calendar-mcp-server` and installs `calendar-mcp` (and an identical `calendar-mcp-server` alias). Point your client at `uvx calendar-mcp-server` instead of `python /path/to/run_server.py`.
- **`run_server.py` still works** — it is now a thin shim over the CLI — but it is deprecated and will be removed in a future release.
- **The FastAPI/uvicorn HTTP API is gone.** There are no REST endpoints, no `/health`, and no separate stdio bridge process; the server is one process on the MCP SDK. If you want HTTP, it is now MCP streamable HTTP at `/mcp`.
- **Authentication no longer happens implicitly.** Run `calendar-mcp auth` once; the server will not open a browser unless `CALENDAR_MCP_ALLOW_BROWSER_AUTH=1`.
- **Tool names are unchanged**, so existing prompts keep working. Results are now structured output rather than JSON stuffed into text.
- **Three new tools:** `move_event`, `respond_to_event`, and `project_recurring_events` (the last previously existed only as internal logic).
- Removed env vars: `RELOAD`, `MCP_API_HOST`. `HOST`/`PORT` now apply to the MCP HTTP transport.
## License
This project is dual-licensed to support both open-source collaboration and sustainable development:
1. **GNU Affero General Public License v3.0 (AGPL-3.0):**
* This software is free to use, modify, and distribute under the terms of the AGPLv3 license.
* Key conditions include that derivative works (including modifications used over a network) must also be licensed under AGPLv3 and their source code made available.
* This license is suitable for open-source projects or internal use where AGPLv3 compliance is feasible.
* See the [LICENSE](LICENSE) file for the full text.
2. **Commercial License:**
* If the terms of the AGPLv3 are not suitable for your specific use case (e.g., integrating this software into a proprietary, closed-source commercial product or service without complying with AGPLv3's source-sharing requirements), a separate commercial license is available.
* Please contact **deciduusleaf@gmail.com** for inquiries regarding commercial licensing options.
By using, modifying, or distributing this software, you agree to be bound by the terms of either the AGPLv3 or a separately negotiated commercial license.
<!-- MCP Registry ownership marker; do not remove -->
mcp-name: io.github.deciduus/calendar-mcp
TDQS
Scored across 23 tools
Most tools are clearly distinct, and the descriptions carefully separate pairs like quick_add_event vs create_event and update_event vs move_event. A few scheduling/availability tools (find_focus_time, block_focus_time, schedule_mutual, suggest_reschedule) are dense and could be confused, but the read/write and propose/book roles are clearly explained.
The majority follow a clean snake_case verb-noun pattern such as list_calendars, create_event, and delete_event. Minor deviations like time_audit, respond_to_event, quick_add_event, and schedule_mutual break the pattern slightly but do not make the set unpredictable.
23 tools falls in the heavy 16-25 range, which feels like more surface than an agent needs to navigate comfortably. The calendar domain is broad enough to justify most tools, but several scheduling and analytics tools could potentially be consolidated.
Event lifecycle coverage is strong: create, search, update, move, delete, attendees, RSVP, and recurring-event projection are all present. Minor gaps include no calendar update/delete, no attendee removal, and no single-event getter, but these are workable around with existing tools.