@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 +6 -3
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +41 -18
- package/package.json +28 -4
- package/scripts/smoke.mjs +2 -1
- package/src/index.js +65 -40
- package/src/tools.js +82 -64
- package/src/zentao.js +650 -110
- 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,5 +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
|
+
|
|
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 +
|
|
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`)
|
|
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
|
-
-
|
|
98
|
-
- `
|
|
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
|
|
104
|
-
- `
|
|
105
|
-
- `
|
|
106
|
-
- `
|
|
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`
|
|
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
|
-
-
|
|
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.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "A minimal MCP server for ZenTao (token +
|
|
7
|
-
"license": "
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
108
|
-
|
|
109
|
-
: {
|
|
110
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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,
|
|
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) => {
|