Update docs: CLAUDE.md, README, config example
- CLAUDE.md: subdomains, OAuth Auth Code+PKCE, shared token store, all backends incl Joplin Data API, content types, current tool list - README: full tool inventory per service, correct setup steps - config.json.example: joplin_login + joplin_data_api fields documented Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 `/<service>/token` with `client_id` + `client_secret`
|
||||
2. Server returns `access_token` (valid 24h)
|
||||
3. Client sends `Authorization: Bearer <token>` 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 <token>` on MCP requests
|
||||
|
||||
OAuth metadata at `/<service>/.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/<user>/`. 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-<name>.service`), managed by uvicorn. nginx at `mcp.home.slohmaier.de` proxies `/<service>/` to the right port. TLS via Certbot.
|
||||
Each server is a systemd unit `mcp-<name>.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 `<name>/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 `/<name>/`
|
||||
4. Create systemd unit `mcp-<name>.service`
|
||||
5. Update `tokens.json` if the new service needs different user mapping
|
||||
1. `<name>/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 `<name>.mcp.home.slohmaier.de` + Certbot
|
||||
4. systemd unit `mcp-<name>.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
|
||||
|
||||
@@ -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://<service>.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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user