@aipper/zentao-mcp-server 0.1.9 → 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 CHANGED
@@ -1,5 +1,5 @@
1
1
  # 禅道基础地址(不要带末尾 /)
2
- ZENTAO_BASE_URL=http://zentao.example.com
2
+ ZENTAO_BASE_URL=https://zentao.example.com
3
3
 
4
4
  # 必填:账号密码(MCP 启动后自动获取并缓存 Token)
5
5
  ZENTAO_ACCOUNT=your_account
@@ -32,5 +32,8 @@ ZENTAO_TOKEN_TTL_MS=3000000
32
32
  # 可选:超时(毫秒)
33
33
  ZENTAO_HTTP_TIMEOUT_MS=30000
34
34
 
35
- # 可选:是否在 get_token 工具里回显完整 token(默认 false)
36
- ZENTAO_EXPOSE_TOKEN=false
35
+ # 可选:仅在临时调试老旧内网实例时允许 HTTP(强烈不推荐)
36
+ # ZENTAO_ALLOW_INSECURE_HTTP=true
37
+
38
+ # 可选:启用调试日志(输出到 stderr)
39
+ # ZENTAO_DEBUG=true
package/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.1.9] - 2024-03-12
6
+
7
+ ### Added
8
+ - Debug logging support via `ZENTAO_DEBUG` environment variable
9
+ - Security warning for HTTP connections
10
+ - JSDoc type annotations for better IDE support
11
+ - MIT License file
12
+ - Node.js version requirement in package.json (>=18.0.0)
13
+
14
+ ### Changed
15
+ - Fixed version inconsistency between package.json and server initialization
16
+ - Locked dependency version for @modelcontextprotocol/sdk to ^1.27.1
17
+ - Changed license from UNLICENSED to MIT
18
+ - Extracted magic numbers and strings to constants
19
+ - Improved error messages with debug context
20
+
21
+ ### Fixed
22
+ - Server version now correctly reads from package.json
23
+ - HTTP security warning now displays when using insecure connections
24
+
25
+ ## [0.1.8] - Previous
26
+
27
+ ### Fixed
28
+ - Bug fixes and improvements
29
+
30
+ ## [0.1.7] - Previous
31
+
32
+ ### Added
33
+ - Initial release with core functionality
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 @aipper/zentao-mcp-server contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,15 +2,19 @@
2
2
 
3
3
  ## 目标
4
4
  - 提供一个可运行的 MCP Server(stdio),连接你的禅道 RESTful API v1。
5
- - 最小功能:自动获取/缓存 Token + 通用 `call` 工具 + 少量便捷工具示例。
5
+ - 最小功能:自动获取/缓存 Token + 常用 bug 工具。
6
6
 
7
7
  ## 非目标
8
8
  - 不在仓库内存任何密钥/Token。
9
- - 不保证覆盖你禅道的全部 API;优先把“通用 call”跑通,再按你的流程补工具。
9
+ - 不暴露无边界的通用 REST 写接口。
10
+ - 不保证覆盖你禅道的全部 API;优先支持高频 bug 流程,再按你的流程补工具。
10
11
 
11
12
  ## 依赖
12
13
  - Node.js 18+(需要内置 `fetch`)
13
14
 
15
+ ## License
16
+ MIT - 详见 [LICENSE](./LICENSE) 文件
17
+
14
18
  ## 配置
15
19
  复制 `.env.example` 为 `.env` 并填写:
16
20
  - `ZENTAO_BASE_URL`
@@ -25,6 +29,8 @@
25
29
  > 注意:不同禅道版本/部署方式的 token 端点与返回结构可能不同;可通过 `ZENTAO_TOKEN_PATH`/`ZENTAO_API_PREFIX` 调整。
26
30
  >
27
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`)。
28
34
 
29
35
  ## 安装与运行
30
36
  ```bash
@@ -91,41 +97,58 @@ npm run smoke
91
97
  ## 常见错误(`-32000`)
92
98
  `-32000` 通常是客户端侧“通用 MCP 调用失败”映射码,优先检查:
93
99
  - `env` 是否完整传入(尤其是 `ZENTAO_BASE_URL`/`ZENTAO_ACCOUNT`/`ZENTAO_PASSWORD`)。
100
+ - 默认要求 HTTPS;如果你的实例只能走 HTTP,必须显式设置 `ZENTAO_ALLOW_INSECURE_HTTP=true`,且只建议临时内网调试使用。
94
101
  - 若报 `Need product id`,请设置 `ZENTAO_PRODUCT_ID`,或在 `get_my_bugs` 传 `productId`。
95
102
  - 若你的 bug 在“项目集/我的视角”而非产品,建议设置 `ZENTAO_PROJECT_SET_ID`,并配置 `ZENTAO_MY_BUGS_PATH=/my/bug`。
96
- - `get_my_bugs` 会按候选路径回退(包含项目集路径);即使首个路径返回空列表也会继续尝试。
97
- - 排查时看工具返回里的 `raw.triedPaths`,可确认每条路径的返回码与命中数量。
98
- - `ZENTAO_API_PREFIX`/`ZENTAO_TOKEN_PATH` 是否和你的禅道实例一致。
103
+ - 项目集场景下,优先直接调用 `get_my_bugs` 并传 `projectSetId`,必要时再显式传 `path="/my/bug"`;不要先依赖 `list_my_projects` 找项目。
104
+ - 有些项目集本身没有创建实际项目,但仍然存在“我的 bug”;这类数据可能不会出现在 `list_my_projects` 结果里。
105
+ - `get_my_bugs` 会按候选路径回退(包含项目集路径);即使首个路径返回空列表也会继续尝试,并会把多端点结果合并去重。
106
+ - `get_my_bugs.total` 表示最终“我的 bug”总数;若需排查底层一共扫描了多少条,可看 `raw.scannedTotal`。
107
+ - 排查时看工具返回里的 `raw.triedPaths` / `raw.paths`,可确认每条路径的返回码与命中数量。
108
+ - `ZENTAO_API_PREFIX`/`ZENTAO_TOKEN_PATH` 是否和你的禅道实例一致,且使用相对路径。
99
109
  - MCP 客户端是否真的在执行 `npx -y @aipper/zentao-mcp-server`(而不是旧的本地命令)。
100
110
  - 客户端日志中是否有启动报错(如找不到命令、401、超时)。
101
111
 
102
112
  ## 已实现工具
