joplin-mcp
Health Uyari
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 8 GitHub stars
Code Gecti
- Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Gecti
- Permissions — No dangerous permissions requested
Bu listing icin henuz AI raporu yok.
Joplin MCP server for AI assistants. Manage notes, notebooks, tags, search, and sync through 17 MCP tools. Docker/HTTP deployment.
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 the published container image. No repository clone required.
docker run -d \
--name joplin-mcp \
--restart unless-stopped \
-p 127.0.0.1:3000:3000 \
-v joplin_data:/home/joplin/.config/joplin \
-e JOPLIN_SERVER_URL=https://joplin.example.com/ \
-e [email protected] \
-e JOPLIN_PASSWORD=your-password \
ghcr.io/gelse/joplin-mcp:latest
Bleeding-edge builds: To test the latest (unreleased) build, use
ghcr.io/gelse/joplin-mcp:latest-testinginstead of:latest.
Tip: If your Joplin Server is running on the same host machine, use
host.docker.internalas the hostname inJOPLIN_SERVER_URL(e.g.,https://host.docker.internal:22300) so the container can reach it over Docker's built-in DNS.
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_API_TOKEN |
No | — | Joplin Data API token (auto-extracted when unset) |
JOPLIN_DATA_API_PORT |
No | 41184 |
Internal Data API listen port (rarely changed) |
LOG_LEVEL |
No | info |
Log level: debug, info, warn, error, silent |
SYNC_INTERVAL_SECONDS |
No | 300 |
Periodic sync interval in seconds |
MCP_HOST_PORT |
No | 3000 |
Host-side MCP port (mapped via -p 127.0.0.1:MCP_HOST_PORT:3000) |
JOPLIN_MASTER_PASSWORD |
No | — | E2EE master password (leave empty to skip encryption) |
Note:
JOPLIN_CORE_URLis no longer an operator-facing variable — the entrypoint sets it internally tohttp://127.0.0.1:<JOPLIN_DATA_API_PORT>(default41184).
MCP Client Configuration
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"
}
}
}
⚠️ End-to-End Encryption (E2EE)
If you have End-to-End Encryption (E2EE) enabled on your Joplin Server, the container must know the master password before it can encrypt notes for upload. Without it, writes appear to succeed locally but silently fail to reach the server — and the sync process will misleadingly report SYNC_PASS.
What E2EE means
When E2EE is enabled, Joplin encrypts all note content on the client before sending it to the server. The server only ever sees ciphertext — decryption happens client-side using the master password. This project's container acts as such a client, so it must have the master password configured.
Setting the master password
Preferred (declarative) — set via environment variable:
Add JOPLIN_MASTER_PASSWORD to your .env file (or pass it via docker compose run / docker run -e). The entrypoint configures the password on every fresh container start, before the initial sync.
Fallback (manual) — configure after container start:
# Set the master password inside the joplin-mcp container
docker exec joplin-mcp joplin config encryption.masterPassword 'THE_PASSWORD'
# Restart so the new config is picked up
docker restart joplin-mcp
Replace THE_PASSWORD with the same master password used when enabling E2EE on Joplin Server (or the one you chose if you enabled it from the CLI).
Tip: The password is persisted in the
joplin_dataDocker volume. When using the environment variable, the entrypoint re-applies it on every start — no manual step is needed after the first run.
⚠️ Warning: joplin e2ee decrypt does NOT persist the password
The command joplin e2ee decrypt -p 'PASSWORD' decrypts data for the current session only and does not store the password for future sync operations. Using it as your setup step will cause encrypted items to silently fail to upload on subsequent syncs. Always use joplin config encryption.masterPassword instead.
How to tell if E2EE is the problem
If you notice notes are missing from Joplin Server despite the container reporting SYNC_PASS, check whether E2EE is enabled on the server and whether the master password has been configured in the container.
Detailed How-To
Docker
Prerequisites
- Docker and Docker Compose installed on your system
- The
.env.examplefile copied to.envand configured with your Joplin Server credentials
The deployment uses a single combined Dockerfile (Dockerfile.combined) orchestrated via docker-compose.yml.
Building
# Build the combined container
docker compose build
Running
docker compose up -d # starts the combined joplin-mcp container
Viewing Logs
docker compose logs -f # combined container logs
docker compose logs -f joplin-mcp # same (single service)
Stopping
docker compose down
How It Works
- Single container:
joplin-mcp— one combined container runs the Joplin CLI + Data API (loopback-only), a bash periodic-sync loop, and the Node.js MCP HTTP server - Multi-stage builds:
Dockerfile.combinedusesnode:22-bookworm-slimwith separate build and production stages - Non-root user:
joplinuser (uid 1001) for all processes - Persistent volume:
joplin_datavolume mounted at/home/joplin/.config/joplinstores the Joplin profile and SQLite database - Loopback-only Data API: The Data API binds to
127.0.0.1:41184inside the container — no socat, no port proxy - Published port: Only port 3000 (MCP) is mapped to the host via
127.0.0.1:${MCP_HOST_PORT:-3000}:3000 - Healthchecks: The container healthcheck probes both
127.0.0.1:41184/ping(Data API) and127.0.0.1:3000/health(MCP server) - Graceful shutdown: The entrypoint traps
SIGTERM, drains the sync loop process group, stops the MCP server, performs a final sync, and exits cleanly
Testing
A dedicated Dockerfile.tests and test service in 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.
Container Integration Tests
End-to-end tests that run the full MCP stack in a Docker container using the integration-test stack (docker-compose.test.yml), built from Dockerfile.combined.
Prerequisites
- Docker and Docker Compose v2
Running
make test-integration
# or
./scripts/run-integration-tests.sh
What it tests
- MCP connection and tool discovery (17 tools)
- Note CRUD via MCP tools
- Folder CRUD via MCP tools
- Search and tag operations
- Error handling and validation
Architecture
Tests connect to joplin-mcp via @modelcontextprotocol/sdk StreamableHTTP transport.
The combined container runs with dummy sync credentials — no real Joplin Server is needed.
The API token is auto-extracted from the Joplin CLI config at startup.
Reports
- JUnit XML:
reports/container/junit.xml - Container logs:
reports/container/*.log
CI/CD
Four GitHub Actions workflows automate testing and releases:
| Workflow | Trigger | Runner | Description |
|---|---|---|---|
unit-tests.yml |
Push / PR to main |
Ubuntu (native) | Installs dependencies via pnpm, runs pnpm test |
integration-tests.yml |
PRs to main |
Ubuntu (native) | Runs container integration tests via scripts/run-integration-tests.sh |
publish-testing.yml |
Push to testing |
Ubuntu (native) | Runs unit and integration tests, then uploads a Docker image to ghcr.io/gelse/joplin-mcp:latest-testing (upload only if both test jobs pass — no GitHub release) |
release.yml |
Release published / manual dispatch | Ubuntu (native) | Verifies lockfile reproducibility, then builds Dockerfile.combined and pushes to ghcr.io/gelse/joplin-mcp with semver + latest tags |
The publish-testing.yml workflow gates the image upload behind both the unit and integration test jobs — the publish job runs only when both test jobs succeed. It uploads a linux/amd64 image tagged as latest-testing; this is an upload, not a release, so it does not create a GitHub release or use versioned tags. The release.yml workflow remains the sole owner of versioned tags and the latest tag.
The release workflow builds a single linux/amd64 image and tags it as {{version}}, {{major}}.{{minor}}, {{major}}, and latest (when appropriate). A pre-build step runs pnpm install --frozen-lockfile to confirm the lockfile is reproducible before the Docker build begins.
The release workflow also supports workflow_dispatch for manual triggers — useful for the initial GHCR publish when the package does not yet exist (semver tags won't resolve on manual dispatch, but the latest fallback tag ensures the image is always pushed with at least one valid tag).
Branch protection: If branch protection rules are configured on
main, add the required status checksunit-testsandintegration-tests(the previoustestjob name no longer exists).
Architecture
Single-Container Deployment (Docker)
graph TD
A[AI Client] -->|"MCP HTTP (port 3000)"| B[joplin-mcp container]
subgraph "joplin-mcp (single container)"
B[MCP HTTP Server :3000]
C[Joplin Data API :41184] -->|"read/write"| D[(Joplin SQLite DB)]
E[Bash Sync Scheduler] -->|"joplin sync"| F[Joplin Server]
F -->|"HTTPS"| E
end
B -->|"loopback 127.0.0.1:41184"| C
- AI Client connects to joplin-mcp via HTTP on port 3000 (MCP StreamableHTTP transport)
- joplin-mcp is a single container that runs both the stateless MCP HTTP server and the stateful Joplin Data API + sync scheduler
- JoplinDataClient in the MCP server issues HTTP requests to the Data API over loopback (
127.0.0.1:41184) — no network proxy needed - Data API binds to
127.0.0.1:41184(loopback-only) inside the container, backed by a persistent SQLite volume - Bash sync scheduler handles periodic sync via the Joplin CLI against Joplin Server
- Write operations persist to the local Joplin Data API and reach Joplin Server on the next scheduled sync (every
SYNC_INTERVAL_SECONDS) - The container uses a single healthcheck probing both the Data API (
/ping) and the MCP server (/health)
Note: SQLITE_BUSY during sync — During periodic sync windows, the Joplin CLI holds a
write lock on the SQLite database, which can cause transientSQLITE_BUSYerrors for
concurrent read requests from the MCP server. The MCP server automatically retries read
(GET) requests onSQLITE_BUSYwith exponential backoff (up to 3 retries). Write requests
(POST/PUT/DELETE) are not retried to avoid duplicate resource creation. This is an
inherent limitation of the two-process architecture (Data API + sync CLI sharing one
SQLite database), now running within a single container. The long-term fix is a single
long-lived process (GitHub Issue #2, Topic 6).
Migration from Two-Container Setup
If migrating from the previous two-container deployment:
- Rename
MCP_PORT→MCP_HOST_PORTin your.envfile (if set) - Remove
JOPLIN_CORE_URLfrom.env(no longer operator-facing; set automatically by the entrypoint) - Stop the old stack and bring up the new one:
docker compose down && docker compose up -d - Volumes carry over — the
joplin_datavolume and uid 1001 are unchanged; your existing notes, E2EE master password, and sync config survive the migration
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 |
Report current sync status | 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[] |
⚠️ Known limitation:
search_notesreturns empty results for notes created/edited via the Data API. Joplin's/searchendpoint reads from a SQLite full-text-search (FTS) index that the headless CLI (joplin server start) does not build or update. Notes created/edited via the Data API remain readable throughlist_notes/read_notebut are not findable throughsearch_notes. This is a known upstream Joplin issue: https://github.com/laurent22/joplin/issues/11631. Until it is resolved, do not rely onsearch_notesto locate recently-written notes — uselist_notesand filter client-side instead.
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 } |
Sync Tool
| Tool | Input | Output |
|---|---|---|
sync |
{} |
{ status: "idle" | "syncing", lastSyncTime: string | null } |
Error Response Format
When a tool execution fails, the MCP server returns a response with isError: true and a content array containing a single text entry:
{
"content": [{ "type": "text", "text": "Error message describing the failure" }],
"isError": true
}
Validation errors (Zod schema mismatch) are logged at warn level and include the specific field path and reason, for example:
Validation error: note_id: Expected 32-character hex ID
Execution errors (API failures, timeouts, etc.) are logged at error level and include the tool name and error message. See the Error Handling section for the full error class hierarchy.
Sync Behaviour
- Initial sync: The entrypoint runs
joplin synconce before starting the MCP server; no SyncManager is involved - Periodic sync: Every 5 minutes (configurable via
SYNC_INTERVAL_SECONDS) - Scheduled sync: Every create/update/delete/untag operation is picked up by the periodic scheduler (within ≤
SYNC_INTERVAL_SECONDS) - Conflict resolution: Remote always wins (Joplin CLI built-in behaviour; conflicted copies are flagged in Joplin)
- Serialized queue: Prevents
SQLITE_BUSYerrors by serializing sync operations
Security Considerations
Token Management
The Joplin Data API uses two layers of token authentication:
- API token (
JOPLIN_API_TOKEN) — a static token passed as a query parameter (?token=...) to every Data API request. Auto-extracted from the Joplin CLI config on startup, or set via.env - Session token (
auth_token) — a short-lived token (~55 minutes) obtained automatically on startup viaPOST /auth. Used as aBearertoken in theAuthorizationheader
The session token is managed by JoplinDataClient and stored in a GuardedString wrapper:
GuardedStringstores the raw value in a private#valuefield, making it inaccessible throughtoString(),toJSON(), or template-literal coercion — all such operations return'[REDACTED]'- The only way to access the actual value is via the explicit
.valueproperty - This prevents accidental leakage through logging, serialisation, or error messages
- Tokens are proactively refreshed 60 seconds before expiry and re-fetched automatically on 401 responses
TLS Requirements for Production
- The Joplin Data API always binds to
127.0.0.1(localhost-only inside the container), so TLS between the MCP server and the Data API is unnecessary — traffic never leaves the container - The MCP server accepts any URL scheme for
JOPLIN_SERVER_URL— there is no protocol enforcement in the config schema (seesrc/config.ts).NODE_ENVis not read by the config and has no effect. If you expose the MCP endpoint beyond loopback, front it with a TLS-terminating reverse proxy yourself - Joplin CLI sync traffic to Joplin Server is plain HTTP by default; ensure your Joplin Server is deployed behind a TLS-terminating reverse proxy
Localhost-Only Defaults
- The Data API binds to
127.0.0.1:41184(loopback-only) inside the container — it is unreachable from outside the container - Only port 3000 (MCP) is published to the host, bound to
127.0.0.1— it is only accessible from the local machine - The Data API is never exposed to the Docker network or the host — all MCP→Data API communication happens over loopback within the container
Token Rotation Best Practices
- The Joplin Data API issues tokens with a configurable expiry (default ~55 minutes, controlled by the Joplin Data API server)
- The client automatically refreshes the token before expiry and on 401 responses
- If a token compromise is suspected, rotate the Joplin Server credentials (
JOPLIN_PASSWORD) and restart the container — a new token will be issued on the nextPOST /authcall
CLI Argument Sanitization
All Joplin CLI subcommands executed via CliExecutor are protected by two layers of defence:
- Subcommand whitelist — Only a predefined set of subcommands (sync, config, ls, cat, etc.) is allowed. Unknown subcommands are rejected before execution
- Shell metacharacter blocking — Arguments containing
;,|,&,$,`,(,),{,},<,>,\nare rejected
These checks are defence-in-depth on top of Node.js execFile, which does not spawn a shell.
Error Handling
Error
├── ConfigError # Missing/invalid environment variables
├── CliError # Joplin CLI subprocess failure
│ └── Properties: result { stdout, stderr, exitCode }
├── DataApiError # Joplin Data API HTTP error
│ ├── statusCode: number
│ ├── responseBody?: string
│ ├── NotFoundError (404) # Resource not found
│ ├── ConflictError (409) # Resource modified since fetch
│ ├── ValidationError (400)# Invalid input
│ └── AuthError (401) # Authentication failed
└── FatalError # Fatal/unexpected error
├── cause?: unknown
└── exitCode: number (default 1)
Rate Limiting
The internal Joplin Data API HTTP client (JoplinDataClient) enforces a configurable concurrency limit to prevent overwhelming the Data API process:
- Default max concurrency: 5 concurrent requests
- Configurable via:
maxConcurrencyconstructor parameter onJoplinDataClient - Behaviour: When the limit is reached, additional requests are queued and executed as soon as a slot becomes available
- Scope: All Data API calls (list, get, create, update, delete, search) share the same concurrency pool
- Per-tool: Individual tool calls make a single Data API request, so concurrency is only relevant under parallel MCP requests
Troubleshooting
Authentication Failures
Symptom: MCP tools return "Authentication failed" or AuthError.
Causes and fixes:
| Cause | Fix |
|---|---|
Invalid JOPLIN_PASSWORD in .env |
Verify the password matches your Joplin Server account |
| Token expired before refresh | Check that the system clock is synchronised (NTP). The client refreshes tokens proactively, but clock drift can cause premature expiry |
| Joplin Server unreachable | Ensure JOPLIN_SERVER_URL is correct and the server is running. Verify TLS certificate if using HTTPS |
| Data API not ready | Wait for the "Data API is healthy" log line before sending requests |
Diagnostic steps:
- Check container logs:
docker compose logs - Look for entries containing
"Failed to obtain Joplin API token"or"AuthError" - Verify credentials by curling the Joplin Server API directly
Sync Conflicts
Symptom: Logs contain "Sync conflicts detected — remote version retained" warnings.
Behaviour: The system uses a remote-wins conflict resolution strategy. Local changes always yield to remote versions.
What to do:
- Conflict notes are flagged in Joplin as conflicted copies. Check for them using the Joplin desktop/client app
- You can programmatically check conflict count via
CliExecutor.checkConflicts() - To resolve, review the conflicted notes in Joplin and manually merge or delete them
Timeout Issues
Symptom: CLI commands fail with "joplin CLI timed out after Nms".
Causes and fixes:
| Cause | Fix |
|---|---|
| Large initial sync (many notes/resources) | Increase SYNC_INTERVAL_SECONDS or let the initial sync complete — subsequent syncs are incremental |
| Joplin Server slow to respond | Check Joplin Server performance (CPU, memory, database). Ensure network latency is low |
| CLI command timeout too short | The default timeout is 60 seconds; for extremely large operations, this can be adjusted in CliExecutor.exec() |
CLI Execution Errors
Symptom: CliError with exit code, stdout, and stderr details.
Common causes:
- Missing
joplinbinary: ThejoplinCLI must be installed in the container and available onPATH. The Dockerfile handles this, but verify if using a custom setup - Config not set: The entrypoint script configures
sync.target 10and server credentials. If skipped,joplin syncwill fail with a configuration error - Permission errors: Ensure the Joplin CLI config directory (
~/.config/joplin) is writable
Rate Limiting
Symptom: Requests are queued or take longer than expected, but no errors are thrown.
Behaviour: The JoplinDataClient enforces a maximum of 5 concurrent API requests (configurable). Additional requests are queued and processed sequentially as slots open up.
If you hit concurrency limits:
- Reduce the number of parallel MCP tool calls from your AI client
- The concurrency limit is a constructor parameter on
JoplinDataClientinsrc/data-client.ts. Increase it if you have a specific need for higher parallelism, but be aware of the Data API's own capacity
Development
Commands
pnpm install # Install dependencies
pnpm dev # Run in development mode with hot reload (tsx watch)
pnpm build # Compile TypeScript (tsc)
pnpm start # Run compiled server (node dist/mcp/entry.js)
pnpm test # Run tests (vitest)
pnpm test:watch # Run tests in watch mode
pnpm lint # Lint source code (eslint)
pnpm format # Format source code (prettier --write)
Project Structure
src/
├── config.ts # Zod-based environment config parsing
├── logger.ts # Pino structured logger
├── cli-executor.ts # Joplin CLI subprocess wrapper
├── sync-manager.ts # Serialized sync queue orchestrator (legacy — not used in combined container; sync is handled by the bash loop)
├── data-client.ts # Joplin Data API HTTP client (26 methods, token auth)
├── api-types.ts # TypeScript type definitions for Joplin API
├── errors.ts # Typed error class hierarchy
├── guarded-string.ts # Secure string wrapper (prevents accidental secret leakage)
├── pagination.ts # Pagination helpers (clampLimit, fetchAllPages)
└── mcp/
├── entry.ts # MCP HTTP server entrypoint
├── server.ts # MCP server setup (stdio + StreamableHTTP transport)
├── schemas.ts # Zod validation schemas for all 17 tools
├── tools.ts # 17 tool handler implementations
└── tool-registry.ts # Tool registration and dispatch
tests/
├── cli-executor.test.ts # CLI executor tests
├── config.test.ts # Config parser tests
├── data-client.test.ts # Data API client tests
├── errors.test.ts # Error class hierarchy tests
├── integration.test.ts # Integration tests against live Joplin Data API
├── logger.test.ts # Logger tests
├── pagination.test.ts # Pagination helper tests
├── sync-manager.test.ts # Sync manager tests
└── mcp/
├── schemas.test.ts # Zod schema validation tests
├── server.test.ts # MCP server lifecycle tests
├── tool-registry.test.ts # Tool registration & dispatch tests
└── tools.test.ts # Tool handler tests
docs/ # Project documentation (see root SBOM.md for dependency inventory)
scripts/
└── smoke-test.sh # Docker container smoke test (checks container up + Data API /ping)
Root-level deployment files:
| File | Purpose |
|---|---|
Dockerfile.combined |
Production: combined Joplin CLI + Data API + MCP HTTP server |
Dockerfile.tests |
Test runner container |
entrypoint-combined.sh |
Production entrypoint: Data API + sync loop + MCP server with graceful shutdown |
docker-compose.yml |
Single-service orchestration with healthchecks |
docker-compose.test.yml |
Integration-test stack (uses Dockerfile.combined, used by make test-integration) |
Startup & Shutdown Pipeline
Production (Single Container)
Combined joplin-mcp container (entrypoint-combined.sh):
- Validate environment variables — Checks
JOPLIN_SERVER_URL,JOPLIN_USERNAME,JOPLIN_PASSWORD - Configure Joplin CLI — Sets
sync.target 10and server credentials in Joplin CLI config - Extract API token — Honours a pre-set
JOPLIN_API_TOKENfrom.env, or auto-extracts from the Joplin CLI config /settings.json - Start Joplin Data API —
joplin server startbinding to127.0.0.1:41184(loopback-only, no socat proxy) - Wait for readiness — Polls
/pingendpoint (up to 30 retries, 2s intervals) - Perform initial sync —
joplin syncwith sync-error diagnostics - Start periodic sync — Bash
while trueloop (runs in its own process group viasetsid) with configurableSYNC_INTERVAL_SECONDS - Start MCP HTTP server —
node dist/mcp/entry.json port 3000 - Liveness monitor —
wait -non both child PIDs; exits non-zero if either dies (triggers Docker restart) - Handle signals — On
SIGTERM/SIGINT: kill sync loop group, stop MCP server, stop Data API, perform final sync, exit 0
Integration-Test Stack
The integration-test stack (docker-compose.test.yml) uses the same Dockerfile.combined as production. This is used exclusively by make test-integration.
Key Design Decisions
- Data API over CLI for data operations — Avoids fragile CLI output parsing; uses structured HTTP API with typed responses
- Single combined container — All components (Data API, MCP server, sync scheduler) run in one container. Communication happens over loopback (
127.0.0.1:41184), eliminating the need for Docker internal networking or a socat proxy. Only port 3000 is published to the host. - Bash-based sync scheduler — Replaces the TypeScript SyncManager with a simple, reliable bash
while trueloop. Logs every sync with PASS/FAIL to/var/log/joplin/sync.log. - Scheduled sync — Write tools persist to the local Data API; the bash scheduler (sole sync mechanism in the combined container) pushes changes to Joplin Server within
SYNC_INTERVAL_SECONDS - Serialized sync queue — The combined container's bash sync loop serializes sync calls sequentially, preventing
SQLITE_BUSYerrors (the legacy TypeScriptSyncManagerprovided the same guarantee but is not invoked in the combined container) - Remote-wins conflict resolution — Delegated to Joplin CLI built-in behaviour; local changes always yield to remote
- Token lifecycle — Auth token obtained via
POST /auth, reused with 60-second proactive refresh buffer before 55-minute expiry, re-fetched on 401 responses - Token auto-extraction — The entrypoint extracts the API token from the Joplin CLI config, eliminating the manual
docker logsretrieval ceremony
License
MIT
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi