文章首发于: https://feinterview.poetries.top/blog/ai-agent-zentao-bug-mcp-integration
用一份自建
MCP Server把禅道 bug 流程接入Claude/Cursor/Codex,文章配套完整的分步落地方案:5 分钟跑通最小版本、关键模块代码、Token缓存、多形态 bug 列表回退、截图落档与归档SKILL,照着做就能把团队的禅道 bug 流程接到 AI Agent 上。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- 为什么不要让 AI Agent 直接「裸调」禅道 REST API
- 用
Node.js 18+实现一份纯stdio的MCP Server的完整步骤 - Token 缓存、HTTPS 强约束、相对路径白名单等安全护栏
- 「我的 bug / 产品 bug / 项目集 bug」三种视角的回退策略
- bug 详情里 HTML 描述、内嵌截图、动态时间线的解析与脱敏裁剪
- 配套
SKILL把 bug 上下文按日期 + 经办人沉淀到本地工作目录 - 生产环境上线前的安全 checklist
# 痛点与解决方案
在一些以禅道(ZenTao)为唯一 bug 平台的团队里,常见的协作节奏是:测试在禅道里提 bug → 开发翻邮件或 IM 提醒 → 进禅道复制描述、下载截图、看历史动态 → 拉本地分支修 → 回禅道点「解决」并写说明。这个流程的核心矛盾不在禅道本身,而在「bug 上下文是网页里的活数据,AI Agent 看不见」:你让 Cursor / Claude 帮你定位代码,它最多看到你贴过来的一段标题,没法读到内嵌截图、字段编辑历史、上一次评审意见。
解决方法是把禅道 RESTful API 包成一份只暴露最小工具集的 MCP Server,加一份配套 SKILL 让 Agent 主动把 bug 落到本地工作底稿。下面会给出一份从 0 到 1 的完整落地步骤,你照着走就能让团队的 AI Agent 接入禅道。
# 一、整体架构
整套系统是「Agent ↔ MCP Server ↔ 禅道 REST API」三段式:
┌─────────────┐ stdio ┌──────────────────┐ HTTPS ┌────────────────┐
│ Claude / │ ◄────────► │ zentao-mcp- │ ◄────────► │ 禅道实例 │
│ Cursor / │ JSON-RPC │ server (Node) │ Token │ /api.php/v1 │
│ Codex │ │ (10 个工具) │ │ │
└─────────────┘ └──────────────────┘ └────────────────┘
│ │ │
│ ▼ │
│ ┌───────────────┐ │
└────────────────► │ bugfix/<date> │ ◄──── 落档 SKILL │
│ /bug-N-... │ 按日期 + 经办人归档 │
└───────────────┘ │

三处选型说明:
- 要求
Node.js 18+:直接用内置fetch+AbortController,省掉node-fetch这类依赖,发布包体积小、攻击面小。 stdio而非HTTP:MCP 客户端把进程 stdin/stdout 接到协议层,Server 不监听端口,省掉鉴权、CORS、TLS 这些自找的麻烦。- 不在仓库里存密钥:所有凭证只从环境变量读,配套
.env.example;连默认日志都做了字段脱敏。
# 二、五分钟跑通最小可用版本
下面是一份照抄就能跑的最小路径,先把链路打通,再做下一步优化。

