关于 laver-mcp
laver-mcp 是一个 MCP 服务,让 Agent 读取和管理 Laver 工作区中的看板、工单与 Wiki 页面。其工具直接调用 Laver 公共 REST API,并保留版本冲突和 API 错误,供 Agent 处理。
主要功能
- 01读取并搜索看板、工单、评论和 Wiki 页面
- 02创建和更新工单、评论、子任务及看板
- 03管理看板列、分组、标签和自定义字段
- 04上传和获取工单附件
- 05通过工单版本检测并发写入冲突
适用场景
- 01查看工作区中逾期或未分配的工单
- 02检测并发变更后更新工单
- 03搜索工作区的看板、工单和 Wiki 内容
- 04为工单附加文件或图片
MCP 相关
该 MCP 服务将 Laver 工作区操作作为工具提供给 MCP 客户端。它使用工作区范围的 API 密钥,并将请求转发到 Laver 公共 REST API。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
npx -y @laver/mcpLAVER_API_KEY密钥A workspace-scoped API key, created in Laver under Admin → API keys. It acts as the person who created it, so it holds exactly their access and can be revoked without touching their account. Supply this or LAVER_API_KEY_FILE.LAVER_API_KEY_FILE密钥Alternative to LAVER_API_KEY: a path to a file holding nothing but the key, or a .env-style file carrying a LAVER_API_KEY= line. Use this to keep the key out of your client's config file.LAVER_API_URLOverrides the API host. Defaults to https://api.laver.app, and must be https except for localhost.本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
工具
59收录的工具列表 · 更新于 3 周前
此工具无需参数。
详细介绍
@laver/mcp
面向 Laver 的 MCP 服务。为 Agent 提供读取和操作看板、工单及工作区 Wiki 的工具。
每个工具都是对 Web 应用所用同一公共 REST API 的轻量调用。没有本地状态、没有缓存,也没有任何功能的第二套实现——如果 Laver 拒绝写入,就会原样返回拒绝信息,因为 Agent 可以根据“409,重新读取并重试”采取行动,却无法根据“出了点问题”采取行动。
设置
在 Laver 的 Admin → API keys 下创建一个限定工作区的 API 密钥。它会以创建者的身份运行,因此只能执行创建者能执行的操作,也不会有任何额外权限;撤销它也不会影响创建者的账户。
{
"mcpServers": {
"laver": {
"command": "npx",
"args": ["-y", "@laver/mcp"],
"env": { "LAVER_API_KEY": "your key here" }
}
}
}
LAVER_API_URL 用于覆盖 API 主机;默认值为 https://api.laver.app。
LAVER_API_KEY_FILE 是 LAVER_API_KEY 的替代方案:指定一个文件路径,文件中可以只包含密钥,也可以是 .env 风格的文件,其中含有一行 LAVER_API_KEY=…,其他内容不限(赋值行优先;引号和 export 前缀均可)。本仓库的 .mcp.json 正是通过这种方式注册服务,避免在受版本控制的文件中存放密钥。
如果文件中没有赋值行,且包含多个 token,则不会获取任何密钥,并会出现“key is not set”错误。过去程序会将整个文件作为 token 发送,这对只存放一个密钥的文件没问题,但对其他文件则会造成泄漏。
LAVER_API_URL 必须使用 https,但 localhost 除外。
在本仓库中开发
仓库根目录下的 .mcp.json 会为打开项目的任何人注册此服务,并从被 git 忽略的 .env 文件中读取密钥。无需导出任何内容。
它运行的是已发布的包 npx -y @laver/mcp,而不是旁边的 mcp/server.js。这是有意为之:指向本地文件意味着这里的每个人都会运行一条用户不会采用的代码路径;而这正是 0.1.0 发布时出现问题的原因——通过 bin 启动时,入口点从未连接其传输层,而真实客户端启动服务时只会采用这种方式。运行我们实际发布的内容,才能确保我们体验到的就是用户体验到的内容。
如果你正在编辑此服务,同样的选择会让你误以为改动生效了:在发布之前,你的改动都不会起作用。开发时,请将客户端指向工作副本——
{ "command": "node", "args": ["mcp/server.js"] }
——并在提交前恢复。无论如何,npm run check 和 frontend/tests/check-mcp-bin-entrypoint.mjs 都会针对工作副本运行,因此测试不会依赖发布。
客户端只会在启动时连接 MCP 服务。 如果会话已经在运行,此时执行 claude mcp add 不会将工具补充到该会话中——工具列表在服务启动之前就已生成。请启动新会话(或从客户端的 MCP 面板重新连接),工具就会显示出来。
工具
读取
| 工具 | 提供的内容 |
|---|---|
list_workspaces | 不知道 uuid 时,从这里开始 |
list_boards | 工作区中的看板 |
get_board | 包含状态列、标签、成员和工单的看板 |
list_tickets | 看板上的工单,可筛选、分页——使用 updated_since 跟踪看板 |
list_workspace_tickets | 一次性对所有看板进行分类——可按 overdue、unassigned 或自由文本筛选 |
get_ticket | 一张工单的完整信息及其子任务,包括其 version |
get_ticket_comments | 评论和活动历史 |
get_ticket_flow | 工单在各列中停留了多长时间 |
list_custom_fields | 看板的自定义字段定义——update_ticket 所依据的 uuid |
list_labels | 工作区中的所有标签,而不仅是某个看板上已使用的标签 |
search | 一次性搜索整个工作区中的看板、工单、知识库页面和评论 |
list_workspace_tickets 是站会时使用的读取工具:它不需要 board_uuid,也是这里唯一一个无需遍历所有看板,就能回答“哪些已逾期”和“哪些无人负责”的工具。它有意设计成 list_tickets 的轻量版本——仅返回一页,默认 50 条,最多 200 条,且没有游标——因此应缩小筛选范围,而不是翻页查询。
get_ticket_flow 根据工单历史记录中已有的状态变更计算数据。如果要汇总多张工单的情况,请查看 visits,而不是 by_status 汇总值:同一批处理的工单会有时间重叠,因此汇总值不能直接相加。gaps 会指出历史记录与工单当前所在列不一致的情况,因此汇总值只是下限,并非精确测量值。
要跟踪看板,请再次调用 list_tickets,并将上次调用返回的 server_time 设为 updated_since;这样只会返回发生变化的工单。没有用于服务器发送事件流 GET /boards/:uuid/events 的工具——一次工具调用只有一个请求和一个答复,而永不结束的流两者都不是。
减少请求内容。 工具结果会占用模型的上下文,而有三项调用无法绕开:status_uuid 或 label_uuid 来自 get_board,page_uuid 来自 get_wiki_tree。在繁忙的看板上,这些结果分别有 154 kB、119 kB 和 180 kB。现在每项调用都可以通过参数缩小返回内容,而且都需要显式启用——不传这些参数的调用与之前完全相同。
| 调用 | 提供的内容 | 测量结果 |
|---|---|---|
get_board(include_tasks: false) | 仅看板结构——状态、标签、字段 | 155 kB → 17 kB |
get_wiki_tree(depth: 1) | 顶层内容,不包含其下的嵌套内容 | 119 kB → 1.8 kB |
get_wiki_tree(parent_page_uuid: …) | 一个页面及其下的所有内容 | 119 kB → 9.8 kB |
list_tickets(include_descriptions: false) | 仅标题和 uuid,不含正文 | 167 kB → 45 kB |
如果调用 get_board(include_tasks: false) 是为了获取 uuid 而不是工单,就优先使用它;如果需要工单,则使用 list_tickets——它还接受 status 和 limit 参数。
写入
| 工具 | 说明 |
|---|---|
create_ticket | status 接受列名称,也可以传入 status_uuid;description 中的 Markdown 会被解析 |
update_ticket | 需要 version;description 中的 Markdown 会被解析;custom_fields 以字段 UUID 为键,并会替换整个对象 |
move_ticket | 需要 version 和一个列;缺少任一项都会返回 400 |
comment_on_ticket | body 中的 Markdown 会被解析;不需要 version,因此不会返回 409 |
update_comment | 只能修改自己的评论;会替换整个正文,并标记为已编辑 |
delete_comment | 只能删除自己的评论;评论会移至回收站,且此处没有恢复功能 |
mark_comments_read | 清除该工单的未读标记,作用于此密钥对应的用户 |
add_subtask | 添加一个清单项;以纯文本追加,不支持 Markdown |
update_subtask | 将其标记为已完成(is_done)、重命名,或在列表中上移 |
delete_subtask | 无法恢复——清单项没有回收站 |
archive_ticket | 移至回收站——30 天内可恢复 |
delete_ticket | 永久删除已在回收站中的工单——无法恢复 |
create_board | 可选择使用模板创建——crm 或 sales-leads |
link_tickets | “此工单在另一工单之前”——方向为 blocks 或 blocked_by |
unlink_tickets | 可从任一端解除关联,并移除这一对工单间的所有类型关联 |
评论没有版本控制,因此三种评论写入操作都不需要 version,也都不会返回 409——最后一次编辑会生效。只有作者可以编辑或删除评论;他人的评论会返回 404,且无法与不存在的评论区分,因此这些操作不会透露评论的作者。
get_ticket_comments 不会刻意将任何评论标记为已读;只有 mark_comments_read 会执行此操作,并会将已读位置标记到当时最新的评论,而不是订阅评论。
子任务是工单上的清单项——它们会显示在卡片的进度计数中;适合放置需要逐项勾选的验收标准。
get_ticket 会返回这些清单项,因此无需单独的列表工具。
看板结构
| 工具 | 说明 |
|---|---|
create_status | 新建列,并追加到最右侧 |
update_status | 重命名、修改颜色或设置 is_complete——每个看板最多只能有一个 |
reorder_statuses | 传入完整的 uuid 列表并指定顺序;列表过时会返回 409 |
delete_status | 列必须为空,且看板至少保留一列 |
create_group | 创建泳道;没有重命名接口,因此名称不对时需要删除后重新创建 |
reorder_groups | 与 reorder_statuses 一样,必须传入完整列表 |
delete_group | 其中的工单会保留,但变为未分组状态——其版本号也都会变化 |
create_custom_field | 仅限工作区所有者或管理员;之后无法更改 type |
update_custom_field | 重命名、调整顺序,或替换选择字段的整个 options 列表 |
delete_custom_field | 删除该字段在所有工单中的值——没有回收站,也无法恢复 |
这是 Agent 创建的看板脱离模板默认设置所需的操作。两个排序工具都要求传入完整列表,因为不完整列表可能导致两个并发排序操作悄无声息地丢失一列:调用前立即从 get_board 读取 uuid,遇到 409 时重新读取。
标签
| 工具 | 说明 |
|---|---|
list_labels | 工作区范围内的标签,按字母顺序排列——get_board 只显示看板正在使用的标签 |
create_label | 必须提供十六进制颜色;忽略大小写和空格后,名称仍须唯一 |
update_label | 重命名后会影响所有地方——标签只有一个,不会按看板分别复制 |
delete_label | 从所有带有该标签的工单中移除标签 |
标签是由所有看板共享的工作区对象,因此重命名前务必确认这一点:更改会影响所有人。这些工具返回的 uuid 可作为 update_ticket 的 label_uuids 参数。
附件
| 工具 | 说明 |
|---|---|
list_ticket_attachments | 名称、类型、大小和 uuid——不包含文件字节 |
get_ticket_attachment | 文本以内联形式返回,图像以图像块返回,其他内容通过 save_to 保存 |
upload_ticket_attachment | 磁盘上的文件使用 file_path;写入的内容使用 text + filename |
delete_ticket_attachment | 移入回收站 30 天;没有恢复工具 |
get_ticket 会报告 attachment_total,据此可以判断是否值得再调用一次来列出附件。
二进制内容不会跨越工具边界。工具结果是文本,Laver 允许每个文件最大为 25 MB,而 25 MB 的 base64 大约相当于 900 万个 token——因此只有文本、CSV 和小于 4 MB 的图像会以内联形式返回,其他内容都需要使用 save_to,将文件写入运行此服务的机器上的路径(通常是 Agent 自己的机器,因为客户端会将其作为子进程启动)。上传方式也一样:file_path 会从那台机器读取文件,且不占用上下文。
图像会以 MCP 图像块而非文本形式返回,这是模型实际能够查看图像的唯一形式——这正是该工具的全部意义,因为有人附上的截图通常就是规格说明。
Wiki
list_wikis、search_wiki、get_wiki_tree、get_wiki_page、
get_wiki_page_version、append_wiki_page、update_wiki_page、
create_wiki_page(正文以 Markdown 格式提供,并嵌套在
parent_page_uuid 下),以及 restore_wiki。
归档 Wiki(在浏览器中操作;这里没有对应工具)会将该 Wiki 及其下的所有页面
从 list_wikis 中完全移除。要查看这些 Wiki,需在 list_wikis 中设置
archived: true——这是唯一能看到已归档 wiki_uuid 的地方;这里唯一能对其
执行操作的工具是 restore_wiki:一次调用即可取消 Wiki 及其下所有页面的归档。
get_wiki_page_version 用于读取已被覆盖的内容。它是一个读取操作:页面不会发生变化。
这里有意不提供恢复旧版本的工具——读取旧版本,然后用 update_wiki_page 写入所需内容即可,
该操作会留下一个新版本。
append_wiki_page 会在页面末尾追加内容,update_wiki_page 则会替换页面内容;添加内容时
优先使用前者,因为它不需要 version,两个 Agent 同时追加时也都能保留各自的文本。
替换内容时需要提供 get_wiki_page 返回的 version;如果有人在此期间写入了页面,
操作会返回携带当前版本的 409,而不是覆盖内容。正是这个保护机制,才让此功能得以存在:
Wiki 页面位于实时协作编辑器之后。替换操作会用刚写入的内容重新初始化该编辑器,因此打开
页面的同事看到的是新文本,而不会把旧文本写回;所有先前保存的版本仍可通过
get_wiki_page_version 读取。目前仍没有删除页面的工具。
Markdown 会经过与工单描述相同的解析器。标题、列表、表格、代码块、引用块、分隔线和链接都会保留;
 图片也会保留为对该 URL 的引用——这里没有上传附件的工具,因此该 URL 必须
