@aipper/zentao-mcp-server 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -3
- package/README.md +29 -17
- package/package.json +11 -2
- package/scripts/smoke.mjs +2 -1
- package/src/index.js +36 -52
- package/src/tools.js +82 -64
- package/src/zentao.js +549 -86
- package/IMPROVEMENTS.md +0 -179
- package/scripts/release-npm.sh +0 -481
package/.env.example
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# 禅道基础地址(不要带末尾 /)
|
|
2
|
-
ZENTAO_BASE_URL=
|
|
2
|
+
ZENTAO_BASE_URL=https://zentao.example.com
|
|
3
3
|
|
|
4
4
|
# 必填:账号密码(MCP 启动后自动获取并缓存 Token)
|
|
5
5
|
ZENTAO_ACCOUNT=your_account
|
|
@@ -32,8 +32,8 @@ ZENTAO_TOKEN_TTL_MS=3000000
|
|
|
32
32
|
# 可选:超时(毫秒)
|
|
33
33
|
ZENTAO_HTTP_TIMEOUT_MS=30000
|
|
34
34
|
|
|
35
|
-
#
|
|
36
|
-
|
|
35
|
+
# 可选:仅在临时调试老旧内网实例时允许 HTTP(强烈不推荐)
|
|
36
|
+
# ZENTAO_ALLOW_INSECURE_HTTP=true
|
|
37
37
|
|
|
38
38
|
# 可选:启用调试日志(输出到 stderr)
|
|
39
39
|
# ZENTAO_DEBUG=true
|
package/README.md
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## 目标
|
|
4
4
|
- 提供一个可运行的 MCP Server(stdio),连接你的禅道 RESTful API v1。
|
|
5
|
-
- 最小功能:自动获取/缓存 Token +
|
|
5
|
+
- 最小功能:自动获取/缓存 Token + 常用 bug 工具。
|
|
6
6
|
|
|
7
7
|
## 非目标
|
|
8
8
|
- 不在仓库内存任何密钥/Token。
|
|
9
|
-
-
|
|
9
|
+
- 不暴露无边界的通用 REST 写接口。
|
|
10
|
+
- 不保证覆盖你禅道的全部 API;优先支持高频 bug 流程,再按你的流程补工具。
|
|
10
11
|
|
|
11
12
|
## 依赖
|
|
12
13
|
- Node.js 18+(需要内置 `fetch`)
|
|
@@ -28,6 +29,8 @@ MIT - 详见 [LICENSE](./LICENSE) 文件
|
|
|
28
29
|
> 注意:不同禅道版本/部署方式的 token 端点与返回结构可能不同;可通过 `ZENTAO_TOKEN_PATH`/`ZENTAO_API_PREFIX` 调整。
|
|
29
30
|
>
|
|
30
31
|
> 默认情况下不需要配置 `ZENTAO_API_PREFIX`(默认值是 `/api.php/v1`)。
|
|
32
|
+
>
|
|
33
|
+
> 安全约束:`ZENTAO_API_PREFIX` / `ZENTAO_TOKEN_PATH` 只支持相对路径;默认要求 `ZENTAO_BASE_URL` 使用 HTTPS。`ZENTAO_BASE_URL` 可为根路径或子目录部署地址(如 `https://zentao.example.com/zentao`)。
|
|
31
34
|
|
|
32
35
|
## 安装与运行
|
|
33
36
|
```bash
|
|
@@ -94,42 +97,51 @@ npm run smoke
|
|
|
94
97
|
## 常见错误(`-32000`)
|
|
95
98
|
`-32000` 通常是客户端侧“通用 MCP 调用失败”映射码,优先检查:
|
|
96
99
|
- `env` 是否完整传入(尤其是 `ZENTAO_BASE_URL`/`ZENTAO_ACCOUNT`/`ZENTAO_PASSWORD`)。
|
|
100
|
+
- 默认要求 HTTPS;如果你的实例只能走 HTTP,必须显式设置 `ZENTAO_ALLOW_INSECURE_HTTP=true`,且只建议临时内网调试使用。
|
|
97
101
|
- 若报 `Need product id`,请设置 `ZENTAO_PRODUCT_ID`,或在 `get_my_bugs` 传 `productId`。
|
|
98
102
|
- 若你的 bug 在“项目集/我的视角”而非产品,建议设置 `ZENTAO_PROJECT_SET_ID`,并配置 `ZENTAO_MY_BUGS_PATH=/my/bug`。
|
|
103
|
+
- 项目集场景下,优先直接调用 `get_my_bugs` 并传 `projectSetId`,必要时再显式传 `path="/my/bug"`;不要先依赖 `list_my_projects` 找项目。
|
|
104
|
+
- 有些项目集本身没有创建实际项目,但仍然存在“我的 bug”;这类数据可能不会出现在 `list_my_projects` 结果里。
|
|
99
105
|
- `get_my_bugs` 会按候选路径回退(包含项目集路径);即使首个路径返回空列表也会继续尝试,并会把多端点结果合并去重。
|
|
106
|
+
- `get_my_bugs.total` 表示最终“我的 bug”总数;若需排查底层一共扫描了多少条,可看 `raw.scannedTotal`。
|
|
100
107
|
- 排查时看工具返回里的 `raw.triedPaths` / `raw.paths`,可确认每条路径的返回码与命中数量。
|
|
101
|
-
- `ZENTAO_API_PREFIX`/`ZENTAO_TOKEN_PATH`
|
|
108
|
+
- `ZENTAO_API_PREFIX`/`ZENTAO_TOKEN_PATH` 是否和你的禅道实例一致,且使用相对路径。
|
|
102
109
|
- MCP 客户端是否真的在执行 `npx -y @aipper/zentao-mcp-server`(而不是旧的本地命令)。
|
|
103
110
|
- 客户端日志中是否有启动报错(如找不到命令、401、超时)。
|
|
104
111
|
|
|
105
112
|
## 已实现工具
|
|
106
|
-
- `get_token`:获取/刷新 token
|
|
107
|
-
- `
|
|
108
|
-
- `
|
|
109
|
-
- `
|
|
110
|
-
- `get_bug_detail`:按 `id` 获取 bug 详情(默认路径模板 `/bugs/{id}`,返回详情与图片链接;会提取富文本 `<img>`、Markdown 图片、附件图片并归一化为可访问 URL)
|
|
113
|
+
- `get_token`:获取/刷新 token(始终只回显脱敏后的 token 摘要)
|
|
114
|
+
- `list_my_projects`:示例:列出“我参与的项目”(字段匹配基于常见返回结构,可能需按你的实例微调;不适合作为项目集 bug 的唯一发现入口)
|
|
115
|
+
- `get_my_bugs`:获取“指派给我”的 bug(支持 `status`/`keyword`/`limit`/`page`/`productId`/`projectSetId`,默认路径 `/bugs`;`path` 仅允许 `/bugs`、`/my/bug`、`/my/bugs`)
|
|
116
|
+
- `get_bug_detail`:按 `id` 获取 bug 详情(固定读取 `/bugs/{id}`;返回安全裁剪后的 bug 摘要与同源图片链接,不直接透传外部图片地址或原始附件外链)
|
|
111
117
|
- `resolve_bug`:按 `id` 处理单个 bug 状态(默认 `resolution=fixed`,支持 `solution` 解决说明)
|
|
112
|
-
- `batch_resolve_my_bugs`:批量处理“我的 bug”(默认筛选 `status=active`,支持 `productId`/`projectSetId
|
|
118
|
+
- `batch_resolve_my_bugs`:批量处理“我的 bug”(默认筛选 `status=active`,支持 `productId`/`projectSetId`,默认遇错即停,`maxItems` 上限 100)
|
|
113
119
|
- `close_bug`:按 `id` 关闭 bug
|
|
114
120
|
- `verify_bug`:验证结果处理(`pass`=关闭,`fail`=激活)
|
|
115
|
-
- `comment_bug`:按 `id`
|
|
121
|
+
- `comment_bug`:按 `id` 添加备注
|
|
116
122
|
|
|
117
123
|
示例参数:
|
|
118
|
-
- `resolve_bug`:`{"id":123,"resolution":"fixed","comment":"
|
|
119
|
-
- `resolve_bug`(建议):`{"id":123,"resolution":"fixed","solution":"
|
|
120
|
-
- `batch_resolve_my_bugs`:`{"status":"active","maxItems":20,"comment":"
|
|
121
|
-
- `batch_resolve_my_bugs`(建议):`{"status":"active","maxItems":20,"solution":"
|
|
122
|
-
- `get_my_bugs`(按产品):`{"status":"active","productId":1,"limit":50}`
|
|
124
|
+
- `resolve_bug`:`{"id":123,"resolution":"fixed","comment":"根因已定位,已按最新字段映射调整处理逻辑"}`
|
|
125
|
+
- `resolve_bug`(建议):`{"id":123,"resolution":"fixed","solution":"补齐分页参数为空时的默认值分支,避免空值继续进入查询构造;同时收敛异常提示,防止前端重复触发提交"}`
|
|
126
|
+
- `batch_resolve_my_bugs`:`{"status":"active","maxItems":20,"comment":"统一补充非空校验并收敛异常分支"}`
|
|
127
|
+
- `batch_resolve_my_bugs`(建议):`{"status":"active","maxItems":20,"solution":"统一修正状态切换时的判空与分支顺序,避免旧数据触发空指针;保存前增加兜底校验,异常场景改为明确提示"}`
|
|
123
128
|
- `get_my_bugs`(项目集):`{"status":"active","projectSetId":1001,"limit":50}`
|
|
124
129
|
- `get_my_bugs`(我的):`{"status":"active","path":"/my/bug","limit":50}`
|
|
130
|
+
- `get_my_bugs`(项目集 + 我的):`{"status":"active","projectSetId":1001,"path":"/my/bug","limit":50}`
|
|
131
|
+
- `get_my_bugs`(按产品):`{"status":"active","productId":1,"limit":50}`
|
|
125
132
|
- `close_bug`:`{"id":123,"comment":"验证通过,关闭"}`
|
|
126
133
|
- `verify_bug`:`{"id":123,"result":"pass","comment":"验证通过"}`
|
|
127
134
|
- `comment_bug`:`{"id":123,"comment":"已复现,正在定位根因"}`
|
|
128
135
|
|
|
136
|
+
`solution` / `comment` 建议直接写“根因 + 修复思路 + 改动逻辑 + 影响范围”,不要默认写 `Evidence:`、`Verify:`、文件路径、编译命令或“已修复并自测”这类无法说明改动内容的表述。
|
|
137
|
+
|
|
138
|
+
使用建议:如果用户提到“项目集”“我的 bug”“项目列表里找不到但禅道里能看到 bug”,优先走项目集视角的 `get_my_bugs`,不要先让用户证明项目已创建。
|
|
139
|
+
|
|
129
140
|
## 安全建议
|
|
130
|
-
-
|
|
141
|
+
- **默认要求使用 HTTPS**:HTTP 会明文传输账号密码和数据,存在安全风险;如确需兼容老旧实例,需显式设置 `ZENTAO_ALLOW_INSECURE_HTTP=true`。
|
|
131
142
|
- 使用最小权限账号(仅需要的项目权限),避免使用管理员账号。
|
|
132
|
-
-
|
|
143
|
+
- `get_token` 不再支持回显完整 token。
|
|
144
|
+
- 调试日志会自动脱敏 `query`、`body`、`comment`、`solution` 等敏感字段,但仍建议仅在排查问题时临时开启。
|
|
133
145
|
|
|
134
146
|
## 调试
|
|
135
147
|
如需查看详细日志,可设置环境变量:
|
package/package.json
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aipper/zentao-mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "A minimal MCP server for ZenTao (token +
|
|
6
|
+
"description": "A minimal MCP server for ZenTao (token + focused bug workflow tools).",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"main": "src/index.js",
|
|
9
9
|
"bin": {
|
|
10
10
|
"zentao-mcp-server": "bin/zentao-mcp-server.js"
|
|
11
11
|
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"src/",
|
|
15
|
+
"scripts/smoke.mjs",
|
|
16
|
+
".env.example",
|
|
17
|
+
"README.md",
|
|
18
|
+
"CHANGELOG.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
12
21
|
"engines": {
|
|
13
22
|
"node": ">=18.0.0"
|
|
14
23
|
},
|
package/scripts/smoke.mjs
CHANGED
|
@@ -12,11 +12,12 @@ function getConfigFromEnv() {
|
|
|
12
12
|
const tokenPath = process.env.ZENTAO_TOKEN_PATH || `${apiPrefix}/tokens`;
|
|
13
13
|
const tokenTtlMs = Number(process.env.ZENTAO_TOKEN_TTL_MS || "3000000");
|
|
14
14
|
const timeoutMs = Number(process.env.ZENTAO_HTTP_TIMEOUT_MS || "30000");
|
|
15
|
+
const allowInsecureHttp = String(process.env.ZENTAO_ALLOW_INSECURE_HTTP || "false").toLowerCase() === "true";
|
|
15
16
|
|
|
16
17
|
const account = requireEnv("ZENTAO_ACCOUNT");
|
|
17
18
|
const password = requireEnv("ZENTAO_PASSWORD");
|
|
18
19
|
|
|
19
|
-
return { baseUrl, apiPrefix, tokenPath, tokenTtlMs, timeoutMs, auth: { account, password } };
|
|
20
|
+
return { baseUrl, apiPrefix, tokenPath, tokenTtlMs, timeoutMs, allowInsecureHttp, auth: { account, password } };
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
async function main() {
|
package/src/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
15
15
|
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8"));
|
|
16
16
|
|
|
17
17
|
const DEBUG = process.env.ZENTAO_DEBUG === "true";
|
|
18
|
+
const ALLOW_INSECURE_HTTP = String(process.env.ZENTAO_ALLOW_INSECURE_HTTP || "false").toLowerCase() === "true";
|
|
18
19
|
|
|
19
20
|
function log(...args) {
|
|
20
21
|
if (DEBUG) {
|
|
@@ -22,9 +23,26 @@ function log(...args) {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
function sanitizeToolArgs(toolName, args) {
|
|
27
|
+
if (!args || typeof args !== "object") return {};
|
|
28
|
+
const redactedKeys = new Set(["body", "query", "comment", "solution", "password", "token"]);
|
|
29
|
+
const sanitized = {};
|
|
30
|
+
for (const [key, value] of Object.entries(args)) {
|
|
31
|
+
if (redactedKeys.has(key)) {
|
|
32
|
+
sanitized[key] = "<redacted>";
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (toolName === "get_token" && key === "force") {
|
|
36
|
+
sanitized[key] = Boolean(value);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
sanitized[key] = value;
|
|
40
|
+
}
|
|
41
|
+
return sanitized;
|
|
42
|
+
}
|
|
43
|
+
|
|
25
44
|
const KNOWN_TOOL_NAMES = new Set([
|
|
26
45
|
"get_token",
|
|
27
|
-
"call",
|
|
28
46
|
"list_my_projects",
|
|
29
47
|
"get_my_bugs",
|
|
30
48
|
"get_bug_detail",
|
|
@@ -37,29 +55,20 @@ const KNOWN_TOOL_NAMES = new Set([
|
|
|
37
55
|
|
|
38
56
|
function normalizeToolName(rawName) {
|
|
39
57
|
if (!rawName || typeof rawName !== "string") return rawName;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (rawName.includes("_")) {
|
|
43
|
-
const withoutPrefix = rawName.replace(/^[^_]+_/, "");
|
|
44
|
-
if (KNOWN_TOOL_NAMES.has(withoutPrefix)) return withoutPrefix;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
for (const name of KNOWN_TOOL_NAMES) {
|
|
48
|
-
if (rawName.endsWith(name)) return name;
|
|
49
|
-
}
|
|
50
|
-
return rawName;
|
|
58
|
+
return KNOWN_TOOL_NAMES.has(rawName) ? rawName : rawName;
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
function requireEnv(name) {
|
|
54
62
|
const value = process.env[name];
|
|
55
63
|
if (!value) throw new Error(`Missing required env: ${name}`);
|
|
56
64
|
|
|
57
|
-
// Security check: warn if using HTTP instead of HTTPS
|
|
58
65
|
if (name === "ZENTAO_BASE_URL" && value.startsWith("http://")) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
if (!ALLOW_INSECURE_HTTP) {
|
|
67
|
+
throw new Error("ZENTAO_BASE_URL must use HTTPS unless ZENTAO_ALLOW_INSECURE_HTTP=true");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (name === "ZENTAO_BASE_URL" && !/^https?:\/\//i.test(value)) {
|
|
71
|
+
throw new Error("ZENTAO_BASE_URL must start with http:// or https://");
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
return value;
|
|
@@ -71,7 +80,6 @@ function getConfigFromEnv() {
|
|
|
71
80
|
const tokenPath = process.env.ZENTAO_TOKEN_PATH || `${apiPrefix}/tokens`;
|
|
72
81
|
const tokenTtlMs = Number(process.env.ZENTAO_TOKEN_TTL_MS || "3000000");
|
|
73
82
|
const timeoutMs = Number(process.env.ZENTAO_HTTP_TIMEOUT_MS || "30000");
|
|
74
|
-
const exposeToken = String(process.env.ZENTAO_EXPOSE_TOKEN || "false").toLowerCase() === "true";
|
|
75
83
|
const defaultProductId = Number(process.env.ZENTAO_PRODUCT_ID || "0") || null;
|
|
76
84
|
const defaultProjectSetId = Number(process.env.ZENTAO_PROJECT_SET_ID || "0") || null;
|
|
77
85
|
const myBugsPath = String(process.env.ZENTAO_MY_BUGS_PATH || "").trim();
|
|
@@ -93,7 +101,7 @@ function getConfigFromEnv() {
|
|
|
93
101
|
tokenPath,
|
|
94
102
|
tokenTtlMs,
|
|
95
103
|
timeoutMs,
|
|
96
|
-
|
|
104
|
+
allowInsecureHttp: ALLOW_INSECURE_HTTP,
|
|
97
105
|
defaultProductId,
|
|
98
106
|
defaultProjectSetId,
|
|
99
107
|
myBugsPath,
|
|
@@ -107,13 +115,7 @@ async function main() {
|
|
|
107
115
|
log("Starting zentao-mcp-server version", pkg.version);
|
|
108
116
|
|
|
109
117
|
const config = getConfigFromEnv();
|
|
110
|
-
log("Config loaded
|
|
111
|
-
baseUrl: config.baseUrl,
|
|
112
|
-
apiPrefix: config.apiPrefix,
|
|
113
|
-
account: config.auth.account,
|
|
114
|
-
productId: config.defaultProductId,
|
|
115
|
-
projectSetId: config.defaultProjectSetId,
|
|
116
|
-
});
|
|
118
|
+
log("Config loaded");
|
|
117
119
|
|
|
118
120
|
const zentao = createZenTaoClient(config);
|
|
119
121
|
|
|
@@ -133,7 +135,7 @@ async function main() {
|
|
|
133
135
|
const toolName = normalizeToolName(rawToolName);
|
|
134
136
|
const args = req.params?.arguments || {};
|
|
135
137
|
|
|
136
|
-
log(`Tool call: ${toolName}`, JSON.stringify(args));
|
|
138
|
+
log(`Tool call: ${toolName}`, JSON.stringify(sanitizeToolArgs(toolName, args)));
|
|
137
139
|
|
|
138
140
|
try {
|
|
139
141
|
assertToolArgs(toolName, args);
|
|
@@ -141,21 +143,13 @@ async function main() {
|
|
|
141
143
|
if (toolName === "get_token") {
|
|
142
144
|
const force = Boolean(args.force);
|
|
143
145
|
const result = await zentao.getToken({ force });
|
|
144
|
-
const output =
|
|
145
|
-
|
|
146
|
-
: {
|
|
147
|
-
|
|
148
|
-
token: result.token ? `${result.token.slice(0, 6)}…${result.token.slice(-4)}` : "",
|
|
149
|
-
};
|
|
146
|
+
const output = {
|
|
147
|
+
...result,
|
|
148
|
+
token: result.token ? `${result.token.slice(0, 6)}…${result.token.slice(-4)}` : "",
|
|
149
|
+
};
|
|
150
150
|
return toMcpTextResult(JSON.stringify(output, null, 2));
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
if (toolName === "call") {
|
|
154
|
-
const { path, method, query, body } = args;
|
|
155
|
-
const resp = await zentao.call({ path, method, query, body });
|
|
156
|
-
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
153
|
if (toolName === "list_my_projects") {
|
|
160
154
|
const resp = await zentao.listMyProjects({ keyword: args.keyword || "" });
|
|
161
155
|
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
@@ -170,7 +164,6 @@ async function main() {
|
|
|
170
164
|
productId: args.productId,
|
|
171
165
|
projectSetId: args.projectSetId,
|
|
172
166
|
path: args.path || "/bugs",
|
|
173
|
-
assignedTo: args.assignedTo || "",
|
|
174
167
|
});
|
|
175
168
|
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
176
169
|
}
|
|
@@ -178,7 +171,6 @@ async function main() {
|
|
|
178
171
|
if (toolName === "get_bug_detail") {
|
|
179
172
|
const resp = await zentao.getBugDetail({
|
|
180
173
|
id: args.id,
|
|
181
|
-
path: args.path || "/bugs/{id}",
|
|
182
174
|
});
|
|
183
175
|
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
184
176
|
}
|
|
@@ -189,7 +181,6 @@ async function main() {
|
|
|
189
181
|
resolution: args.resolution || "fixed",
|
|
190
182
|
solution: args.solution || "",
|
|
191
183
|
comment: args.comment || "",
|
|
192
|
-
path: args.path || "/bugs/{id}/resolve",
|
|
193
184
|
});
|
|
194
185
|
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
195
186
|
}
|
|
@@ -203,13 +194,11 @@ async function main() {
|
|
|
203
194
|
productId: args.productId,
|
|
204
195
|
projectSetId: args.projectSetId,
|
|
205
196
|
maxItems: args.maxItems,
|
|
206
|
-
assignedTo: args.assignedTo || "",
|
|
207
197
|
resolution: args.resolution || "fixed",
|
|
208
198
|
solution: args.solution || "",
|
|
209
199
|
comment: args.comment || "",
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
stopOnError: Boolean(args.stopOnError),
|
|
200
|
+
path: args.path || "/bugs",
|
|
201
|
+
stopOnError: args.stopOnError !== undefined ? Boolean(args.stopOnError) : true,
|
|
213
202
|
});
|
|
214
203
|
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
215
204
|
}
|
|
@@ -218,7 +207,6 @@ async function main() {
|
|
|
218
207
|
const resp = await zentao.closeBug({
|
|
219
208
|
id: args.id,
|
|
220
209
|
comment: args.comment || "",
|
|
221
|
-
path: args.path || "/bugs/{id}/close",
|
|
222
210
|
});
|
|
223
211
|
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
224
212
|
}
|
|
@@ -228,8 +216,6 @@ async function main() {
|
|
|
228
216
|
id: args.id,
|
|
229
217
|
result: args.result || "pass",
|
|
230
218
|
comment: args.comment || "",
|
|
231
|
-
closePath: args.closePath || "/bugs/{id}/close",
|
|
232
|
-
activatePath: args.activatePath || "/bugs/{id}/activate",
|
|
233
219
|
});
|
|
234
220
|
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
235
221
|
}
|
|
@@ -238,7 +224,6 @@ async function main() {
|
|
|
238
224
|
const resp = await zentao.commentBug({
|
|
239
225
|
id: args.id,
|
|
240
226
|
comment: args.comment || "",
|
|
241
|
-
path: args.path || "/bugs/{id}/comment",
|
|
242
227
|
});
|
|
243
228
|
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
244
229
|
}
|
|
@@ -252,9 +237,8 @@ async function main() {
|
|
|
252
237
|
tool: rawToolName,
|
|
253
238
|
message: String(err?.message || err),
|
|
254
239
|
status: err?.status ?? null,
|
|
255
|
-
data: err?.data ?? null,
|
|
256
240
|
hint: "If you see 'Need product id', set env ZENTAO_PRODUCT_ID or pass productId in get_my_bugs.",
|
|
257
|
-
hint2: "For project-set instances,
|
|
241
|
+
hint2: "For project-set instances, prefer get_my_bugs with projectSetId or ZENTAO_MY_BUGS_PATH=/my/bug; list_my_projects may miss project sets without concrete projects.",
|
|
258
242
|
};
|
|
259
243
|
return toMcpTextResult(JSON.stringify(errorPayload, null, 2), { isError: true });
|
|
260
244
|
}
|