Skip to main content
Glama
foghorn

CalCaddy

by foghorn

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 Authorization header.

  • Feed URLs stay secret. They never appear in tool output, errors, or logs.

Tools

Tool

What it does

list_calendars

Ids, descriptions, and fetch status of every configured calendar, plus the current date/time and timezone. The agent's starting point.

get_events

Every event in a time window (default: the next 7 days, up to a year), sorted, with recurring events expanded into occurrences.

search_events

Keyword search over title, description, and location (every word must match). Defaults to 30 days back through a year ahead.

find_free_time

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 check
Config 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 404

check 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

auth_token

required

Bearer token clients must send. At least 16 characters. The CALCADDY_AUTH_TOKEN env var overrides it.

timezone

UTC

IANA zone used for all output and for times given without an offset.

cache_ttl_seconds

600

How long a fetched feed is reused. 0 fetches on every call.

fetch_timeout_seconds

20

Per-feed HTTP timeout.

max_feed_bytes

20000000

Feeds larger than this are refused.

max_description_chars

2000

Longer event descriptions are truncated.

host / port / path

0.0.0.0 / 8000 / /mcp

Listen address and endpoint inside the container.

calendars[].id

required

What the agent uses to pick a calendar: lowercase letters, digits, -, _.

calendars[].description

""

Tells the agent what the calendar is for. Write it for the agent: "Dentist, school, and family events" beats "Cal 2".

calendars[].url

required

https://, http://, or webcal:// (fetched over https).

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)
  1. BearerAuthMiddleware checks Authorization: Bearer … with a constant-time compare. Only GET /health skips the check.

  2. The MCP server runs stateless streamable HTTP. No sessions are kept, so restarting the container never strands a client.

  3. 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, FeedCache fetches 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.

  4. recurring-ical-events expands 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.yaml as 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 down httpx'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 Docker

License

MIT. See LICENSE.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    B
    maintenance
    Calendar 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.
    54
    252 npm
    Apache 2.0
  • A
    license
    Not graded
    quality
    C
    maintenance
    Provides 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
  • A
    license
    Not graded
    quality
    C
    maintenance
    Turns 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