Skip to main content
Glama
README.md
# MCP Joplin Server

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.

## Features

- 🔍 **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

## Requirements

1. **Joplin Desktop** - Ensure it's installed and running
2. **Node.js 20** - Required to run the MCP server
3. **Web Clipper Enabled** - Enable Web Clipper service in Joplin

## Installation & Setup

### 1. Enable Joplin Web Clipper

1. Open Joplin desktop application
2. Go to **Tools → Options → Web Clipper**
3. Check **Enable Web Clipper Service**
4. Note the **port number** displayed (usually 41184)
5. **Copy the API Token** (if authentication is required)

### 2. Install MCP Joplin Server

```bash
# Clone or download this project
cd mcp-joplin

# Install dependencies
npm install

# Compile TypeScript
npm run build
```

### 3. Test Execution

```bash
# 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
```

### 4. Using npx

```bash
# 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:41184
```

### 5. Streamable HTTP Mode (Remote)

For MCP clients that support URL-based servers (Streamable HTTP), run the server as a standalone HTTP service:

```bash
# 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:http
```

The 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`)

## MCP Client Configuration

### Configure Perplexity or other MCP clients

Add the following configuration to your MCP client configuration file:

#### 🔐 Configuration (API Token Required)

This MCP server requires a Joplin API token to function properly:

```json
{
  "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-url` instead of `--port` when the Joplin API is exposed at a specific URL, for example through a tunnel or non-local host. `--base-url` can also be set with `JOPLIN_BASE_URL`.

### Claude Desktop Configuration Example

#### 🧩 stdio Configuration (local spawn)

In `~/Library/Application Support/Claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "joplin": {
      "command": "npx",
      "args": [
        "/Users/yourusername/path/to/mcp-joplin",
        "--token",
        "YOUR_API_TOKEN"
      ]
    }
  }
}
```

#### 🌐 Streamable HTTP Configuration (remote URL)

Requires `mcp-joplin-http` to be running as a separate process first.

```json
{
  "mcpServers": {
    "joplin": {
      "url": "http://localhost:3100/mcp"
    }
  }
}
```

> The `url` format lets the MCP host connect over HTTP without spawning a child process. The HTTP server must be started before the host launches.

## Available MCP Tools

### Pagination rules

The collection tools `search_notes`, `search_todo_notes`, `search_notebooks`, `list_notes`, `list_todo_notes`, and `get_note_revisions` use cursor-style pagination:

- `first` is optional and defaults to a safe page size; it never means "all results".
- `first` is capped at `100`.
- `after` is an opaque cursor returned as `endCursor` by the previous page.
- Responses include `pageInfo` metadata: `returnedCount`, `pageSize`, `hasNextPage`, and `endCursor` when another page exists.
- Cursors are scoped to the original request parameters. Use `endCursor` only with the same tool arguments.
- If `hasNextPage` is `true`, the response is incomplete. Continue with `after=endCursor` before concluding coverage.

### Search Semantics

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_notebooks` for hierarchy navigation over `search_notebooks` unless you need a specific name match.

### 1. get_note_content

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)
```

### 2. search_notes

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.

### 2.1. search_todo_notes

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 })
```

### 3. search_notebooks

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

### 4. list_root_notebooks

List top-level/root notebooks from the Joplin folder tree

```
Parameters: None
```

### 5. list_notes

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.

### 5.1. list_todo_notes

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" })
```

### 5.2. list_sub_notebooks

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

### 6. create_note

Create a new note

```
Parameters:
- title (string) - Note title
- body (string) - Note content (Markdown format)
- notebookId (string, optional) - Target notebook ID
```

### 6.1. create_todo_note

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" })
```

### 7. create_notebook

Create a new notebook

```
Parameters:
- title (string) - Notebook title
- parentId (string, optional) - Parent notebook ID (for sub-notebooks)
```

### 8. delete_note

Delete a note

```
Parameters:
- noteId (string) - ID of the note to delete
- permanent (boolean, optional) - Whether to permanently delete (default: false, moves to trash)
```

### 9. delete_notebook

Delete a notebook

```
Parameters:
- notebookId (string) - ID of the notebook to delete
- permanent (boolean, optional) - Whether to permanently delete (default: false, moves to trash)
```

### 10. move_note

Move a note to a different notebook

```
Parameters:
- noteId (string) - ID of the note to move
- targetNotebookId (string) - Target notebook ID
```

### 11. update_note

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 `title` or `body` must be provided
- Use this when replacing note content or renaming a note

### 12. append_to_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_note` when the intent is to add content without replacing the existing body

### 13. update_notebook

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

### 14. list_note_images

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.

### 15. get_note_image

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.

### 16. attach_image_to_note

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.

### 17. get_note_revisions

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

### 18. get_note_revision_content

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

### 19. Native todo lifecycle and conversion tools

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" })
```

## Editing Semantics

- Use `update_note` to replace a note's title and/or body
- Use `append_to_note` to add content to the end of a note while preserving existing content
- Use `move_note` to change which notebook a note belongs to
- Use `update_notebook` to rename a notebook
- Use native todo tools for Joplin todo-type notes; Markdown checkbox scanning is not part of this server's todo operations

## Usage Examples

### Conversation examples in AI clients:

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

## Troubleshooting

### Connection Issues

1. **Confirm Joplin is running**
   - Joplin desktop application must remain open

2. **Check Web Clipper settings**
   - Ensure Web Clipper service is enabled
   - Check port settings (default 41184)

3. **View error messages**
   ```bash
   # Use verbose mode to see errors
   DEBUG=* npm start
   ```

### Common Errors

- **"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 Required

**API Token is required for this MCP server to function properly.**

### Getting an API Token

1. In Joplin go to **Tools → Options → Web Clipper**
2. Copy the displayed **token**
3. Add `--token YOUR_TOKEN` to the startup command

## Development

```bash
# 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
```

## Technical Architecture

- **Language**: TypeScript/Node.js
- **MCP SDK**: @modelcontextprotocol/sdk (stdio + Streamable HTTP)
- **HTTP Client**: axios
- **CLI**: commander
- **API**: Joplin Web Clipper API

### Technical Details

- **Pagination Handling**: Collection tools request one Joplin page at a time using native `page` / `limit` parameters. They expose MCP-friendly `first` / `after` inputs 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 `/search` endpoint with `type=folder` and page-based pagination.
- **Notebook Navigation**: Uses Joplin's folder tree for `list_root_notebooks` and direct-child `list_sub_notebooks` navigation; 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)

## License

MIT License

## Contributing

Issues and Pull Requests are welcome!

## Support

If you encounter problems, please:

1. Check if Joplin Web Clipper is running normally
2. Review error messages and logs
3. Submit an Issue with detailed error information