# Step 1 准备禅道实例信息
向运维 / DBA 拿到三样东西并验证:
# 1. 禅道访问入口(必须 HTTPS)
ZENTAO_BASE_URL="https://zentao.example.com"
# 2. 一个最小权限的服务账号(强烈不要用管理员账号)
ZENTAO_ACCOUNT="bot_account"
ZENTAO_PASSWORD="<from-vault>"
# 3. 验证 token 接口是否通
curl -sS -X POST "$ZENTAO_BASE_URL/api.php/v1/tokens" \
-H 'Content-Type: application/json' \
-d "{\"account\":\"$ZENTAO_ACCOUNT\",\"password\":\"$ZENTAO_PASSWORD\"}"
# 期望返回 { "token": "xxxxxxxxxxxxxxxx", ... }
返回里没有 token 字段就先不要往下做,先找运维确认 apiPrefix 是不是 /api.php/v1、是否开启了 RESTful API v1 接口。
# Step 2 创建工程骨架
mkdir zentao-mcp-server && cd zentao-mcp-server
npm init -y
npm i @modelcontextprotocol/sdk
node -e "console.log(process.versions.node)" # 必须 >= 18
在 package.json 里加上 "type": "module" 和启动脚本:
{
"type": "module",
"main": "src/index.js",
"scripts": { "start": "node src/index.js" },
"dependencies": { "@modelcontextprotocol/sdk": "^1.0.0" }
}
# Step 3 写一个能拿 Token 的最小 client
新建 src/zentao.js,先把「鉴权 + 请求」这条骨架打通:
export function createZenTaoClient({ baseUrl, account, password }) {
let cachedToken = "";
let cachedAt = 0;
const TTL = 50 * 60 * 1000; // 50 分钟
async function getToken() {
if (cachedToken && Date.now() - cachedAt < TTL) return cachedToken;
const resp = await fetch(`${baseUrl}/api.php/v1/tokens`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ account, password }),
});
const data = await resp.json();
const token = data?.token || data?.data?.token;
if (!token) throw new Error("token field missing");
cachedToken = token;
cachedAt = Date.now();
return token;
}
async function call(path, { method = "GET", query, body } = {}) {
const token = await getToken();
const url = new URL(`${baseUrl}/api.php/v1${path}`);
if (query) for (const [k, v] of Object.entries(query)) {
if (v != null) url.searchParams.set(k, String(v));
}
const resp = await fetch(url, {
method,
headers: { Token: token, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json();
}
return { getToken, call };
}
这里是最小骨架,省略了
AbortController超时、相对路径白名单、HTTPS 校验等安全护栏 —— 第四节会补齐。
# Step 4 注册第一个工具
新建 src/index.js,先只暴露一个 get_my_bugs 工具,跑通端到端:
# Step 5 接入 Claude Desktop / Cursor
在 MCP 客户端配置里加这一段(路径换成你自己的):
{
"mcpServers": {
"zentao": {
"command": "node",
"args": ["/abs/path/to/zentao-mcp-server/src/index.js"],
"env": {
"ZENTAO_BASE_URL": "https://zentao.example.com",
"ZENTAO_ACCOUNT": "bot_account",
"ZENTAO_PASSWORD": "<from-vault>"
}
}
}
}
重启客户端,跟 Agent 说一句「列出我现在 active 的 bug」,如果能拿到列表,端到端就跑通了。
# 三、完整工具集设计
最小版本跑通后,再补到生产可用需要 10 个工具,按职责分组:
| 类别 | 工具 | 说明 |
|---|---|---|
| 鉴权 | get_token |
拿/刷新 Token,回显只给脱敏摘要 |
| 探查 | list_my_projects |
列出「我参与的项目」,方便对路径 |
| 读取 | get_my_bugs |
取「指派给我」的 bug,支持产品/项目集 |
| 读取 | get_bug_detail |
单条 bug 详情,含动态时间线 |
| 读取 | get_bug_image |
把 bug 截图按 fileId 拉成 base64 |
| 写入 | resolve_bug |
处理 bug,默认 resolution=fixed |
| 写入 | batch_resolve_my_bugs |
批量处理「我的 bug」,默认遇错即停 |
| 写入 | close_bug |
关闭 bug |
| 写入 | verify_bug |
验证结果:pass=关闭 / fail=激活 |
| 写入 | comment_bug |
添加备注 |
设计要点:
- 读写分离命名:所有动作类工具名是动宾结构(
resolve_bug/close_bug),让模型一眼看出副作用。 - 每个工具都有结构化错误:
{ ok: false, tool, message, status, hint },并在hint里写出最常见的修复建议(比如「报Need product id时设置ZENTAO_PRODUCT_ID」),减少来回试错的轮次。 - 批量动作默认 stopOnError:批量改 bug 是个高危操作,宁可半途停下让用户复核,也不要静默吞错继续跑。
# 四、Token 与请求层的安全护栏
Step 3 的骨架代码够用但不够安全,上生产前一定要补齐三条硬护栏:

# 1)默认 HTTPS,不允许裸 HTTP
# 2)相对路径白名单
# 3)请求超时
# 4)日志脱敏
# 五、bug 列表的多形态适配
