leo-google-calendar-mcp
README.md
# leo-google-calendar-mcp
Google Calendar as a Leo **calendar provider**, over MCP. Fills the `calendar`
provider role; Leo's own calendar screen, agenda and search read what it syncs.
## The contract
| Tool | Arguments | Returns |
|---|---|---|
| `sync_user` | `{user_id, access_token}` | `{calendars: [{external_calendar_id, calendar_name?, color?, events: [...]}]}` |
**It touches no database and owns no OAuth app.** The hub writes
`calendar_events` from what this returns, and mints an access token per call
from the Google account you already connected — so moving this package out of
Leo's binary costs you no re-authorization.
**Calendars, not a flat event list.** The hub sweeps events that vanished
upstream, and that sweep is scoped to one calendar: a pass that read two
calendars and failed on the third must not have the third's events taken as
deleted. The shape carries that scope.
## What the tests pin
The Google API's errors are loud. A misread event is not — it lands as a real
row on a real calendar, on the wrong days.
- **Google's all-day end date is *exclusive*; Leo's is inclusive.** A one-day
event on the 9th arrives as `start 03-09, end 03-10`. Stored verbatim, every
all-day event is a day too long and a single-day one renders across two.
Month, year and leap-day boundaries are all checked.
- **Three kinds of event are skipped**, each for a non-obvious reason. A
`cancelled` event *is* listed by Google, and the hub's sweep only removes what
a pass did **not** list — so keeping it would leave a deleted event alive
forever. An instance of a series (`recurringEventId`) arrives beside its
master; this hub stores one master carrying an RRULE and expands on read, so
importing instances writes duplicates on top of that expansion. And an event
with no usable start/end cannot be placed at all.
- **`singleEvents=false`** for the same reason — one master with its rule, not
hundreds of expanded rows the reader expands again.
- **Absent optionals stay absent.** The hub takes `Option<&str>`; `Some("")`
would overwrite a description the user had with an empty one.
## Development
```bash
npm install
node test.js # no network needed
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues