A Model Context Protocol (MCP) server that integrates with Joplin notes, allowing AI clients (like Perplexity) to access and manipulate your notebooks and notes through Joplin's Web Clipper API.
- π Search Functionality: Search notes and notebooks page-by-page
- π Content Reading: Get complete content of specific notes
- π§ Notebook Navigation: Navigate root notebooks and direct child notebooks
- π Creation Features: Create new notes and notebooks
- βοΈ Update/Edit Features: Update note content, append to notes, and rename notebooks
- ποΈ Deletion Features: Delete notes and notebooks (supports trash or permanent deletion)
- π Move Functionality: Move notes to different notebooks
- π Paginated Lists: List notes with safe cursor-style pagination; no default full dumps
- πΌοΈ Image Support: List images attached to notes, retrieve image content, and attach local images to notes
- β Native Todo Notes: Create, list, search, complete, reopen, due-date, and conversion tools for Joplin todo-type notes
- π Note History: List note revisions with timestamps and diff stats; reconstruct full content at any past revision
- π Dual Transport: stdio for local spawn, Streamable HTTP for remote connections
- Joplin Desktop - Ensure it's installed and running
- Node.js 20 - Required to run the MCP server
- Web Clipper Enabled - Enable Web Clipper service in Joplin
- Open Joplin desktop application
- Go to Tools β Options β Web Clipper
- Check Enable Web Clipper Service
- Note the port number displayed (usually 41184)
- Copy the API Token (if authentication is required)
# Clone or download this project
cd mcp-joplin
# Install dependencies
npm install
# Compile TypeScript
npm run build# Run directly (will auto-detect Joplin service)
npm start
# Or specify port
npm start -- --port 41184
# Or specify token (if needed)
npm start -- --token YOUR_API_TOKEN
# Or specify a full Joplin API base URL instead of auto-discovered localhost port
npm start -- --base-url http://localhost:41184
# View help
npm start -- --help# Global installation (recommended)
npm install -g .
# Then use anywhere
npx mcp-joplin
# Or run locally
npx . --port 41184
# Or connect to a specific Joplin API base URL
npx . --base-url http://localhost:41184For MCP clients that support URL-based servers (Streamable HTTP), run the server as a standalone HTTP service:
# Start HTTP server (default port 3100)
npm run start:http
# Custom port and host
npm run start:http -- --http-port 8080 --http-host 0.0.0.0
# Development mode with hot reload
npm run dev:httpThe server listens on http://127.0.0.1:3100/mcp by default and requires Joplin to be running with Web Clipper enabled, just like stdio mode.
Environment variables:
MCP_JOPLIN_HTTP_PORTβ HTTP server port (default:3100)MCP_JOPLIN_HTTP_HOSTβ HTTP server host (default:127.0.0.1)
Add the following configuration to your MCP client configuration file:
This MCP server requires a Joplin API token to function properly:
{
"mcpServers": {
"joplin": {
"command": "npx",
"args": [
"/ABSOLUTE/PATH/TO/mcp-joplin",
"--port",
"41184",
"--token",
"YOUR_API_TOKEN"
]
}
}
}π‘ Important: The API token is required for this MCP server to work properly. Use
--base-urlinstead of--portwhen the Joplin API is exposed at a specific URL, for example through a tunnel or non-local host.--base-urlcan also be set withJOPLIN_BASE_URL.
In ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"joplin": {
"command": "npx",
"args": [
"/Users/yourusername/path/to/mcp-joplin",
"--token",
"YOUR_API_TOKEN"
]
}
}
}Requires mcp-joplin-http to be running as a separate process first.
{
"mcpServers": {
"joplin": {
"url": "http://localhost:3100/mcp"
}
}
}The
urlformat lets the MCP host connect over HTTP without spawning a child process. The HTTP server must be started before the host launches.
The collection tools search_notes, search_todo_notes, search_notebooks, list_notes, list_todo_notes, and get_note_revisions use cursor-style pagination:
firstis optional and defaults to a safe page size; it never means "all results".firstis capped at100.afteris an opaque cursor returned asendCursorby the previous page.- Responses include
pageInfometadata:returnedCount,pageSize,hasNextPage, andendCursorwhen another page exists. - Cursors are scoped to the original request parameters. Use
endCursoronly with the same tool arguments. - If
hasNextPageistrue, the response is incomplete. Continue withafter=endCursorbefore concluding coverage.
The three search tools use different Joplin engines:
| Tool | Engine | Wildcard | Emoji / Special Chars |
|---|---|---|---|
search_notebooks |
SQL LIKE |
* β % (any position) |
Treated as literal characters; auto-fallback to *query* if exact search returns no results |
search_notes |
SQLite FTS4 | suffix * only |
Discarded as token separators; avoid prefixing queries with emoji β search by keywords |
search_todo_notes |
SQLite FTS4 + filters | suffix * only |
Same as search_notes; filters (type:todo, iscompleted:) are appended automatically |
Key differences:
- Notebook search is a simple substring match β use
*for wildcards, or rely on the automatic fallback. - Note search is full-text β each word is a token; hyphens, dots, and emoji act as separators, not searchable characters.
- When searching for a notebook by name, prefer
list_root_notebooks/list_sub_notebooksfor hierarchy navigation oversearch_notebooksunless you need a specific name match.
Get the complete content of a specific note
Parameters:
- noteId (string) - The ID of the note to retrieve
- includeImages (boolean, optional) - Whether to list attached image metadata (default: true)
Search one paginated page of notes
Parameters:
- query (string) - Search keywords
- first (number, optional) - Page size (default: 20, max: 100); this never means all results
- after (string, optional) - Opaque cursor from the previous page endCursor
Search results include native todo metadata (Type, Status, Due, Completed) when available.
Search native Joplin todo notes globally using Joplin todo search syntax. This is intentionally global and does not accept notebookId.
Parameters:
- query (string, optional) - Additional global search query
- status ("open" | "completed" | "all", optional) - Todo status filter (default: "open")
- first (number, optional) - Page size (default: 20, max: 100)
- after (string, optional) - Opaque cursor from the previous page endCursor
Examples:
search_todo_notes({ status: "open" })
search_todo_notes({ query: "project-x", status: "all", first: 10 })
Search one paginated page of notebooks
Parameters:
- query (string) - Search keywords using Joplin folder search syntax. Use `*` for wildcard/prefix matches, e.g. `archive*` matches `archive-250124`; plain `archive` only matches a notebook titled exactly `archive`.
- first (number, optional) - Page size (default: 20, max: 100); this never means all results
- after (string, optional) - Opaque cursor from the previous page endCursor
List top-level/root notebooks from the Joplin folder tree
Parameters: None
List one paginated page of notes in a specific notebook
Parameters:
- notebookId (string) - The ID of the notebook
- first (number, optional) - Page size (default: 50, max: 100); this never means all results
- after (string, optional) - Opaque cursor from the previous page endCursor
Note lists show native todo metadata for todo-type notes, including open/completed status and due dates.
List native Joplin todo notes under a notebook by stable notebook ID. Set includeSubNotebooks to scan child notebooks too. This does not use title-based notebook: search.
Parameters:
- notebookId (string) - The ID of the notebook to list todo notes from
- includeSubNotebooks (boolean, optional) - Include child notebooks (default: false)
- status ("open" | "completed" | "all", optional) - Todo status filter (default: "open")
- first (number, optional) - Page size (default: 50, max: 100)
- after (string, optional) - Opaque scanner cursor from the previous page endCursor
Examples:
list_todo_notes({ notebookId: "abc123" })
list_todo_notes({ notebookId: "abc123", includeSubNotebooks: true, status: "all" })
List direct child notebooks within a specific notebook. This uses Joplin's folder tree hierarchy and is not cursor-paginated.
Parameters:
- parentNotebookId (string) - The ID of the parent notebook
Create a new note
Parameters:
- title (string) - Note title
- body (string) - Note content (Markdown format)
- notebookId (string, optional) - Target notebook ID
Create a native Joplin todo note. These are Joplin todo-type notes (is_todo = 1), not Markdown checkbox items.
Parameters:
- title (string) - Todo title
- body (string, optional) - Todo note body (Markdown format)
- notebookId (string, optional) - Target notebook ID
- dueAt (string, optional) - ISO date/time or millisecond timestamp
- completedAt (string, optional) - ISO date/time or millisecond timestamp
Example:
create_todo_note({ title: "Review Phase 2", notebookId: "abc123", dueAt: "2026-06-01T09:00:00+09:00" })
Create a new notebook
Parameters:
- title (string) - Notebook title
- parentId (string, optional) - Parent notebook ID (for sub-notebooks)
Delete a note
Parameters:
- noteId (string) - ID of the note to delete
- permanent (boolean, optional) - Whether to permanently delete (default: false, moves to trash)
Delete a notebook
Parameters:
- notebookId (string) - ID of the notebook to delete
- permanent (boolean, optional) - Whether to permanently delete (default: false, moves to trash)
Move a note to a different notebook
Parameters:
- noteId (string) - ID of the note to move
- targetNotebookId (string) - Target notebook ID
Update an existing note title and/or body
Parameters:
- noteId (string) - ID of the note to update
- title (string, optional) - New note title
- body (string, optional) - New note content (full replacement, not a patch)
Notes:
- At least one of
titleorbodymust be provided - Use this when replacing note content or renaming a note
Append content to the end of an existing note
Parameters:
- noteId (string) - ID of the note to append to
- content (string) - Content to append
- separator (string, optional) - Separator inserted before appended content (default: "\n\n")
Notes:
- Use this for logs, meeting notes, supplementary info, or test results
- Prefer this over
update_notewhen the intent is to add content without replacing the existing body
Update an existing notebook title and optionally move it under another notebook
Parameters:
- notebookId (string) - ID of the notebook to update
- title (string) - New notebook title
- parentId (string, optional) - New parent notebook ID
List image resources attached to a specific note. Supports both Joplin API resource index and note body markdown image reference parsing.
Parameters:
- noteId (string) - The ID of the note whose images to list
Returns a JSON array with image id, title, mime type, file size, and markdown reference.
Retrieve an image resource from Joplin by resource ID and return the image content directly.
Parameters:
- resourceId (string) - The ID of the image resource to retrieve
Supported image types: PNG, JPEG, GIF, WebP.
Attach a local image file to a Joplin note and insert a markdown image reference into the note body.
Parameters:
- noteId (string) - The ID of the note to attach the image to
- filePath (string) - Absolute path to the local image file to attach
- altText (string, optional) - Alt text for the markdown image
- title (string, optional) - Title to use for the Joplin image resource (default: filename)
- position ('end' | 'start', optional) - Where to insert the markdown image (default: end)
- separator (string, optional) - Separator between note body and inserted markdown (default: "\\n\\n")
Supported image types: PNG, JPEG, GIF, WebP. Max file size: 3MB. SVG is not supported.
List one paginated page of historical revisions for a specific note, showing timestamps and diff statistics. Revisions are created automatically by Joplin as notes are edited.
Parameters:
- noteId (string) - The ID of the note to get revisions for
- first (number, optional) - Page size (default: 50, max: 100); this never means all results
- after (string, optional) - Opaque cursor from the previous page endCursor
Get the full reconstructed note content (title and body) at a specific historical revision. The revision content is rebuilt by applying all diffs from the beginning of the revision chain up to the target revision.
Parameters:
- noteId (string) - The ID of the note
- revisionId (string) - The ID of the revision to view
These tools update Joplin native todo metadata directly. todo_due = 0 means no due date; todo_completed = 0 means open; non-zero todo_completed is a millisecond completion timestamp.
complete_todo_note({ noteId, completedAt? })
reopen_todo_note({ noteId })
set_todo_due({ noteId, dueAt })
clear_todo_due({ noteId })
convert_note_to_todo({ noteId, dueAt?, completedAt? })
convert_todo_to_note({ noteId })
Examples:
complete_todo_note({ noteId: "todo123" })
set_todo_due({ noteId: "todo123", dueAt: "2026-06-01T09:00:00+09:00" })
convert_note_to_todo({ noteId: "note123", dueAt: "2026-06-01" })
convert_todo_to_note({ noteId: "todo123" })
- Use
update_noteto replace a note's title and/or body - Use
append_to_noteto add content to the end of a note while preserving existing content - Use
move_noteto change which notebook a note belongs to - Use
update_notebookto rename a notebook - Use native todo tools for Joplin todo-type notes; Markdown checkbox scanning is not part of this server's todo operations
You: "Search for notes containing 'Python'"
AI: Using search_notes tool to search for relevant notes...
AI: If pageInfo.hasNextPage is true, continue with after=endCursor before summarizing all matches.
You: "Create a new notebook called 'Learning Plan'"
AI: Using create_notebook tool to create a new notebook...
You: "Create a note about JavaScript in the Learning Plan notebook"
AI: Using list_root_notebooks and list_sub_notebooks to navigate to the notebook ID, then using create_note to create the note...
You: "Show the complete content of a specific note"
AI: Using get_note_content tool to retrieve note content...
You: "Update the title of note abc123 to Weekly Review"
AI: Using update_note tool to rename the note...
You: "Append these test results to note abc123"
AI: Using append_to_note tool to add the new content to the end of the note...
You: "Rename the notebook 'Recipes' to 'Cooking'"
AI: Using update_notebook tool to rename the notebook...
You: "List images attached to note abc123"
AI: Using list_note_images tool to retrieve image metadata...
You: "Show me the image with resource ID def456"
AI: Using get_note_image tool to retrieve and display the image...
You: "Attach this screenshot to note abc123"
AI: Using attach_image_to_note tool to upload the image and embed it in the note...
You: "Show me the revision history of note abc123"
AI: Using get_note_revisions tool to list all revisions with timestamps and diff sizes...
AI: If pageInfo.hasNextPage is true, continue with after=endCursor before concluding coverage.
You: "Show me what note abc123 looked like at revision rev_xxx"
AI: Using get_note_revision_content tool to reconstruct the historical content...
-
Confirm Joplin is running
- Joplin desktop application must remain open
-
Check Web Clipper settings
- Ensure Web Clipper service is enabled
- Check port settings (default 41184)
-
View error messages
# Use verbose mode to see errors DEBUG=* npm start
- "Joplin Web Clipper service not found": Ensure Joplin is running and Web Clipper is enabled
- "Connection refused": Check if port settings are correct
- "Unauthorized" or "403 Forbidden": API token is required (see instructions below)
API Token is required for this MCP server to function properly.
- In Joplin go to Tools β Options β Web Clipper
- Copy the displayed token
- Add
--token YOUR_TOKENto the startup command
# Run in development mode (stdio)
npm run dev
# Run in development mode (Streamable HTTP)
npm run dev:http
# Compile
npm run build
# Prepare for publishing
npm run prepublishOnly- Language: TypeScript/Node.js
- MCP SDK: @modelcontextprotocol/sdk (stdio + Streamable HTTP)
- HTTP Client: axios
- CLI: commander
- API: Joplin Web Clipper API
- Pagination Handling: Collection tools request one Joplin page at a time using native
page/limitparameters. They expose MCP-friendlyfirst/afterinputs and opaque cursors. - No Full-Dump Defaults: The server does not fetch every note or notebook by default, and it does not expose legacy full-dump resources.
- Notebook Search: Uses Joplin's
/searchendpoint withtype=folderand page-based pagination. - Notebook Navigation: Uses Joplin's folder tree for
list_root_notebooksand direct-childlist_sub_notebooksnavigation; tree navigation is not fake-paginated. - Error Handling: Complete error handling mechanism, including Joplin API errors, invalid pagination cursors, and network connection errors
- Auto-detection: Supports automatic detection of Joplin Web Clipper port (41184-41194)
MIT License
Issues and Pull Requests are welcome!
If you encounter problems, please:
- Check if Joplin Web Clipper is running normally
- Review error messages and logs
- Submit an Issue with detailed error information