io.github.bbik52/docmost-mcp-remote
by bbik52
README.md
# docmost-mcp-remote
A self-hosted remote [MCP](https://modelcontextprotocol.io) server for
[Docmost](https://docmost.com) Community Edition. Run it next to your Docmost,
put it behind your domain, and let AI clients such as Claude, Claude Code,
Cursor or VS Code read and edit the wiki on behalf of each member, with that
member's own Docmost account and rights.
<!-- mcp-name: io.github.bbik52/docmost-mcp-remote -->
- **One server, one Docmost, many members.** Each member signs in with their
own Docmost account in the browser. Everything their AI client does is done
as them in Docmost.
- **Authorization is mandatory.** OAuth 2.1 for remote connectors and personal
bearer tokens for scripts. There is no unauthenticated mode.
- **Safe by default.** `read` is the only scope granted without asking.
Writing and moving to Trash need explicit scopes. Nothing is deleted
permanently, and the server keeps an encrypted snapshot of a page body
before every change.
- **Self-hosted.** One container plus Postgres, with a ready `compose.yaml`.
## Supported Docmost versions
Docmost Community Edition **0.70.0 or newer**. Older versions are refused at
sign-in. CI runs the integration tests against Docmost 0.70.3, 0.96.0 and
0.97.0; newer releases work in most cases and log a warning until they are
added to the tested range. Docmost users who sign in with SSO only (an
Enterprise feature) cannot use this server: it signs in with email and
password.
## Tools
| Tool | Scope | What it does |
|---|---|---|
| `whoami` | read | The Docmost account, scopes and Docmost version of this connection |
| `list_spaces` | read | Spaces you can access, with your role |
| `list_pages` | read | Page tree of a space, one level at a time |
| `recent_pages` | read | Recently updated pages |
| `get_page` | read | One page as Markdown, by id, slug id or Docmost URL |
| `search` | read | Full-text search with snippets |
| `list_comments` | read | Comments and replies on a page |
| `list_trash` | read | Pages in the Trash of a space |
| `list_snapshots` | read | Snapshots this server saved before your changes to a page |
| `create_page` | write | New page with a Markdown body, optionally under a parent |
| `update_page` | write | Append, prepend or replace a page body with Markdown |
| `rename_page` | write | Change a page title |
| `move_page` | write | Move a page within its space, to the start, the end or after a sibling |
| `add_comment` | write | Comment on a page or reply to a comment |
| `restore_snapshot` | write | Put a page body back to a snapshot |
| `trash_page` | delete | Move a page to the Trash |
| `restore_page` | delete | Restore a page from the Trash |
Page bodies, comments and snippets are returned inside a
`<docmost-untrusted-content>` envelope so the model treats them as data.
Before every body change the server saves the current body as a snapshot in
its own database, encrypted, for `SNAPSHOT_RETENTION_DAYS`; if the snapshot
cannot be saved, nothing is written. After the change it reads the page back
and reports an error if Docmost did not apply it (this happens when Docmost
runs with `COLLAB_DISABLE_REDIS`). Snapshots can hold content of pages that
were later deleted in Docmost until they expire.
## Security model
Members sign in with their own Docmost account; everything a client does is
done as that member, with their Docmost rights. The server keeps the Docmost
session token encrypted with a key derived from `SECRET_KEY` and does not
store passwords unless `RELOGIN_MODE=stored_password`. Personal tokens are
stored as hashes and shown once. The sign-in page has CSRF protection and
per-IP and per-email rate limits.
The server is its own OAuth 2.1 authorization server, following the MCP
authorization spec: dynamic client registration (limited per IP, redirect
URIs must be https, loopback http or a native app scheme), authorization
code with PKCE (S256 only), exact redirect URI match, tokens bound to this
server's `/mcp` resource, one-hour access tokens and 30-day refresh tokens
that rotate on every use. A reused refresh token or a replayed code revokes
the whole grant. OAuth grants and personal tokens are listed and revoked on
the same account page.
Page content is treated as untrusted data and labelled as such for the model.
Prompt injection in a page can still make a client act within the scopes the
member granted, so grant `write` and `delete` only to clients you trust.
Tool calls are limited per member and recorded in an audit log. See the
[threat model](https://github.com/bbik52/docmost-mcp-remote/blob/master/docs/security-model.md), [SECURITY.md](https://github.com/bbik52/docmost-mcp-remote/blob/master/SECURITY.md) for
how to report a vulnerability and the [design](https://github.com/bbik52/docmost-mcp-remote/blob/master/docs/DESIGN.md).
## Quick start
You need Docker with Compose, a Docmost reachable from the server, and a host
name with https for the server itself, for example `mcp.example.com`.
1. Download the compose file and the settings template:
```sh
mkdir docmost-mcp && cd docmost-mcp
curl -fsSLO https://raw.githubusercontent.com/bbik52/docmost-mcp-remote/master/compose.yaml
curl -fsSL -o .env https://raw.githubusercontent.com/bbik52/docmost-mcp-remote/master/.env.example
```
2. Edit `.env`: set `PUBLIC_URL` (the https URL of this server),
`DOCMOST_URL`, a long random `POSTGRES_PASSWORD` and `SECRET_KEY`
(`openssl rand -hex 32` gives a good value for both). Consider
`ALLOWED_EMAILS` to limit who can sign in.
3. Start it and check that it is up:
```sh
docker compose up -d
curl http://127.0.0.1:8000/healthz
```
4. Put a reverse proxy with https in front of `127.0.0.1:8000` (see below)
and open `PUBLIC_URL/login` in a browser to check that you can sign in with
your Docmost account.
5. Connect a client (see [Connecting a client](#connecting-a-client)).
The server applies its database migrations at startup. Back up the Postgres
volume like you back up Docmost: it holds the encrypted sessions, the
snapshots and the audit log. Losing `SECRET_KEY` signs every member out and
makes the snapshots unreadable.
<details>
<summary>The <code>compose.yaml</code></summary>
```yaml
# docmost-mcp-remote: the server and its Postgres.
# Copy .env.example to .env and fill in the required values first, then put a
# reverse proxy with https in front of port 8000.
services:
server:
# "1" follows 1.x releases; set IMAGE_TAG to a full version such as 1.0.0 to pin one.
image: ghcr.io/bbik52/docmost-mcp-remote:${IMAGE_TAG:-1}
restart: unless-stopped
environment:
PUBLIC_URL: ${PUBLIC_URL:?set PUBLIC_URL to the public https URL of this server}
DOCMOST_URL: ${DOCMOST_URL:?set DOCMOST_URL to the base URL of your Docmost}
DOCMOST_ALLOW_INSECURE_HTTP: ${DOCMOST_ALLOW_INSECURE_HTTP:-false}
DATABASE_URL: postgresql://docmost_mcp:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/docmost_mcp
SECRET_KEY: ${SECRET_KEY:?set SECRET_KEY to a random value of at least 32 characters}
SECRET_KEY_PREVIOUS: ${SECRET_KEY_PREVIOUS:-}
ALLOWED_EMAILS: ${ALLOWED_EMAILS:-}
ALLOWED_SPACES: ${ALLOWED_SPACES:-}
RELOGIN_MODE: ${RELOGIN_MODE:-ask}
SNAPSHOT_RETENTION_DAYS: ${SNAPSHOT_RETENTION_DAYS:-30}
AUDIT_RETENTION_DAYS: ${AUDIT_RETENTION_DAYS:-90}
TOOL_CALLS_PER_MINUTE: ${TOOL_CALLS_PER_MINUTE:-60}
MAX_PAGE_CHARS: ${MAX_PAGE_CHARS:-60000}
# The port is published on 127.0.0.1 only, so every client is a local proxy.
FORWARDED_ALLOW_IPS: ${FORWARDED_ALLOW_IPS:-*}
LOG_LEVEL: ${LOG_LEVEL:-info}
ports:
- "127.0.0.1:8000:8000"
depends_on:
postgres:
condition: service_healthy
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_USER: docmost_mcp
POSTGRES_DB: docmost_mcp
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U docmost_mcp -d docmost_mcp"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres-data:
```
</details>
The server is also on PyPI (`pip install docmost-mcp-remote` or
`uvx docmost-mcp-remote`) for setups without containers; it needs Postgres
and the same settings as environment variables.
## Reverse proxy
The server listens on plain http and expects a reverse proxy that terminates
https. Serve it at the root of its own host name; a sub-path is not
supported. The proxy must not buffer responses, because MCP streams them.
Caddy (gets certificates by itself):
```caddyfile
mcp.example.com {
reverse_proxy 127.0.0.1:8000
}
```
nginx:
```nginx
server {
listen 443 ssl;
server_name mcp.example.com;
ssl_certificate /etc/letsencrypt/live/mcp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mcp.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 1h;
}
}
```
The server uses `X-Forwarded-For` for its per-IP rate limits. The compose
file publishes the port on 127.0.0.1 only and trusts the header from every
client; if you publish the port elsewhere, set `FORWARDED_ALLOW_IPS` to the
proxy's address.
## Connecting a client
The MCP endpoint is `PUBLIC_URL/mcp`, for example
`https://mcp.example.com/mcp`. Clients that support MCP authorization open a
browser window: sign in with your Docmost email and password and choose what
the client may do. `read` is always granted; tick `write` or `delete` only
for clients you trust.
**Claude.ai and Claude Desktop**: Settings, Connectors, Add custom connector,
then enter the endpoint URL. The server must be reachable from the internet
over https for Claude.ai.
**Claude Code**:
```sh
claude mcp add --transport http docmost https://mcp.example.com/mcp
```
Then run `/mcp` in Claude Code to sign in.
**Cursor** (`~/.cursor/mcp.json`):
```json
{
"mcpServers": {
"docmost": { "url": "https://mcp.example.com/mcp" }
}
}
```
**VS Code** (`.vscode/mcp.json`):
```json
{
"servers": {
"docmost": { "type": "http", "url": "https://mcp.example.com/mcp" }
}
}
```
**With a personal token** (scripts and clients without OAuth):
1. Open `PUBLIC_URL/login` and sign in with your Docmost email and password.
2. On the account page, create a personal token with the scopes you need.
3. Configure the client with the endpoint and the header
`Authorization: Bearer <token>`:
```sh
claude mcp add --transport http docmost https://mcp.example.com/mcp \
--header "Authorization: Bearer dmr_..."
```
Connected apps and tokens are listed and revoked on the account page,
`PUBLIC_URL/account`.
## Administration
The `admin` command runs where the server runs, with the same environment:
```sh
docker compose exec server docmost-mcp-remote admin members
docker compose exec server docmost-mcp-remote admin migrate
docker compose exec server docmost-mcp-remote admin grants anna@example.com
docker compose exec server docmost-mcp-remote admin revoke-grant <grant id>
docker compose exec server docmost-mcp-remote admin revoke-member anna@example.com
docker compose exec server docmost-mcp-remote admin disable-member anna@example.com
docker compose exec server docmost-mcp-remote admin audit --email anna@example.com
```
`revoke-member` revokes every grant of the member and drops their stored
Docmost session; `disable-member` blocks them until `enable-member`. Every
tool call is recorded in the audit log with the tool, the page or space id
and the outcome, never with content or arguments.
To rotate `SECRET_KEY`: set the old value as `SECRET_KEY_PREVIOUS` and a new
`SECRET_KEY`, restart, run `admin rotate-key`, then remove
`SECRET_KEY_PREVIOUS` and restart again.
## Upgrading
1. Read the [changelog](https://github.com/bbik52/docmost-mcp-remote/blob/master/CHANGELOG.md)
for "Upgrade notes".
2. Back up the database:
```sh
docker compose exec -T postgres pg_dump -U docmost_mcp -Fc docmost_mcp > docmost-mcp-$(date +%F).dump
```
3. Pull the new image and restart:
```sh
docker compose pull && docker compose up -d
```
The server upgrades its database schema at startup, in one transaction and
one process at a time, so a failed upgrade leaves the old schema in place.
`docker compose exec server docmost-mcp-remote admin migrate` shows the
schema revision.
The image tag `1` follows 1.x releases; set `IMAGE_TAG` in `.env` to pin a
version. Going back to an older version after its schema was upgraded is not
supported: an older server refuses to start on a newer schema. To roll back,
restore the backup with the old image:
```sh
docker compose stop server
docker compose exec -T postgres pg_restore -U docmost_mcp -d docmost_mcp --clean --if-exists < docmost-mcp-2026-01-31.dump
IMAGE_TAG=1.0.0 docker compose up -d
```
## Configuration
| Variable | Required | Default | Meaning |
|---|---|---|---|
| `PUBLIC_URL` | yes | | Public https URL of this server |
| `DOCMOST_URL` | yes | | Base URL of your Docmost |
| `DOCMOST_ALLOW_INSECURE_HTTP` | no | `false` | Allow an `http://` `DOCMOST_URL` on a trusted private network |
| `DATABASE_URL` | yes | | Postgres URL (`postgresql://user:password@host/db`) |
| `SECRET_KEY` | yes | | At least 32 characters; encrypts stored Docmost sessions and signs cookies |
| `SECRET_KEY_PREVIOUS` | no | | The old key during a rotation |
| `ALLOWED_EMAILS` | no | empty | Comma-separated emails allowed to sign in; empty allows every Docmost user |
| `ALLOWED_SPACES` | no | empty | Comma-separated space slugs or ids members may use through this server; empty allows all |
| `MAX_PAGE_CHARS` | no | `60000` | Longest page body returned; longer bodies are truncated |
| `SNAPSHOT_RETENTION_DAYS` | no | `30` | Days a snapshot of a page body is kept |
| `AUDIT_RETENTION_DAYS` | no | `90` | Days an audit log entry is kept |
| `TOOL_CALLS_PER_MINUTE` | no | `60` | Tool calls one member may make per minute, over all their clients |
| `RELOGIN_MODE` | no | `ask` | `ask`: members sign in again when their Docmost session expires; `stored_password`: the password is kept encrypted and used to sign in again |
| `DOCMOST_TIMEOUT_SECONDS` | no | `15` | Timeout for one request to Docmost |
| `DOCMOST_MAX_RESPONSE_BYTES` | no | `5242880` | Largest Docmost response the server reads |
| `FORWARDED_ALLOW_IPS` | no | `127.0.0.1` | Proxies trusted for `X-Forwarded-For` (`*` in the compose file) |
| `HOST` / `PORT` | no | `0.0.0.0` / `8000` | Listen address |
| `LOG_LEVEL` | no | `info` | `debug`, `info`, `warning` or `error` |
| `IMAGE_TAG` | no | `1` | Image tag used by `compose.yaml` |
## Development
```sh
uv sync
uv run ruff check . && uv run ruff format --check .
uv run mypy
DATABASE_TEST_URL=postgresql://test:test@localhost:5433/test uv run pytest
```
Tests that need Postgres are skipped without `DATABASE_TEST_URL`; for example
`docker run -d -p 5433:5432 -e POSTGRES_USER=test -e POSTGRES_PASSWORD=test postgres:17-alpine`.
Integration tests run against a real Docmost:
```sh
DOCMOST_VERSION=0.97.0 docker compose -f tests/integration/compose.yaml up -d
DOCMOST_TEST_URL=http://localhost:3000 uv run pytest tests/integration
```
To run the compose stack with a local build:
```sh
docker build -t ghcr.io/bbik52/docmost-mcp-remote:dev .
IMAGE_TAG=dev docker compose up -d
```
See [CONTRIBUTING.md](https://github.com/bbik52/docmost-mcp-remote/blob/master/CONTRIBUTING.md) and the [roadmap](https://github.com/bbik52/docmost-mcp-remote/blob/master/docs/ROADMAP.md).
Русская версия: [README.ru.md](https://github.com/bbik52/docmost-mcp-remote/blob/master/README.ru.md).
## License
[Apache License 2.0](https://github.com/bbik52/docmost-mcp-remote/blob/master/LICENSE). The license does not cover the project
name: forks are welcome under another name. This project is an independent
implementation and is not affiliated with Docmost. It was inspired by
[docmost-mcp-oss](https://github.com/abelsr/docmost-mcp-oss).
This server cannot be deployed
Maintenance
ActivityNo data
ResponsivenessNo issues