joplin-mcp

mcp
Guvenlik Denetimi
Uyari
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.

SUMMARY

Joplin MCP server for AI assistants. Manage notes, notebooks, tags, search, and sync through 17 MCP tools. Docker/HTTP deployment.

README.md

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-testing instead of :latest.

Tip: If your Joplin Server is running on the same host machine, use host.docker.internal as the hostname in JOPLIN_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_URL is no longer an operator-facing variable — the entrypoint sets it internally to http://127.0.0.1:<JOPLIN_DATA_API_PORT> (default 41184).

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_data Docker 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.example file copied to .env and 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.combined uses node:22-bookworm-slim with separate build and production stages
  • Non-root user: joplin user (uid 1001) for all processes
  • Persistent volume: joplin_data volume mounted at /home/joplin/.config/joplin stores the Joplin profile and SQLite database
  • Loopback-only Data API: The Data API binds to 127.0.0.1:41184 inside 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) and 127.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 checks unit-tests and integration-tests (the previous test job 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
  1. AI Client connects to joplin-mcp via HTTP on port 3000 (MCP StreamableHTTP transport)
  2. joplin-mcp is a single container that runs both the stateless MCP HTTP server and the stateful Joplin Data API + sync scheduler
  3. JoplinDataClient in the MCP server issues HTTP requests to the Data API over loopback (127.0.0.1:41184) — no network proxy needed
  4. Data API binds to 127.0.0.1:41184 (loopback-only) inside the container, backed by a persistent SQLite volume
  5. Bash sync scheduler handles periodic sync via the Joplin CLI against Joplin Server
  6. Write operations persist to the local Joplin Data API and reach Joplin Server on the next scheduled sync (every SYNC_INTERVAL_SECONDS)
  7. 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 transient SQLITE_BUSY errors for
concurrent read requests from the MCP server. The MCP server automatically retries read
(GET) requests on SQLITE_BUSY with 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:

  1. Rename MCP_PORTMCP_HOST_PORT in your .env file (if set)
  2. Remove JOPLIN_CORE_URL from .env (no longer operator-facing; set automatically by the entrypoint)
  3. Stop the old stack and bring up the new one: docker compose down && docker compose up -d
  4. Volumes carry over — the joplin_data volume 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_notes returns empty results for notes created/edited via the Data API. Joplin's /search endpoint 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 through list_notes/read_note but are not findable through search_notes. This is a known upstream Joplin issue: https://github.com/laurent22/joplin/issues/11631. Until it is resolved, do not rely on search_notes to locate recently-written notes — use list_notes and 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 sync once 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_BUSY errors by serializing sync operations

Security Considerations

Token Management

The Joplin Data API uses two layers of token authentication:

  1. 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
  2. Session token (auth_token) — a short-lived token (~55 minutes) obtained automatically on startup via POST /auth. Used as a Bearer token in the Authorization header

The session token is managed by JoplinDataClient and stored in a GuardedString wrapper:

  • GuardedString stores the raw value in a private #value field, making it inaccessible through toString(), toJSON(), or template-literal coercion — all such operations return '[REDACTED]'
  • The only way to access the actual value is via the explicit .value property
  • 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 (see src/config.ts). NODE_ENV is 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 next POST /auth call

CLI Argument Sanitization

All Joplin CLI subcommands executed via CliExecutor are protected by two layers of defence:

  1. Subcommand whitelist — Only a predefined set of subcommands (sync, config, ls, cat, etc.) is allowed. Unknown subcommands are rejected before execution
  2. Shell metacharacter blocking — Arguments containing ;, |, &, $, `, (, ), {, }, <, >, \n are 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: maxConcurrency constructor parameter on JoplinDataClient
  • 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:

  1. Check container logs: docker compose logs
  2. Look for entries containing "Failed to obtain Joplin API token" or "AuthError"
  3. 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 joplin binary: The joplin CLI must be installed in the container and available on PATH. The Dockerfile handles this, but verify if using a custom setup
  • Config not set: The entrypoint script configures sync.target 10 and server credentials. If skipped, joplin sync will 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 JoplinDataClient in src/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):

  1. Validate environment variables — Checks JOPLIN_SERVER_URL, JOPLIN_USERNAME, JOPLIN_PASSWORD
  2. Configure Joplin CLI — Sets sync.target 10 and server credentials in Joplin CLI config
  3. Extract API token — Honours a pre-set JOPLIN_API_TOKEN from .env, or auto-extracts from the Joplin CLI config / settings.json
  4. Start Joplin Data APIjoplin server start binding to 127.0.0.1:41184 (loopback-only, no socat proxy)
  5. Wait for readiness — Polls /ping endpoint (up to 30 retries, 2s intervals)
  6. Perform initial syncjoplin sync with sync-error diagnostics
  7. Start periodic sync — Bash while true loop (runs in its own process group via setsid) with configurable SYNC_INTERVAL_SECONDS
  8. Start MCP HTTP servernode dist/mcp/entry.js on port 3000
  9. Liveness monitorwait -n on both child PIDs; exits non-zero if either dies (triggers Docker restart)
  10. 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

  1. Data API over CLI for data operations — Avoids fragile CLI output parsing; uses structured HTTP API with typed responses
  2. 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.
  3. Bash-based sync scheduler — Replaces the TypeScript SyncManager with a simple, reliable bash while true loop. Logs every sync with PASS/FAIL to /var/log/joplin/sync.log.
  4. 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
  5. Serialized sync queue — The combined container's bash sync loop serializes sync calls sequentially, preventing SQLITE_BUSY errors (the legacy TypeScript SyncManager provided the same guarantee but is not invoked in the combined container)
  6. Remote-wins conflict resolution — Delegated to Joplin CLI built-in behaviour; local changes always yield to remote
  7. Token lifecycle — Auth token obtained via POST /auth, reused with 60-second proactive refresh buffer before 55-minute expiry, re-fetched on 401 responses
  8. Token auto-extraction — The entrypoint extracts the API token from the Joplin CLI config, eliminating the manual docker logs retrieval ceremony

License

MIT

Yorumlar (0)

Sonuc bulunamadi