diff --git a/CLAUDE.md b/CLAUDE.md index adb3196..71cace5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,69 +1,89 @@ # mcp-home -Self-hosted MCP (Model Context Protocol) servers for Claude. Provides access to personal data services via remote HTTP endpoints with OAuth client_credentials auth. +Self-hosted MCP (Model Context Protocol) servers for Claude. Provides access to personal data services via remote HTTP endpoints with OAuth (Authorization Code + PKCE, and client_credentials). ## Architecture -5 independent MCP servers, each on its own port, behind nginx reverse proxy at `mcp.home.slohmaier.de`: +5 independent MCP servers, each on its own subdomain + port: -- **Mail** (Port 5100) — searches IMAP backup Maildirs -- **Calendar** (Port 5101) — CalDAV against Radicale (events + tasks/reminders) -- **Contacts** (Port 5102) — CardDAV against Radicale -- **Files** (Port 5103) — WebDAV against oCIS -- **Notes** (Port 5104) — Joplin REST API +- **Mail** (`mail.mcp.home.slohmaier.de`, Port 5100) — searches IMAP backup Maildirs, reads attachments, creates drafts +- **Calendar** (`calendar.mcp.home.slohmaier.de`, Port 5101) — CalDAV against Radicale (events + tasks/reminders, travel time, geocoding, alarms) +- **Contacts** (`contacts.mcp.home.slohmaier.de`, Port 5102) — CardDAV against Radicale (contacts + photos) +- **Files** (`files.mcp.home.slohmaier.de`, Port 5103) — WebDAV against oCIS (full CRUD, images inline, documents as blobs) +- **Notes** (`notes.mcp.home.slohmaier.de`, Port 5104) — Joplin Data API (notes + attachments) -All servers share `common.py` for OAuth token handling and user resolution. +Each subdomain is its own nginx vhost proxying to the local port. All servers share `common.py` for OAuth + user resolution. ## Auth Flow -OAuth `client_credentials` grant — no browser redirect needed: +claude.ai uses OAuth 2.0 **Authorization Code + PKCE**. client_credentials is also supported (used by tests). -1. Client POSTs to `//token` with `client_id` + `client_secret` -2. Server returns `access_token` (valid 24h) -3. Client sends `Authorization: Bearer ` with MCP requests -4. User is identified by `client_id` (matches `tokens.json`) +1. claude.ai discovers `/.well-known/oauth-authorization-server` +2. `GET /authorize` → auto-approves valid client_id, redirects with code +3. `POST /token` (authorization_code + code_verifier, optional client_secret) → access_token +4. `Authorization: Bearer ` on MCP requests -OAuth metadata at `//.well-known/oauth-authorization-server`. +Access tokens are stored in a shared file `.active_tokens.json` so all 5 services validate tokens issued by any service. client_secret is verified at token exchange when provided. ## User Separation -`tokens.json` (not in git, chmod 600) maps client_id → user. Each user only sees their own data. Shared CalDAV calendars are visible to both users. +`tokens.json` (not in git, chmod 600) maps client_id (`stefan`, `kati`, `test`) → secret. `USER_ALIASES` in `common.py` maps `test` → `stefan`. Each user sees only their own data; shared CalDAV calendars are visible to both. + +## Config + +- `tokens.json` — OAuth client_id/secret per user (gitignored) +- `config.json` — all backend credentials (Radicale, oCIS, Joplin, IMAP), gitignored. See `config.json.example`. +- `.active_tokens.json` — runtime OAuth tokens (gitignored) + +## Backends + +- **Mail**: reads Maildirs under `/mnt/ssd/Backup/{stefan,kati}/imap/`. Drafts via IMAP APPEND (1blu / Gmail). +- **Calendar/Contacts**: Radicale CalDAV/CardDAV on `127.0.0.1:5232`. Note: Radicale uses default `DAV:` namespace (no `d:` prefix) and `CR:` for CardDAV — regex must match both. +- **Files**: oCIS WebDAV on `127.0.0.1:9200/remote.php/dav/files//`. oCIS username may differ from MCP user (mapping in config). +- **Notes**: Joplin **Data API** via joplin-cli sync clients on `127.0.0.1:41184` (stefan) / `41185` (kati). See `/opt/joplin-mcp/README.md`. NOT the Joplin Server sync API (that has no clean REST/search). ## Files -- `common.py` — OAuth endpoints, Bearer middleware, user resolution via contextvars -- `tokens.json` — client_id/secret per user (generate with `python3 -c "import secrets; print(secrets.token_urlsafe(48))"`) -- `mail/server.py` — Maildir reader, search across subject/from/to/body -- `calendar/server.py` — CalDAV REPORT queries, event/task CRUD, vobject serialization -- `contacts/server.py` — CardDAV addressbook-query, vCard parsing, contact CRUD -- `files/server.py` — WebDAV PROPFIND/GET, recursive search, text file reading -- `notes/server.py` — Joplin REST API wrapper (needs per-user API token in code) +- `common.py` — OAuth (authorize/token/metadata), Bearer middleware, user resolution (contextvars), config loader +- `mail/server.py` — Maildir reader; tools: list_accounts, list_folders, search_mail, read_mail (lists attachments), read_attachment, create_draft +- `calendar/server.py` — CalDAV REPORT + vobject; events/tasks CRUD, travel time, geocoding, reminders +- `contacts/server.py` — CardDAV; search/get/create, contact photos +- `files/server.py` — WebDAV; list/read/write/delete/move/search, images + documents +- `notes/server.py` — Joplin Data API; list/search/read/create notes, list_note_resources, read_resource +- `tests/test_all.py` — 54 integration tests (pytest), daily via `mcp-tests.timer` ## Dependencies ``` -pip install mcp[cli] httpx vobject python-dateutil +pip install mcp[cli] httpx vobject python-dateutil pytest ``` -Python 3.12+, venv at `./venv/`. +Python 3.12+, venv at `./venv/`. joplin-cli (npm global) for Notes backend. ## Deployment -Each server runs as systemd unit (`mcp-.service`), managed by uvicorn. nginx at `mcp.home.slohmaier.de` proxies `//` to the right port. TLS via Certbot. +Each server is a systemd unit `mcp-.service` (uvicorn). Each subdomain is an nginx vhost with rate-limited `/authorize` + `/token`. TLS via Certbot (shared cert on mail.mcp...). Joplin: `joplin-cli-{stefan,kati}.service` + `joplin-sync.timer` (15 min). + +## Content Types (returning binary) + +- Text → `TextContent` +- Images (jpg/png/gif/webp) → `ImageContent` (base64 inline) +- Documents (PDF, docx, xlsx) / other binary → `EmbeddedResource` with `BlobResourceContents` (base64 + mimeType) — claude.ai processes these client-side ## Adding a New Service -1. Create `/server.py` with `FastMCP` instance + `create_app()` using the same pattern -2. Import `get_current_user`, `OAUTH_ROUTES`, `BearerAuthMiddleware` from `common.py` -3. Add nginx location block for `//` -4. Create systemd unit `mcp-.service` -5. Update `tokens.json` if the new service needs different user mapping +1. `/server.py` with `FastMCP` instance + `create_app()` (copy mail/server.py pattern) +2. Import `get_current_user`, `OAUTH_ROUTES`, `BearerAuthMiddleware`, `load_config` from `common.py` +3. nginx vhost `.mcp.home.slohmaier.de` + Certbot +4. systemd unit `mcp-.service` +5. Backend creds in `config.json` +6. Tests in `tests/test_all.py`, update dashboard + this file ## Tool Design Guidelines -- Every tool docstring is the first thing Claude sees — make it actionable ("Call this first", "Use the UID from search results") -- Use `Annotated[str, Field(description="...")]` with example values for every parameter -- Indicate which parameters are optional ("Leave empty for all") -- Reference other tools in descriptions ("Use get_contact with the UID for full details") -- Return structured text, not JSON — Claude parses text faster -- Include UIDs/keys in output so Claude can chain to detail tools without asking the user +- Docstring is the first thing Claude sees — make it actionable ("Call this first", "Use the UID from search results") +- `Annotated[str, Field(description="...")]` with example values for every parameter +- Mark optional params ("Leave empty for all") +- Reference other tools ("Use read_attachment with the index from read_mail") +- Return structured text, not JSON +- Include IDs/keys in output so Claude can chain to detail tools diff --git a/README.md b/README.md index 6095186..42eba77 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,43 @@ # mcp-home -Self-hosted [MCP](https://modelcontextprotocol.io) servers for Claude. Gives Claude access to your email, calendar, contacts, files, and notes — all running on your own hardware. +Self-hosted [MCP](https://modelcontextprotocol.io) servers for Claude. Gives Claude access to your email, calendar, contacts, files, and notes — all running on your own hardware, behind OAuth. -## Services +## Services & Tools -- **Mail** — search and read emails from IMAP backups (Maildir format) -- **Calendar + Tasks** — read/write events and reminders via CalDAV (Radicale) -- **Contacts** — search/read/write contacts via CardDAV (Radicale) -- **Files** — browse and read files via WebDAV (oCIS) -- **Notes** — search/read/write notes via Joplin API +- **Mail** — `list_accounts`, `list_folders`, `search_mail` (full text), `read_mail` (lists attachments), `read_attachment` (image/PDF/text), `create_draft` +- **Calendar + Tasks** — `list_calendars`, `get_events`, `search_events`, `list_task_lists`, `get_tasks`, `create_event` (travel time + geocoding + reminders), `create_task` +- **Contacts** — `search_contacts`, `get_contact` (with photo), `create_contact`, `set_contact_photo` +- **Files** — `list_files`, `read_file` (image/document/text), `file_info`, `search_files`, `write_file`, `create_folder`, `delete_file`, `move_file` +- **Notes** — `list_notebooks`, `list_notes`, `search_notes`, `read_note`, `create_note`, `list_note_resources`, `read_resource` (attachments) ## Setup ```bash python3 -m venv venv -venv/bin/pip install mcp[cli] httpx vobject python-dateutil +venv/bin/pip install mcp[cli] httpx vobject python-dateutil pytest +cp tokens.json.example tokens.json # set OAuth client secrets +cp config.json.example config.json # set backend credentials ``` -Copy `tokens.json.example` to `tokens.json` and set client secrets. +Notes needs joplin-cli sync clients exposing the Data API (see `/opt/joplin-mcp/`). ## Usage with claude.ai -Add as Custom MCP Server in claude.ai Settings → Integrations: +Settings → Integrations → Custom MCP Server: -- **URL**: `https://your-domain/mail/mcp` (or calendar, contacts, files, notes) -- **OAuth Client ID**: your username -- **OAuth Client Secret**: your secret from tokens.json +- **URL**: `https://.mcp.your-domain/mcp` (mail, calendar, contacts, files, notes) +- **OAuth Client ID**: your username (e.g. `stefan`) +- **OAuth Client Secret**: from `tokens.json` + +claude.ai runs the OAuth Authorization Code + PKCE flow automatically. + +## Tests + +```bash +venv/bin/python -m pytest tests/test_all.py -v +``` + +54 integration tests (OAuth, all tools, CRUD in isolated test collections). Run daily via `mcp-tests.timer`. ## License diff --git a/config.json.example b/config.json.example index 1b34879..db29436 100644 --- a/config.json.example +++ b/config.json.example @@ -57,5 +57,15 @@ "url": "http://127.0.0.1:41185", "token": "CHANGE_ME" } + }, + "joplin_login": { + "stefan": { + "email": "CHANGE_ME", + "password": "CHANGE_ME" + }, + "kati": { + "email": "CHANGE_ME", + "password": "CHANGE_ME" + } } } \ No newline at end of file