Joplin笔记管理MCP服务

Joplin Server MCP 是一款 Model Context Protocol (MCP) 笔记管理服务,通过 Joplin Server REST API 为 LLM 提供对笔记、笔记本、标签和附件的完整读写能力,并支持安全的局部编辑,适用于 AI 笔记检索、知识库管理和文档自动化处理场景。

  • by Alexander-Zhukov
  • Python
  • 混合部署
  • 5 星标
  • 更新于
在 GitHub 查看 Alexander-Zhukov/joplin-server-mcp

使用说明

Joplin Server MCP

Model Context Protocol server for Joplin Server. Gives LLMs full access to your notes, notebooks, tags, and attachments via the Joplin Server REST API.

What is Joplin Server?

Joplin is an open-source note-taking app with Markdown support, end-to-end encryption, and sync across devices. Joplin Server is the sync backend that stores and syncs your data. You can run it yourself using the official Docker image or use the managed Joplin Cloud service.

Note: This MCP server connects to Joplin Server REST API, not the Joplin Desktop Web Clipper.

Joplin Cloud: Use JOPLIN_SERVER_URL=https://api.joplincloud.com.

Tools

Tool Description
ping_joplin Check server connectivity
list_notebooks List all notebooks
get_notebook Get notebook details with notes and sub-notebooks
create_notebook Create a new notebook
get_or_create_notebook Resolve a /-separated notebook path, creating missing levels
update_notebook Rename or move a notebook (with circular reference check)
delete_notebook Delete a notebook (with optional force for non-empty)
list_notes List notes, optionally filtered by notebook, tag, and/or to-do state
get_all_notes Get all notes with pagination, sorting (incl. todo_due/todo_completed), notebook and to-do filters
search_notes Search notes (multi-term AND) with scope and notebook/tag filters
get_note Get note text (resource references replaced with names, or raw=True for the verbatim body)
get_notes_batch Read multiple notes at once (up to 50, parallel)
get_note_full Get note with all resources embedded as base64
create_note Create a new note (optionally as a to-do with a due date)
export_note Export note as markdown with resources as named base64 blocks
update_note Update note title, body, or move to another notebook
get_note_outline Map a note's headings with line numbers and section sizes
append_to_note Add text inside a note or section, or as a sibling before/after a section
replace_in_note Replace an exact string inside a note body
replace_section Replace everything under a heading
set_todo Set/clear a note's to-do state, completion, and due date
delete_note Delete a note
list_tags List all tags
create_tag Create a new tag
delete_tag Delete a tag
get_note_tags List tags assigned to a note
add_tag_to_note Add a tag to a note
remove_tag_from_note Remove a tag from a note
get_note_resources List resources attached to a note
get_resource_info Get resource metadata
download_resource Download a resource as base64

Partial edits

update_note takes the complete new body, which makes it a poor fit for large notes: adding two lines to a 70 KB note means an LLM has to reproduce all 70 KB verbatim. That is expensive, and a single silent typo corrupts the note.

append_to_note, replace_in_note and replace_section resolve the edit on the server instead. The client sends only the fragment it wants added or changed, so untouched text — including :/<resource-id> links, which the read tools render as human-readable names — is preserved byte for byte.

append_to_note(note_id, "| web-1 | 10.0.0.5 |", section="Hosts", separator="\n")
append_to_note(note_id, "## 2026-08-26\n\nDeployed.", section="## 2026-08-25", position="before")
replace_in_note(note_id, "status: draft", "status: final")
replace_section(note_id, "## Current state", "Rewritten from scratch.")

Every write reports what it did — character and line deltas, heading count before and after, and the numbered lines around the change — so the result can be verified without re-reading the note:

Appended to: **Work Log** (ID: `0123456789abcdef0123456789abcdef`)
Inserted 745 chars at the start of section '# Work Log' (line 1)
Chars 76141 -> 76887 (+746), lines 1858 -> 1885 (+27), headings 117 -> 118

