Skip to main content
Glama
TheBananaStand

leo-google-calendar-mcp

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.

Related MCP server: Google-Calendar Universal MCP

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

npm install
node test.js        # no network needed

Related MCP Connectors

Related MCP Servers