Documentation for the Keeper.sh MCP server and REST API. Connect an MCP client over OAuth 2.1 to read and write every calendar you have synced, or call the same endpoints with an API token.
What the server does
Keeper.sh syncs events between Google Calendar, Outlook, iCloud, Fastmail, CalDAV servers, and iCal links, as the calendar sync features page describes. The MCP server puts that same merged view in front of an assistant: one connection reads and writes every calendar you have connected, rather than one integration per provider.
It speaks the Model Context Protocol over streamable HTTP at https://www.keeper.sh/mcp, and every tool is a thin wrapper over the public REST API described further down. Writes go through to the provider that owns the calendar, so an event an assistant creates appears in Google Calendar or Outlook exactly like one you made by hand, and Keeper.sh keeps syncing it from there.
Self-hosted instances serve the same endpoint at /mcp on your own domain. Everything on this page applies to them, with your instance URL in place of www.keeper.sh.
Quickstart
You need a Keeper.sh account (create one free) with at least one calendar connected. Client-by-client steps for Claude are in how to give Claude access to your calendar. There is no key to copy: the client registers itself and you approve it in the browser.
- Point an MCP client at
https://www.keeper.sh/mcp. - The first request comes back
401with aWWW-Authenticateheader, and the client follows it into the OAuth flow. - Sign in and approve the consent screen. The client stores the tokens it gets back.
- Ask it something like "what is on my calendar tomorrow?". It should call
get_events.
Configuration for a client that takes a JSON server list:
{
"mcpServers": {
"keeper": {
"type": "url",
"url": "https://www.keeper.sh/mcp"
}
}
}
Authentication with OAuth 2.1
The MCP server is an OAuth 2.1 protected resource. It never sees your password, and there is nothing to paste into a client.
An unauthenticated request is answered 401 with WWW-Authenticate: Bearer resource_metadata=…, which points at the protected resource metadata document. That document names the authorization server, and the client discovers the rest from there.
curl https://www.keeper.sh/.well-known/oauth-protected-resource
{
"resource": "https://www.keeper.sh/mcp",
"authorization_servers": ["https://www.keeper.sh/api/auth"],
"scopes_supported": ["keeper.read", "keeper.events.read", "..."]
}
Clients register themselves through dynamic client registration, so you do not create an OAuth app first. Authorization uses the authorization code flow with PKCE, and the consent screen is hosted by Keeper.sh at /oauth/consent. Access tokens last 30 days and refresh tokens 90 days; a client that asks for offline_access can refresh without sending you back through consent.
Every tool call requires the keeper.read scope. A token without it is answered 403 with error="insufficient_scope". The scopes a client may request are:
| Scope | Covers |
|---|---|
keeper.read | Required by every tool call. Without it the server answers 403. |
keeper.write | Creating, editing, deleting and responding to events, triggering or pausing a sync, and changing syncs, calendar settings, calendar links and shareable calendar links. |
keeper.events.read | Reading events and invitations. |
keeper.sources.read | Reading the calendars events are copied from. |
keeper.destinations.read | Reading the calendars events are copied into. |
keeper.mappings.read | Reading which calendar is copied into which. |
keeper.sync-status.read | Reading sync state. |
Write tools are covered by the same grant: the token is passed straight through to the REST API, which authorizes it as your account. Removing the Keeper.sh connector in the client cuts it off from both.
Tools
The server exposes 27 tools. Date ranges are ISO 8601 datetimes, and read tools that take a timezone return times already localized to it, so an assistant does not have to convert anything itself.
Calendars and accounts
What the user has connected. Most sessions start here, because every other tool takes a calendar ID.
| Tool | What it does | Arguments |
|---|---|---|
list_calendars | Lists every calendar connected to Keeper.sh with its provider and the account it belongs to. | No arguments |
list_accounts | Lists connected calendar accounts and the provider each one signs in with. | No arguments |
refresh_calendars | Re-discovers the calendars on one account, or on every account: imports calendars added at the provider and flags ones that are gone. Refreshing every account is throttled to one request per minute. | Optional accountId |
rename_calendar | Renames a calendar in Keeper.sh. The name at the provider stays as it is. | calendarId, name |
Reading events
Reads across every synced calendar at once. Times come back localized to the timezone you pass in.
| Tool | What it does | Arguments |
|---|---|---|
get_events | Returns the events in a date range, with their calendar, title, description, location, and times. | from, to, timezone |
get_event | Returns one event by the ID that get_events reported. | eventId |
get_event_count | Returns only how many events fall in a range, so a busy week can be measured without listing it. | from and to, both optional |
get_pending_invites | Returns invitations on a calendar that have not been responded to yet. | calendarId, from, to, timezone |
Writing events
Writes land on the provider the calendar belongs to, so a created event appears in Google Calendar, Outlook, or a CalDAV server the same as one made by hand.
| Tool | What it does | Arguments |
|---|---|---|
create_event | Creates an event on a connected calendar. Pass a timezone so CalDAV providers show it in local time. | calendarId, title, startTime, endTime, and optional description, location, isAllDay, availability, timezone |
update_event | Updates an existing event. Only the fields you send are changed. | eventId, plus any fields to change |
delete_event | Deletes an event by ID. | eventId |
rsvp_event | Responds to an invitation with accepted, declined, or tentative. | eventId, rsvpStatus |
Scheduling and sync
Finding an open slot, and controlling when syncing happens.
| Tool | What it does | Arguments |
|---|---|---|
find_free_time | Finds open slots of at least a given length across every synced calendar. Events marked free or working-elsewhere never block; busy, out-of-office, and all-day events do. | from, to, timezone, durationMinutes, and optional workingHoursStart, workingHoursEnd, workingDays, calendarId, ignoreAllDayEvents, limit |
trigger_sync | Forces a sync now instead of waiting for the next pass. Throttled to one request per minute per user. | No arguments |
pause_sync | Pauses or resumes one calendar without disconnecting it. Nothing is read from it or written to it while paused, and its stored events are kept. | calendarId, paused |
Syncs and calendar settings
Which calendar copies into which, and what a copy shows. Changing a sync replaces the whole destination list, so an assistant reads it first.
| Tool | What it does | Arguments |
|---|---|---|
list_syncs | Lists every sync as a source calendar ID and a destination calendar ID. | No arguments |
set_sync_destinations | Sets the calendars a calendar copies its events to, replacing its current list. Copies already made on a removed destination are removed on the next sync. The Free plan allows 3 syncs. | calendarId, destinationCalendarIds |
get_calendar_settings | Returns a calendar's settings, and the calendars it copies to and from. | calendarId |
update_calendar_settings | Changes what a calendar's copies show: the title, which events and details are left out, privacy, reminders and how far back and ahead events are copied. Requires Pro. | calendarId, plus any of customEventName, excludeAllDayEvents, excludeAvailabilities, excludeEventDescription, excludeEventLocation, excludeEventName, excludeFocusTime, excludeOutOfOffice, excludeTitleKeywords, markEventsAsPrivate, disableEventReminders, syncHistoricRange, syncFutureRange, treatFullDayTimedEventsAsAllDay |
Calendars from iCal links
Read-only calendars subscribed from a link. Each link counts as one linked account on the plan.
| Tool | What it does | Arguments |
|---|---|---|
add_calendar_link | Subscribes to a calendar from its iCal link. | url, and optional name |
remove_calendar_link | Removes a calendar added from an iCal link. Calendars from a signed-in account cannot be removed this way. | calendarId |
Shareable calendar links
The iCal feeds Keeper.sh publishes from your calendars, for subscribing from any calendar app. Every account has a default feed, which cannot be deleted. The Free plan allows 1 feed, and feed settings other than the name require Pro.
| Tool | What it does | Arguments |
|---|---|---|
list_ical_feeds | Lists your feeds with their subscription URLs, settings and calendar IDs. | No arguments |
create_ical_feed | Creates a feed with its own URL. It starts with no calendars. | name |
update_ical_feed | Renames a feed or changes what its events show. Only the fields you send are changed. | feedId, plus any of name, includeEventName, includeEventDescription, includeEventLocation, excludeAllDayEvents, excludeFocusTime, excludeOutOfOffice, customEventName |
set_ical_feed_calendars | Sets the calendars a feed publishes, replacing its current list. | feedId, calendarIds |
delete_ical_feed | Deletes a feed. Its URL stops working for every subscriber. | feedId |
Feedback
For the assistant itself, not your calendar. When a tool errors unexpectedly, returns something confusing, lacks a capability the assistant needed, or forces a workaround, the assistant can tell the Keeper.sh team directly. The server's instructions point it here, and unexpected errors end with a reminder.
| Tool | What it does | Arguments |
|---|---|---|
send_feedback | Sends feedback about Keeper.sh's tools and API to the Keeper.sh team and answers with a one-line confirmation. | message (the goal, what was tried and where it got stuck, up to 2,000 characters), category (tool_error, missing_capability, confusing_output, documentation or other), and optional toolName and severity (high, medium or low, default medium) |
Feedback goes to Keeper.sh from every server, self-hosted ones included. It carries the message, category, severity, tool name, the intent given on that call, the server's Keeper.sh version, and whether it is hosted or self-hosted. It never carries your token, user ID, email address, calendar data or your server's hostname.
To opt out, turn off Agent feedback in Settings. Your assistant is then not offered send_feedback, nothing is sent if it calls it anyway, and the server stops mentioning feedback in its instructions and error messages. On a self-hosted server, setting AGENT_FEEDBACK_DISABLED=true on the MCP service turns it off for everyone.
Intent
Every tool also takes an optional intent of up to 300 characters, where the assistant says what it is trying to do. Keeper.sh records it with the tool name in its request logs to learn what assistants are using the tools for, unless you have turned off agent feedback, in which case it is accepted and dropped. It is never passed to the API or echoed back, and it does not change what the tool does.
Worked examples
What a session actually looks like. The prompts are yours; the calls are what the assistant makes. More requests an assistant can carry out are in what an AI agent can do with your calendar.
"What does my Thursday look like across all my calendars?"
One read covers every connected calendar, so the assistant does not need to know which provider an event came from. Each event carries its calendar name and provider back with it.
get_events(from: "2026-09-10T00:00:00Z", to: "2026-09-11T00:00:00Z", timezone: "America/New_York")
"Find 45 minutes next week that works for me and book a design review."
find_free_time reads busy time from every calendar at once, then create_event writes the booking back to the provider that owns the calendar you name.
find_free_time(from: …, to: …, timezone: "America/New_York", durationMinutes: 45, workingHoursStart: "09:00", workingHoursEnd: "17:00", workingDays: "1,2,3,4,5")
list_calendars()
create_event(calendarId: …, title: "Design review", startTime: …, endTime: …, timezone: "America/New_York")
"Decline anything on my work calendar I have not answered yet."
Invitations are listed per calendar, and the RSVP is written back to the provider, so the organizer sees the response.
get_pending_invites(calendarId: …, from: …, to: …, timezone: "America/New_York")
rsvp_event(eventId: …, rsvpStatus: "declined")
"Copy my work calendar into my personal one."
The assistant looks up both calendar IDs, reads the work calendar's current destinations so none are dropped, then writes the new list.
list_calendars()
get_calendar_settings(calendarId: …)
set_sync_destinations(calendarId: …, destinationCalendarIds: [… , …])
REST API
The MCP tools call the same public API you can call yourself, under /api/v1. Anything an assistant can do through MCP you can do with curl.
Create a token from Settings → API Tokens in the dashboard. Tokens are prefixed with kpr_ and shown once, at creation. Pass one as a bearer token. The same routes also accept a logged-in browser session or an MCP OAuth access token, so all three callers reach the same handlers.
curl https://www.keeper.sh/api/v1/calendars \
-H "Authorization: Bearer kpr_..."
| Endpoint | Description |
|---|---|
GET /api/v1/calendars | List connected calendars. Accepts an optional comma-delimited provider filter. |
PATCH /api/v1/calendars/{calendarId} | Pause or resume syncing for a calendar without disconnecting it. |
GET /api/v1/calendars/{calendarId}/invites | List invitations on a calendar that have not been responded to, within a date range. |
GET /api/v1/accounts | List connected accounts and how many calendars each one has. |
GET /api/v1/events | List events in a date range. Accepts calendarId, availability, and isAllDay filters, and count=true to return only a count. |
POST /api/v1/events | Create an event. Requires calendarId, title, startTime, and endTime. |
GET /api/v1/events/{id} | Get a single event. |
PATCH /api/v1/events/{id} | Update an event's fields, or send rsvpStatus to respond to an invitation. |
DELETE /api/v1/events/{id} | Delete an event. |
GET /api/v1/events/free-time | Find free slots of at least durationMinutes in a date range. Requires timezone, and accepts working-hours options. |
POST /api/v1/sync | Trigger a sync immediately. Throttled to one request per minute per user. |
GET /api/v1/ical | Get the URL of your iCal feed. |
GET /api/v1/mappings | List which calendars sync into which, as source and destination calendar ID pairs. |
PUT /api/v1/calendars/{calendarId}/destinations | Replace the calendars a calendar syncs to. Send calendarIds. |
GET /api/v1/calendars/{calendarId}/settings | Get a calendar's settings and the calendars it syncs to and from. |
PATCH /api/v1/calendars/{calendarId}/settings | Rename a calendar with name, or change its settings. Settings other than the name require Pro. |
POST /api/v1/accounts/{id}/refresh | Re-discover the calendars on one account. |
POST /api/v1/accounts/refresh-calendars | Re-discover the calendars on every account. Throttled to one request per minute per user. |
POST /api/v1/ics | Subscribe to a calendar from its iCal link. Send url, and optionally name. |
DELETE /api/v1/ics/{calendarId} | Remove a calendar added from an iCal link. |
GET /api/v1/ical/feeds | List your shareable calendar links with their URLs, settings and calendar IDs. |
POST /api/v1/ical/feeds | Create a shareable calendar link. Send name. |
PATCH /api/v1/ical/feeds/{id} | Rename a shareable calendar link or change its settings. Settings other than the name require Pro. |
PUT /api/v1/ical/feeds/{id}/calendars | Replace the calendars a shareable calendar link publishes. Send calendarIds. |
DELETE /api/v1/ical/feeds/{id} | Delete a shareable calendar link. The default one cannot be deleted. |
Range parameters from and to are ISO 8601 datetimes. If omitted, from defaults to now and to to a week later. A range may not exceed 732 days.
Finding free time
Events marked free or working-elsewhere are treated as non-blocking; everything else, including all-day events, is busy. Pass ignoreAllDayEvents=true to stop all-day events blocking. Working hours are 24-hour local times and workingDays counts 0 as Sunday, both read against timezone, so the hours hold across daylight saving transitions.
curl -G https://www.keeper.sh/api/v1/events/free-time \
-H "Authorization: Bearer kpr_..." \
--data-urlencode "from=2026-09-07T00:00:00Z" \
--data-urlencode "to=2026-09-11T00:00:00Z" \
--data-urlencode "timezone=America/New_York" \
--data-urlencode "durationMinutes=45" \
--data-urlencode "workingHoursStart=09:00" \
--data-urlencode "workingHoursEnd=17:00" \
--data-urlencode "workingDays=1,2,3,4,5"
Creating an event
The calendar ID comes from GET /api/v1/calendars. Sending a timezone keeps CalDAV providers such as iCloud and Fastmail from rendering the event in GMT.
curl -X POST https://www.keeper.sh/api/v1/events \
-H "Authorization: Bearer kpr_..." \
-H "Content-Type: application/json" \
-d '{
"calendarId": "6f1c0c2e-1f5f-4a2f-9a1e-6a1b7f0c9d3e",
"title": "Design review",
"startTime": "2026-09-08T15:00:00Z",
"endTime": "2026-09-08T15:45:00Z",
"timezone": "America/New_York"
}'
Limits
On the Free plan the API and MCP server together are capped at 25 requests per day (plans and limits, pricing), after which requests return 429. Pro is uncapped, and self-hosted instances running without commercial mode are treated as Pro. POST /api/v1/sync and trigger_sync are separately throttled to one request per minute per user, and so is refreshing every account at once. Both answer 429 with a Retry-After header rather than queueing a second run.