内容
# vikunja-mcp
[](https://www.npmjs.com/package/@eargollo/vikunja-mcp)
[](https://github.com/eargollo/vikunja-mcp/actions/workflows/ci.yml)
[](https://github.com/eargollo/vikunja-mcp/actions/workflows/ci.yml)
[](https://nodejs.org)
[](LICENSE)
一个最小、独立的 [MCP](https://modelcontextprotocol.io/) 服务器,用于 [Vikunja](https://vikunja.io/),设计小到可以在一坐下读完。
## 为什么存在
可用的第三方 Vikunja MCP 服务器会在运行时从 npm 拉取十几个依赖项,并在代理的容器中运行,访问其机密。这个服务器则相反:
- **一个直接依赖** — 官方的 `@modelcontextprotocol/sdk`。所有 HTTP 请求都使用 Node 内置的 `fetch`;该项目不添加任何 API 客户端库。唯一的传递依赖是 SDK 的依赖,以及此处使用的 stdio 传输,它从不加载其 HTTP/OAuth 堆栈。
- **一个出口点** — 所有请求都通过 `api.js` 中的 `makeApi()` 进行,该函数仅调用带有您的令牌的 `VIKUNJA_URL`。grep 它;这就是整个网络表面。
- **默认范围** — 读取和添加工具始终开启(列表、获取、为您的令牌创建)。写入工具(更新、共享、网络钩子、凭证铸造)需要 `VIKUNJA_MCP_ALLOW_WRITE=1`;删除工具需要 `VIKUNJA_MCP_ALLOW_DELETE=1`。默认安装的代理被劫持后,可以添加任务或读取列表 — 不能修改、共享、批量替换指派者或删除。
- **环境变量中的机密**,从不硬编码。
- **无构建步骤** — 普通 Node ESM,`node index.js`。
## 快速入门
**要求:** Node ≥ 20 和一个 Vikunja 实例(针对 Vikunja `2.3.0`,API v1 测试)。正常使用时,无需构建步骤,无需 Docker。
直接从 npm 运行:
```bash
VIKUNJA_URL=https://app.vikunja.cloud/api/v1 \
VIKUNJA_API_TOKEN=tk_... \
npx @eargollo/vikunja-mcp
```
或将其写入 MCP 客户端的配置中:
```json
{
"mcpServers": {
"vikunja": {
"command": "npx",
"args": ["-y", "@eargollo/vikunja-mcp@latest"],
"env": {
"VIKUNJA_URL": "https://app.vikunja.cloud/api/v1",
"VIKUNJA_API_TOKEN": "tk_..."
}
}
}
}
```
这会公开读取 + 添加工具。要开启写入或删除工具,请参见 [配置](#配置);有关从源代码或空气隔离网关运行,请参见 [运行](#运行)。
## 工具
按权限层级分组(读取 + 添加始终开启;写入和删除是可选的 - 参见 [配置](#配置)):
| 工具 | 效果 | 端点 |
| --- | --- | --- |
| `list_projects` | 读取 | `GET /projects` |
| `get_project` | 读取 | `GET /projects/{id}` |
| `list_tasks` | 读取 | `GET /projects/{id}/tasks` |
| `list_all_tasks` | 读取 | `GET /tasks` |
| `get_task` | 读取 | `GET /tasks/{id}` |
| `list_labels` | 读取 | `GET /labels` |
| `search_users` | 读取 | `GET /users?s=` |
| `list_task_assignees` | 读取 | `GET /tasks/{id}` |
| `list_task_comments` | 读取 | `GET /tasks/{id}/comments` |
| `list_task_relations` | 读取 | `GET /tasks/{id}` |
| `list_task_attachments` | 读取 | `GET /tasks/{id}/attachments` |
| `list_buckets` | 读取 | `GET /projects/{id}/views/{view}/buckets` |
| `list_teams` | 读取 | `GET /teams` |
| `get_team` | 读取 | `GET /teams/{id}` |
| `list_saved_filters` | 读取 | `GET /projects`(负 ID) |
| `list_notifications` | 读取 | `GET /notifications` |
| `get_current_user` | 读取 | `GET /user` |
| `list_api_tokens` | 读取 | `GET /tokens` |
| `list_webhooks` | 读取 | `GET /projects/{id}/webhooks` |
| `get_caldav_info` | 读取 | `GET /user`(+ CalDAV 连接信息) |
| `create_project` | 添加 | `PUT /projects` |
| `create_task` | 添加 | `PUT /projects/{id}/tasks` |
| `add_label_to_task` | 添加 | `PUT /tasks/{id}/labels` |
| `assign_user` | 添加 | `PUT /tasks/{id}/assignees` |
| `add_task_comment` | 添加 | `PUT /tasks/{id}/comments` |
| `create_task_relation` | 添加 | `PUT /tasks/{id}/relations` |
| `upload_task_attachment` | 添加 | `PUT /tasks/{id}/attachments` |
| `create_bucket` | 添加 | `PUT /projects/{id}/views/{view}/buckets` |
| `create_team` | 添加 | `PUT /teams` |
| `add_team_member` | 添加 | `PUT /teams/{id}/members` |
| `create_label` | 添加 | `PUT /labels` |
| `create_saved_filter` | 添加 | `PUT /filters` |
| `subscribe` | 添加 | `PUT /subscriptions/{entity}/{id}` |
| `update_task` | 写入 | `POST /tasks/{id}` |
| `set_task_done` | 写入 | `POST /tasks/{id}` |
| `bulk_update_tasks` | 写入 | `POST /tasks/bulk` |
| `set_task_labels` | 写入 | `POST /tasks/{id}/labels/bulk` |
| `set_task_assignees` | 写入 | `POST /tasks/{id}/assignees/bulk` |
| `update_project` | 写入 | `POST /projects/{id}` |
| `archive_project` | 写入 | `POST /projects/{id}` |
| `update_task_comment` | 写入 | `POST /tasks/{id}/comments/{commentId}` |
| `update_label` | 写入 | `POST /labels/{id}` |
| `update_bucket` | 写入 | `POST /projects/{id}/views/{view}/buckets/{bucketId}` |
| `move_task_to_bucket` | 写入 | `POST /projects/{id}/views/{view}/buckets/{bucket_id}/tasks` |
| `update_team` | 写入 | `POST /teams/{id}` |
| `toggle_team_member_admin` | 写入 | `POST /teams/{id}/members/{username}/admin` |
| `share_project_with_user` | 写入 | `PUT /projects/{id}/users` |
| `share_project_with_team` | 写入 | `PUT /projects/{id}/teams` |
| `create_link_share` | 写入 | `PUT /projects/{id}/shares` |
| `create_webhook` | 写入 | `PUT /projects/{id}/webhooks` |
| `update_webhook` | 写入 | `POST /projects/{id}/webhooks/{webhookId}` |
| `update_saved_filter` | 写入 | `POST /filters/{id}` |
| `mark_notification_read` | 写入 | `POST /notifications/{id}` |
| `create_api_token` | 写入 | `PUT /tokens` |
| `create_caldav_token` | 写入 | `PUT /user/settings/token/caldav` |
| `delete_task` | 删除 | `DELETE /tasks/{id}` |
| `delete_project` | 删除 | `DELETE /projects/{id}` |
| `remove_label_from_task` | 删除 | `DELETE /tasks/{id}/labels/{labelId}` |
| `unassign_user` | 删除 | `DELETE /tasks/{id}/assignees/{userId}` |
| `delete_task_comment` | 删除 | `DELETE /tasks/{id}/comments/{commentId}` |
| `delete_task_relation` | 删除 | `DELETE /tasks/{id}/relations/{kind}/{otherId}` |
| `delete_task_attachment` | 删除 | `DELETE /tasks/{id}/attachments/{attachmentId}` |
| `delete_label` | 删除 | `DELETE /labels/{id}` |
| `delete_bucket` | 删除 | `DELETE /projects/{id}/views/{view}/buckets/{bucketId}` |
| `remove_team_member` | 删除 | `DELETE /teams/{id}/members/{username}` |
| `delete_saved_filter` | 删除 | `DELETE /filters/{id}` |
| `unsubscribe` | 删除 | `DELETE /subscriptions/{entity}/{id}` |
| `delete_webhook` | 删除 | `DELETE /projects/{id}/webhooks/{webhookId}` |
| `delete_caldav_token` | 删除 | `DELETE /user/settings/token/caldav/{id}` |
`list_projects`、`list_tasks` 和 `list_all_tasks` 支持可选的 `page` 和 `per_page`。响应包括 `{ page, total_pages, count, items }`;通过请求连续的页面进行分页,直到 `page < total_pages`。Vikunja 将 `per_page` 限制为其配置的最大值,因此请依赖 `total_pages` 而不是返回的页面大小。
`list_tasks` 和 `list_all_tasks` 还接受可选的 `filter`(Vikunja 过滤器查询 - 将谓词与 `&&` / `||` 组合,比较使用 `=`、`!=`、`<`、`>`、`<=`、`>=`;过滤字段与 `sort_by` 字段不相同)、`sort_by`(裸 Vikunja 字段名;方向放在 `order` 中,而不是此处)和 `order`(`asc`/`desc`)。`get_task` 返回任务的完整详细信息(描述、日期、优先级、percent_done、标签、指派者)。`create_task` 接受可选的 `description`、`due_date`(需要 ISO 8601)和 `priority`。`update_task`(写入)仅更改传递的字段;`set_task_done`(写入)完成或重新打开任务;`delete_task`(删除)永久删除任务。
有意添加工具 - 工具列表位于 `tools.js` 中的 `buildTools` 工厂中,`index.js` 仅公开其环境标志允许的层级。每个工具还为客户端公开一个 MCP **标题**(例如 `list_projects` → “列出项目”)。
## 使用工具
服务器通过 stdio 进行 MCP 通信;任何 MCP 客户端(Cursor、Claude Desktop、OpenClaw、SDK)都可以通过名称调用工具。参数和结果为 JSON。
```jsonc
// list_projects(无参数,或 { "page": 1, "per_page": 50 })
{ "page": 1, "total_pages": 1, "count": 2,
"items": [ { "id": 1, "title": "Inbox" }, { "id": 7, "title": "Work" } ] }
// list_tasks { "project_id": 7 }
{ "page": 1, "total_pages": 1, "count": 1,
"items": [ { "id": 42, "title": "Ship v0.1.0", "done": false } ] }
// create_task { "project_id": 7, "title": "Write the changelog" }
{ "id": 43, "title": "Write the changelog", "done": false, "project_id": 7, ... }
// assign_user { "task_id": 42, "user_id": 3 }
{ "ok": true, "task_id": 42, "user_id": 3 }
```
列表工具返回 `{ count, items }`(以及分页或上下文字段)。`list_task_relations` 是例外:它返回一个键控的 `relations` 映射。突变返回实体详细信息(`create_task` → 与 `get_task` 相同)或 `{ ok: true, ...ids }` 用于删除和关联更改。输入使用 `*_id`;列表/详细信息输出使用 `id`。
无效输入(错误的 `project_id`、空 `title`、未知工具)以 MCP 工具错误(`isError: true`)和消息返回,从不崩溃。
## 范围和空白
目标是 **完全覆盖 Vikunja v1 API**,每个工具都经过 TDD 风格的添加(单元 + 端到端)。剩下的少数表面被排除,因为它们 **不适合作为代理工具** 或会破坏安全态势(不是因为缺乏访问权限,请参见下文)。上面的 [工具](#工具) 表是按区域划分的权威、逐工具覆盖列表;
**设计范围外:** CalDAV *同步*(这是 `/dav` WebDAV 协议,而不是代理进行的 REST 调用),任意 `/routes` 代理(通用直通会削弱作用范围的意义),服务器 **管理员** 端点(特权操作代理无权运行),以及 **用户级** 网络钩子(仅项目网络钩子,以保持范围狭窄)。工具结果带有 MCP `structuredContent`,但没有声明的 `outputSchema`。
有关最近的添加和路线图,请参见 [CHANGELOG.md](CHANGELOG.md) 和 [#21](https://github.com/eargollo/vikunja-mcp/issues/21)。
## 配置
| 环境变量 | 示例 |
| --- | --- |
| `VIKUNJA_URL` | `https://app.vikunja.cloud/api/v1`(注意 `/api/v1`) |
| `VIKUNJA_API_TOKEN` | `tk_...`(Vikunja API 令牌;将其范围限定为您启用的工具区域 - 项目 + 任务涵盖默认的读取/添加集,但团队、标签、网络钩子、筛选器、订阅、令牌和 CalDAV 每个都需要自己的范围) |
| `VIKUNJA_MCP_ALLOW_WRITE` | `1` 以公开 **写入** 工具(更新、共享、网络钩子、API 令牌)- 默认关闭 |
| `VIKUNJA_MCP_ALLOW_DELETE` | `1` 以公开 **删除**(破坏性)工具 - 默认关闭 |
读取和添加工具始终可用。变更和破坏性工具仅在匹配标志设置时注册(`1`/`true`/`yes`/`on`),因此默认安装可以读取和添加但不能修改或销毁。服务器通过 MCP `instructions` 和每个工具的注释(`readOnlyHint`、`destructiveHint`)宣传其立场,以便主机自动批准读取并确认删除。
`subscribe` / `unsubscribe` 需要 `entity`(`project` 或 `task`)和 `entity_id`.
## 运行
两个运行模式,每种集成都引用如下。发布到 npm 为 [`@eargollo/vikunja-mcp`](https://www.npmjs.com/package/@eargollo/vikunja-mcp)(带有构建出处)。
- **从 npm(推荐)** — `npx @eargollo/vikunja-mcp` 从注册表获取包;无需克隆,无需管理 `node_modules`。`@latest` 跟踪最新版本,或使用 `@eargollo/vikunja-mcp@<version>` 固定确切版本。需要 npm/网络访问。
- **从源代码** — 克隆,`npm install` 一次(仅安装 SDK),然后 `node index.js`。无需构建步骤。唯一规则:`node_modules` **必须** 与 `index.js` 位于同一目录下,否则进程在启动时退出。这是空气隔离路径。
```bash
# 从 npm
VIKUNJA_URL=https://app.vikunja.cloud/api/v1 \
VIKUNJA_API_TOKEN=tk_... \
npx @eargollo/vikunja-mcp
# 从源代码
git clone https://github.com/eargollo/vikunja-mcp
cd vikunja-mcp && npm install
VIKUNJA_URL=https://app.vikunja.cloud/api/v1 \
VIKUNJA_API_TOKEN=tk_... \
node index.js
```
### 在 Cursor / Claude Desktop 中注册
将快速入门 `.mcp.json` 块添加到您的项目(或客户端的全局 MCP 配置);npm 形式不需要克隆。要从源代码运行,请使用 `"command": "node"` 和 `"args": ["/path/to/vikunja-mcp/index.js"]` — 请记住 `node_modules` 必须与 `index.js` 位于同一目录下(参见 [运行](#运行))。
### 在 OpenClaw 中注册
OpenClaw 在其容器内作为本地进程运行 MCP 服务器,因此使用 `mcp add` 注册 vikunja 并使用 `mcp probe` 验证。使用源代码安装的空气隔离设置在网关的持久化主目录中运行 **从源代码** — 克隆到网关的重启后仍存在的目录中,并指向 `index.js`(`node_modules` 必须与之位于同一目录中):
```bash
# 从源代码(运行时无需 npm/npx)
sudo docker exec openclaw node dist/index.js mcp add vikunja \
--command node \
--arg /path/to/vikunja-mcp/index.js \
--env VIKUNJA_URL=https://app.vikunja.cloud/api/v1 \
--env VIKUNJA_API_TOKEN=tk_...
# 或者,如果网关有网络访问权限,直接从 npm
sudo docker exec openclaw node dist/index.js mcp add vikunja \
--command npx \
--arg -y --arg @eargollo/vikunja-mcp@latest \
--env VIKUNJA_URL=https://app.vikunja.cloud/api/v1 \
--env VIKUNJA_API_TOKEN=tk_...
# 验证工具已公开
sudo docker exec openclaw node dist/index.js mcp probe vikunja --json
```
用您的容器名称替换 `openclaw`。其他 MCP 网关使用不同的 CLI,但格式相同:命令(`node` 或 `npx`)、参数和两个环境变量。
## 测试
单元测试涵盖 `lib.js` 中的纯帮助程序(验证、查询生成、分页形状)和 `tools.js` 中的每个工具处理程序,使用注入的 `api()` — 成功的路径、输入验证和空/损坏的响应保护 — 因此不需要服务器或网络:
```bash
npm test # 运行 test/*.test.js;如果没有配置 Vikunja,e2e 会自动跳过
npm run test:coverage # 相同,使用 Node 的内置覆盖率报告(无额外依赖)
```
`test:coverage` 是 CI 运行的,确保覆盖率低于 100% 行 / 90% 分支 / 100% 函数 — 上方覆盖率徽章反映的保证。无需覆盖率服务或额外依赖。
端到端测试通过 stdio 将真正的 MCP 服务器驱动到活动 Vikunja。 编排文件将 **Vikunja `2.3.0`** 固定(故意,不使用 `:latest`,以免 CI 在上游发布时无声破坏)。使用 Docker(也需要 Node 20+)启动一个:
```bash
npm install # 一次
npm run up # 在 http://localhost:3456 启动 Vikunja(SQLite,临时)
npm run bootstrap # 注册测试用户,登录,写入 .env
npm run test:e2e # 通过 MCP 列出项目 -> 创建任务 -> 列出任务 + 错误路径
npm run down # 停止并清除(数据存在于容器中,因此这会重置)
```
`bootstrap` 写入一个 `.env`(gitignored),包含 `VIKUNJA_URL` 和一个持有者令牌。测试设置使用登录 **会话 JWT** 作为持有者令牌 — Vikunja 接受它放在 `index.js` 发送 `Bearer` 的地方,这使得测试设置保持简单。在生产环境中,请使用作用域的 `tk_` API 令牌代替(参见上面的配置)。
## 发布
版本从 `v*` git 标签通过 [.github/workflows/release.yml](.github/workflows/release.yml) 切割,该工作流运行单元测试,发布包到 npm(通过 OIDC Trusted Publishing,无存储令牌,带有构建出处),并创建具有生成说明的 GitHub 版本。版本仅在发布时更改(`npm version`),而不在功能 PR 中。参见 [`docs/RELEASING.md`](docs/RELEASING.md)。
## 安全
整个设计目标是小型、可审计的信任表面(一个出口点,可选写入/删除,环境变量中的机密)。如果您发现漏洞,请私下报告 — 有关披露路径,请参见 [SECURITY.md](SECURITY.md)。
## 许可
MIT
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器