Joplin MCP Server
A FastMCP-based Model Context Protocol (MCP) server for Joplin note-taking application via its Python API joppy, enabling AI assistants to interact with your Joplin notes, notebooks, and tags through a standardized interface.
Table of Contents
- What You Can Do
- Quick Start
- Supported Clients
- Example Usage
- Tool Permissions
- Notebook Allowlist
- Advanced Configuration
- Docker
- Project Structure
- Testing
- Complete Tool Reference
- Changelog
What You Can Do
This MCP server provides 26 optimized tools for comprehensive Joplin integration:
Note Management
- Find & Search:
find_notes(supportstrash=Truefor trashed notes),find_notes_with_tag,find_notes_in_notebook,get_all_notes - CRUD Operations:
get_note,get_note_resources(read OCR text from attached images/PDFs),get_links,create_note,update_note,edit_note,delete_note
Notebook Management
- Organize:
list_notebooks,create_notebook,update_notebook,delete_notebook
Tag Management
- Categorize:
list_tags,create_tag,update_tag,delete_tag,get_tags_by_note - Link:
tag_note,untag_note
Trash Management
- Recover:
restore_from_trash- Restore soft-deleted notes or notebooks
Import
- File Import:
import_from_file- Import Markdown, HTML, CSV, TXT, JEX files and directories
System
- Health:
ping_joplin
Quick Start
- Open Joplin Desktop → Tools → Options → Web Clipper
- Enable the Web Clipper service
- Copy the Authorization token
- Set up your preferred client below
Supported Clients
Any MCP-compatible client should work. Below are the ones with documented setup instructions.
Claude Desktop
Run the automated installer:
# Install and configure everything automatically (pip)
pip install joplin-mcp
joplin-mcp-install
# Or use zero-install with uvx (recommended if you have uv)
uvx --from joplin-mcp joplin-mcp-install
# Optional: pin a specific version/range for stability
uvx --from joplin-mcp==0.4.1 joplin-mcp-install
uvx --from 'joplin-mcp>=0.4,<0.5' joplin-mcp-install
This script will:
- Configure your Joplin API token
- Set tool permissions (Create/Update/Delete)
- Set up Claude Desktop automatically
- Test the connection
After setup, restart Claude Desktop and you're ready to go!
Claude Code
Install the orchestration plugin for smarter tool usage (edit vs update, long-note reading, bulk tagging):
/plugin marketplace add alondmnt/joplin-mcp
/plugin install joplin-mcp
You'll be prompted for your Joplin API token on first use. The skill is invoked automatically when working with Joplin tools, or manually with /joplin.
Jan AI
Install Jan AI from https://jan.ai
Add MCP Server in Jan's interface:
- Open Jan AI
- Go to Settings → Extensions → Model Context Protocol
- Click Add MCP Server
- Configure:
- Name:
joplin - Command:
uvx --from joplin-mcp joplin-mcp-server(requiresuvinstalled) - Environment Variables:
JOPLIN_TOKEN:your_joplin_api_token_here
- Name:
- Enable the server
Start chatting with access to your Joplin notes!
Automated Setup (Alternative)
# Install and configure Jan AI automatically (if Jan is already installed)
pip install joplin-mcp
joplin-mcp-install
This will detect and configure Jan AI automatically, just like Claude Desktop.
OllMCP (Local Ollama Models)
Auto-discovery (if you set up Claude Desktop first)
# Install ollmcp
pip install ollmcp
# Run with auto-discovery (requires existing Claude Desktop config)
ollmcp --auto-discovery --model qwen3:4b
Manual setup (works independently)
# Install ollmcp
pip install ollmcp
# Set environment variable
export JOPLIN_TOKEN="your_joplin_api_token_here"
# Run with uvx (requires uv installed)
ollmcp --server "joplin:uvx --from joplin-mcp joplin-mcp-server" --model qwen3:4b
# Or with an installed package (pip install joplin-mcp)
ollmcp --server "joplin:joplin-mcp-server" --model qwen3:4b
Example Usage
Once configured, you can ask your AI assistant:
- "List all my notebooks" - See your Joplin organization
- "Find notes about Python programming" - Search your knowledge base
- "Create a meeting note for today's standup" - Quick note creation
- "Tag my recent AI notes as 'important'" - Organize with tags
- "Show me my todos" - Find task items with
find_notes(task=True)
Tool Permissions
The setup script offers 4 permission levels:
- Read (always enabled): Browse and search your notes safely
- Write (optional): Create new notes, notebooks, and tags
- Update (optional): Modify existing content
- Delete (optional): Remove content permanently
Choose the level that matches your comfort and use case.
Notebook Allowlist
Restrict AI access to specific notebooks using pattern-based access control. When configured, only matching notebooks (and their contents) are visible — all other notebooks are hidden.
Quick Setup
JSON config (joplin-mcp.json):
{
"token": "your_token",
"notebook_allowlist": ["Work", "Projects/Public"]
}
Environment variable:
export JOPLIN_NOTEBOOK_ALLOWLIST="Work,Projects/Public"
Pattern Syntax
Patterns use gitignore/gitwildmatch semantics:
| Pattern | Matches | Example |
|---|---|---|
Work |
Exact notebook name (and all children) | Work, Work/Tasks, Work/Notes |
Projects/* |
Direct children of Projects | Projects/Alpha, Projects/Beta |
Projects/** |
All descendants recursively | Projects/Alpha/Tasks/Q1 |
!Projects/Secret |
Exclude (negate) a specific path | Everything in Projects except Secret |
Negation patterns always win over positive patterns (any negation match on a path or ancestor denies access).
How It Works
- Hierarchical access: Allowing a parent notebook grants access to all its children. Allowing
Projectsmeans notes inProjects/Work/Tasksare also accessible. - Read protection:
get_note,get_note_resources,find_notes,get_links— notes in blocked notebooks are filtered out or rejected. - Write protection:
create_note,update_note,edit_note,delete_note— operations on notes in blocked notebooks are rejected. - Notebook operations:
list_notebooksonly shows accessible notebooks.create_notebookis rejected both under a blocked parent and at the top level (noparent_name) when an allowlist is set, andupdate_notebookrejects moves to top-level (parent_name="/") under the same policy — both would let the agent silently move a notebook out of allowlist-enforced scope. To grow the allowlist, create or relocate the notebook in the Joplin UI, then add it tonotebook_allowlistand restart the server. - Search filtering:
find_notesresults are filtered to only include notes in accessible notebooks. - Tag operations:
tag_note,untag_note,get_tags_by_noteenforce access on the note's notebook. - Error privacy: Blocked access raises a generic "Notebook not accessible" error without revealing notebook names or IDs.
Configuration Examples
Single project focus (notebook name containing a space):
{ "notebook_allowlist": ["Work Projects"] }
Multiple notebooks with exclusion:
{ "notebook_allowlist": ["Projects", "!Projects/Secret", "AI", "Reference"] }
Glob patterns:
{ "notebook_allowlist": ["Projects/*", "!Projects/Private"] }
No allowlist (default) — all notebooks accessible:
{ "notebook_allowlist": null }
Startup Behavior
At server startup, the allowlist is validated and logged:
- Each entry is resolved against existing notebooks
- Unresolvable patterns trigger warnings (but never block startup)
- If the allowlist resolves to zero accessible notebooks, a warning is logged
Advanced Configuration
Development Installation
For developers or users who want the latest features:
git clone https://github.com/alondmnt/joplin-mcp.git
cd joplin-mcp
python bootstrap.py
bootstrap.py is cross-platform: it offers to create a ./venv, runs pip install -e ., then launches the interactive installer. Pass --no-venv to install into whichever Python is already active.
Manual Configuration
If you prefer manual setup or the script doesn't work:
Note on
uvx:uvxruns Python applications without permanently installing them (requiresuv:pip install uv). It can read and write user configuration files (e.g., Claude/Jan configs), souvx --from joplin-mcp joplin-mcp-installworks for setup just like a pip install.
Version pinning (optional): For long‑lived client configs or CI, you can pin or range-constrain the version for reproducibility, e.g.
uvx --from joplin-mcp==0.4.1 joplin-mcp-installoruvx --from 'joplin-mcp>=0.4,<0.5' joplin-mcp-install.
1. Create Configuration File
Create joplin-mcp.json in your project directory:
{
"token": "your_api_token_here",
"host": "localhost",
"port": 41184,
"timeout": 30,
"verify_ssl": false
}
2. Claude Desktop Configuration
Add to your claude_desktop_config.json:
Option A: Using uvx (Zero-install)
{
"mcpServers": {
"joplin": {
"command": "uvx",
"args": ["--from", "joplin-mcp", "joplin-mcp-server"],
"env": {
"JOPLIN_TOKEN": "your_token_here"
}
}
}
}
Requires uv installed: pip install uv
Option B: Using installed package
{
"mcpServers": {
"joplin": {
"command": "joplin-mcp-server",
"env": {
"JOPLIN_TOKEN": "your_token_here"
}
}
}
}
3. More Client Configuration Examples
For additional client configurations including different transport options (HTTP, SSE, Streamable HTTP), see client-config.json.example.
This file includes configurations for:
- STDIO transport (default, most compatible)
- HTTP transport (basic HTTP server mode)
- SSE transport (recommended for gemini-cli and OpenAI clients)
- Streamable HTTP transport (advanced web clients)
- HTTP-compat transport (bridges modern
/mcpJSON-RPC with legacy/sse//messagesclients)
Tool Permission Configuration
Fine-tune which operations the AI can perform by editing your config:
{
"tools": {
"create_note": true,
"update_note": true,
"delete_note": false,
"create_notebook": true,
"update_notebook": false,
"delete_notebook": false,
"create_tag": true,
"update_tag": false,
"delete_tag": false,
"get_all_notes": false,
"import_from_file": true
}
}
Environment Variables
Alternative to JSON configuration:
# Connection settings
export JOPLIN_TOKEN="your_api_token_here"
export JOPLIN_HOST="localhost"
export JOPLIN_PORT="41184"
export JOPLIN_TIMEOUT="30"
Per-Tool Env Vars
Every tool can be toggled individually via JOPLIN_TOOL_<NAME>=true|false. These take precedence over config file settings.
| Env var | Default |
|---|---|
JOPLIN_TOOL_FIND_NOTES |
true |
JOPLIN_TOOL_FIND_NOTES_WITH_TAG |
true |
JOPLIN_TOOL_FIND_NOTES_IN_NOTEBOOK |
true |
JOPLIN_TOOL_FIND_IN_NOTE |
true |
JOPLIN_TOOL_GET_ALL_NOTES |
false |
JOPLIN_TOOL_GET_NOTE |
true |
JOPLIN_TOOL_GET_LINKS |
true |
JOPLIN_TOOL_CREATE_NOTE |
true |
JOPLIN_TOOL_UPDATE_NOTE |
true |
JOPLIN_TOOL_EDIT_NOTE |
true |
JOPLIN_TOOL_DELETE_NOTE |
false |
JOPLIN_TOOL_LIST_NOTEBOOKS |
true |
JOPLIN_TOOL_CREATE_NOTEBOOK |
true |
JOPLIN_TOOL_UPDATE_NOTEBOOK |
false |
JOPLIN_TOOL_DELETE_NOTEBOOK |
false |
JOPLIN_TOOL_LIST_TAGS |
true |
JOPLIN_TOOL_CREATE_TAG |
true |
JOPLIN_TOOL_UPDATE_TAG |
false |
JOPLIN_TOOL_DELETE_TAG |
false |
JOPLIN_TOOL_GET_TAGS_BY_NOTE |
true |
JOPLIN_TOOL_TAG_NOTE |
true |
JOPLIN_TOOL_UNTAG_NOTE |
true |
JOPLIN_TOOL_PING_JOPLIN |
true |
JOPLIN_TOOL_RESTORE_FROM_TRASH |
true |
JOPLIN_TOOL_IMPORT_FROM_FILE |
false |
Notebook Allowlist Env Var
| Env var | Default | Description |
|---|---|---|
JOPLIN_NOTEBOOK_ALLOWLIST |
(not set) | Comma-separated list of notebook patterns (e.g., Work,Projects/*,!Projects/Secret) |
HTTP Transport Support
The server supports both STDIO and HTTP transports:
# STDIO (default)
joplin-mcp-server --config ~/.joplin-mcp.json
# HTTP transport (development, from repo)
PYTHONPATH=src python -m joplin_mcp.server --transport http --port 8000 --config ./joplin-mcp.json
# Opt-in HTTP compatibility bundle (modern + legacy SSE endpoints)
PYTHONPATH=src python -m joplin_mcp.server --transport http-compat --port 8000 --config ./joplin-mcp.json
# or keep --transport http and export MCP_HTTP_COMPAT=1/true to toggle the same behavior.
HTTP client config
Note: Claude Desktop currently uses STDIO transport and does not consume HTTP/SSE configs directly. The following example applies to clients that support network transports.
{
"mcpServers": {
"joplin": {
"transport": "http",
"url": "http://localhost:8000/mcp"
}
}
}
Configuration Reference
Basic Settings
| Option | Default | Description |
|---|---|---|
token |
required | Joplin API authentication token |
host |
localhost |
Joplin server hostname |
port |
41184 |
Joplin Web Clipper port |
timeout |
30 |
Request timeout in seconds |
verify_ssl |
false |
SSL certificate verification |
Tool Permissions
| Option | Default | Description |
|---|---|---|
tools.create_note |
true |
Allow creating new notes |
tools.update_note |
true |
Allow modifying existing notes |
tools.edit_note |
true |
Allow precision edits (find/replace, append, prepend) |
tools.delete_note |
false |
Allow deleting notes (disabled by default — destructive) |
tools.create_notebook |
true |
Allow creating new notebooks |
tools.update_notebook |
false |
Allow modifying notebook titles and emoji icons |
tools.delete_notebook |
false |
Allow deleting notebooks (disabled by default — destructive) |
tools.create_tag |
true |
Allow creating new tags |
tools.update_tag |
false |
Allow modifying tag titles |
tools.delete_tag |
false |
Allow deleting tags (disabled by default — destructive) |
tools.tag_note |
true |
Allow adding tags to notes |
tools.untag_note |
true |
Allow removing tags from notes |
tools.restore_from_trash |
true |
Allow restoring soft-deleted notes or notebooks |
tools.find_notes |
true |
Allow text search across notes (with task filtering) |
tools.find_notes_with_tag |
true |
Allow finding notes by tag (with task filtering) |
tools.find_notes_in_notebook |
true |
Allow finding notes by notebook (with task filtering) |
tools.find_in_note |
true |
Allow regex search within a single note |
tools.get_all_notes |
false |
Allow getting all notes (disabled by default - can fill context window) |
tools.get_note |
true |
Allow getting specific notes |
tools.get_note_resources |
true |
Allow reading a note's resources and their OCR text |
tools.get_links |
true |
Allow extracting links to other notes |
tools.list_notebooks |
true |
Allow listing all notebooks |
tools.list_tags |
true |
Allow listing all tags |
tools.get_tags_by_note |
true |
Allow getting tags for specific notes |
tools.ping_joplin |
true |
Allow testing server connectivity |
tools.import_from_file |
false |
Allow importing files/directories (MD, HTML, CSV, TXT, JEX) |
Notebook Allowlist
| Option | Default | Description |
|---|---|---|
notebook_allowlist |
null |
List of notebook patterns to allow access to. null = no restriction. Supports gitignore-style patterns: exact names, * wildcards, ** recursive, ! negation |
Content Exposure (Privacy Settings)
| Option | Default | Description |
|---|---|---|
content_exposure.search_results |
"preview" |
Content visibility in search results: "none", "preview", "full" |
content_exposure.individual_notes |
"full" |
Content visibility for individual notes: "none", "preview", "full" |
content_exposure.listings |
"none" |
Content visibility in note listings: "none", "preview", "full" |
content_exposure.max_preview_length |
300 |
Maximum length of content previews (characters) |
Docker
Run the MCP server in a container. Default transport is HTTP for broad compatibility; switch via environment variables.
Build
docker build -t joplin-mcp .
Run (HTTP default)
docker run --rm \
-p 8000:8000 \
-e JOPLIN_TOKEN=your_api_token \
joplin-mcp
With mounted config
docker run --rm \
-p 8000:8000 \
-v $PWD/joplin-mcp.json:/config/joplin-mcp.json:ro \
joplin-mcp
Choose transport
- SSE (streaming):
-e MCP_TRANSPORT=sse - Streamable HTTP:
-e MCP_TRANSPORT=streamable-http - STDIO (no port):
-e MCP_TRANSPORT=stdio
Example (SSE):
docker run --rm \
-p 8000:8000 \
-e JOPLIN_TOKEN=your_api_token \
-e MCP_TRANSPORT=sse \
joplin-mcp
The container listens on 0.0.0.0:8000 by default. If exposing publicly, place behind a reverse proxy and terminate TLS there. For SSE, ensure proxy keep-alives and buffering are configured appropriately.
Project Structure
src/joplin_mcp/- Main package directoryfastmcp_server.py- Server implementation with 26 tools and Pydantic validation typesconfig.py- Configuration management (including notebook allowlist)notebook_utils.py- Notebook path resolution, allowlist matching, and cachingserver.py- Server entrypoint (module and CLI)tools/- Tool implementations (notes, notebooks, tags)ui_integration.py- UI integration utilities
docs/- Documentation (troubleshooting, privacy controls, enhancement proposals)tests/- Unit test suitetests/e2e/- End-to-end tests against a real Joplin Desktop (3.x) via the Web Clipper API; see "Running Tests"
Testing
Test your connection:
# For pip install
joplin-mcp-server --config ~/.joplin-mcp.json
# For development (from repo)
PYTHONPATH=src python -m joplin_mcp.server --config ./joplin-mcp.json
You should see:
Starting Joplin FastMCP Server...
Successfully connected to Joplin!
Found X notebooks, Y notes, Z tags
FastMCP server starting...
Available tools: 26 tools ready
Running Tests
# Unit tests (no Joplin instance required)
pytest tests/ --ignore=tests/e2e
# E2E tests (requires a running Joplin instance)
JOPLIN_TOKEN=your_api_token \
JOPLIN_HOST=localhost \
JOPLIN_PORT=41184 \
pytest tests/e2e/ -v -m e2e --override-ini="addopts="
The E2E suite talks to a real Joplin Desktop via the Web Clipper API and exercises every tool including notebook allowlist enforcement. If JOPLIN_HOST:JOPLIN_PORT is unreachable the suite skips itself, so it's safe to run alongside the unit tests. Requires Joplin 3.x (the trash schema introduced in 3.0 — earlier versions fail with no such column: deleted_time).
Complete Tool Reference
| Tool | Permission | Description |
|---|---|---|
| Finding Notes | ||
find_notes |
Read | Full-text search across all notes (supports task filtering; trash=True with query="*" lists trashed notes) |
find_notes_with_tag |
Read | Find notes with specific tag (supports task filtering) |
find_notes_in_notebook |
Read | Find notes in specific notebook (supports task filtering) |
get_all_notes |
Read | Get all notes, most recent first (disabled by default) |
get_note |
Read | Get specific note by ID |
find_in_note |
Read | Regex search within a single note (paginated matches & context, multiline anchors on by default) |
get_links |
Read | Extract links to other notes from a note |
get_note_resources |
Read | List a note's resources (images, PDFs, attachments) and read their OCR text |
| Managing Notes | ||
create_note |
Write | Create new notes |
update_note |
Update | Modify existing notes (incl. moving between notebooks) |
edit_note |
Update | Precision edit note content (find/replace, append, prepend) |
delete_note |
Delete | Remove notes |
| Managing Notebooks | ||
list_notebooks |
Read | Browse all notebooks |
create_notebook |
Write | Create new notebooks under an optional parent (by name or path), optionally with an emoji icon |
update_notebook |
Update | Rename, change emoji icon, or move a notebook under another parent (or to top-level with parent_name="/") |
delete_notebook |
Delete | Remove notebooks |
| Managing Tags | ||
list_tags |
Read | View all available tags |
create_tag |
Write | Create new tags |
update_tag |
Update | Modify tag titles |
delete_tag |
Delete | Remove tags |
get_tags_by_note |
Read | List tags on specific note |
| Tag-Note Relationships | ||
tag_note |
Update | Add one or more tags to one or more notes (accepts lists) |
untag_note |
Update | Remove one or more tags from one or more notes (accepts lists) |
| Trash Management | ||
restore_from_trash |
Update | Restore a soft-deleted note or notebook (pass item_type='note' or 'notebook') |
| Import Tools | ||
import_from_file |
Write | Import files/directories (MD, HTML, CSV, TXT, JEX) |
| System Tools | ||
ping_joplin |
Read | Test connectivity |