内容
# Joplin API MCP 服务器
MCP(模型上下文协议)服务器,暴露 Joplin 的记笔记功能 — 笔记、文件夹、标签、搜索和同步 — 通过 17 种工具提供给 AI 助手。
## 简介 / 快速开始
### Docker(推荐)
推荐的部署方式使用两个容器 — 一个有状态的同步后端([`joplin-core`](#架构))和一个无状态的 MCP HTTP 服务器([`joplin-mcp`](#架构))— 通过 [`docker-compose.yml`](docker-compose.yml) 协调:
```bash
cp .env.example .env # 填写必填变量
docker compose up -d # 使用健康检查启动两个容器
```
将以下内容添加到您的 MCP 客户端配置中:
```json
{
"mcpServers": {
"joplin": {
"url": "http://localhost:3000/mcp"
}
}
}
```
> `joplin-mcp` 在端口 3000 上公开一个 HTTP 端点(不是 stdio)。有关其他设置,请参见 [MCP 客户端配置](#mcp-客户端配置)。
### 原生安装
对于本地开发或不使用 Docker,MCP HTTP 服务器([`src/mcp/entry.ts`](src/mcp/entry.ts))连接到正在运行的 Joplin 数据 API:
```bash
git clone <仓库网址> && cd joplin-api
cp .env.example .env # 填写 JOPLIN_API_TOKEN 和 JOPLIN_CORE_URL
pnpm install
pnpm build && pnpm start
```
MCP 客户端配置(HTTP):
```json
{
"mcpServers": {
"joplin": {
"url": "http://localhost:3000/mcp"
}
}
}
```
---
## 详细步骤
### 直接安装
#### 先决条件
- **Node.js** 20 或更高版本(项目的 [`package.json`](package.json) `engines` 字段需要 `>=22.0.0`)
- **[pnpm](https://pnpm.io/)** 9 或更高版本(用于包管理)
- **Joplin 桌面应用程序** 运行并启用 **数据 API(剪贴板服务器)**:
- 在 Joplin 中:*网页剪贴 → 选项 → 启用剪贴板服务器*
- 服务器默认绑定到 `127.0.0.1:41184` 并忽略 `--host`/`--port` 标志
- **Joplin 服务器**(可选但推荐)— 用于多设备同步的同步目标。没有它,写入同步将失败,笔记将仅限本地使用
#### 安装
```bash
git clone <仓库网址>
cd joplin-api
pnpm install
pnpm build
```
#### 配置
复制环境模板并填写您的值:
```bash
cp .env.example .env
```
所有配置都通过环境变量完成:
| 变量 | 必填 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `JOPLIN_SERVER_URL` | **是** | — | Joplin 服务器网址(例如,`https://joplin.example.com/`) |
| `JOPLIN_USERNAME` | **是** | — | Joplin 服务器用户名/邮箱 |
| `JOPLIN_PASSWORD` | **是** | — | Joplin 服务器密码 |
| `JOPLIN_DATA_API_PORT` | 否 | `41184` | 内部数据 API 监听端口(Joplin 剪贴板服务器硬编码默认值) |
| `LOG_LEVEL` | 否 | `info` | 日志级别:`debug`、`info`、`warn`、`error`、`silent` |
| `SYNC_INTERVAL_SECONDS` | 否 | `300` | 周期性同步间隔(秒) |
| `NODE_ENV` | 否 | — | 设置为 `production` 以强制 `JOPLIN_SERVER_URL` 使用 HTTPS |
> **注意:** `JOPLIN_API_TOKEN` 不是面向用户的变量。[核心入口点脚本](entrypoint-core.sh) 自动从 Joplin 的配置(`joplin config api.token`)中提取它并为服务器导出。如果在没有入口点的情况下原生运行,您必须手动设置 `JOPLIN_API_TOKEN`(在终端中运行 `joplin config api.token` 以获取它)。
#### 运行服务器
```bash
# 生产环境(编译)
pnpm build && pnpm start
# 开发环境(热重载 via tsx 监视)
pnpm dev
```
MCP HTTP 服务器连接到正在运行的 Joplin 数据 API(通过 `JOPLIN_CORE_URL`),验证连接,然后开始在端口 3000 上通过 HTTP 提供 MCP 请求(可通过 `MCP_PORT` 配置)。
#### MCP 客户端配置(原生 / Node.js)
MCP HTTP 服务器公开一个 **HTTP 端点**(不是 stdio)。配置您的 MCP 客户端通过 URL 连接:
```json
{
"mcpServers": {
"joplin": {
"url": "http://localhost:3000/mcp"
}
}
}
```
#### 测试
```bash
# 运行所有测试
pnpm test
# 监视模式
pnpm test:watch
# 校验
pnpm lint
# 格式化
pnpm format
```
测试使用 [Vitest](https://vitest.dev/) 并涵盖所有模块:配置解析、CLI 执行器、数据客户端、错误类、同步管理器、分页、MCP 模式、工具处理程序和针对实时 Joplin 数据 API 的集成测试。
### Docker
#### 先决条件
- **Docker** 和 **Docker Compose** 已安装在您的系统上
- [.env.example](.env.example) 文件已复制到 `.env` 并使用您的 Joplin 服务器凭证配置
部署使用两个 Dockerfile([`Dockerfile.core`](Dockerfile.core) 和 [`Dockerfile.mcp`](Dockerfile.mcp))通过 [`docker-compose.yml`](docker-compose.yml) 协调。
#### 构建
```bash
# 构建两个容器
docker compose build
# 或单独构建
docker compose build joplin-core
docker compose build joplin-mcp
```
#### 运行
```bash
docker compose up -d # 启动 joplin-core,然后在健康检查通过后启动 joplin-mcp
```
#### 查看日志
```bash
docker compose logs -f # 两个容器
docker compose logs -f joplin-core # 仅同步/数据 API 日志
docker compose logs -f joplin-mcp # 仅 MCP HTTP 服务器日志
```
#### 停止
```bash
docker compose down
```
#### 环境变量
将变量放在 `.env` 文件中(由 [`docker-compose.yml`](docker-compose.yml) 自动拾取)。
| 变量 | 容器 | 必填 | 默认值 | 描述 |
| --- | --- | --- | --- | --- |
| `JOPLIN_SERVER_URL` | joplin-core | **是** | — | Joplin 服务器网址(例如,`https://joplin.example.com/`) |
| `JOPLIN_USERNAME` | joplin-core | **是** | — | Joplin 服务器用户名/邮箱 |
| `JOPLIN_PASSWORD` | joplin-core | **是** | — | Joplin 服务器密码 |
| `JOPLIN_API_TOKEN` | 两个容器 | **是** | — | Joplin 数据 API 令牌(从 `joplin config api.token` 提取) |
| `JOPLIN_CORE_URL` | joplin-mcp | **是** | — | joplin-core 数据 API 的网址(例如,`http://joplin-core:41184`) |
| `JOPLIN_DATA_API_PORT` | joplin-core | 否 | `41184` | 内部数据 API 监听端口 |
| `MCP_PORT` | joplin-mcp | 否 | `3000` | MCP HTTP 服务器端口(公开给主机) |
| `LOG_LEVEL` | 两个容器 | 否 | `info` | 日志级别:`debug`、`info`、`warn`、`error`、`silent` |
| `SYNC_INTERVAL_SECONDS` | joplin-core | 否 | `300` | 周期性同步间隔(秒) |
| `NODE_ENV` | joplin-core | 否 | — | 设置为 `production` 以强制 `JOPLIN_SERVER_URL` 使用 HTTPS |
#### MCP 客户端配置(Docker)
`joplin-mcp` 容器公开一个 **HTTP 端点**(不是 stdio)。配置您的 MCP 客户端通过 URL 连接:
```json
{
"mcpServers": {
"joplin": {
"url": "http://localhost:3000/mcp"
}
}
}
```
#### 工作原理
- **两个容器**:`joplin-core`(有状态,运行 Joplin CLI + 数据 API + bash 同步调度器)和 `joplin-mcp`(无状态,仅 Node.js MCP HTTP 服务器)
- **多阶段构建**:[`Dockerfile.mcp`](Dockerfile.mcp) 使用 `node:22-bookworm-slim`,具有单独的构建和生产阶段;[`Dockerfile.core`](Dockerfile.core) 是一个基于 Debian 的单阶段镜像
- **非 root 用户**:`joplin-core` 中的 `joplin` 用户,`joplin-mcp` 中的 `mcp` 用户
- **持久卷**:`joplin_data` 卷挂载在 `/home/joplin/.config/joplin` 以存储 Joplin 配置文件和 SQLite 数据库
- **内部网络**:`joplin-mcp` 通过 Docker 内部网络使用服务名称 `joplin-core` 与 `joplin-core` 通信
- **健康检查**:`joplin-core` 健康检查 `/ping` 端口 41184;`joplin-mcp` 在 `joplin-core` 健康检查通过后启动
- **同步调度器**:`entrypoint-core.sh` 中的 bash `while true` 循环处理周期性同步,并将日志记录到 `/var/log/joplin/`
#### 使用 Docker 测试
专用的 [`Dockerfile.tests`](Dockerfile.tests) 和 `test` 服务在 [`docker-compose.yml`](docker-compose.yml) 中允许在容器中运行测试套件:
```bash
# 构建测试镜像
docker build -f Dockerfile.tests -t joplin-api-tests .
# 运行测试
docker run --rm joplin-api-tests
# 或通过 docker compose(需要 --profile test,因为测试服务使用配置文件)
docker compose --profile test run --rm tests
```
测试使用 [Vitest](https://vitest.dev/),带有 v8 覆盖率(阈值:70% 语句,60% 分支,70% 函数,70% 行),并将 JUnit XML 报告输出到 `./reports/`。当通过 docker compose 运行测试时, `./reports` 目录被挂载到容器中,以便在主机上保留报告。
测试套件不需要运行的 Joplin 实例 — 单元测试使用模拟,集成测试在 Joplin 数据 API 不可用时将被跳过。
---
## 架构
### 双容器部署(Docker)
```mermaid
graph TD
A[AI 客户端] -->|"MCP HTTP (端口 3000)"| B[容器 B: joplin-mcp]
B -->|"HTTP fetch() + Bearer Token"| C[容器 A: joplin-core]
subgraph "容器 A: joplin-core"
C[Joplin 数据 API :41184] -->|"读写"| D[(Joplin SQLite DB)]
E[Bash 同步调度器] -->|"joplin sync"| F[Joplin 服务器]
F -->|"HTTPS"| E
end
subgraph "容器 B: joplin-mcp"
B -->|"工具处理程序"| G[JoplinDataClient]
end
```
1. **AI 客户端** 通过端口 3000 上的 HTTP 连接到 **joplin-mcp** (MCP StreamableHTTP 传输)
2. **joplin-mcp** 是一个 **无状态** Node.js 服务器 — 没有 Joplin CLI,没有同步逻辑,没有本地数据库
3. **JoplinDataClient** 在 `joplin-mcp` 中通过内部 Docker 网络向 **joplin-core** 发出 HTTP 请求
4. **joplin-core** 在内部运行 **Joplin 数据 API**,监听 `127.0.0.1:41185`,并使用 socat TCP 代理公开 `0.0.0.0:41184` 到 Docker 网络,支持持久的 SQLite 卷
5. **Bash 同步调度器** 在 `joplin-core` 中通过 Joplin CLI 处理周期性同步
6. 来自 MCP 服务器的写操作通过数据 API 触发同步;bash 调度器提供周期性备份同步
7. 两个容器都使用 **健康检查** — `joplin-mcp` 等待 `joplin-core` 健康检查通过后启动
## 可用的 MCP 工具
### 工具概述
| 工具 | 描述 | 是否写入? |
| --- | --- | --- |
| `list_notebooks` | 列出所有笔记本/文件夹 | 否 |
| `list_notes` | 列出带有分页和元数据字段的笔记 | 否 |
| `search_notes` | 搜索笔记、文件夹和标签 | 否 |
| `read_note` | 读取 ID 的单个笔记 | 否 |
| `read_notebook` | 读取 ID 的单个笔记本 | 否 |
| `read_multinote` | 读取多个 ID 的笔记 | 否 |
| `read_tags` | 获取笔记的标签 | 否 |
| `create_note` | 创建新的笔记 | **是** |
| `create_folder` | 创建新的文件夹 | **是** |
| `edit_note` | 编辑现有的笔记 | **是** |
| `edit_folder` | 编辑现有的文件夹 | **是** |
| `create_tag` | 创建新的标签 | **是** |
| `tag_note` | 将标签应用于笔记 | **是** |
| `untag_note` | 从笔记中删除标签 | **是** |
| `delete_note` | 删除笔记 | **是** |
| `delete_folder` | 删除文件夹 | **是** |
| `sync` | 手动触发同步 | 否 |
### 输入 / 输出模式
所有工具输入都通过 [Zod](https://zod.dev/) 模式进行验证。下面是预期的输入字段和返回类型。
#### 读取工具
| 工具 | 输入 | 输出 |
| --- | --- | --- |
| `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[]` |
#### 写工具
| 工具 | 输入 | 输出 |
| --- | --- | --- |
| `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_note` | `{ note_id }` | `{ success: true }` |
| `delete_folder` | `{ folder_id }` | `{ success: true }` |
#### 同步工具
| 工具 | 输入 | 输出 |
| --- | --- | --- |
| `sync` | `{}` | `{ status: "idle" \| "syncing", lastSyncTime: string \| null }` |
### 错误响应格式
当工具执行失败时,MCP 服务器返回一个响应,带有 `isError: true` 和一个包含单个文本条目的 `content` 数组:
```json
{
"content": [{ "type": "text", "text": "描述失败的错误消息" }],
"isError": true}
```
**验证错误**(Zod 模式不匹配)以 `warn` 级别记录,并包括特定的字段路径和原因,例如:
```
Validation error: note_id: Expected 32-character hex ID`
```
**执行错误**(API 失败、超时等)以 `error` 级别记录,并包括工具名称和错误消息。有关完整的错误类层次结构,请参见 [错误处理](#error-handling) 部分。
### Token 管理
Joplin 数据 API 使用 bearer token 身份验证。token 在启动时通过 `POST /auth` 请求自动获取,并存储在 [`GuardedString`](src/guarded-string.ts) 包装器中:
- **`GuardedString`** 将原始值存储在私有 `#value` 字段中,使其无法通过 `toString()`、`toJSON()` 或模板字面量强制转换为字符串 — 所有此类操作都返回 `'[REDACTED]'`
- 唯一访问实际值的方法是通过显式的 `.value` 属性
- 这可以防止通过日志记录、序列化或错误消息意外泄露
- token 在过期前 60 秒主动刷新,并在 401 响应时自动重新获取
### 生产环境 TLS 要求
- Joplin 数据 API 始终绑定到 `127.0.0.1`(仅限本地),因此服务器和数据 API 之间不需要 TLS — 流量永远不会离开机器
- **Joplin 服务器 URL (`JOPLIN_SERVER_URL`) 必须在生产环境中使用 HTTPS** — 这由配置模式强制执行(参见 [`src/config.ts`](src/config.ts#L5))。仅当 `NODE_ENV` 未设置为 `production` 时,才允许使用 HTTP
- Joplin CLI 与 Joplin 服务器的同步流量默认使用纯 HTTP;确保您的 Joplin 服务器部署在 TLS 终止的反向代理后面
### 仅限本地默认设置
- Joplin ClipperServer 硬编码绑定到 `127.0.0.1`,使其无法直接从容器外部访问。要解决此问题,数据 API 被转移到内部端口(`JOPLIN_DATA_API_PORT + 1`,例如 `41185`)通过 `joplin config api.port`,并且 **socat TCP 代理** 将 `0.0.0.0:JOPLIN_DATA_API_PORT` 转发到 `127.0.0.1:(JOPLIN_DATA_API_PORT + 1)`,在标准端口上提供网络可访问的数据 API
- 在双容器 Docker 部署中,**joplin-mcp** 容器通过内部 Docker 网络(服务名称 `joplin-core`)连接到 **joplin-core**,而不是本地主机
- **Docker 部署**:**joplin-mcp** 容器在端口 3000 上公开一个 **HTTP 端点**(MCP StreamableHTTP 传输)映射到主机。joplin-mcp 和 joplin-core 之间的数据 API 通信保留在内部 Docker 网络上,永远不会暴露给主机
- `docker-compose.yml` 将 `MCP_PORT`(默认 `3000`)映射到主机以供 MCP 客户端访问,并可选地映射 `JOPLIN_DATA_API_PORT`(默认 `41184`)以便在主机上直接访问数据 API。两者都绑定到主机侧的 `127.0.0.1`
- 如果您不需要数据 API 的主机侧访问,请从 `docker-compose.yml` 中删除 `joplin-core` 的 `ports:` 块,以保持数据 API 容器内部访问权限
### Token 轮换最佳实践
- Joplin 数据 API 发布具有可配置过期时间(默认约 55 分钟,由 Joplin 数据 API 服务器控制)的 token
- 客户端在过期前自动刷新 token,并在 401 响应时刷新
- 如果怀疑 token 泄露,请旋转 Joplin 服务器凭证 (`JOPLIN_PASSWORD`) 并重启容器 — 新的 token 将在下一次 `POST /auth` 调用时发布
### CLI 参数净化
所有通过 [`CliExecutor`](src/cli-executor.ts) 执行的 Joplin CLI 子命令都受到两层防御的保护:
1. **子命令白名单** — 仅允许预定义的子命令集(sync、config、ls、cat 等)。未知子命令在执行前被拒绝
2. **Shell 元字符阻止** — 拒绝包含 `;`、`|`、`&`、`$`、` `、`(`、`)`、`{`、`}`、`<`、`>`、`\n` 的参数
这些检查是在 Node.js `execFile` 之上进行防御,以防止 shell 注入攻击。
## 错误处理
```
错误
├── 配置错误 # 缺少/无效的环境变量
├── CLI 错误 # Joplin CLI 子进程失败
│ └── 属性:结果 { stdout, stderr, exitCode }
├── 数据 API 错误 # Joplin 数据 API HTTP 错误
│ ├── 状态码:数字
│ ├── 响应正文?字符串
│ ├── 未找到错误 (404) # 资源未找到
│ ├── 冲突错误 (409) # 资源在获取后修改
│ ├── 验证错误 (400)# 无效输入
│ └── 身份验证错误 (401) # 身份验证失败
└── 致命错误 # 致命/意外错误
├── 原因?未知
└── 退出码:数字(默认 1)
```
## 速率限制
内部 Joplin 数据 API HTTP 客户端 (`JoplinDataClient`) 强制执行可配置的并发限制,以防止数据 API 进程不堪重负:
- **默认最大并发量**:5 个并发请求
- **可通过**:`JoplinDataClient` 构造函数中的 `maxConcurrency` 参数进行配置
- **行为**:当达到限制时,额外请求被排队并在可用时执行
- **范围**:所有数据 API 调用(列表、获取、创建、更新、删除、搜索)共享相同的并发池
- **按工具**:单个工具调用会发出单个数据 API 请求,因此并发性仅在并行 MCP 请求下相关
## 故障排除
### 身份验证失败
**症状**:MCP 工具返回 `"身份验证失败"` 或 `AuthError`。
**原因和解决方法:**
| 原因 | 解决方法 |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `.env` 中的 `JOPLIN_PASSWORD` 无效 | 验证密码是否与您的 Joplin 服务器帐户匹配 |
| token 在刷新前过期 | 检查系统时钟是否同步(NTP)。客户端会主动刷新 token,但时钟漂移可能导致过期 |
| Joplin 服务器无法访问 | 确保 `JOPLIN_SERVER_URL` 正确且服务器正在运行。验证使用 HTTPS 的 TLS 证书 |
| 数据 API 未就绪 | 在发送请求之前等待“数据 API 服务器已就绪”的日志行 |
**诊断步骤:**
1. 检查容器日志:`docker compose logs`
2. 查找包含 `"Failed to obtain Joplin API token"` 或 `"AuthError"` 的条目
3. 通过直接使用 curl 命令验证 Joplin 服务器 API
### 同步冲突
**症状**:日志中包含 `"检测到同步冲突 - 保留远程版本"` 警告。
**行为**:系统使用 **远程赢** 冲突解决策略。本地更改始终让位于远程版本。
**解决方法:**
- 冲突笔记在 Joplin 中被标记为冲突副本。使用 Joplin 桌面/客户端应用程序检查它们
- 您可以通过 `CliExecutor.checkConflicts()` 以编程方式检查冲突计数
- 要解决,请在 Joplin 中查看冲突的笔记并手动合并或删除它们
### 超时问题
**症状**:CLI 命令在 `Nms` 后超时失败。
**原因和解决方法:**
| 原因 | 解决方法 |
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| 初始同步(许多笔记/资源)过大 | 增加 `SYNC_INTERVAL_SECONDS` 或让初始同步完成 — 随后的同步是增量式的 |
| Joplin 服务器响应缓慢 | 检查 Joplin 服务器性能(CPU、内存、数据库)。确保网络延迟较低 |
| CLI 命令超时时间过短 | 默认超时时间为 60 秒;对于极大操作,可以在 `CliExecutor.exec()` 中调整此超时时间 |
### CLI 执行错误
**症状**:带有退出代码、stdout 和 stderr 详细信息的 `CliError`。
**常见原因:**
- **缺少 `joplin` 二进制文件**:`joplin` CLI 必须安装在容器中并在 `PATH` 上可用。Dockerfile 负责处理此问题,但如果使用自定义设置,请验证
- **未配置**:入口点脚本配置 `sync.target 10` 和服务器凭证。如果跳过此步骤,`joplin sync` 将失败并出现配置错误
- **权限错误**:确保 Joplin CLI 配置目录 (`~/.config/joplin`) 是可写的
### 速率限制
**症状**:请求被排队或比预期时间更长,但不抛出错误。
**行为**:`JoplinDataClient` 强制执行最大 5 个并发 API 请求(可配置)。额外请求被排队并顺序处理。
**如果您遇到并发限制:**
- 减少来自 AI 客户端的并行 MCP 工具调用
- 并发限制是 `JoplinDataClient` 中的构造函数参数 [`src/data-client.ts`](src/data-client.ts)。如果您有特定的需求,需要更高的并行度,请增加它,但请注意数据 API 自身的处理能力
## 开发
### 命令
```bash
pnpm install # 安装依赖
pnpm dev # 在开发模式下运行,支持热重载(tsx 监视)
pnpm build # 编译 TypeScript(tsc)
pnpm start # 运行编译后的服务器(node dist/mcp/entry.js)
pnpm test # 运行测试(vitest)
pnpm test:watch # 在监视模式下运行测试
pnpm lint # 校验源代码(eslint)
pnpm format # 格式化源代码(prettier --write)
```
## 项目结构
```
src/
├── server.ts # (已弃用 - 在双容器架构中被 mcp/entry.ts 替换)
├── config.ts # 基于 Zod 的环境配置解析
├── logger.ts # Pino 结构化日志记录器
├── cli-executor.ts # Joplin CLI 子进程包装器
├── sync-manager.ts # 序列化同步队列协调器(由容器 B 用于写入同步触发器)
├── data-client.ts # Joplin 数据 API HTTP 客户端(26 种方法,令牌身份验证)
├── api-types.ts # Joplin API 的 TypeScript 类型定义
├── errors.ts # 类型化错误类层次结构
├── guarded-string.ts # 安全字符串包装器(防止意外机密泄露)
├── pagination.ts # 分页助手(clampLimit,fetchAllPages)
└── mcp/
├── entry.ts # 容器 B 入口点:独立 MCP HTTP 服务器
├── server.ts # MCP 服务器设置(stdio + StreamableHTTP 传输)
├── schemas.ts # 所有 17 个工具的 Zod 验证模式
├── tools.ts # 17 个工具处理程序实现
└── tool-registry.ts # 工具注册和调度
tests/
├── cli-executor.test.ts # CLI 执行器测试
├── config.test.ts # 配置解析器测试
├── data-client.test.ts # 数据 API 客户端测试
├── errors.test.ts # 错误类层次结构测试
├── integration.test.ts # 针对实时 Joplin 数据 API 的集成测试
├── logger.test.ts # 日志记录器测试
├── pagination.test.ts # 分页助手测试
├── sync-manager.test.ts # 同步管理器测试
└── mcp/
├── schemas.test.ts # Zod 模式验证测试
├── server.test.ts # MCP 服务器生命周期测试
├── tool-registry.test.ts # 工具注册和调度测试
└── tools.test.ts # 工具处理程序测试
docs/ # 项目文档(参见根目录 SBOM.md 以获取依赖项清单)
scripts/
└── smoke-test.sh # 容器烟雾测试(检查容器启动 + 数据 API /ping)
```
根级部署文件:
| 文件 | 用途 |
| ---- | ------- |
| [`Dockerfile.core`](Dockerfile.core) | 容器 A:Joplin CLI + 数据 API + bash 同步调度器 |
| [`Dockerfile.mcp`](Dockerfile.mcp) | 容器 B:无状态 MCP HTTP 服务器 |
| [`Dockerfile.tests`](Dockerfile.tests) | 测试运行器容器 |
| [`entrypoint-core.sh`](entrypoint-core.sh) | 容器 A 入口点:bash 同步调度器,日志记录详尽 |
| [`entrypoint-mcp.sh`](entrypoint-mcp.sh) | 容器 B 入口点:验证环境变量并启动 MCP HTTP 服务器 |
| [`docker-compose.yml`](docker-compose.yml) | 两项服务编排,健康检查和依赖项排序 |
## 启动和关闭管道
### 双容器(Docker)
**joplin-core(容器 A):**
1. **验证环境变量** — [`entrypoint-core.sh`](entrypoint-core.sh) 检查 `JOPLIN_SERVER_URL`、`JOPLIN_USERNAME`、`JOPLIN_PASSWORD`
2. **配置 Joplin CLI** — 在 Joplin CLI 配置中设置 `sync.target 10` 和服务器凭证
3. **提取 API 令牌** — 从 Joplin 配置中检索 `api.token` 用于数据 API
4. **启动 Joplin 数据 API 和 socat 代理** — 将数据 API 移至内部端口(`JOPLIN_DATA_API_PORT + 1`)通过 `joplin config api.port`,启动 `joplin server start`(仅绑定 `127.0.0.1`),然后启动 `socat` TCP 代理从 `0.0.0.0:JOPLIN_DATA_API_PORT` → `127.0.0.1:(JOPLIN_DATA_API_PORT + 1)`。所有日志记录到 `/var/log/joplin/`
5. **等待就绪** — 轮询 `/ping` 端点(最多 30 次重试,间隔 2 秒)
6. **执行初始同步** — `joplin sync` 带有重试循环(最大 30 次尝试)
7. **启动周期同步** — bash `while true` 循环,配置 `SYNC_INTERVAL_SECONDS`
8. **阻塞子进程** — `wait` 在后台数据 API 进程上
9. **处理信号** — 在 `SIGTERM`/`SIGINT` 上:带有信号转发的功能性关闭
**joplin-mcp(容器 B):**
1. **验证环境变量** — [`entrypoint-mcp.sh`](entrypoint-mcp.sh) 检查 `JOPLIN_API_TOKEN` 和 `JOPLIN_CORE_URL`
2. **启动 Node.js 服务器** — `node dist/mcp/entry.js`
3. **解析配置** — Zod 模式验证环境变量并设置默认值
4. **初始化日志记录器** — 在配置的日志级别初始化 Pino 结构化日志记录器
5. **连接到 joplin-core** — ping `JOPLIN_CORE_URL/ping` 以验证连接性
6. **初始化工具注册表** — 注册所有 17 个 MCP 工具处理程序(无 SyncManager)
7. **启动 MCP HTTP 服务器** — 在 `MCP_PORT`(默认 3000)上侦听,提供 `/health` 和 `/mcp` 端点
8. **处理信号** — 在 `SIGTERM`/`SIGINT` 上:关闭 HTTP 服务器并退出
## 关键设计决策
1. **数据 API 而非 CLI 进行数据操作** — 避免脆弱的 CLI 输出解析;使用结构化 HTTP API 和类型化响应
2. **关注点分离(两个容器)** — 状态后端(joplin-core)处理同步和数据持久性;无状态前端(joplin-mcp)处理 MCP 协议。独立可扩展和可调试
3. **基于 Bash 的同步调度器** — 用简单的可靠的 bash `while true` 循环替换 TypeScript SyncManager。每个同步记录 `/var/log/joplin/sync.log`
4. **写入同步** — 写入工具触发立即同步,以便始终保持 Joplin 服务器更新
5. **序列化同步队列** — 防止并发同步调用导致 `SQLITE_BUSY` 错误
6. **远程赢冲突解决** — 委托给 Joplin CLI 内置行为;本地更改始终让位于远程
7. **令牌生命周期** — 通过 `POST /auth` 获取令牌,在 55 分钟过期前 60 秒主动刷新,在 401 响应时重新获取
## 许可证
MIT
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器