Install
$ agentstack add mcp-gelse-joplin-mcp ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ● Shell / process execution Used
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Joplin API MCP Server
An MCP (Model Context Protocol) server that exposes Joplin's note-taking functionality — notes, folders, tags, search, and sync — to AI assistants via 17 tools.
tl;dr / Quick Start
Docker (recommended)
The recommended deployment uses two containers — a stateful sync backend ([joplin-core](#architecture)) and a stateless MCP HTTP server ([joplin-mcp](#architecture)) — orchestrated via [docker-compose.yml](docker-compose.yml):
cp .env.example .env # fill in required variables
docker compose up -d # starts both containers with healthchecks
Add this to your MCP client config:
{
"mcpServers": {
"joplin": {
"url": "http://localhost:3000/mcp"
}
}
}
> joplin-mcp exposes an HTTP endpoint on port 3000 (not stdio). See [MCP Client Configuration](#mcp-client-configuration) for other setups.
Native Installation
For local development or without Docker, the MCP HTTP server ([src/mcp/entry.ts](src/mcp/entry.ts)) connects to a running Joplin Data API:
git clone && cd joplin-api
cp .env.example .env # fill in JOPLIN_API_TOKEN and JOPLIN_CORE_URL
pnpm install
pnpm build && pnpm start
MCP client config (HTTP):
{
"mcpServers": {
"joplin": {
"url": "http://localhost:3000/mcp"
}
}
}
Detailed How-To
Direct Installation
Prerequisites
- Node.js 20 or later (the project's [
package.json](package.json)enginesfield requires>=22.0.0) - pnpm 9 or later (for package management)
- Joplin desktop app running with the Data API (ClipperServer) enabled:
- In Joplin: Web Clipper → Options → Enable Clipper Server
- The server binds to
127.0.0.1:41184by default and ignores--host/--portflags - Joplin Server (optional but recommended) — a sync target for multi-device synchronisation. Without it, write-through sync will fail and notes remain local-only
Installation
git clone
cd joplin-api
pnpm install
pnpm build
Configuration
Copy the environment template and fill in your values:
cp .env.example .env
All configuration is done via environment variables:
| Variable | Required | Default | Description | | ----------------------- | -------- | ------- | ------------------------------------------------------------ | | JOPLIN_SERVER_URL | Yes | — | Joplin Server URL (e.g., https://joplin.example.com/) | | JOPLIN_USERNAME | Yes | — | Joplin Server username/email | | JOPLIN_PASSWORD | Yes | — | Joplin Server password | | JOPLIN_DATA_API_PORT | No | 41184 | Internal Data API listen port (Joplin ClipperServer hardcoded default) | | LOG_LEVEL | No | info | Log level: debug, info, warn, error, silent | | SYNC_INTERVAL_SECONDS | No | 300 | Periodic sync interval in seconds | | NODE_ENV | No | — | Set to production to enforce HTTPS for JOPLIN_SERVER_URL |
> Note: JOPLIN_API_TOKEN is not a user-facing variable. The [core entrypoint script](entrypoint-core.sh) automatically extracts it from Joplin's config (joplin config api.token) and exports it for the server. If running natively without the entrypoint, you must set JOPLIN_API_TOKEN manually (run joplin config api.token in your terminal to get it).
Running the Server
# Production (compiled)
pnpm build && pnpm start
# Development (hot reload via tsx watch)
pnpm dev
The MCP HTTP server connects to a running Joplin Data API (via JOPLIN_CORE_URL), validates connectivity, then begins serving MCP requests over HTTP on port 3000 (configurable via MCP_PORT).
MCP Client Configuration (Native / Node.js)
The MCP HTTP server exposes an HTTP endpoint (not stdio). Configure your MCP client to connect via URL:
{
"mcpServers": {
"joplin": {
"url": "http://localhost:3000/mcp"
}
}
}
Testing
# Run all tests
pnpm test
# Watch mode
pnpm test:watch
# Lint
pnpm lint
# Format
pnpm format
Tests use Vitest and cover all modules: config parsing, CLI executor, data client, error classes, sync manager, pagination, MCP schemas, tool handlers, and integration tests against a live Joplin Data API.
Docker
Prerequisites
- Docker and Docker Compose installed on your system
- The [
.env.example](.env.example) file copied to.envand configured with your Joplin Server credentials
The deployment uses two Dockerfiles ([Dockerfile.core](Dockerfile.core) and [Dockerfile.mcp](Dockerfile.mcp)) orchestrated via [docker-compose.yml](docker-compose.yml).
Building
# Build both containers
docker compose build
# Or build individually
docker compose build joplin-core
docker compose build joplin-mcp
Running
docker compose up -d # starts joplin-core first, then joplin-mcp after healthcheck passes
Viewing Logs
docker compose logs -f # both containers
docker compose logs -f joplin-core # sync/Data API logs only
docker compose logs -f joplin-mcp # MCP HTTP server logs only
Stopping
docker compose down
Environment Variables
Place variables in the .env file (automatically picked up by [docker-compose.yml](docker-compose.yml)).
| Variable | Container | Required | Default | Description | | ----------------------- | --------------- | -------- | ------- | ------------------------------------------------------------ | | JOPLIN_SERVER_URL | joplin-core | Yes | — | Joplin Server URL (e.g., https://joplin.example.com/) | | JOPLIN_USERNAME | joplin-core | Yes | — | Joplin Server username/email | | JOPLIN_PASSWORD | joplin-core | Yes | — | Joplin Server password | | JOPLIN_API_TOKEN | both | Yes | — | Joplin Data API token (extracted from joplin config api.token) | | JOPLIN_CORE_URL | joplin-mcp | Yes | — | URL of the joplin-core Data API (e.g., http://joplin-core:41184) | | JOPLIN_DATA_API_PORT | joplin-core | No | 41184 | Internal Data API listen port | | MCP_PORT | joplin-mcp | No | 3000 | MCP HTTP server port (exposed to host) | | LOG_LEVEL | both | No | info | Log level: debug, info, warn, error, silent | | SYNC_INTERVAL_SECONDS | joplin-core | No | 300 | Periodic sync interval in seconds | | NODE_ENV | joplin-core | No | — | Set to production to enforce HTTPS for JOPLIN_SERVER_URL |
MCP Client Configuration (Docker)
The joplin-mcp container exposes an HTTP endpoint (not stdio). Configure your MCP client to connect via URL:
{
"mcpServers": {
"joplin": {
"url": "http://localhost:3000/mcp"
}
}
}
How It Works
- Two containers:
joplin-core(stateful, runs Joplin CLI + Data API + bash sync scheduler) andjoplin-mcp(stateless, Node.js MCP HTTP server only) - Multi-stage builds: [
Dockerfile.mcp](Dockerfile.mcp) usesnode:22-bookworm-slimwith separate build and production stages; [Dockerfile.core](Dockerfile.core) is a single-stage Debian-based image - Non-root users:
joplinuser in joplin-core,mcpuser in joplin-mcp - Persistent volume:
joplin_datavolume mounted at/home/joplin/.config/joplinstores the Joplin profile and SQLite database - Internal networking: joplin-mcp communicates with joplin-core via the Docker internal network using the service name
joplin-core - Healthchecks: joplin-core healthchecks
/pingon port 41184; joplin-mcp waits for joplin-core to be healthy before starting - Sync scheduler: A bash
while trueloop in [entrypoint-core.sh](entrypoint-core.sh) handles periodic sync with extensive logging to/var/log/joplin/
Testing with Docker
A dedicated [Dockerfile.tests](Dockerfile.tests) and test service in [docker-compose.yml](docker-compose.yml) allow running the test suite in a container:
# Build the test image
docker build -f Dockerfile.tests -t joplin-api-tests .
# Run tests
docker run --rm joplin-api-tests
# Or via docker compose (requires --profile test since the test service uses profiles)
docker compose --profile test run --rm tests
Tests use Vitest with v8 coverage (thresholds: 70% statements, 60% branches, 70% functions, 70% lines) and output JUnit XML reports to ./reports/. When running via docker compose, the ./reports directory is mounted into the container so reports persist on the host.
The test suite does not require a running Joplin instance — unit tests use mocks, and integration tests are skipped when the Joplin Data API is unavailable.
Architecture
Two-Container Deployment (Docker)
graph TD
A[AI Client] -->|"MCP HTTP (port 3000)"| B[Container B: joplin-mcp]
B -->|"HTTP fetch() + Bearer Token"| C[Container A: joplin-core]
subgraph "Container A: joplin-core"
C[Joplin Data API :41184] -->|"read/write"| D[(Joplin SQLite DB)]
E[Bash Sync Scheduler] -->|"joplin sync"| F[Joplin Server]
F -->|"HTTPS"| E
end
subgraph "Container B: joplin-mcp"
B -->|"Tool handlers"| G[JoplinDataClient]
end
- AI Client connects to joplin-mcp via HTTP on port 3000 (MCP StreamableHTTP transport)
- joplin-mcp is a stateless Node.js server — no Joplin CLI, no sync logic, no local DB
- JoplinDataClient in joplin-mcp issues HTTP requests to joplin-core on the internal Docker network
- joplin-core runs the Joplin Data API internally on
127.0.0.1:41185, with a socat TCP proxy exposing0.0.0.0:41184to the Docker network, backed by a persistent SQLite volume - Bash sync scheduler in joplin-core handles periodic sync via the Joplin CLI against Joplin Server
- Write operations from the MCP server trigger sync via the Data API; bash scheduler provides periodic backup sync
- Both containers use healthchecks — joplin-mcp waits for joplin-core to be healthy before starting
Available MCP Tools
Tool Overview
| Tool | Description | Writes? | | ---------------- | ------------------------------------------------- | ------- | | list_notebooks | List all notebooks/folders | No | | list_notes | List notes with pagination and metadata fields | No | | search_notes | Search notes, folders, and tags | No | | read_note | Read a single note by ID | No | | read_notebook | Read a single notebook by ID | No | | read_multinote | Read multiple notes by IDs | No | | read_tags | Get tags for a note | No | | create_note | Create a new note | Yes | | create_folder | Create a new notebook | Yes | | edit_note | Edit an existing note | Yes | | edit_folder | Edit an existing folder | Yes | | create_tag | Create a new tag | Yes | | tag_note | Apply a tag to a note | Yes | | untag_note | Remove a tag from a note | Yes | | delete_note | Delete a note | Yes | | delete_folder | Delete a folder | Yes | | sync | Manually trigger sync | No |
Input / Output Schemas
All tool input is validated through Zod schemas. Below are the expected input fields and return types.
Read Tools
| Tool | Input | Output | | ---------------- | ---------------------------------------------------------------------- | ------------------------------------------------- | | list_notebooks | {} | Folder[] | | list_notes | { limit?: number (1–100), page?: number (≥1) } | { items: Note[], has_more: boolean } | | search_notes | { query: string (1–1000 chars), type?: "note" \| "folder" \| "tag" } | SearchResult[] | | read_note | { note_id: string (32-char hex) } | Note | | read_notebook | { notebook_id: string (32-char hex) } | Folder | | read_multinote | { note_ids: string[] (array of 32-char hex IDs) } | { notes: Note[], errors: { note_id, error }[] } | | read_tags | { note_id: string (32-char hex) } | Tag[] |
Write Tools
| Tool | Input | Output | | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | create_note | { title (1–500 chars), parent_id?, body? (max 1 MB), author? (max 200), source_url? (validated URL), is_todo? (boolean \| number 0/1), todo_due? (unix ms) } | Note | | create_folder | { title (1–500 chars), parent_id?, icon? (max 100) } | Folder | | edit_note | { note_id, title?, parent_id?, body?, author? (max 200), source_url? (validated URL), is_todo? (boolean \| number 0/1), todo_due? (unix ms) } | Note | | edit_folder | { folder_id, title?, parent_id?, icon? (max 100) } | Folder | | create_tag | { title (1–200 chars) } | Tag | | tag_note | { note_id, tag_id } | NoteTag | | untag_note | { note_id, tag_id } | { success: true } |
Delete Tools
| Tool | Input | Output | | --------------- | --------------- | ------------------- | | delete_note | { note_id } | { success: true } | | delete_folder | { folder_id } | { success: true } |
Sy
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: gelse
- Source: gelse/joplin-mcp
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.