MCP Server & REST API
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. 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 with at least one calendar connected. 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.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 and triggering a sync. |
Write tools are covered by the same grant: the token is passed straight through to the REST API, which authorizes it as your account. Revoking access in the dashboard cuts the client off from both.
Tools
The server exposes 14 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 |
get_ical_feed | Returns the URL of the shareable calendar link, for subscribing to it from another calendar app. | No arguments |
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 |
Worked examples
What a session actually looks like. The prompts are yours; the calls are what the assistant makes.
“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")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. |
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, 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 answer 429 with a Retry-After header rather than queueing a second run.
Connect your calendars to your assistant
Create an account, connect a calendar, and point your MCP client at Keeper.sh. Free to use, no credit card required.