Skip to main content
Glama
mihirkavi

ICS Calendar MCP

by mihirkavi

ICS Calendar MCP

Let an AI assistant check when you're free, without showing it what's on your calendar.

ICS Calendar MCP is a small, read-only Model Context Protocol server. It reads one or more private ICS subscription feeds (for example, published iCloud or Google calendars) and answers two questions: when am I busy? and when am I free? Event titles, notes, attendees, locations, and the feed URLs themselves never leave the server. The assistant only ever sees time ranges.

It runs as a single Cloudflare Worker, protected by OAuth 2.1 with GitHub sign-in and a one-account allowlist, so you can connect it to ChatGPT, Claude, or any MCP client that supports remote servers with OAuth.

Status: v0.1, an early read-only foundation. It works end to end, but it hasn't been battle-tested across many calendar providers. See Limitations.

What it does

Three MCP tools, all read-only:

Tool

What it returns

list_calendars

Configured calendar IDs, display names, and timezones, plus the current time. Never feed URLs or secrets.

get_free_busy

Merged busy intervals (start, end, busy/tentative) across the selected calendars for a range of up to 93 days, with per-calendar coverage.

find_free_slots

Free windows of at least N minutes inside daily working hours (default 09:00–17:00 on weekdays), with an optional buffer around meetings.

Example get_free_busy result (abridged):

{
  "timezone": "America/Los_Angeles",
  "busy": [
    { "start": "2026-10-14T09:00:00-07:00", "end": "2026-10-14T10:30:00-07:00", "status": "busy", "calendarIds": ["work", "family"] }
  ],
  "coverage": [
    { "calendarId": "work", "status": "fresh", "fetchedAt": "2026-10-14T05:00:00.000Z" },
    { "calendarId": "family", "status": "fresh", "fetchedAt": "2026-10-14T05:00:00.000Z" }
  ],
  "complete": true
}

If any calendar can't be read, its coverage is unavailable and complete is false. The server never treats an unreadable calendar as an empty one.

Calendar handling

  • Timed, all-day, multi-day, and overnight events

  • Recurrence (RRULE, RDATE, EXDATE), modified and cancelled occurrences, and RANGE=THISANDFUTURE

  • STATUS:CANCELLED and TRANSP:TRANSPARENT events are skipped. STATUS:TENTATIVE events are reported as tentative.

  • IANA timezones, embedded VTIMEZONE definitions, floating times (interpreted in the calendar's default timezone), and DST transitions

  • Overlapping events from any number of calendars are merged

Related MCP server: calendar-mcp

Security and privacy model

  • No event content. The ICS adapter reads only start, end, duration, recurrence, status, and transparency. It projects each occurrence into a bare time interval before anything else sees it. Tests confirm that titles, descriptions, locations, organizers, attendees, UIDs, and raw ICS never appear in tool output or errors.

  • Callers can't choose URLs. Feeds come only from an operator-defined registry and Worker secrets. Each fetch requires HTTPS and an exact hostname match. It rejects embedded credentials, custom ports, and redirects, times out after 10 seconds, and stops reading past 5 MiB.

  • Bounded work. Query ranges are capped at 93 days. Each calendar may emit at most 10,000 busy occurrences per request and generate at most 100,000 recurrence candidates while expanding.

  • Authenticated access. Every MCP request needs an OAuth access token with the calendar:read scope. Tokens are issued only after GitHub sign-in, and only to the GitHub account whose immutable numeric ID matches ALLOWED_GITHUB_USER_ID. A username is never enough. The MCP handler re-checks that ID and the scope on every request.

  • Safe OAuth flow. It uses PKCE (S256), exact redirect-URI validation, and a consent page that can't be framed and is bound to the browser with cookies (CSRF protection). The GitHub state is single-use and browser-bound, and the GitHub access token is discarded after the identity check. OAuth state lives in Cloudflare KV, with tokens stored only as hashes.

  • Redacted errors. Public errors carry a stable code and a generic message. URLs, query strings, credentials, and calendar text are scrubbed.

Calendar subscription URLs are bearer secrets: anyone with the link can read the full calendar. Keep them in Worker secrets, and regenerate them in your calendar app if they're ever exposed.

Architecture

MCP client ──OAuth 2.1──▶ Cloudflare Worker
                           ├─ OAuth provider (/authorize, /token, /register, discovery)
                           │    └─ GitHub sign-in + numeric-ID allowlist
                           └─ /mcp (stateless Streamable HTTP)
                                └─ CalendarService
                                     ├─ registry (IDs → secret bindings)
                                     ├─ fetch policy (HTTPS, exact host, limits)
                                     ├─ ICS adapter (ICAL.js → detail-free intervals)
                                     └─ interval merge + free-slot search

Path

Responsibility

src/index.ts

Worker entry: OAuth provider wiring and the protected /mcp handler

src/auth/

Consent page, GitHub sign-in, numeric-ID allowlist

src/mcp/

Tool schemas, registration, and text summaries

src/calendars/

Registry, service, ICS adapter, interval math, timezone helpers

src/security/

Feed fetch policy and error redaction

The server keeps no event data. Each request fetches the feeds live.

Requirements

  • Node.js 22 or newer (24 recommended; see .nvmrc)

  • A Cloudflare account (the Workers free plan is enough to try it)

  • A GitHub account and a GitHub OAuth App for sign-in

Local development

npm install
npm run check   # TypeScript
npm run lint    # ESLint
npm test        # Vitest inside the Workers runtime (Miniflare); no network access

To run the Worker locally, copy .dev.vars.example to .dev.vars, fill in your values, and run npm run dev. The OAuth consent cookies use the __Host- prefix, which needs HTTPS. Use wrangler dev --local-protocol https or test the full sign-in flow against a deployed Worker.

Configuring calendars

Without configuration, the server uses one synthetic example calendar (example-calendar, read from the CALENDAR_EXAMPLE_ICS_URL secret, host calendar.example.com). To use your own calendars, set the CALENDAR_REGISTRY secret to a JSON array:

[
  {
    "id": "work",
    "name": "Work",
    "enabled": true,
    "timezone": "America/New_York",
    "secretBinding": "CALENDAR_1_ICS_URL",
    "allowedHostname": "calendar.example.com"
  }
]
  • id: lowercase letters, digits, and hyphens. This is what MCP clients pass as calendarIds.

  • timezone: an IANA zone, used for all-day and floating events and as the default output zone.

  • secretBinding: the name of a separate Worker secret that holds the feed URL.

  • allowedHostname: the exact host the feed URL must use.

Feed URLs must be https://. If your calendar app gives you a webcal:// link, replace webcal with https when you store it.

Deploying to Cloudflare

  1. Create a KV namespace for OAuth state:

    npx wrangler kv namespace create OAUTH_KV

    Put its ID in wrangler.jsonc. If you don't want the ID in Git, copy the file to wrangler.local.jsonc (ignored by Git), edit the copy, and pass -c wrangler.local.jsonc to the Wrangler commands below. npm run deploy:local does this for you.

  2. Create a GitHub OAuth App. Set the homepage URL to your Worker origin and the authorization callback URL to https://<your-worker-host>/callback.

  3. Set the secrets (each command prompts for the value):

    npx wrangler secret put GITHUB_CLIENT_ID
    npx wrangler secret put GITHUB_CLIENT_SECRET
    npx wrangler secret put ALLOWED_GITHUB_USER_ID   # numeric: gh api user --jq .id
    npx wrangler secret put CALENDAR_REGISTRY        # JSON array, see above
    npx wrangler secret put CALENDAR_1_ICS_URL       # one per registry entry
  4. Deploy:

    npm run deploy

    Each secret put asks for its value interactively, so secrets never land in your shell history or in files.

The MCP endpoint is https://<your-worker-host>/mcp. OAuth discovery is published at /.well-known/oauth-protected-resource/mcp and /.well-known/oauth-authorization-server, and clients can register dynamically at /register.

Connecting a client

  • ChatGPT: add a custom MCP server (connector) with the /mcp URL and OAuth authentication, then sign in with GitHub when prompted. Availability of custom connectors, and of connectors in mobile and Voice modes, depends on your ChatGPT plan, workspace settings, and OpenAI's current rollout.

  • MCP Inspector: run npx @modelcontextprotocol/inspector, choose Streamable HTTP, enter the /mcp URL, and complete the OAuth flow.

  • Other clients: any client that supports remote Streamable HTTP servers with OAuth 2.1 discovery and dynamic client registration should work.

Example prompts: "What does my calendar look like tomorrow?", "Find me a free hour on Thursday afternoon", "Am I free Friday between 2 and 4?"

Limitations

  • Read-only. There's no way to create, edit, or delete events, and no CalDAV.

  • One allowed GitHub account per deployment. Multi-user access isn't supported.

  • Feeds are fetched live on every request. A slow or offline provider makes that calendar unavailable (reported, never silently empty). There's no cache.

  • ICS feeds published by calendar apps can lag behind the app by several minutes, depending on the provider.

  • Working-hour windows can't cross midnight, and find_free_slots returns at most 100 windows.

  • A calendar that needs more than 100,000 recurrence candidates to reach the end of the query (for example, a minute-level rule running for years), or that has more than 10,000 busy occurrences in the range, is reported as unavailable for that request.

  • Timezone names without a matching VTIMEZONE must be IANA names. Windows-style names such as Pacific Standard Time are only understood when the feed includes their definition.

Roadmap

These are ideas, not commitments, and none of them are implemented:

  • Optional short-lived caching with explicit stale coverage

  • Per-calendar working hours

  • CalDAV as an alternative source

Contributing and security

See CONTRIBUTING.md. Please report vulnerabilities privately as described in SECURITY.md, not in public issues.

License

MIT © 2026 Mihir Kavishwar

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Provides AI assistants with intelligent access to Google Calendar data, enabling natural language queries about availability, upcoming events, schedule conflicts, and meeting summaries through context-aware calendar integration.
    -
  • 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
    Enables AI assistants to read and manage a user's Google Calendar, including listing calendars, searching, creating, updating, and deleting events, and checking free/busy availability via OAuth 2.0.
    109 npm
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables AI assistants to read, create, update, move, and delete calendar events, list calendars, and query free/busy across multiple independent Google accounts through a single interface, with each account routed explicitly and gated by role-based access control. It also includes a built-in personal time-management assistant and stores each account's OAuth tokens in a local encrypted vault.
    25
    109 npm
    1
    MIT