ICS Calendar MCP
Reads private Google Calendar ICS subscription feeds to compute merged busy intervals and available free slots without exposing event titles or calendar contents.
Reads private iCloud Calendar ICS subscription feeds to compute merged busy intervals and available free slots without exposing event titles or calendar contents.
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., "@ICS Calendar MCPWhen am I free for a 30-minute call tomorrow?"
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.
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 |
| Configured calendar IDs, display names, and timezones, plus the current time. Never feed URLs or secrets. |
| Merged busy intervals ( |
| 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, andRANGE=THISANDFUTURESTATUS:CANCELLEDandTRANSP:TRANSPARENTevents are skipped.STATUS:TENTATIVEevents are reported astentative.IANA timezones, embedded
VTIMEZONEdefinitions, floating times (interpreted in the calendar's default timezone), and DST transitionsOverlapping 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:readscope. Tokens are issued only after GitHub sign-in, and only to the GitHub account whose immutable numeric ID matchesALLOWED_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
stateis 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 searchPath | Responsibility |
| Worker entry: OAuth provider wiring and the protected |
| Consent page, GitHub sign-in, numeric-ID allowlist |
| Tool schemas, registration, and text summaries |
| Registry, service, ICS adapter, interval math, timezone helpers |
| 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 accessTo 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 ascalendarIds.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
Create a KV namespace for OAuth state:
npx wrangler kv namespace create OAUTH_KVPut its ID in
wrangler.jsonc. If you don't want the ID in Git, copy the file towrangler.local.jsonc(ignored by Git), edit the copy, and pass-c wrangler.local.jsoncto the Wrangler commands below.npm run deploy:localdoes this for you.Create a GitHub OAuth App. Set the homepage URL to your Worker origin and the authorization callback URL to
https://<your-worker-host>/callback.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 entryDeploy:
npm run deployEach
secret putasks 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
/mcpURL 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/mcpURL, 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_slotsreturns 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
unavailablefor that request.Timezone names without a matching
VTIMEZONEmust be IANA names. Windows-style names such asPacific Standard Timeare 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
stalecoveragePer-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
This server cannot be deployed
Maintenance
Related MCP Connectors
GDPR-compliant calendar access for AI assistants: read, create, edit, RSVP. Google, MS 365, Apple.
Merged free/busy, find mutual time, propose bookings with human approval. Never event contents.
Hosted Google Calendar MCP server for AI agents. No self-hosting or Google Cloud setup.
Calendar API for AI agents: events, availability, Google/Microsoft setup, scheduling, and iCal.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceProvides 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.-
- 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 gradedqualityCmaintenanceEnables 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 npmMIT
- AlicenseAqualityBmaintenanceEnables 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.25109 npm1MIT