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.

  1. Point an MCP client at https://www.keeper.sh/mcp.
  2. The first request comes back 401 with a WWW-Authenticate header, and the client follows it into the OAuth flow.
  3. Sign in and approve the consent screen. The client stores the tokens it gets back.
  4. 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:

ScopeCovers
keeper.readRequired by every tool call. Without it the server answers 403.
keeper.events.readReading events and invitations.
keeper.sources.readReading the calendars events are copied from.
keeper.destinations.readReading the calendars events are copied into.
keeper.mappings.readReading which calendar is copied into which.
keeper.sync-status.readReading 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.

ToolWhat it doesArguments
list_calendarsLists every calendar connected to Keeper.sh with its provider and the account it belongs to.No arguments
list_accountsLists connected calendar accounts and the provider each one signs in with.No arguments
get_ical_feedReturns 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.

ToolWhat it doesArguments
get_eventsReturns the events in a date range, with their calendar, title, description, location, and times.from, to, timezone
get_eventReturns one event by the ID that get_events reported.eventId
get_event_countReturns 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_invitesReturns 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.

ToolWhat it doesArguments
create_eventCreates 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_eventUpdates an existing event. Only the fields you send are changed.eventId, plus any fields to change
delete_eventDeletes an event by ID.eventId
rsvp_eventResponds to an invitation with accepted, declined, or tentative.eventId, rsvpStatus

Scheduling and sync

Finding an open slot, and controlling when syncing happens.

ToolWhat it doesArguments
find_free_timeFinds 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_syncForces a sync now instead of waiting for the next pass. Throttled to one request per minute per user.No arguments
pause_syncPauses 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_..."
EndpointDescription
GET /api/v1/calendarsList 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}/invitesList invitations on a calendar that have not been responded to, within a date range.
GET /api/v1/accountsList connected accounts and how many calendars each one has.
GET /api/v1/eventsList events in a date range. Accepts calendarId, availability, and isAllDay filters, and count=true to return only a count.
POST /api/v1/eventsCreate 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-timeFind free slots of at least durationMinutes in a date range. Requires timezone, and accepts working-hours options.
POST /api/v1/syncTrigger a sync immediately. Throttled to one request per minute per user.
GET /api/v1/icalGet 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.

Can Keeper.sh use cookies for analytics?