An MCP (Model Context Protocol) server that exposes Joplin'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) that syncs to a
Joplin Server 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 (v0.6.31+, which added native MCP support) and Claude.
Notes
search_notes— free-text search with optionalnotebook/tag/is_todofiltersget_note— fetch full content of a note by idlist_all_notes— paginated listing across every notebooklist_notes_in_notebook— notes inside a specific notebooklist_recent_changes— notes ordered by most recently modifiedcreate_noteupdate_note— title/body/to-do state, and can move a note between notebooksdelete_note
Notebooks
list_notebookscreate_notebookupdate_notebook— rename and/or reparentdelete_notebook
Tags
list_tagsget_notes_by_tagget_note_tagstag_note— creates the tag if it doesn't already existuntag_noterename_tagdelete_tag
Other
list_note_resources— list attachments on a noteping_joplin— health check
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.
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'sapi.portis41184inside the container, but only bound to127.0.0.1there — it re-exposes it externally on port 80 viasocat. SoJOPLIN_API_URLshould point athttp://<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, putapi.tokenin the JSON config file mounted into that container (see its README), not viajoplin configat runtime.
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:latestSee 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 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.
pip install -r requirements.txt
JOPLIN_API_URL=http://localhost:41184 JOPLIN_TOKEN=... MCP_TRANSPORT=stdio python server.pyjspiers/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:
*/5 * * * * docker exec joplin-headless joplin sync >> /var/log/joplin-sync.log 2>&1Adjust the interval to taste. Without this (or an equivalent scheduled sync), changes only
propagate when you manually run docker exec joplin-headless joplin sync.
- Admin Settings → External Tools → Add Server
- Type:
MCP (Streamable HTTP) - Server URL:
https://<your-host>/mcp - Auth: Bearer token — the value of
MCP_API_KEY
- 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.
MIT — see LICENSE.