103
- - `get_token`:获取/刷新 token(默认不回显完整 token
104
- - `call`:调用任意相对 API 路径(自动带 Token 头)
105
- - `list_my_projects`:示例:列出“我参与的项目”(字段匹配基于常见返回结构,可能需按你的实例微调)
106
- - `get_my_bugs`:获取“指派给我”的 bug(支持 `status`/`keyword`/`limit`/`page`/`productId`/`projectSetId`,默认路径 `/bugs`)
107
- - `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 摘要与同源图片链接,不直接透传外部图片地址或原始附件外链)
108
117
  - `resolve_bug`:按 `id` 处理单个 bug 状态(默认 `resolution=fixed`,支持 `solution` 解决说明)
109
- - `batch_resolve_my_bugs`:批量处理“我的 bug”(默认筛选 `status=active`,支持 `productId`/`projectSetId`)
118
+ - `batch_resolve_my_bugs`:批量处理“我的 bug”(默认筛选 `status=active`,支持 `productId`/`projectSetId`,默认遇错即停,`maxItems` 上限 100)
110
119
  - `close_bug`:按 `id` 关闭 bug
111
120
  - `verify_bug`:验证结果处理(`pass`=关闭,`fail`=激活)
112
- - `comment_bug`:按 `id` 添加备注(默认路径 `/bugs/{id}/comment`)
121
+ - `comment_bug`:按 `id` 添加备注
113
122
 
114
123
  示例参数:
115
- - `resolve_bug`:`{"id":123,"resolution":"fixed","comment":"已修复并自测"}`
116
- - `resolve_bug`(建议):`{"id":123,"resolution":"fixed","solution":"修复空指针并补充参数校验"}`
117
- - `batch_resolve_my_bugs`:`{"status":"active","maxItems":20,"comment":"批量修复"}`
118
- - `batch_resolve_my_bugs`(建议):`{"status":"active","maxItems":20,"solution":"统一修复分页参数为空导致的报错"}`
119
- - `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":"统一修正状态切换时的判空与分支顺序,避免旧数据触发空指针;保存前增加兜底校验,异常场景改为明确提示"}`
120
128
  - `get_my_bugs`(项目集):`{"status":"active","projectSetId":1001,"limit":50}`
121
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}`
122
132
  - `close_bug`:`{"id":123,"comment":"验证通过,关闭"}`
123
133
  - `verify_bug`:`{"id":123,"result":"pass","comment":"验证通过"}`
124
134
  - `comment_bug`:`{"id":123,"comment":"已复现,正在定位根因"}`
125
135
 
136
+ `solution` / `comment` 建议直接写“根因 + 修复思路 + 改动逻辑 + 影响范围”,不要默认写 `Evidence:`、`Verify:`、文件路径、编译命令或“已修复并自测”这类无法说明改动内容的表述。
137
+
138
+ 使用建议:如果用户提到“项目集”“我的 bug”“项目列表里找不到但禅道里能看到 bug”,优先走项目集视角的 `get_my_bugs`,不要先让用户证明项目已创建。
139
+
126
140
  ## 安全建议
141
+ - **默认要求使用 HTTPS**:HTTP 会明文传输账号密码和数据,存在安全风险;如确需兼容老旧实例,需显式设置 `ZENTAO_ALLOW_INSECURE_HTTP=true`。
127
142
  - 使用最小权限账号(仅需要的项目权限),避免使用管理员账号。
128
- - 默认 `get_token` 不回显完整 token;如确需调试,可设 `ZENTAO_EXPOSE_TOKEN=true`。
143
+ - `get_token` 不再支持回显完整 token
144
+ - 调试日志会自动脱敏 `query`、`body`、`comment`、`solution` 等敏感字段,但仍建议仅在排查问题时临时开启。
145
+
146
+ ## 调试
147
+ 如需查看详细日志,可设置环境变量:
148
+ ```bash
149
+ ZENTAO_DEBUG=true npx -y @aipper/zentao-mcp-server
150
+ ```
151
+ 日志会输出到 stderr,不影响 MCP 协议通信。
129
152
 
130
153
  ## 发布到 npm
131
154
  脚本:`scripts/release-npm.sh`(参考 `aiws` 的发布流程,默认 dry-run)。
package/package.json CHANGED
@@ -1,14 +1,38 @@
1
1
  {
2
2
  "name": "@aipper/zentao-mcp-server",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "private": false,
5
5
  "type": "module",
6
- "description": "A minimal MCP server for ZenTao (token + generic REST call + a few helper tools).",
7
- "license": "UNLICENSED",
6
+ "description": "A minimal MCP server for ZenTao (token + focused bug workflow tools).",
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
+ ],
21
+ "engines": {
22
+ "node": ">=18.0.0"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "mcp-server",
27
+ "zentao",
28
+ "bug-tracking",
29
+ "project-management",
30
+ "api-client"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/aipper/zentao-mcp.git"
35
+ },
12
36
  "scripts": {
13
37
  "start": "node src/index.js",
14
38
  "lint": "node -c src/index.js && node -c src/zentao.js && node -c src/tools.js && node -c scripts/smoke.mjs && node -c bin/zentao-mcp-server.js",
@@ -16,6 +40,6 @@
16
40
  "release:npm": "bash scripts/release-npm.sh"
17
41
  },
18
42
  "dependencies": {
19
- "@modelcontextprotocol/sdk": "latest"
43
+ "@modelcontextprotocol/sdk": "^1.27.1"
20
44
  }
21
45
  }
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
@@ -7,10 +7,42 @@ import {
7
7
  } from "./tools.js";
8
8
  import { createZenTaoClient } from "./zentao.js";
9
9
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
10
+ import { readFileSync } from "fs";
11
+ import { fileURLToPath } from "url";
12
+ import { dirname, join } from "path";
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8"));
16
+
17
+ const DEBUG = process.env.ZENTAO_DEBUG === "true";
18
+ const ALLOW_INSECURE_HTTP = String(process.env.ZENTAO_ALLOW_INSECURE_HTTP || "false").toLowerCase() === "true";
19
+
20
+ function log(...args) {
21
+ if (DEBUG) {
22
+ process.stderr.write(`[zentao-mcp] ${args.join(" ")}\n`);
23
+ }
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
+ }
10
43
 
11
44
  const KNOWN_TOOL_NAMES = new Set([
12
45
  "get_token",
13
- "call",
14
46
  "list_my_projects",
15
47
  "get_my_bugs",
16
48
  "get_bug_detail",
@@ -23,22 +55,22 @@ const KNOWN_TOOL_NAMES = new Set([
23
55
 
24
56
  function normalizeToolName(rawName) {
25
57
  if (!rawName || typeof rawName !== "string") return rawName;
26
- if (KNOWN_TOOL_NAMES.has(rawName)) return rawName;
27
-
28
- if (rawName.includes("_")) {
29
- const withoutPrefix = rawName.replace(/^[^_]+_/, "");
30
- if (KNOWN_TOOL_NAMES.has(withoutPrefix)) return withoutPrefix;
31
- }
32
-
33
- for (const name of KNOWN_TOOL_NAMES) {
34
- if (rawName.endsWith(name)) return name;
35
- }
36
- return rawName;
58
+ return KNOWN_TOOL_NAMES.has(rawName) ? rawName : rawName;
37
59
  }
38
60
 
39
61
  function requireEnv(name) {
40
62
  const value = process.env[name];
41
63
  if (!value) throw new Error(`Missing required env: ${name}`);
64
+
65
+ if (name === "ZENTAO_BASE_URL" && value.startsWith("http://")) {
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://");
72
+ }
73
+
42
74
  return value;
43
75
  }
44
76
 
@@ -48,7 +80,6 @@ function getConfigFromEnv() {
48
80
  const tokenPath = process.env.ZENTAO_TOKEN_PATH || `${apiPrefix}/tokens`;
49
81
  const tokenTtlMs = Number(process.env.ZENTAO_TOKEN_TTL_MS || "3000000");
50
82
  const timeoutMs = Number(process.env.ZENTAO_HTTP_TIMEOUT_MS || "30000");
51
- const exposeToken = String(process.env.ZENTAO_EXPOSE_TOKEN || "false").toLowerCase() === "true";
52
83
  const defaultProductId = Number(process.env.ZENTAO_PRODUCT_ID || "0") || null;
53
84
  const defaultProjectSetId = Number(process.env.ZENTAO_PROJECT_SET_ID || "0") || null;
54
85
  const myBugsPath = String(process.env.ZENTAO_MY_BUGS_PATH || "").trim();
@@ -70,7 +101,7 @@ function getConfigFromEnv() {
70
101
  tokenPath,
71
102
  tokenTtlMs,
72
103
  timeoutMs,
73
- exposeToken,
104
+ allowInsecureHttp: ALLOW_INSECURE_HTTP,
74
105
  defaultProductId,
75
106
  defaultProjectSetId,
76
107
  myBugsPath,
@@ -81,14 +112,20 @@ function getConfigFromEnv() {
81
112
  }
82
113
 
83
114
  async function main() {
115
+ log("Starting zentao-mcp-server version", pkg.version);
116
+
84
117
  const config = getConfigFromEnv();
118
+ log("Config loaded");
119
+
85
120
  const zentao = createZenTaoClient(config);
86
121
 
87
122
  const server = new Server(
88
- { name: "zentao-mcp-server", version: "0.1.0" },
123
+ { name: pkg.name, version: pkg.version },
89
124
  { capabilities: { tools: {} } }
90
125
  );
91
126
 
127
+ log("MCP server initialized");
128
+
92
129
  server.setRequestHandler(ListToolsRequestSchema, async () => {
93
130
  return { tools: TOOLS };
94
131
  });
@@ -98,27 +135,21 @@ async function main() {
98
135
  const toolName = normalizeToolName(rawToolName);
99
136
  const args = req.params?.arguments || {};
100
137
 
138
+ log(`Tool call: ${toolName}`, JSON.stringify(sanitizeToolArgs(toolName, args)));
139
+
101
140
  try {
102
141
  assertToolArgs(toolName, args);
103
142
 
104
143
  if (toolName === "get_token") {
105
144
  const force = Boolean(args.force);
106
145
  const result = await zentao.getToken({ force });
107
- const output = config.exposeToken
108
- ? result
109
- : {
110
- ...result,
111
- token: result.token ? `${result.token.slice(0, 6)}…${result.token.slice(-4)}` : "",
112
- };
146
+ const output = {
147
+ ...result,
148
+ token: result.token ? `${result.token.slice(0, 6)}…${result.token.slice(-4)}` : "",
149
+ };
113
150
  return toMcpTextResult(JSON.stringify(output, null, 2));
114
151
  }
115
152
 
116
- if (toolName === "call") {
117
- const { path, method, query, body } = args;
118
- const resp = await zentao.call({ path, method, query, body });
119
- return toMcpTextResult(JSON.stringify(resp, null, 2));
120
- }
121
-
122
153
  if (toolName === "list_my_projects") {
123
154
  const resp = await zentao.listMyProjects({ keyword: args.keyword || "" });
124
155
  return toMcpTextResult(JSON.stringify(resp, null, 2));
@@ -133,7 +164,6 @@ async function main() {
133
164
  productId: args.productId,
134
165
  projectSetId: args.projectSetId,
135
166
  path: args.path || "/bugs",
136
- assignedTo: args.assignedTo || "",
137
167
  });
138
168
  return toMcpTextResult(JSON.stringify(resp, null, 2));
139
169
  }
@@ -141,7 +171,6 @@ async function main() {
141
171
  if (toolName === "get_bug_detail") {
142
172
  const resp = await zentao.getBugDetail({
143
173
  id: args.id,
144
- path: args.path || "/bugs/{id}",
145
174
  });
146
175
  return toMcpTextResult(JSON.stringify(resp, null, 2));
147
176
  }
@@ -152,7 +181,6 @@ async function main() {
152
181
  resolution: args.resolution || "fixed",
153
182
  solution: args.solution || "",
154
183
  comment: args.comment || "",
155
- path: args.path || "/bugs/{id}/resolve",
156
184
  });
157
185
  return toMcpTextResult(JSON.stringify(resp, null, 2));
158
186
  }
@@ -166,13 +194,11 @@ async function main() {
166
194
  productId: args.productId,
167
195
  projectSetId: args.projectSetId,
168
196
  maxItems: args.maxItems,
169
- assignedTo: args.assignedTo || "",
170
197
  resolution: args.resolution || "fixed",
171
198
  solution: args.solution || "",
172
199
  comment: args.comment || "",
173
- listPath: args.listPath || "/bugs",
174
- resolvePath: args.resolvePath || "/bugs/{id}/resolve",
175
- stopOnError: Boolean(args.stopOnError),
200
+ path: args.path || "/bugs",
201
+ stopOnError: args.stopOnError !== undefined ? Boolean(args.stopOnError) : true,
176
202
  });
177
203
  return toMcpTextResult(JSON.stringify(resp, null, 2));
178
204
  }
@@ -181,7 +207,6 @@ async function main() {
181
207
  const resp = await zentao.closeBug({
182
208
  id: args.id,
183
209
  comment: args.comment || "",
184
- path: args.path || "/bugs/{id}/close",
185
210
  });
186
211
  return toMcpTextResult(JSON.stringify(resp, null, 2));
187
212
  }
@@ -191,8 +216,6 @@ async function main() {
191
216
  id: args.id,
192
217
  result: args.result || "pass",
193
218
  comment: args.comment || "",
194
- closePath: args.closePath || "/bugs/{id}/close",
195
- activatePath: args.activatePath || "/bugs/{id}/activate",
196
219
  });
197
220
  return toMcpTextResult(JSON.stringify(resp, null, 2));
198
221
  }
@@ -201,21 +224,21 @@ async function main() {
201
224
  const resp = await zentao.commentBug({
202
225
  id: args.id,
203
226
  comment: args.comment || "",
204
- path: args.path || "/bugs/{id}/comment",
205
227
  });
206
228
  return toMcpTextResult(JSON.stringify(resp, null, 2));
207
229
  }
208
230
 
209
231
  throw new Error(`Unknown tool: ${rawToolName}`);
210
232
  } catch (err) {
233
+ log(`Tool error: ${toolName}`, err?.message || err);
234
+
211
235
  const errorPayload = {
212
236
  ok: false,
213
237
  tool: rawToolName,
214
238
  message: String(err?.message || err),
215
239
  status: err?.status ?? null,
216
- data: err?.data ?? null,
217
240
  hint: "If you see 'Need product id', set env ZENTAO_PRODUCT_ID or pass productId in get_my_bugs.",
218
- hint2: "For project-set instances, set ZENTAO_PROJECT_SET_ID or ZENTAO_MY_BUGS_PATH.",
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.",
219
242
  };
220
243
  return toMcpTextResult(JSON.stringify(errorPayload, null, 2), { isError: true });
221
244
  }
@@ -223,6 +246,8 @@ async function main() {
223
246
 
224
247
  const transport = new StdioServerTransport();
225
248
  await server.connect(transport);
249
+
250
+ log("MCP server connected and ready");
226
251
  }
227
252
 
228
253
  main().catch((err) => {