已公开。原始 HTML 会作为字面文本保留,而不会被解析。
自动化
| 工具 | 说明 |
|---|---|
list_automations | 看板上的规则,每条规则都包含其 version |
get_automation | 获取一条完整规则——编辑时会替换的条件和操作 |
list_automation_runs | 查看规则实际执行过的操作,按时间从新到旧排列 |
create_automation | 仅限所有者或管理员——调用前请先阅读下文 |
update_automation | 需要 version;设置 enabled: false 可暂停,且可恢复 |
delete_automation | 需要 version,并会一并删除该规则的全部运行历史 |
自动化规则由触发器、可选条件和最多二十个操作组成,并存储在看板中。Agent 使用这些工具前, 有三点值得了解:
- 在这里创建的规则会立即生效。 它会在触发条件满足后的几秒内运行,因此不要为了查看效果而
试探性地创建规则;创建后应通过
list_automation_runs查看运行情况。 - 规则是一项持续授权。 每次触发时,只要规则仍存在,它就会以此密钥所代表的用户身份运行—— 与此 MCP 服务中的其他写入操作不同,它不是只执行一次。撤销密钥不会停止规则;禁用或删除规则才会。
- 删除前先暂停。 使用
update_automation并设置enabled: false,可立即停止异常规则并保留其历史; 删除则会连同规则一起销毁历史。conditions和actions会替换已存储的列表,而不是与其合并, 因此请根据get_automation的结果构建这些列表,不要依靠记忆。
这两个写入操作都需要规则的 version,并会在版本冲突时返回携带当前版本的 409,
与工单写入操作的约定相同。
已发布的链接
list_published_links 是用于读取清单的工具:当前此工作区有哪些内容发布到了公共互联网,每行都包含其分享令牌、发布者、发布时间,以及有多少陌生人查看过。live 表示陌生人实际能看到什么,dark_reason 说明某行为何不可见。这是查找当前已归档、恢复后就会立即重新上线的已发布页面的唯一方法。仅限工作区所有者或管理员使用;普通成员会收到 403。故意不提供撤下链接的工具——只报告已发布的内容,由人决定哪些内容应该撤下。
未涵盖的内容
REST API 的范围比此服务器更大,这种差异是有意为之,并非疏忽——mcp/route-coverage.js 列出了每个后端路由,并注明调用它的工具或未覆盖的原因;如果某个路由既没有对应工具,也没有注明未覆盖原因,npm run check 就会失败。这能确保本节内容准确:此前它曾悄然过时,导致服务器整个生命周期都无法读取工单附件,而所有检查却始终通过。
尚未实现——需要,但尚未构建:
- 通知。 这仍是最大的缺口,而且现在受阻于工具无法解决的问题:Agent 无法知道自己被提及或指派了任务,并且
backend/notifications/index.js在整个插件上安装了session_only,因此那里的每个路由都会向密钥返回 401,无论工具多好都无济于事。要添加一个工具,首先得决定密钥是否可以读取其所有者的收件箱——那里包含其他人的消息——这是产品和安全方面的决策,而非交付任务。list_workspace_tickets覆盖了分诊需求,是目前最接近的替代方案。 - 单独列出子任务。 这不是缺少工具,而是不需要工具:
get_ticket已经会返回清单项本身——uuid、标题和完成状态——因此专门的列表路由只是多一种方式来询问同一个问题。创建清单项的需求已有覆盖。 - 看板模板。 工作区可以将自己的某个看板保存为模板,并用它创建下一个看板。没有工具可列出模板,因此无法访问已保存的模板:
create_board在其 schema 中列出了两个内置 id,而已保存模板的 id 是 uuid,这里没有任何东西可以发现它。保存和删除功能是有意不提供的,而不是尚待实现——模板是工作区范围内的设置,会出现在每个人的创建看板表单中;添加或移除模板属于应在看板界面中做出的决定。 - 附件缩略图。 这是有意为之,并非缺口。缩略图路由会向 Web 客户端提供缩小后的 webp,这样卡片封面只需几 KB,而不是几 MB;Agent 需要的是某人实际上传的文件,而
get_ticket_attachment已经会以原始格式返回完整分辨率的文件。 - 上次停留位置。 ⌘K 面板会在用户输入任何内容之前提供五项内容——他们最近打开或处理过的工单和页面。通过密钥完成的工作会被有意排除在外,因此此处的工具会询问其所有者读过哪些工单;对于该处理什么的问题,
list_workspace_tickets是更好的答案,而更具体的问题则可以用search。 - 看板分析。 现在分诊需求已由
list_workspace_tickets覆盖,因此这只是个小小的缺憾。剩下的功能是为图表而非决策设计的:统计数据和看板整体流转情况是供用户在屏幕上查看的汇总信息,活动记录是动态信息流,search-text 用于输入时查找,而导出会返回一个文件。get_ticket_flow覆盖了 Agent 会采取行动的唯一数据点:单个工单的流转情况,可归因到具体工单。 - 看板对应的 wiki 页面。 这是有意为之,因为答案的所有部分都已可访问,而该路由只是将它们关联起来。
get_board会返回看板的wiki_page_uuid,而get_wiki_page和get_wiki_tree可以在相同访问规则下读取该页面及其下的所有内容,而且内容更丰富。这个路由的用途是让查看看板的用户无需记住手册名称就能找到它;持有 uuid 的 Agent 不需要这样的快捷方式。 - 负责人记分板。 永远不会提供,而且并非因为这个工具很难实现。该路由会读取某人撰写的工单在 Agent 接手后进展如何,且只对该用户可见;让 Agent 读取向它提供简报的人员的评分,方向就错了,而能读取这些评分的 Agent 也可能会迎合评分。
- 触发自动化按钮。 这是有意不提供,而非待实现:
manual规则的存在就是为了让用户决定何时运行它,而提供一个能触发它的工具就会把这个决定权交出去。创建规则是安全的部分,已经有相应工具。 - 工单历史记录、复制和周期性安排。
- 冲刺。 受阻于与通知相同的问题:
backend/sprints/index.js在插件上安装了session_only,因此密钥调用那里的每个路由都会收到 401——包括POST /boards/:uuid/sprints。是否向密钥开放这些功能,取决于密钥可以如何影响团队的规划节奏(冲刺结转会将所有未完成工单迁移到一个新看板),而非编写工具的问题。 - 删除和重新排列 wiki 页面。 现在可以通过
update_wiki_page编辑页面;但归档、移动和复制功能没有提供,因为它们改变的是同事能找到什么,而不是页面说了什么,而且没人能找到的页面也没有版本历史可供查看。移除整个 wiki(DELETE /wikis/:wiki_uuid,会将其及其下的所有页面归档)也属于同一类:它会在归档这些页面的同时,将 wiki 已发布的页面从互联网撤下;这是关于同意的决定,而非编辑。相反,恢复路由已由restore_wiki覆盖,现在list_wikis可以找到已归档的 uuid 并传给它。 - 在 wiki 页面上发表评论。 之所以没有提供,是因为锚定方式,而非调用形式。页面上的评论会附着在其所针对的文字上——包括引用内容,以及它出现的位置——因为页面是协作编辑的,文档中存储的位置会在同事于其上方输入内容的那一刻开始指向不同的文字;客户端会在刚刚渲染的文档中重新查找该引用。Agent 不持有文档,因此它会发送自己认为存在于页面上的引用,而它最常遇到的失败是悄无声息的:某个短语出现两次,两端对出现次数的计数不同,结果评论就锚定到了错误的句子。一个值得提供的工具应接收页面和引用,并在找不到引用时明确说明;如果以后要让 Agent 审阅页面,这就是下一项合适的工作。在此之前,密钥已经有一种诚实的方式来表达对页面的看法:
append_wiki_page和update_wiki_page会将内容写入页面本身,供阅读页面的人看到,而不是写在其他工具都无法读取的边栏里。编辑、撤回和解决评论都以此为前提——无法发表评论时,密钥就没有内容可供编辑;特别是解决评论,会移除他人文字上的高亮,并宣告人与人之间的对话已经结束。读取评论线程本身也没有提供,这是有充分理由的:这些评论是关于草稿的讨论,而 Agent 要了解页面内容时应该使用get_wiki_page。 - 导入和反馈表单。 绝不使用密钥:
- 报告部署情况。
POST /tasks/:task_uuid/deployments是构建流水线通知 Laver 某个工单的拉取请求已部署到某个环境或部署失败的方式,以便通知分配给该工单的人员。调用方是持有密钥的 CI 作业,密钥存放在机密中,此时没有模型在运行;如果为此提供工具,助手就能宣布一次流水线根本没有执行的部署,而这条通知的全部价值在于报告事实,而非声称。Agent 可能想要的读取功能已经具备:部署是工单事件,因此get_ticket_flow和时间线会记录发生了什么、何时发生,以及由哪个密钥报告。 - 报告拉取请求已合并。
POST /tasks/:task_uuid/pull-requests/merged也遵循同样的原则:CI 作业告知工单的拉取请求已合并,任务看板便将工单移入标记为已合并的列。提供工具就相当于提供第二种移动卡片的方式——Agent 已有move_ticket——区别仅在于它还会在时间线上写下“此工单的拉取请求已合并”,而没有合并任何内容的助手无权声称此事。读取功能已有覆盖:合并是工单事件,因此get_ticket_flow会记录此事。 - 公开链接和发布。 发布会将私有内容变成任何拥有 URL 的人都能阅读的内容。这需要征得同意,而工具调用并不适合承载这种同意。
- 撤下已发布的链接。 读取功能已上线,而两个 DELETE 接口没有上线,原因相同:工具无法承载同意,但这不代表不能询问哪些内容已公开。
list_published_links可以回答这个问题,且不会赋予调用方原本没有的权限,因为它列出的每个链接按定义都是公开的。撤下链接是一项范围有限的管理权限,应当在屏幕显示即将被撤下的内容时行使;因此,Agent 负责报告清单,由人决定撤下哪些内容。 - 请人签名。 这些请求并非只是尚未实现,而是在路由层被拒绝:三种请求都会在读取任何内容之前对密钥返回 401。发送请求会向陌生人的收件箱发出消息,并使用我们的 SPF 和 DKIM;判断某个地址是否值得被要求签名,应由人来做——密钥会以创建者的身份行事,并继承其编辑权限,因此权限不会构成阻碍。列出待处理请求是此对话自身的读取功能:它会将请求与根据实时文档计算出的相关章节一并返回,而这些章节可能会附加在该文档上;
get_wiki_page已能获取文本。撤回请求也需要从另一方做出同样的判断。 - 移除看板及其外观。 这一组中的结构管理功能已覆盖——状态、分组和自定义字段都有对应工具。剩下的是移除和装饰。归档和删除看板只是同一写入操作的两种名称;如果为其中任何一种提供工具,密钥就能通过一次调用让整个看板及其上的所有工单从工作区视图中消失。恢复功能也因此没有覆盖,因为 Agent 无法执行归档操作,而
list_boards有意不查找已归档的 uuid。永久删除的后果更严重——看板记录、工单及其附件数据都会被删除——这需要由人做出决定,而且要在屏幕显示即将失去的内容后再做一次确认。看板的颜色和背景图片是在查看看板时选择的装饰,而 Agent 无法查看看板。列的进入条件也属于这里:读取无需工具,因为get_board已会返回每列的entry_requirements,而被拒绝的move_ticket会列出工单未满足的每项条件;但能设置这些条件的密钥也能移除它们,然后移动工单,因此这道门槛的强度只取决于它所依赖的最弱工具。 - 在工作区之间移动看板。 这并非关于影响范围的判断——密钥根本无法发起此调用。密钥仅限于其签发所属的单个工作区,而
POST /boards/:board_uuid/move-workspace涉及两个工作区:看板当前所在的工作区,以及它将要移入的工作区。提供此操作的工具需要一个能访问两个工作区的密钥,这就违背了访问限制;否则,它就会拒绝所有收到的调用。这也不是 Agent 应做的操作:两个工作区的管理员要决定整个看板——包括其工单、评论、附件和历史记录——应归属哪个工作区;该路由要求用户在移动任何内容之前,必须在两端都拥有所有者或管理员身份。 - 工作区和成员管理。 密钥会以创建者的身份行事;重命名或删除其工作区,或代其回复邀请,都超出了委托看板任务的本意。
- 整个工作区的导出。
POST /workspaces/:uuid/exports及其相邻路由会生成一个文件,其中包含工作区内的所有工单、评论和 Wiki 页面,而且在查看任何其他内容之前就会拒绝密钥。如果 CI 变量中的密钥能够请求此类导出,那么一旦密钥泄漏,就会造成全面数据泄露,而非密钥原本获准的有限访问。复制公司数据应由人员登录后从 Admin 执行,审计记录也会注明具体人员。 - 批量工单写入。
POST /tasks/archive一次调用即可将多个工单移入回收站。archive_ticket则要求一次处理一个工单,体现了审慎的选择。 - 回收站。 这是有意设计为单向的:Agent 可以归档,也可以销毁自己已归档的内容;恢复内容则由人来操作。
- 看板事件流。 这是服务器发送事件;工具调用只有一次请求和一次响应。替代方案是使用带有
updated_since的list_tickets。 - 调度器端点。 这些是部署自身的 cron 钩子。
移除工单
有意分为两个步骤,以免一次调用就销毁内容:
archive_ticket task_uuid → the workspace trash, recoverable for 30 days
delete_ticket workspace_uuid + task_uuid → gone, and nothing brings it back
delete_ticket 会拒绝删除尚未归档的工单,因此执行顺序由服务端强制保证,而非依赖约定。这里没有恢复工具——回收站中的工单需要从 Web 应用中恢复——因此请将 archive_ticket 视为你能自行执行的最远一步。
如果打算销毁工单,请先阅读它,再将其归档:
delete_ticket 需要 workspace_uuid,可通过 get_ticket 获取;而工单归档后就无法再读取。
如何确定下一步做什么
每次读取工单都会返回 blocked_by、blocks 和 is_blocked。当所有阻塞项都进入完成列后,is_blocked 就会变为 false,因此看板上已准备就绪的工单就是 is_blocked 为 false 的那些工单。link_tickets 会记录依赖关系;如果链接会形成循环,系统会以 409 拒绝,因为循环会导致无法确定执行顺序。
唯一值得牢记的规则
工单带有 version。每次写入都必须发送读取到的版本;如果针对过期版本写入,系统会以 409 拒绝,而不会悄悄覆盖先写入的内容。服务端会将这一点转化为操作指引,而 Laver 的拒绝响应会附带当前版本,因此指引中可以直接包含该版本,无需再多调用一次:
Laver 409:任务已被另一个请求更新。
有人先进行了写入,因此你发送的版本已过期。当前版本是 12。如果你的更改不依赖之前读取的内容——例如将工单移动到指定列——请使用该版本重试。如果更改依赖之前读取的内容,请再次调用 get_ticket,并根据工单当前状态作出决定,否则你可能会悄悄撤销对方的写入。
先读取,再写入。不要跨较长的操作过程缓存版本。
密钥被拒绝时
401 表示密钥有问题:缺失、拼写错误、已撤销、已过期,或是从未填入实际值的占位符。底层消息未必能准确说明发生了什么——JWT 层无法解析的密钥会返回 “Authorization token is invalid: The token is malformed”,听起来像是字符串损坏,而通常的原因只是密钥已被替换。服务端会附上后续处理方法,包括容易被忽略的一点:
MCP 客户端在启动此服务时只会读取一次该环境变量,因此之后必须重启客户端——在运行中的会话里编辑配置不会产生任何效果。
403 则不同,绝不值得重试:密钥已被接受,但该操作被拒绝。密钥的授权范围属于另一个工作区,或者其代表的用户角色为只读,又或者该用户是无权访问该看板的访客。
密钥只会在一个位置读取,并作为 bearer token 发送,绝不会被记录、回显或包含在任何错误文本中。
检查
node check.js # schema, then every read-only tool actually called
node check.js --require-live # …and a skipped sweep is a failure, for CI
# the same calls against the API the published package actually talks to
LAVER_API_KEY_FILE=../.env node check.js --live-api
分为两部分。第一部分是静态检查:确保每个工具都已注册、归类为读取或写入、完成描述并提供架构。第二部分会从 ../backend 启动后端,占用一个空闲端口,为自身创建一个工作区,针对该工作区生成一个密钥,并调用每个只读工具——先通过工具自己的 zod 架构,再调用其处理程序,并发送该工具声明的所有参数。
之所以需要第二部分,是因为第一部分曾经通过了,但 list_wikis 发送的是 ?workspace=,而路由要求 workspace_uuid。它每次调用都会返回 400;又因为它是唯一能提供 wiki_uuid 的工具,因此从发布之日起,这个服务的整个 wiki 部分就无法访问——尽管注册、描述和架构形式始终完美无缺。
它需要的条件与 backend/ 中运行 npm test 相同:该目录、其中的 node_modules、它的 .env,以及这些文件指向的 Postgres。它不需要 API 密钥,也不需要 localhost 之外的网络——环境中的真实 LAVER_API_KEY 会被忽略。如果没有后端,它会打印一条提示横幅,说明未调用这些工具,并且只运行架构检查;--require-live 会将这种情况视为失败。
不过,它是否仅在本地运行,取决于 backend/.env 指向哪里。运行它会写入该文件指向的任何数据库:创建一个工作区、一个看板、两张工单、一条评论、一个 wiki、一个页面和一个 API 密钥,并在结束时将它们删除。它还会在自己的进程中加载后端。它使用 app.server.listen 而不是 app.listen 进行监听——collab 和 board-events 集成测试也使用了同样的惯用写法——因此 Fastify 的 onListen 钩子不会触发,七个调度器都不会启动;否则仅账单扫描就会针对该数据库中的每个工作区运行。如果将 backend/.env 指向 staging,这项检查就会向 staging 写入数据。
这项_扫描_是只读的,并且会一直保持只读:每次运行都会在别人的工作区中创建工单的检查,人们很快就会停止运行。写入工具仅由架构检查覆盖——参见 check.js 末尾的说明,了解如何在不执行写入的情况下覆盖它们。
按 Ctrl-C 是安全的。扫描会在当前调用完成后停止,并且在进程退出前删除测试工作区;如果卡住的正是当前调用,再按一次 Ctrl-C 就会立即终止进程。
针对已部署的 API
--live-api 会将相同的调用指向 LAVER_API_URL——除非另有说明,否则为 https://api.laver.app——并使用真实密钥。密钥取自 LAVER_API_KEY,或取自 LAVER_API_KEY_FILE 指定的文件,方式与服务端读取密钥的方式完全相同。
之所以提供此模式,是因为本地运行全绿和已发布的软件包正常工作,是两种不同的说法。本地全面检查可以证明工具与眼前的代码一致;而此软件包会调用已部署的 API,因此,如果路由先于软件包发布重命名,实地使用的每个 Agent 都会出问题,本地检查却仍然全绿。这种时间窗口很短——工具和路由都在同一个仓库中,一起变更——但这正是发布到 npm 会打开的窗口。
两种模式都运行 cases.js 中的同一组用例,每个用例都包含两种模式各自的预期:本地模式下,在 fixture 已知的情况下检查精确数量;线上模式下,在工作区属于真实用户、无法预先填充或拆除的情况下,检查结构和不变量。用例如果只覆盖其中一种模式,就算失败,这样新调用就不能只覆盖一种传输方式而跳过另一种。
它不会创建或删除任何内容,这一点不是口头承诺,而是由代码强制保证:线上模式会将 fetch 替换为一个只允许 GET 方法的版本,因此即使误调用写入工具,也完全无法访问网络。frontend/tests/check-mcp-live-api-mode.mjs 会通过 stub API 运行整个模式,并断言所有离开进程的请求都是 GET。
它不会使用 fixture,而是会主动寻找可供测试的对象,并要求某个看板至少包含两列、每列至少有两张工单,且所在工作区必须有包含页面的 wiki。如果条件不足,它会拒绝运行,而不会悄悄通过:针对空看板的过滤器用例,无论是否应用了过滤器都会通过,而这个文件正是为了防止此类问题。
此模式需要主动启用,绝不会纳入 npm run check:all 或 CI——它需要密钥和网络,而在没有密钥的机器上运行的检查不应依赖这两者。
发布
0.1.0 已发布,但存在问题。不要告诉任何人安装它。 它可以启动、注册全部 20 个工具,却不会连接任何传输,并以退出码 0 结束,且不会向 stdout 或 stderr 写入任何内容——因此客户端只能看到进程结束,除此之外什么也没有。入口点守卫会将 process.argv[1] 的_basename_与此文件名进行比较,只有运行 node mcp/server.js 时才会匹配;而 npm 为 bin 创建的 shim 会将 argv[1] 设为 node_modules/.bin/laver-mcp,因此 README 告诉所有人使用的 npx -y @laver/mcp 根本无法匹配。此问题已在 0.1.1 中修复;现在 tests/check-mcp-bin-entrypoint.mjs 会通过符号链接启动服务端,并与其进行 MCP 通信,因此同类问题不会再次发布。
0.1.1 发布后,请将有问题的版本标记出来,以免有人误装:
npm deprecate @laver/[email protected] "Never connects its stdio transport when run via npx or the bin shim. Use 0.1.1 or later."
另一种选择是取消发布 0.1.0,但更糟:72 小时内虽然可以移除该版本,版本号却无论如何都会被占用,而且任何已经锁定该版本的用户都会遇到问题,而不是收到警告。
发布操作应由所有者执行,因为此操作公开可见、具有足够的永久性,并且会占用一个此后其他人无法再使用的名称。
名称。 npm 上的 laver 已被占用——v1.0.0 于 2021 年由一位无关的维护者发布——因此无法使用裸名称,今后也不会有机会使用。本软件包因此命名为 @laver/mcp:将品牌名称保留为 scope,通用部分则放在应有的位置。已于 2026 年 8 月 6 日向 registry 核实——@laver/mcp 尚可用,旧的无 scope 名称 laver-mcp 也从未发布过,因此更名毫无损失。
scope 已创建,首次发布也已完成。 0.1.0 和 0.1.1 已于 2026-08-06T22:14Z 发布到 @laver/mcp 名下——这才是证明该 scope 可正常解析且发布账号有权向其发布内容的唯一有效依据。本段过去曾写着该 scope 尚不存在;当时确实如此,现在则不然。
不要再用 https://registry.npmjs.org/-/org/laver 测试。无论该组织是否存在,这个 URL 对未认证请求都会返回 404,因此无法区分两者——请改为查看包文档:
curl -s 'https://registry.npmjs.org/@laver%2Fmcp' | python3 -m json.tool
对于以自有 scope 发布的可自托管分支,首次发布仍需要在 https://www.npmjs.com/org/create 创建该 scope(公开包免费),或确认该 scope 就是账号自己的用户名,并用 npm whoami 检查成员资格。如果 scope 不存在,npm publish 会失败,并显示
404 Not Found - PUT https://registry.npmjs.org/@<scope>%2fmcp,看起来像网络故障,而不是组织缺失。
可执行文件仍为 laver-mcp。 包名是 @laver/mcp,但我们特意没有把 bin 重命名为 mcp:全局安装会在 PATH 中放入一个名为 mcp 的命令,这个名称过于通用,会与任何人安装的其他 MCP 服务冲突。无论命令叫什么,npx -y @laver/mcp 都能正常运行——npx 会运行包中唯一的 bin——因此上面的配置片段并不依赖命令名称。
repository 和 bugs 指向公开镜像。 当时唯一的主页 github.com/Developyn/laver 是私有仓库,因此未设置这两个字段——npm 会将它们都呈现为包页面上的链接,而把仅有的两个“来源在哪里”链接指向 404,还不如都不设。自 2026 年 8 月起,发布文件会镜像到公开仓库 github.com/Developyn/laver-mcp,所以这两个链接现在都能正常访问。注意这里没有 "directory" 键:镜像的根目录就是包本身,而在此仓库中,同一批文件位于 mcp/ 下。
所有 MCP 目录都会将条目链接到该镜像,因此镜像必须保持更新。每次发布后,将已发布 tarball 中的内容复制到镜像——npm pack @laver/mcp && tar xzf laver-mcp-<version>.tgz——然后提交。镜像的 Dockerfile、glama.json 和 CI 工作流仅用于镜像,不在 files 中,因此不会发布到 npm。
mcpName 用于官方 MCP 注册表,该注册表会将包与在那里发布的服务名称进行匹配。它必须与 mcp-publisher login github 实际授予你的命名空间一致——如果授权的是组织,则为 io.github.developyn/…;如果只授权了个人账号,则为 io.github.melvyn-developyn/…。填错并非致命问题,但更正需要再发布一个版本。
每次发布前
- 如果发布账号设置了发布时需要 2FA,则启用 2FA(npm 会对某些账号和包强制要求,也会对其他账号和包进行提示)。传入
--otp可避免非交互运行因提示而失败;如果未注册 2FA,则移除该参数。 - 确认
npm whoami返回的是该账号;否则运行npm login。 - 如果使用 CI 而不是笔记本电脑:在
NPM_TOKEN中设置一个 automation token(粒度权限限定为对此包的写入权限)。Automation token 会跳过 2FA 提示,而 classic read-write token 不会。 - 使用注册表中尚不存在的版本。 npm 不允许重新发布已有版本,因此这不是整理工作,而是发布得以进行的必要条件。先检查当前线上版本,因为仓库中的版本号可能与注册表中的相同,但内容却不同,而且不会有任何警告:
npm view @laver/mcp version。新增工具时递增 minor;修复现有工具时递增 patch。
发布
cd mcp
npm ci # the lockfile, not whatever resolves today
npm run check # schema + every read-only tool actually called
npm pack --dry-run # confirm the file list is LICENSE, README.md, package.json, server.js
npm publish --access public --otp=<code-from-your-authenticator>
npm ci会删除并重新安装mcp/node_modules。在共享开发检出目录中,该目录由所有基于它运行的 Agent 共用,中途移除会导致它们的测试失败——这就是为什么 Agent 操作说明禁止这样做,也因此准备发布的 Agent 会在此代码块前停止。对于实际执行发布的人来说,这样做是正确且符合预期的;只要不要在其他人还在同一工作树中操作时运行它即可。--access public在此处是必需的:作用域包默认受限,而免费账户上的受限发布会被直接拒绝。package.json 中的publishConfig已设置此项,因此该标志只是双重保险,并非防止包变为私有的唯一保障。
之后
npx -y @laver/mcp # should start and wait on stdio, not exit
npm view @laver/mcp
只有短时间内可以挽回错误:npm unpublish @laver/mcp@<version> 在 72 小时内有效,此后该版本号将永久作废。取消发布某个版本不会让包 名称 重新回到可用池中。
许可证
MIT — 详见随包一同发布的 LICENSE。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。