Guard rails:

  • Sections are addressed by heading, either bare ("Hosts") or with its level ("## Hosts"), and a section ends at the next heading of the same or higher level. Headings inside fenced code blocks are ignored. An ambiguous or missing heading is refused, never guessed. get_note_outline lists what is available without reading the body.
  • position picks inside or beside. "start" / "end" write inside the note or section, joining the existing content with separator. "before" / "after" place the text beside a named section instead — above its heading, or past its whole span with subsections included. Those two require section, ignore separator, and are strictly additive: no existing byte is rewritten and blank lines appear only where the join needs them. Reach for "before" to put a new entry at the top of a newest-first log whose first heading is preceded by a preamble — "start" would land above the preamble.
  • replace_in_note demands a unique match. A missing anchor or an unexpected second match is an error, with the mismatching lines reported. Use get_note(raw=True) to copy an anchor verbatim, or replace_all=True when you do mean every occurrence.
  • dry_run=True reports the change and its context without writing.
  • if_absent="marker" skips the append when the marker is already in the note, which makes a re-run a no-op.

Setup

Prerequisites

Environment variables

Variable Required Default Description
JOPLIN_SERVER_URL Yes — Joplin Server URL
JOPLIN_EMAIL Yes — User email
JOPLIN_PASSWORD Yes — User password
MCP_TRANSPORT No stdio (sse in the Docker image) Transport: stdio, sse or streamable-http
MCP_HOST No 0.0.0.0 HTTP listen host (sse and streamable-http)
MCP_PORT No 8081 HTTP listen port (sse and streamable-http)

streamable-http serves the endpoint /mcp and is the transport the current MCP specification recommends; sse (endpoint /sse) is the older one, kept as the image default for compatibility.

Run with Docker

docker run -d \
  -e JOPLIN_SERVER_URL=https://your-joplin-server.example.com \
  -e JOPLIN_EMAIL=your@email.com \
  -e JOPLIN_PASSWORD=your_password \
  -p 8081:8081 \
  alexfail2/joplin-mcp

The container defaults to SSE transport. The endpoint will be available at http://localhost:8081/sse. Add -e MCP_TRANSPORT=streamable-http to serve http://localhost:8081/mcp instead.

Run locally (stdio)

pip install -r requirements.txt   # mcp, httpx, uvicorn, pinned
python app/server.py

Build from source

docker build -t joplin-mcp .

MCP client configuration

Streamable HTTP

{
  "mcpServers": {
    "joplin": {
      "url": "http://localhost:8081/mcp"
    }
  }
}

SSE (Docker default)

{
  "mcpServers": {
    "joplin": {
      "url": "http://localhost:8081/sse"
    }
  }
}

stdio (local)

{
  "mcpServers": {
    "joplin": {
      "command": "python",
      "args": ["/path/to/app/server.py"],
      "env": {
        "JOPLIN_SERVER_URL": "https://your-joplin-server.example.com",
        "JOPLIN_EMAIL": "your@email.com",
        "JOPLIN_PASSWORD": "your_password"
      }
    }
  }
}

How it works

The server authenticates with Joplin Server via email/password sessions and builds an in-memory index of all items (notes, notebooks, tags) with a 2-minute TTL cache. Incremental sync compares server-side updated_time with cached etags, fetching only changed items (typical refresh: ~5s vs ~35s full rebuild). The index is persisted to /tmp/joplin_index_cache.json so a restarted container starts instantly; a recreated one rebuilds it on first use. Resource metadata is loaded lazily on first access. Background refresh keeps the index up to date without blocking requests. All IDs are validated (32-char hex) before API calls.

Joplin's internal serialization format (title + markdown body + metadata block) is parsed and presented as clean structured output.

Development

The pure helpers (markdown section resolution, splicing, edit reports, to-do state) are covered by a dependency-free test suite:

python tests/test_helpers.py

Contributors

  • @paoloviviani — to-do state: completion and due dates surfaced and settable, set_todo, create_note flags, todo filters and ordering (#7)
  • @LasseLegarth — share_id inherited from the parent notebook, so notes stay visible to a shared notebook's participants (#5)

License

MIT