Skip to main content
Glama

joplin-mcp

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.

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

Related MCP server: Joplin MCP Server

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), not via joplin config at runtime.

Running

Docker

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 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.

Locally (stdio, for use with Claude Desktop etc.)

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:

*/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.

Related MCP Connectors

Related MCP Servers