joplin-mcp
by techfixpros
README.md
# joplin-mcp
An [MCP](https://modelcontextprotocol.io) (Model Context Protocol) server that exposes
[Joplin](https://joplinapp.org)'s Data API (the "Web Clipper" REST API) as a set of tools
an LLM can call — search, read, create, update, and organize your Joplin notes, notebooks,
and tags.
Built to run against a **headless Joplin instance** (e.g.
[`jspiers/headless-joplin`](https://github.com/jspiers/headless-joplin)) that syncs to a
[Joplin Server](https://joplinapp.org/help/apps/sync/) backend, rather than talking to
Joplin Server's own API directly (which is sync-protocol only and doesn't expose notes in
a friendly way).
Transport is Streamable HTTP, so it works with any MCP client that supports that transport
— tested against [Open WebUI](https://openwebui.com) (v0.6.31+, which added native MCP
support) and Claude.
## Tools
**Notes**
- `search_notes` — free-text search with optional `notebook` / `tag` / `is_todo` filters
- `get_note` — fetch full content of a note by id
- `list_all_notes` — paginated listing across every notebook
- `list_notes_in_notebook` — notes inside a specific notebook
- `list_recent_changes` — notes ordered by most recently modified
- `create_note`
- `update_note` — title/body/to-do state, and can move a note between notebooks
- `delete_note`
**Notebooks**
- `list_notebooks`
- `create_notebook`
- `update_notebook` — rename and/or reparent
- `delete_notebook`
**Tags**
- `list_tags`
- `get_notes_by_tag`
- `get_note_tags`
- `tag_note` — creates the tag if it doesn't already exist
- `untag_note`
- `rename_tag`
- `delete_tag`
**Other**
- `list_note_resources` — list attachments on a note
- `ping_joplin` — health check
## Architecture
```
LLM client (OWUI / Claude / etc.)
│ Streamable HTTP + bearer auth
▼
joplin-mcp (this repo)
│ Joplin Data API (HTTP, token auth)
▼
headless Joplin instance (jspiers/headless-joplin)
│ Joplin sync protocol
▼
Joplin Server ◄──sync──► Joplin desktop/mobile apps
```
The headless Joplin container is required because Joplin Server itself only speaks the
sync protocol — actual note content is only queryable through a running Joplin
client's Data API, which is why this sits between the MCP server and Joplin Server.
## Configuration
Environment variables:
| Variable | Description | Default |
|---|---|---|
| `JOPLIN_API_URL` | Base URL of the Joplin Data API | `http://localhost:41184` |
| `JOPLIN_TOKEN` | Joplin Web Clipper auth token | *(required)* |
| `MCP_API_KEY` | Bearer token required on every request to this server. If unset, the server accepts unauthenticated requests — only safe for a fully internal network. | *(unset)* |
| `MCP_TRANSPORT` | `stdio` or `streamable-http` | `streamable-http` |
| `MCP_PORT` | Port for streamable-http transport | `8080` |
> **Note on `jspiers/headless-joplin`:** that image's `api.port` is `41184` *inside* the
> container, but only bound to `127.0.0.1` there — it re-exposes it externally on port
> **80** via `socat`. So `JOPLIN_API_URL` should point at `http://<headless-container>:80`,
> not `:41184`, when running against that image over a Docker network.
>
> Also note that image hard-codes `api.token: "mytoken"` as a config *default* that gets
> reapplied on every container start. To set a real token that survives restarts, put
> `api.token` in the JSON config file mounted into that container (see its
> [README](https://github.com/jspiers/headless-joplin)), not via `joplin config` at runtime.
## Running
### Docker
```bash
docker build -t joplin-mcp:latest .
docker run -d --name joplin-mcp \
-e JOPLIN_API_URL=http://joplin-headless:80 \
-e JOPLIN_TOKEN=<your-token> \
-e MCP_API_KEY=<a-random-key> \
-p 8080:8080 \
joplin-mcp:latest
```
### docker-compose
See [`docker-compose.example.yml`](./docker-compose.example.yml) for a full example
alongside a headless Joplin container and Joplin Server, including Traefik labels for
reverse-proxying it with a public hostname + TLS.
You'll also need a Joplin sync config file for the headless container — copy
[`joplin-headless-config.example.json`](./joplin-headless-config.example.json) to
`joplin-headless-config.json` (same directory as your compose file, matching the bind
mount in the example) and fill in your real sync server URL, account credentials, and a
generated `api.token`. This file is gitignored so your credentials don't get committed.
### Locally (stdio, for use with Claude Desktop etc.)
```bash
pip install -r requirements.txt
JOPLIN_API_URL=http://localhost:41184 JOPLIN_TOKEN=... MCP_TRANSPORT=stdio python server.py
```
## Syncing the headless client
`jspiers/headless-joplin` hardcodes `sync.interval: 0` (disabled) as a config default that
gets reapplied on every container start — so **automatic interval-based sync is not
possible through Joplin's own config on this image**, regardless of what you set in
`joplin-headless-config.json`.
To get notes created/edited via this MCP server to actually reach Joplin Server (and from
there, your desktop/mobile apps), trigger `joplin sync` periodically from outside the
container — a host crontab entry is the simplest approach:
```cron
*/5 * * * * docker exec joplin-headless joplin sync >> /var/log/joplin-sync.log 2>&1
```
Adjust the interval to taste. Without this (or an equivalent scheduled sync), changes only
propagate when you manually run `docker exec joplin-headless joplin sync`.
## Connecting from Open WebUI
1. **Admin Settings → External Tools → Add Server**
2. **Type:** `MCP (Streamable HTTP)`
3. **Server URL:** `https://<your-host>/mcp`
4. **Auth:** Bearer token — the value of `MCP_API_KEY`
## Security notes
- If exposing this publicly (e.g. behind a reverse proxy with a public hostname), **always
set `MCP_API_KEY`**. Without it, anyone who can reach the URL can read/write/delete your
notes.
- The server disables the MCP SDK's built-in Host-header DNS-rebinding check
(`enable_dns_rebinding_protection=False`), since it's expected to run behind a reverse
proxy with a hostname the SDK wouldn't otherwise trust. This is safe specifically
*because* the bearer-token middleware provides the actual access control instead — don't
disable both.
## License
MIT — see [LICENSE](./LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues