MCP server
hypercal can serve the Model Context Protocol at /mcp, so an AI assistant can answer "what's on Thursday", find a slot that suits three people, and put the meeting in the calendar. It speaks streamable HTTP, authenticates with the same app-specific passwords CalDAV clients use, and is scoped to one account.
Off by default. An admin switches it on in Settings → Admin → AI agent access; until then /mcp answers 503. The setting is read per request, so it takes effect at once, with no restart.
Authentication. Authorization: Bearer <app password>. A read password gets only the read-only tools, and they are the only ones its client is even shown. Revoke from Settings → Account like any other app password.
Scoping. Every call runs as the credential's owner. The endpoint reaches the event, calendar and free-busy routes and nothing else: an agent cannot mint another app password, change the account password, read the encryption bundle, or touch admin settings.
Switching it on
Settings → Admin → AI agent access (MCP). Only an admin sees that section (the first registered account, plus anyone since made one). There is nothing to configure in the environment and nothing to restart.
Switching it back off is immediate too, including for a client that is already connected. While it is off the endpoint answers 503 without looking at the credential at all, so a disabled instance is not somewhere to test app passwords.
One thing to know before you do
Turning this on lets every existing app password on the instance reach /mcp, including ones minted long ago for a phone. The credential and its read/read-write scope are unchanged, but the surface it can reach is not. Each person still only ever reaches their own calendar. On an instance with other people on it, say so, or have everyone re-mint their passwords afterwards.
Connecting a client
Mint a password first: Settings → Account → CalDAV and agent access → New app password. Choose Read only if the agent should never change anything. The token is shown once.
For Claude Code:
claude mcp add --transport http hypercal https://calendar.example.com/mcp \
--header "Authorization: Bearer YOUR-APP-PASSWORD"For a client configured by file, the equivalent is an HTTP server entry:
{
"mcpServers": {
"hypercal": {
"type": "http",
"url": "https://calendar.example.com/mcp",
"headers": { "Authorization": "Bearer YOUR-APP-PASSWORD" }
}
}
}Against a local development server the URL is http://localhost:3001/mcp. A fresh instance answers 503 there until an admin switches the endpoint on under Settings, Admin.
To check the endpoint without a client at all, the reference inspector works:
npx @modelcontextprotocol/inspectorPoint it at the URL, set the same Authorization header, and list the tools.
Tools
Times are ISO 8601 with an explicit offset (2025-03-03T09:00:00+01:00). All-day events use plain dates (2025-03-03) instead, with endDate being the last day, inclusive; the endpoint works out the local midnights the storage format wants, so an agent cannot get that subtly wrong.
Reading
| Tool | Notes |
|---|---|
list_calendars | Reports encrypted, readOnly and the caller's access role on each. |
list_events | A time range, with recurring series already expanded and per-occurrence edits applied. Maximum 62 days. |
search_events | Text over titles, descriptions and locations, with paging. |
get_event | One stored event by id. For a series this is the master, with its rrule. |
find_free_slots | Gaps of at least durationMinutes, optionally across several people and confined to working hours. Maximum 31 days. Other people must be discoverable; an account that has opted out of discovery resolves to "No such user". |
check_conflicts | What already overlaps a proposed time. |
Writing
Only with a read-write password.
| Tool | Notes |
|---|---|
create_event | Timed or all-day, optionally recurring. |
update_event | Changes only the fields passed. On a series, changes the whole series. |
delete_event | To the trash by default; permanent, or following to end a series early. |
restore_event | Back out of the trash. |
override_occurrence | Edit or cancel a single date of a series. |
split_series | "This and all following events". |
Recurrence
rrule is an RFC 5545 rule body with no DTSTART, e.g. FREQ=WEEKLY;BYDAY=TU;COUNT=10. A malformed rule is refused with a message that names the part at fault. list_events returns each occurrence with a masterId and its occurrenceStart, which is the occurrence's original start and stays its identity even after an override moves it.
The three ways to change a series are worth keeping straight:
| Want | Tool |
|---|---|
| Just this date | override_occurrence |
| This date and everything after | split_series |
| The whole series | update_event |
Encrypted calendars
A calendar marked encrypted holds content encrypted in the browser, and the server has no key for it (see Security model). Over MCP that means:
- titles, descriptions and locations come back as
null, withencrypted: true, rather than as a page of base64; - those events never match
search_events, because matching needs a blind index the agent cannot compute; - their times are still visible, so they do count towards
find_free_slotsandcheck_conflicts, which is usually what you want: an agent can schedule around a private appointment without learning what it is; create_eventrefuses to write into one.
If you want an assistant to work with a calendar's contents, keep that calendar unencrypted.
Limits and refusals
- The endpoint is unavailable on an instance whose admin has not switched it on, and on the Android standalone build, which has no HTTP listener.
list_eventsis capped at 62 days per call andfind_free_slotsat 31;search_eventspages instead.- Requests are limited to 120 a minute per client address, and repeated failed credentials back off on the escalating delay the sign-in routes use.
- Nothing prevents a double booking; that is what
check_conflictsis for. find_free_slotscounts only the account's own events, not calendars shared to it, matching what the app's own find-a-time overlay shows.- Read-only calendars (subscription feeds) reject writes.
What is deliberately not here
OAuth. The MCP specification describes an OAuth 2.1 authorization flow with dynamic client registration for remote servers. hypercal does not implement it. This is a self-hosted app whose credential model is already a revocable, high-entropy, per-client token with a scope, and a static bearer token is that model spelled the way MCP clients can already send it. Adding an authorization server would be a large amount of new code and a new thing to get wrong, for no capability this endpoint lacks.
stdio. There is no local bridge process to install. The endpoint is remote and every current client can reach it over HTTP.
Resources and prompts. Tools only, for now.
Tasks, attendees, sharing and attachments. Reachable over the REST API and over CalDAV, but not exposed as tools yet.
Android. The standalone app runs the same backend in a Web Worker with no HTTP listener, so nothing can connect to it. Point an agent at a server deployment instead.