Add OAuth server implementation and Docker Compose configuration

- Introduced a new `compose.yaml` file to define the Docker service for the OAuth server.
- Created `oauth.py` to implement a single-user OAuth authorization server with PKCE, SQLite persistence, and token management.
- Added necessary database schema for OAuth clients, flows, codes, tokens, and login failures.
- Implemented login routes with CSRF protection and error handling.
- Updated `requirements.txt` to include dependencies for the OAuth server.
This commit is contained in:
2026-10-28 23:08:43 -04:00
commit 91b7f312aa
8 changed files with 1342 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
# WebDAV Notes MCP
A Dockerized MCP server that lets ChatGPT browse, search, and read an Obsidian vault stored on WebDAV. The server is fixed to one vault root (`/Notes/Personal`) and uses HTTPS Basic authentication to access `dav.ryankegel.com` from inside the container.
It uses Streamable HTTP for remote MCP connections. ChatGPT's custom MCP connection supports OAuth authentication; this project includes a small, single-user OAuth login flow so the MCP endpoint does not have to be public and anonymous. The SDK serves the MCP authorization discovery and token routes. See [ChatGPT custom MCP server setup](https://developers.openai.com/api/docs/guides/custom-mcp-server) and the [MCP Python SDK authorization guide](https://py.sdk.modelcontextprotocol.io/run/authorization/).
The local OAuth login is intended for one trusted user and has password protection, CSRF checks, and rate limiting. It does not provide MFA or identity-provider federation. The MCP login password must be at least 16 bytes; use a unique passphrase.
## Tools
Read-only tools are available by default:
- `list_directory(path)` lists the immediate children of a vault folder. Use an empty path for the vault root.
- `read_file(path)` reads a UTF-8 text or Markdown file and returns its ETag. Files are limited to 256 KiB.
- `search_files(query, path, limit)` searches file and folder paths recursively.
- `search_notes(query, path, limit)` searches text in Markdown and `.txt` files recursively.
When `MCP_READ_ONLY=false`, the server also exposes:
- `write_file(path, content)` creates a new note. It will not replace an existing file by default.
- `write_file(path, content, overwrite=true, expected_etag=...)` replaces a note only when its current ETag matches the value returned by `read_file`.
- `create_directory(path)` creates one folder; its parent must already exist.
There is no delete or move tool. Read and search calls are capped to keep large vaults and responses bounded. Recursive searches visit at most 500 directories and inspect at most 1,500 text files.
## Deploy
Use a dedicated public hostname such as `mcp.ryankegel.com` and terminate HTTPS at your existing reverse proxy. Docker binds the app to `127.0.0.1` by default, so port 8000 is not directly exposed to the internet.
1. Copy `.env.example` to `.env`. Set `MCP_PUBLIC_URL`, `MCP_ALLOWED_HOSTS`, `WEBDAV_USERNAME`, and `MCP_AUTH_USERNAME`. The public URL must be the HTTPS origin only, without `/mcp`.
For example:
```dotenv
MCP_PUBLIC_URL=https://mcp.ryankegel.com
MCP_ALLOWED_HOSTS=mcp.ryankegel.com,mcp.ryankegel.com:*
WEBDAV_USERNAME=your-webdav-username
MCP_AUTH_USERNAME=ryan
```
2. Create the Docker secret files. Keep both passwords out of `.env` and source control. On the server:
```sh
mkdir -p secrets
chmod 700 secrets
read -rsp 'WebDAV password: ' DAV_SECRET
printf '%s' "$DAV_SECRET" > secrets/webdav_password
unset DAV_SECRET
printf '\n'
read -rsp 'New MCP login password: ' MCP_SECRET
printf '%s' "$MCP_SECRET" > secrets/mcp_auth_password
unset MCP_SECRET
printf '\n'
# Compose file-backed secrets keep the host file's group and permissions.
# The container runs as uid/gid 10001, so give that group read access.
sudo chgrp 10001 secrets/webdav_password secrets/mcp_auth_password
chmod 640 secrets/webdav_password secrets/mcp_auth_password
```
Docker Compose mounts file-backed secrets without remapping their ownership and mode, so the group-readable permission above lets the non-root container user read them while keeping the files private from other users on the server. See the [Compose secrets reference](https://docs.docker.com/reference/compose-file/services/#secrets).
The MCP login password is separate from your WebDAV password. ChatGPT users enter the MCP login credentials on the OAuth consent page. The WebDAV credential remains server-side.
3. Start the container:
```sh
docker compose up -d --build
docker compose logs -f dav-mcp
```
4. Point your reverse proxy to `127.0.0.1:8000`, preserving the incoming `Host` header. Example Caddy site:
```caddyfile
mcp.ryankegel.com {
reverse_proxy 127.0.0.1:8000
}
```
The MCP endpoint is `https://mcp.ryankegel.com/mcp`. The OAuth discovery and login endpoints share that hostname. The MCP allowed-host setting also prevents requests to unexpected hostnames.
5. In ChatGPT on the web, add a custom MCP server with that `/mcp` URL and choose OAuth authentication. The server supports Dynamic Client Registration (DCR). On the first authorization, sign in with `MCP_AUTH_USERNAME` and the password stored in `secrets/mcp_auth_password`, then approve the requested vault access.
## Enable writes
Keep `MCP_READ_ONLY=true` until you want write tools. To enable them:
1. Confirm the WebDAV account has only the permissions you want ChatGPT to have. Restrict it to the notes vault if your WebDAV provider supports that.
2. Set `MCP_READ_ONLY=false` in `.env` and restart with `docker compose up -d --build`.
3. Refresh or recreate the MCP connection in ChatGPT so it discovers the new tools, then reconnect and grant the write scope.
The write tools still require an OAuth token carrying `notes:write`. Replacing an existing file also requires `overwrite=true` and the exact ETag returned by `read_file`; this reduces the chance of replacing a newer edit.
## Configuration
| Variable | Default | Purpose |
| --- | --- | --- |
| `WEBDAV_URL` | `https://dav.ryankegel.com` | WebDAV origin or base path |
| `WEBDAV_ROOT` | `/Notes/Personal` | The only subtree exposed to MCP tools |
| `WEBDAV_USERNAME` | — | WebDAV username |
| `WEBDAV_PASSWORD_FILE` | Docker secret | Path to WebDAV password file |
| `MCP_PUBLIC_URL` | — | Public HTTPS origin serving the MCP endpoint and OAuth login |
| `MCP_ALLOWED_HOSTS` | — | Comma-separated Host values allowed by MCP transport security |
| `MCP_ALLOWED_ORIGINS` | empty | Optional comma-separated browser origins allowed by MCP transport security |
| `MCP_AUTH_USERNAME` | — | Username for the local OAuth login page |
| `MCP_AUTH_PASSWORD_FILE` | Docker secret | Path to the local OAuth login password file |
| `MCP_READ_ONLY` | `true` | Hides write tools and disallows the write scope when true |
| `MCP_BIND_ADDRESS` | `127.0.0.1` | Host interface for the published Docker port |
| `MCP_HOST_PORT` | `8000` | Host port forwarded to the container |
OAuth clients, authorization codes, and refresh tokens are stored in the `oauth-data` Docker volume. The volume contains credentials and should be backed up and protected like other application data.
## ChatGPT availability
Custom MCP servers are connected from ChatGPT on the web. OpenAI's current guide says Pro accounts can use custom servers for read/fetch, while full MCP support including write actions is available on Business, Enterprise, and Edu. Workspace settings also control who can add them. Check [OpenAI's current connection guide](https://help.openai.com/en/articles/12584461-connecting-custom-mcp-servers-in-chatgpt) for current account requirements.