Skip to main content
Glama
coretracker

CalDAV MCP Server

by coretracker
README.md
# Calendar Server

This repo runs a real CalDAV server with Docker Compose and connects MCP clients through an existing CalDAV MCP server.

Stack:

- CalDAV: Radicale in Docker using `ghcr.io/kozea/radicale:3.7.6`
- MCP HTTP: `dav-mcp` in Docker, built from `mcp/Dockerfile`
- Public access: Cloudflare Tunnel / Zero Trust

## Requirements

- Docker Desktop or Docker Engine with Compose
- Node.js 20 or newer for `npx` MCP usage
- `cloudflared` for Cloudflare Tunnel

## Configure

```sh
cp .env.example .env
```

Edit `.env`:

```sh
CALDAV_PORT=5232
CALDAV_BASE_URL=http://127.0.0.1:5232
CALDAV_PUBLIC_URL=https://calendar.example.com
CALDAV_USER=calendar
CALDAV_PASSWORD=use-a-long-random-password
MCP_PORT=3000
MCP_BEARER_TOKEN=use-a-long-random-token
```

`CALDAV_USER` and `CALDAV_PASSWORD` are written into Radicale's htpasswd file when the container starts. Restart the container after changing them.

## Start Radicale

```sh
docker compose up -d
```

If you want to use Apple Calendar locally, export and trust Caddy's local HTTPS CA:

```sh
npm run export:caddy-ca
npm run trust:caddy-ca
```

Create the initial calendar collection:

```sh
npm run create:default-calendar
```

Logs:

```sh
docker compose logs -f radicale
```

Stop:

```sh
docker compose down
```

Local CalDAV server:

```text
http://127.0.0.1:5232/
```

Local HTTPS CalDAV proxy for Apple Calendar:

```text
https://localhost:8443/
```

Default calendar URL:

```text
http://127.0.0.1:5232/<CALDAV_USER>/default/
```

## Apple Calendar

Use **Other CalDAV Account** with account type **Advanced**:

- Account type: `Advanced`
- User name: value of `CALDAV_USER`
- Password: value of `CALDAV_PASSWORD`
- Server address: `127.0.0.1`
- Server path: `/<CALDAV_USER>/`
- Port: value of `CALDAV_PORT`, usually `5232`
- Use SSL: off
- Use Kerberos: off

For your current `.env` values:

```text
User name: coretracker
Server address: localhost
Server path: /coretracker/
Port: 8443
Use SSL: on
```

The calendar itself is at `/<CALDAV_USER>/default/`, but Apple Calendar should be pointed at the user collection `/<CALDAV_USER>/` so it can discover the calendar home. Do not put `https://...` into Server Address in Advanced mode; use the host only.

If Apple logs `anonymous user` over plain HTTP, use the HTTPS Caddy endpoint above. Recent macOS builds may refuse to send Basic Auth credentials over plain HTTP even to localhost.

Before retrying Apple Calendar, verify Basic Auth from the terminal:

```sh
npm run check:auth
```

If Radicale logs only say `anonymous user`, Apple Calendar has not sent the username/password yet. Remove the failed account, delete saved Calendar/CalDAV passwords for the same host from Keychain Access, then add the account again with the fields above.

After Cloudflare is configured, use:

```text
https://calendar.example.com/
```

Some Apple Calendar versions do not work well behind browser-based Cloudflare Access. Test local CalDAV first. If local works but the Cloudflare URL fails, use a Cloudflare Access service token or bypass Access for trusted client traffic.

## HTTP MCP Setup

The Docker Compose stack runs an HTTP MCP server at:

```text
http://127.0.0.1:<MCP_PORT>/mcp
```

With the current `.env`, that is:

```text
http://127.0.0.1:9996/mcp
```

Use bearer authentication:

```text
Authorization: Bearer <MCP_BEARER_TOKEN>
```

Generate a local MCP client config from `.env`:

```sh
npm run write:mcp-config
```

That writes `mcp.local.json`, which is ignored by git because it contains the bearer token.

Smoke test the HTTP MCP endpoint:

```sh
npm run smoke:mcp
```

The MCP container talks to Radicale internally at `http://radicale:5232`, so it does not need the local Caddy HTTPS certificate.

Logs:

```sh
npm run logs:mcp
```

Expected MCP tools include calendar, event, contact, and todo operations from `dav-mcp`.

## Cloudflare Zero Trust Setup

Create a tunnel:

```sh
cloudflared tunnel login
cloudflared tunnel create calendar
```

Create `~/.cloudflared/config.yml`:

```yaml
tunnel: <TUNNEL_ID>
credentials-file: /Users/<you>/.cloudflared/<TUNNEL_ID>.json

ingress:
  - hostname: calendar.example.com
    service: http://127.0.0.1:5232
  - service: http_status:404
```

Route DNS:

```sh
cloudflared tunnel route dns calendar calendar.example.com
```

Run the tunnel:

```sh
cloudflared tunnel run calendar
```

In Cloudflare Zero Trust:

1. Go to **Access** -> **Applications** -> **Add an application**.
2. Choose **Self-hosted**.
3. Set the application domain to `calendar.example.com`.
4. Add an access policy for your user or device.
5. Save the application.

CalDAV clients still use Radicale Basic Auth with `CALDAV_USER` and `CALDAV_PASSWORD`.

## Notes

- Calendar data is stored in `radicale/data/`.
- The Radicale config is in `radicale/config/config`.
- The Docker Compose file binds Radicale only to `127.0.0.1`, so it is not exposed directly to the LAN.
- The htpasswd file uses plain text inside the local Docker volume because Radicale runs behind localhost binding and Cloudflare Access. Use filesystem permissions and full-disk encryption on the host.