Skip to main content
Glama
jbeker

calendar-mcp

README.md
# calendar-mcp

An [MCP](https://modelcontextprotocol.io) server that exposes **read-only**
calendar data from iCal (`.ics`) feed URLs to an MCP-compatible agent.

You point it at one or more iCal URLs. It fetches them on a regular interval,
expands recurring events, caches everything in memory, and offers tools an agent
can call to answer questions like *"what's on my calendar this week?"*, *"am I free
Thursday afternoon?"*, or *"find events mentioning 'dentist'"*.

It never writes to any calendar — iCal feeds are pull-only sources.

## Install

Requires Python ≥ 3.11 and [`uv`](https://docs.astral.sh/uv/).

```bash
uv sync
```

## Configure

Copy the example config and edit it:

```bash
cp calendars.example.toml calendars.toml
```

```toml
refresh_interval_minutes = 15          # how often to re-fetch (min 5)
default_timezone = "America/New_York"  # applied to floating times & agenda math
max_results = 200                      # cap on events returned per tool call

[[calendars]]
name = "Work"
url  = "https://example.com/work.ics"

[[calendars]]
name = "Family"
url  = "https://calendar.google.com/.../basic.ics"
```

> **Feed URLs are secrets.** Google/Apple/Outlook "private address" URLs embed an
> access token. The server never logs or exposes the URL — only the friendly
> `name` appears in output. `calendars.toml` is git-ignored; keep it private.

## Run

```bash
uv run calendar-mcp calendars.toml
```

The server speaks the MCP **stdio** protocol, so it's normally launched by your MCP
client rather than by hand. Register it like this:

```json
{
  "mcpServers": {
    "calendar": {
      "command": "uv",
      "args": ["run", "calendar-mcp", "/absolute/path/to/calendars.toml"]
    }
  }
}
```

## Tools

| Tool | What it does |
| --- | --- |
| `list_calendars` | Each feed's refresh state, event count, last success (no URLs). |
| `list_events` | Events in a window (recurrences expanded), sorted by start. |
| `get_event` | Full detail (description, attendees) for one event UID. |
| `search_events` | Case-insensitive match on summary/description/location. |
| `free_busy` | Merged busy intervals and the free gaps between them. |
| `agenda` | Convenience: `today` \| `tomorrow` \| `week` \| `next`. |
| `refresh` | Force an immediate re-fetch of all feeds. |

Times are ISO-8601. If you omit a window, a sensible default is used. Results are
capped at `max_results` and flagged `truncated` when trimmed, so large calendars
don't overrun the agent's context.

## Design notes

- **Recurrence** (`RRULE`/`EXDATE`/`RECURRENCE-ID` overrides) is expanded by
  [`recurring-ical-events`](https://pypi.org/project/recurring-ical-events/).
- **Timezones**: UTC, `TZID`, and floating times are all normalized to
  timezone-aware ISO-8601; floating times get `default_timezone`.
- **Resilience**: a feed that fails to fetch or parse keeps its last-known-good
  data and records the error in its status — it never wipes the cache or crashes
  the refresh loop.
- **HTTP efficiency**: `ETag`/`Last-Modified` are stored and sent as conditional
  requests, so an unchanged feed returns `304` and skips re-parsing.

## Develop

```bash
uv run pytest        # unit tests + fixtures
```

## Not in scope (v1)

Writing/creating events, tasks (`VTODO`) and reminders (`VALARM`), an HTTP/daemon
transport, and on-disk cache persistence across restarts.

## License

Copyright (C) 2026 Jeremy Beker.

This program is free software: you can redistribute it and/or modify it under
the terms of the [GNU Affero General Public License](LICENSE) as published by
the Free Software Foundation, either version 3 of the License, or (at your
option) any later version.

It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU Affero General Public License for more details.