Skip to main content
Glama

caldav-mcp

English | 简体中文

A self-hosted, read-only MCP server for Lark / Feishu calendars. Ask an MCP client to list meetings or retrieve event details without passing large iCalendar payloads into the conversation.

The server exposes two tools over Streamable HTTP, checks calendar freshness on every request, and supports Docker deployment behind Cloudflare Tunnel. It has been tested against Lark CalDAV; compatibility with other CalDAV providers is not guaranteed. This is an independent project, not an official Lark or Feishu product.

Features

  • Compact event lists with organizer, attendee count, meeting link, and your RSVP when identifiable.

  • Full descriptions, attendees, and recurrence rules available through a separate detail tool.

  • Local expansion of recurring events, exceptions, rescheduled instances, all-day events, and time zones.

  • In-memory caches validated by ctag, with explicitly marked stale responses during upstream outages.

  • Mandatory access-key validation through a Bearer header or a URL key for clients without custom headers.

Related MCP server: ical-mcp

Requirements

  • Python 3.12 or later and uv.

  • A Lark / Feishu CalDAV username and dedicated CalDAV password.

  • Docker Compose and a Cloudflare Tunnel token for the container deployment below.

The service selects one calendar. Dates and output timestamps use Asia/Shanghai. It does not create, edit, or delete calendar events.

Quick start

gh repo clone jokerlin/caldav-mcp
cd caldav-mcp
cp .env.example .env
uv sync --frozen --python 3.12
openssl rand -hex 32

Edit .env. Set CALDAV_USERNAME, CALDAV_PASSWORD, and MCP_BEARER_TOKEN (use the generated value). Keep these values in the local file.

Start a local server:

uv run uvicorn caldav_mcp.server:create_app --factory --host 127.0.0.1 --port 8080 --no-access-log

The endpoint is http://127.0.0.1:8080/mcp. The caldav-mcp console command binds to 0.0.0.0 for container networking; the command above explicitly binds to loopback.

Configuration

Variable

Required

Default / meaning

CALDAV_URL

No

https://caldav.larkoffice.com

CALDAV_USERNAME

Yes

CalDAV account username

CALDAV_PASSWORD

Yes

Dedicated CalDAV password

MCP_BEARER_TOKEN

Yes

Shared access key generated with openssl rand -hex 32

CALDAV_CALENDAR_ID

No

Empty: discover the account's single calendar

SELF_CN

No

Empty: use the calendar display name for RSVP identification

MCP_PORT

No

8080 for the console command and Compose

CLOUDFLARE_TUNNEL_TOKEN

For Compose

Credential for the dedicated tunnel

MCP_URL

For verification scripts

Set explicitly to the local or public /mcp endpoint

The Cloudflare management fields in .env.example are optional deployment bookkeeping. The running MCP container does not receive the Cloudflare API token.

Deploy with Docker and Cloudflare Tunnel

  1. Create a dedicated Cloudflare Tunnel and put its token in CLOUDFLARE_TUNNEL_TOKEN.

  2. Route a hostname such as calendar.example.com to http://mcp:8080. If you change MCP_PORT, update the tunnel's origin port as well.

  3. Start the containers:

docker compose up -d --build
docker compose ps

Compose exposes the host port only on 127.0.0.1. The tunnel reaches mcp over the Docker network. Both containers restart unless explicitly stopped. The host and Docker must stay running; caches are cleared when the service restarts.

See deployment notes for authentication and verification details. All domains and identities in public examples are placeholders, not a shared calendar service.

Connect an MCP client

For clients with custom headers, configure:

URL: https://calendar.example.com/mcp
Authorization: Bearer <MCP_BEARER_TOKEN>

For clients without a Bearer field, including the ChatGPT connection flow tested with this project, choose None / No Authentication in the client and use:

https://calendar.example.com/mcp?key=<MCP_BEARER_TOKEN>

The server still validates the URL key. Missing or invalid keys return HTTP 401. An explicit Authorization header takes precedence. Treat a URL containing the key as a credential: do not share it or include it in screenshots, issues, or logs. The service does not implement OAuth, and Cloudflare credentials are not MCP access keys.

After connecting, enable the plugin in a conversation and ask it to list your meetings, then retrieve one meeting's details.

Tools

Tool

Arguments

Result

list_events

start, end, optional keyword, optional include_cancelled

Sorted, expanded event instances without attendee lists or descriptions

get_event

instance_id from a list result

Event fields plus full description, attendees, RRULE, creation and modification times

Example tool arguments:

{"start":"2026-09-14","end":"2026-09-21","keyword":"review"}

start and end are dates in YYYY-MM-DD format. The range is [start, end) and must span 1 to 90 days. Keywords match summary or location, ignoring case. Cancelled events are excluded by default.

An instance ID combines the UID and its actual start time, for example example-event@2026-09-15T11:00:00+08:00. Detail lookup works with a cold cache and reports a missing instance if that start time no longer exists.

For all-day events, the displayed end is midnight on the final included day, converted from iCalendar's exclusive end date. Floating times are interpreted in Asia/Shanghai. my_response is null when the account's attendee cannot be identified reliably.

Every request checks ctag before reusing cached results. Upstream outages can return available cached data with stale: true; authentication failures remain errors. Invalid event components are counted in skipped.

Development and verification

uv sync --frozen
uv run pytest -q
uv run ruff check --config pyproject.toml src tests scripts
uv run ruff format --config pyproject.toml --check src tests scripts

Synthetic tests run without credentials. The optional private-fixture test is skipped in a fresh clone. To capture your own fixtures, configure .env and run:

uv run python scripts/capture_fixtures.py 2026-09-08 2026-09-15

Captures and identity metadata stay in the ignored tests/fixtures/ directory. Do not attach real captures to bug reports.

With the server running:

MCP_URL=http://127.0.0.1:8080/mcp scripts/smoke.sh
MCP_URL=https://calendar.example.com/mcp scripts/smoke.sh --auth query
MCP_URL=https://calendar.example.com/mcp uv run python scripts/benchmark.py --samples 20

The benchmark checks an unchanged ctag and measures local and public requests separately. Warm-cache targets are median ≤300 ms / P95 ≤500 ms locally and median ≤1 s / P95 ≤2 s publicly, under normal network conditions. These exclude model reasoning time. See the measured performance report for the sample size and limitations.

Contributing

Bug reports and pull requests are welcome. Use GitHub Issues with reproducible steps and synthetic calendar data. Run the checks above before submitting a change. Never include account credentials, access-key URLs, colleague names, or private calendar content.

The design document describes the protocol constraints and module boundaries.

License

MIT, copyright 2026 jokerlin.

Related MCP Connectors

Related MCP Servers