CalCaddy
Provides read-only access to Google Calendar via its secret iCal feed, allowing agents to list calendars, retrieve and search events, and find free time.
Provides read-only access to iCloud Calendar via its public iCal/webcal feed, allowing agents to list calendars, retrieve and search events, and find free time.
Provides read-only access to Proton Calendar via its iCal feed, allowing agents to list calendars, retrieve and search events, and find free time.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@CalCaddyfind a free hour tomorrow afternoon across my work and personal calendars"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
CalCaddy
A tiny, stateless MCP server that gives AI
agents read-only access to your calendars through their iCal (.ics) feed
links.
Point it at any number of feeds (Google Calendar, Outlook/Microsoft 365,
iCloud, Fastmail, Proton, sports schedules, holiday lists, anything that
publishes .ics). Give each one an id and a short description, and your agent
can list them, read events, search them, and find free time. CalCaddy runs as a
single container whose only configuration is one YAML file.
One config file, nothing else. No database, no volumes, no OAuth dance.
Ephemeral. Feeds are prefetched and kept fresh in memory. The container runs on a read-only filesystem and can be killed and replaced at any time.
Correct recurring events. Expands RRULE/RDATE series and honors EXDATE exceptions and moved instances (RECURRENCE-ID), all in your timezone.
Streamable HTTP + bearer token. Works with any MCP client that can send an
Authorizationheader.Feed URLs stay secret. They never appear in tool output, errors, or logs.
Tools
Tool | What it does |
| Ids, descriptions, and fetch status of every configured calendar, plus the current date/time and timezone. The agent's starting point. |
| Every event in a time window (default: the next 7 days, up to a year), sorted, with recurring events expanded into occurrences. |
| Keyword search over title, description, and location (every word must match). Defaults to 30 days back through a year ahead. |
| Open slots within working hours (default 09:00–17:00, weekdays) that no busy event covers, across any set of calendars. Up to 62 days. |
Every tool takes optional calendar_ids (omit for all calendars). Time
arguments accept 2026-10-20, 2026-10-20T14:00, 2026-10-20T14:00-04:00,
now, today, tomorrow, or yesterday. A bare date as an end bound covers
that whole day, so start=2026-10-20, end=2026-10-20 means all of
October 20.
Every response includes now and timezone, so the agent always knows what
"today" means. A feed that fails to load is reported in an errors list while
the other calendars still answer. If an older cached copy exists, CalCaddy keeps
serving it and says so.
An example event:
{
"calendar_id": "work",
"title": "Acme risk review",
"start": "2026-10-20T14:00:00-04:00",
"end": "2026-10-20T15:00:00-04:00",
"all_day": false,
"busy": true,
"location": "Zoom",
"description": "Quarterly risk assessment walkthrough.",
"organizer": "Jane Doe <jane@example.com>",
"uid": "abc123@google.com"
}All-day events report dates rather than datetimes, and their end is the
last day, inclusive. That's different from raw iCalendar, where the end is
exclusive.
Related MCP server: Chronary
Quick start
git clone https://github.com/foghorn/CalCaddy.git
cd CalCaddy
cp calcaddy.example.yaml calcaddy.yaml
openssl rand -hex 32 # paste this into auth_token
$EDITOR calcaddy.yaml # add your feeds
docker compose up -d --build
curl http://localhost:8000/health # {"status":"ok"}The MCP endpoint is http://<host>:8000/mcp.
To check the config and test-fetch every feed without starting the server:
docker compose run --rm calcaddy checkConfig OK: 3 calendar(s), timezone America/New_York
OK work: 42 event(s) in the next 30 days
OK personal: 9 event(s) in the next 30 days
FAIL us-holidays: feed returned HTTP 404check exits with 0 when everything is fine, 1 when a feed failed, and 2 when
the config is invalid.
Configuration
Everything lives in calcaddy.yaml (see
calcaddy.example.yaml):
auth_token: "output of: openssl rand -hex 32"
timezone: America/New_York
cache_ttl_seconds: 600
calendars:
- id: work
description: "Work calendar: client meetings, calls, and deadlines"
url: "https://outlook.office365.com/owa/calendar/.../calendar.ics"
- id: personal
description: "Personal and family commitments"
url: "webcal://p01-caldav.icloud.com/published/2/..."Key | Default | Meaning |
| required | Bearer token clients must send. At least 16 characters. The |
|
| IANA zone used for all output and for times given without an offset. |
|
| How long a fetched feed is reused. |
|
| Per-feed HTTP timeout. |
|
| Feeds larger than this are refused. |
|
| Longer event descriptions are truncated. |
|
| Listen address and endpoint inside the container. |
| required | What the agent uses to pick a calendar: lowercase letters, digits, |
|
| Tells the agent what the calendar is for. Write it for the agent: "Dentist, school, and family events" beats "Cal 2". |
| required |
|
The config is read once at startup. To change it, edit the file and recreate the
container (docker compose up -d --force-recreate).
Finding your feed URL
Google Calendar: Settings → your calendar → Integrate calendar → Secret address in iCal format.
Outlook / Microsoft 365: Settings → Calendar → Shared calendars → Publish a calendar → pick "Can view all details" → copy the ICS link.
iCloud: Calendar app → share icon next to the calendar → Public Calendar → copy the
webcal://link.
Connecting a client
Any MCP client that supports streamable HTTP with custom headers works.
Claude Code
claude mcp add --transport http calcaddy http://<host>:8000/mcp \
--header "Authorization: Bearer <auth_token>"JSON-configured clients (Claude Desktop via a remote connector, Cursor, and others):
{
"mcpServers": {
"calcaddy": {
"type": "http",
"url": "http://<host>:8000/mcp",
"headers": { "Authorization": "Bearer <auth_token>" }
}
}
}How it works
agent ──HTTP + Bearer──▶ auth middleware ──▶ MCP tools ──▶ in-memory feed cache ──▶ .ics URLs
(/health is open) (TTL, stale fallback)BearerAuthMiddlewarechecksAuthorization: Bearer …with a constant-time compare. OnlyGET /healthskips the check.The MCP server runs stateless streamable HTTP. No sessions are kept, so restarting the container never strands a client.
At startup a background task fetches every feed, then refreshes each one before its copy is older than
cache_ttl_seconds. Large calendars can take several seconds to download, and this keeps that wait off tool calls. If a copy is somehow stale when a tool needs it,FeedCachefetches it inline (with a size cap and timeout), and concurrent calls share that one fetch. If a fetch fails, the last good copy is served with a warning, and the next call retries right away.recurring-ical-eventsexpands the data into concrete occurrences inside the requested window. CalCaddy then converts times to your timezone, drops cancelled events, and returns plain JSON.
Busy vs. free
busy follows the event's TRANSP property when it has one ("Show as: Free"
in most apps sets TRANSPARENT). Without it, timed events count as busy and
all-day events as free. Most all-day entries are birthdays and holidays, and
vacation blocks are usually marked busy explicitly. find_free_time only
treats busy events as conflicts.
Security notes
Treat
calcaddy.yamlas a secret. Most iCal links are bearer credentials: anyone with the URL can read the calendar. The file is gitignored and dockerignored. CalCaddy never echoes the URLs. It also turns downhttpx's request logging, which would otherwise print them.The agent picks ids, never URLs. It can't make CalCaddy fetch arbitrary addresses.
Event text is untrusted input. Anyone who sends you an invite controls its title and description. CalCaddy tells the agent (in the server instructions) to treat that text as data. Still, think about what other tools the same agent holds. An agent that can read your calendar and also send email is an exfiltration path for a crafted invite.
Use TLS beyond your LAN. CalCaddy speaks plain HTTP. Put it behind a reverse proxy with TLS if the token will cross an untrusted network.
The container runs as a non-root user on a read-only root filesystem, and the provided compose file drops every capability.
Development
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
pytest
ruff check .
CALCADDY_CONFIG=./calcaddy.yaml calcaddy serve # run locally without DockerLicense
MIT. See LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Calendar API for AI agents: events, availability, Google/Microsoft setup, scheduling, and iCal.
Hosted Google Calendar MCP server for AI agents. No self-hosting or Google Cloud setup.
Connects ChatGPT to your Apple Calendar via a local Mac agent + Vercel relay
Scheduling infrastructure for AI agents across Google and Microsoft calendars.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceA read-only MCP server that exposes iCalendar feeds as queryable tools for LLM agents, enabling calendar event retrieval and filtering.MIT
- AlicenseAqualityBmaintenanceCalendar API purpose-built for AI agents. Exposes tools to manage agents, calendars, and events, find meeting times, run scheduling proposals, set availability rules, manage webhooks, and subscribe to iCal feeds.54252 npmApache 2.0
- AlicenseNot gradedqualityCmaintenanceProvides read-only access to calendar events from iCal feeds, enabling agents to query schedules, search events, and check availability via natural language.AGPL 3.0
- AlicenseNot gradedqualityCmaintenanceTurns a published ICS calendar feed into ready-to-use JSON tools for AI assistants. Enables querying events by day or date range and inspecting feed diagnostics without exposing secret URLs or returning truncated calendar data.MIT