@dannyvan/zentao-mcp 0.1.0
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/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +156 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +17 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/read.d.ts +4 -0
- package/dist/tools/read.js +184 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/write.d.ts +9 -0
- package/dist/tools/write.js +144 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/zentao-client.d.ts +46 -0
- package/dist/zentao-client.js +140 -0
- package/dist/zentao-client.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dannyvan
|
|
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
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @dannyvan/zentao-mcp
|
|
2
|
+
|
|
3
|
+
禅道(ZenTao)**MCP + CLI**,直连 REST API(`api.php/v1`)。只做 Bug:读详情 / 列表 / 搜索,写评论与状态流转。写操作默认 dry-run。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx -y @dannyvan/zentao-mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
发布到 npm 之前,可在本仓 `npm install && npm run build` 后用 `node dist/index.js`。
|
|
12
|
+
|
|
13
|
+
## 认证
|
|
14
|
+
|
|
15
|
+
| 变量 | 说明 |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `ZENTAO_URL` | **必填**,禅道地址(不含 `/api.php/v1`) |
|
|
18
|
+
| `ZENTAO_ACCOUNT` + `ZENTAO_PASSWORD` | 推荐:自动换取并刷新 token |
|
|
19
|
+
| `ZENTAO_TOKEN` | 或直接给现成 token(会过期) |
|
|
20
|
+
|
|
21
|
+
## MCP
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"zentao": {
|
|
27
|
+
"command": "npx",
|
|
28
|
+
"args": ["-y", "@dannyvan/zentao-mcp"],
|
|
29
|
+
"env": {
|
|
30
|
+
"ZENTAO_URL": "https://<禅道域名>/zentao",
|
|
31
|
+
"ZENTAO_ACCOUNT": "<账号>",
|
|
32
|
+
"ZENTAO_PASSWORD": "<密码>"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Tools**
|
|
40
|
+
|
|
41
|
+
- 读:`zentao_get_bug` / `zentao_list_bugs` / `zentao_search_bugs`
|
|
42
|
+
- 写:`zentao_comment_bug`(v1 不支持,会报错)/ `resolve` `close` `activate` `confirm` `assign`(默认 dry-run,带 `confirm=true` 才提交)
|
|
43
|
+
|
|
44
|
+
`zentao_list_bugs` 的 `total` 禅道可能不准,以翻页实际列出为准。
|
|
45
|
+
|
|
46
|
+
## CLI
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx -y @dannyvan/zentao-mcp help
|
|
50
|
+
npx -y @dannyvan/zentao-mcp get-bug 123
|
|
51
|
+
npx -y @dannyvan/zentao-mcp list-bugs --product 1 --limit 50
|
|
52
|
+
npx -y @dannyvan/zentao-mcp assign-bug 123 --assigned-to someone
|
|
53
|
+
npx -y @dannyvan/zentao-mcp assign-bug 123 --assigned-to someone --confirm
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
无参数或 `serve` 时走 MCP stdio。输出 JSON;出错写 stderr,退出码非 0。
|
|
57
|
+
|
|
58
|
+
## 安全
|
|
59
|
+
|
|
60
|
+
- 写操作默认 dry-run(MCP:`confirm=true`;CLI:`--confirm`)
|
|
61
|
+
- 批量改状态请串行逐条,不要并行
|
|
62
|
+
- 写操作在禅道可见
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCli(argv: string[]): Promise<void>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 门面 —— 与 MCP 共用同一套 ZentaoClient 与同一套安全约束。
|
|
3
|
+
*
|
|
4
|
+
* 写操作默认 dry-run,必须显式 --confirm 才真正提交。
|
|
5
|
+
* 输出一律 JSON;出错时 JSON 写 stderr,退出码非 0。
|
|
6
|
+
*/
|
|
7
|
+
import { clientFromEnv, formatZentaoError } from "./zentao-client.js";
|
|
8
|
+
const USAGE = `zentao-mcp —— 禅道 CLI / MCP 双门面
|
|
9
|
+
|
|
10
|
+
用法:
|
|
11
|
+
zentao-mcp 以 MCP server 启动(stdio,默认)
|
|
12
|
+
zentao-mcp serve 同上,显式写法
|
|
13
|
+
zentao-mcp <命令> [参数] CLI 模式
|
|
14
|
+
|
|
15
|
+
读命令(无副作用):
|
|
16
|
+
get-bug <id> 查单个 Bug
|
|
17
|
+
list-bugs --product <id> [--status <s>] [--limit <n>]
|
|
18
|
+
search-bugs --product <id> --keyword <kw>
|
|
19
|
+
|
|
20
|
+
写命令(默认仅预览,加 --confirm 才提交):
|
|
21
|
+
resolve-bug <id> --resolution <r> [--comment <c>] [--confirm]
|
|
22
|
+
close-bug <id> [--comment <c>] [--confirm]
|
|
23
|
+
activate-bug <id> [--assigned-to <a>] [--comment <c>] [--confirm]
|
|
24
|
+
confirm-bug <id> [--assigned-to <a>] [--confirm]
|
|
25
|
+
assign-bug <id> --assigned-to <a> [--comment <c>] [--confirm]
|
|
26
|
+
|
|
27
|
+
认证(环境变量):
|
|
28
|
+
ZENTAO_URL 必填,禅道地址
|
|
29
|
+
ZENTAO_ACCOUNT + ZENTAO_PASSWORD 推荐,自动换取并刷新 token
|
|
30
|
+
ZENTAO_TOKEN 临时/测试,会过期
|
|
31
|
+
|
|
32
|
+
⚠️ 写操作在禅道可见。批量改状态请串行逐条执行,不要并行。`;
|
|
33
|
+
function parseArgs(argv) {
|
|
34
|
+
const positional = [];
|
|
35
|
+
const flags = {};
|
|
36
|
+
for (let i = 0; i < argv.length; i++) {
|
|
37
|
+
const a = argv[i];
|
|
38
|
+
if (!a.startsWith("--")) {
|
|
39
|
+
positional.push(a);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const key = a.slice(2);
|
|
43
|
+
const next = argv[i + 1];
|
|
44
|
+
if (next === undefined || next.startsWith("--")) {
|
|
45
|
+
flags[key] = true;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
flags[key] = next;
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { positional, flags };
|
|
53
|
+
}
|
|
54
|
+
function out(data) {
|
|
55
|
+
console.log(JSON.stringify(data, null, 2));
|
|
56
|
+
}
|
|
57
|
+
function fail(message) {
|
|
58
|
+
console.error(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
function requireId(positional) {
|
|
62
|
+
const id = Number(positional[0]);
|
|
63
|
+
if (!Number.isInteger(id) || id <= 0)
|
|
64
|
+
fail("缺少或非法的 Bug ID(应为正整数)");
|
|
65
|
+
return id;
|
|
66
|
+
}
|
|
67
|
+
function requireFlag(flags, name) {
|
|
68
|
+
const v = flags[name];
|
|
69
|
+
if (typeof v !== "string" || !v)
|
|
70
|
+
fail(`缺少必填参数 --${name}`);
|
|
71
|
+
return v;
|
|
72
|
+
}
|
|
73
|
+
async function highRisk(client, id, confirm, preview, exec) {
|
|
74
|
+
const bug = await client.getBug(id);
|
|
75
|
+
if (!confirm) {
|
|
76
|
+
out({
|
|
77
|
+
ok: true,
|
|
78
|
+
dryRun: true,
|
|
79
|
+
preview: preview(bug),
|
|
80
|
+
bug: { id: bug.id, title: bug.title, status: bug.status },
|
|
81
|
+
hint: "确认无误后加 --confirm 重新执行以真正提交。⚠️ 该操作在禅道可见。",
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
out({ ok: true, dryRun: false, result: await exec() });
|
|
86
|
+
}
|
|
87
|
+
export async function runCli(argv) {
|
|
88
|
+
const [command, ...rest] = argv;
|
|
89
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
90
|
+
console.log(USAGE);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const { positional, flags } = parseArgs(rest);
|
|
94
|
+
const confirm = flags.confirm === true || flags.confirm === "true";
|
|
95
|
+
const client = clientFromEnv();
|
|
96
|
+
try {
|
|
97
|
+
switch (command) {
|
|
98
|
+
case "get-bug":
|
|
99
|
+
out({ ok: true, bug: await client.getBug(requireId(positional)) });
|
|
100
|
+
return;
|
|
101
|
+
case "list-bugs": {
|
|
102
|
+
const product = Number(requireFlag(flags, "product"));
|
|
103
|
+
const params = { limit: Number(flags.limit ?? 50) };
|
|
104
|
+
if (typeof flags.status === "string")
|
|
105
|
+
params.status = flags.status;
|
|
106
|
+
out({ ok: true, result: await client.listProductBugs(product, params) });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
case "search-bugs": {
|
|
110
|
+
const product = Number(requireFlag(flags, "product"));
|
|
111
|
+
const keyword = requireFlag(flags, "keyword");
|
|
112
|
+
out({ ok: true, result: await client.listProductBugs(product, { keyword, limit: Number(flags.limit ?? 50) }) });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
case "resolve-bug": {
|
|
116
|
+
const id = requireId(positional);
|
|
117
|
+
const resolution = requireFlag(flags, "resolution");
|
|
118
|
+
const comment = typeof flags.comment === "string" ? flags.comment : undefined;
|
|
119
|
+
await highRisk(client, id, confirm, (b) => `将把 Bug #${id}「${b.title}」标记为已解决(resolution=${resolution}),当前状态 ${b.status}`, () => client.resolveBug(id, { resolution, comment }));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
case "close-bug": {
|
|
123
|
+
const id = requireId(positional);
|
|
124
|
+
const comment = typeof flags.comment === "string" ? flags.comment : undefined;
|
|
125
|
+
await highRisk(client, id, confirm, (b) => `将关闭 Bug #${id}「${b.title}」,当前状态 ${b.status}`, () => client.closeBug(id, { comment }));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
case "activate-bug": {
|
|
129
|
+
const id = requireId(positional);
|
|
130
|
+
const assignedTo = typeof flags["assigned-to"] === "string" ? flags["assigned-to"] : undefined;
|
|
131
|
+
const comment = typeof flags.comment === "string" ? flags.comment : undefined;
|
|
132
|
+
await highRisk(client, id, confirm, (b) => `将激活 Bug #${id}「${b.title}」,当前状态 ${b.status}${assignedTo ? `,指派给 ${assignedTo}` : ""}`, () => client.activateBug(id, { comment, assignedTo }));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
case "confirm-bug": {
|
|
136
|
+
const id = requireId(positional);
|
|
137
|
+
const assignedTo = typeof flags["assigned-to"] === "string" ? flags["assigned-to"] : undefined;
|
|
138
|
+
await highRisk(client, id, confirm, (b) => `将确认 Bug #${id}「${b.title}」,当前状态 ${b.status}${assignedTo ? `,指派给 ${assignedTo}` : ""}`, () => client.confirmBug(id, { assignedTo }));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
case "assign-bug": {
|
|
142
|
+
const id = requireId(positional);
|
|
143
|
+
const assignedTo = requireFlag(flags, "assigned-to");
|
|
144
|
+
const comment = typeof flags.comment === "string" ? flags.comment : undefined;
|
|
145
|
+
await highRisk(client, id, confirm, (b) => `将把 Bug #${id}「${b.title}」指派给 ${assignedTo},当前状态 ${b.status}`, () => client.assignBug(id, { assignedTo, comment }));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
default:
|
|
149
|
+
fail(`未知命令:${command}\n\n${USAGE}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
fail(formatZentaoError(e));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** 共享常量 */
|
|
2
|
+
/** 单次响应字符上限,超出则截断并提示分页。 */
|
|
3
|
+
export declare const CHARACTER_LIMIT = 25000;
|
|
4
|
+
/** 解决方案枚举(禅道 bug resolution)。 */
|
|
5
|
+
export declare const RESOLUTIONS: readonly ["bydesign", "duplicate", "external", "fixed", "notrepro", "postponed", "willnotfix", "tostory"];
|
|
6
|
+
/** bug 状态枚举。 */
|
|
7
|
+
export declare const BUG_STATUSES: readonly ["active", "resolved", "closed"];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** 共享常量 */
|
|
2
|
+
/** 单次响应字符上限,超出则截断并提示分页。 */
|
|
3
|
+
export const CHARACTER_LIMIT = 25000;
|
|
4
|
+
/** 解决方案枚举(禅道 bug resolution)。 */
|
|
5
|
+
export const RESOLUTIONS = [
|
|
6
|
+
"bydesign", // 设计如此
|
|
7
|
+
"duplicate", // 重复 bug
|
|
8
|
+
"external", // 外部原因
|
|
9
|
+
"fixed", // 已解决
|
|
10
|
+
"notrepro", // 无法重现
|
|
11
|
+
"postponed", // 延期处理
|
|
12
|
+
"willnotfix", // 不予解决
|
|
13
|
+
"tostory", // 转为需求
|
|
14
|
+
];
|
|
15
|
+
/** bug 状态枚举。 */
|
|
16
|
+
export const BUG_STATUSES = ["active", "resolved", "closed"];
|
|
17
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,WAAW;AAEX,2BAA2B;AAC3B,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC;AAErC,iCAAiC;AACjC,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,UAAU,EAAE,OAAO;IACnB,WAAW,EAAE,SAAS;IACtB,UAAU,EAAE,OAAO;IACnB,OAAO,EAAE,MAAM;IACf,UAAU,EAAE,OAAO;IACnB,WAAW,EAAE,OAAO;IACpB,YAAY,EAAE,OAAO;IACrB,SAAS,EAAE,OAAO;CACV,CAAC;AAEX,gBAAgB;AAChB,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAU,CAAC"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @dannyvan/zentao-mcp
|
|
4
|
+
*
|
|
5
|
+
* 禅道 MCP + CLI,直连 REST API(api.php/v1)。
|
|
6
|
+
* 读:get_bug / list_bugs / search_bugs
|
|
7
|
+
* 写:comment_bug(v1 不支持,会报错)、resolve/close/activate/confirm/assign(默认 dry-run)
|
|
8
|
+
*
|
|
9
|
+
* 认证(环境变量):
|
|
10
|
+
* ZENTAO_URL 禅道地址,必填
|
|
11
|
+
* ZENTAO_ACCOUNT 账号(推荐)
|
|
12
|
+
* ZENTAO_PASSWORD 密码(推荐)
|
|
13
|
+
* ZENTAO_TOKEN 或直接给现成 token(临时/测试,会过期)
|
|
14
|
+
*/
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
+
import { clientFromEnv } from "./zentao-client.js";
|
|
18
|
+
import { registerReadTools } from "./tools/read.js";
|
|
19
|
+
import { registerWriteTools } from "./tools/write.js";
|
|
20
|
+
import { runCli } from "./cli.js";
|
|
21
|
+
function assertUrl() {
|
|
22
|
+
if (!process.env.ZENTAO_URL?.trim()) {
|
|
23
|
+
console.error("ERROR: 缺少 ZENTAO_URL。请设置为禅道地址,例如 https://example.com/zentao");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function assertAuth() {
|
|
28
|
+
const hasToken = !!process.env.ZENTAO_TOKEN;
|
|
29
|
+
const hasUserPass = !!(process.env.ZENTAO_ACCOUNT && process.env.ZENTAO_PASSWORD);
|
|
30
|
+
if (!hasToken && !hasUserPass) {
|
|
31
|
+
console.error("ERROR: 缺少认证。请设置 ZENTAO_TOKEN,或同时设置 ZENTAO_ACCOUNT 与 ZENTAO_PASSWORD。");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function serve() {
|
|
36
|
+
assertUrl();
|
|
37
|
+
assertAuth();
|
|
38
|
+
const server = new McpServer({ name: "zentao-mcp", version: "0.1.0" });
|
|
39
|
+
const client = clientFromEnv();
|
|
40
|
+
registerReadTools(server, client);
|
|
41
|
+
registerWriteTools(server, client);
|
|
42
|
+
await server.connect(new StdioServerTransport());
|
|
43
|
+
console.error("zentao-mcp running via stdio");
|
|
44
|
+
}
|
|
45
|
+
async function main() {
|
|
46
|
+
const argv = process.argv.slice(2);
|
|
47
|
+
if (argv.length === 0 || argv[0] === "serve") {
|
|
48
|
+
await serve();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const needsAuth = !["help", "--help", "-h"].includes(argv[0]);
|
|
52
|
+
if (needsAuth) {
|
|
53
|
+
assertUrl();
|
|
54
|
+
assertAuth();
|
|
55
|
+
}
|
|
56
|
+
await runCli(argv);
|
|
57
|
+
}
|
|
58
|
+
main().catch((error) => {
|
|
59
|
+
console.error("Fatal:", error);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
});
|
|
62
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,SAAS,SAAS;IAChB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,CAAC;QACpC,OAAO,CAAC,KAAK,CAAC,6DAA6D,CAAC,CAAC;QAC7E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,SAAS,UAAU;IACjB,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;IAC5C,MAAM,WAAW,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAClF,IAAI,CAAC,QAAQ,IAAI,CAAC,WAAW,EAAE,CAAC;QAC9B,OAAO,CAAC,KAAK,CACX,sEAAsE,CACvE,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,KAAK,UAAU,KAAK;IAClB,SAAS,EAAE,CAAC;IACZ,UAAU,EAAE,CAAC;IACb,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IACvE,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAC/B,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;AAChD,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO,EAAE,CAAC;QAC7C,MAAM,KAAK,EAAE,CAAC;QACd,OAAO;IACT,CAAC;IACD,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,IAAI,SAAS,EAAE,CAAC;QACd,SAAS,EAAE,CAAC;QACZ,UAAU,EAAE,CAAC;IACf,CAAC;IACD,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC/B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatZentaoError } from "../zentao-client.js";
|
|
3
|
+
import { CHARACTER_LIMIT } from "../constants.js";
|
|
4
|
+
function pickAccount(v) {
|
|
5
|
+
if (!v)
|
|
6
|
+
return "";
|
|
7
|
+
if (typeof v === "object")
|
|
8
|
+
return v.account || v.realname || "";
|
|
9
|
+
return String(v);
|
|
10
|
+
}
|
|
11
|
+
function pickRealname(v) {
|
|
12
|
+
if (!v)
|
|
13
|
+
return "";
|
|
14
|
+
if (typeof v === "object")
|
|
15
|
+
return v.realname || v.account || "";
|
|
16
|
+
return String(v);
|
|
17
|
+
}
|
|
18
|
+
function stripHtml(html) {
|
|
19
|
+
return html
|
|
20
|
+
.replace(/<[^>]+>/g, "\n")
|
|
21
|
+
.replace(/ /g, " ")
|
|
22
|
+
.replace(/&/g, "&")
|
|
23
|
+
.replace(/[ \t]+/g, " ")
|
|
24
|
+
.replace(/\n\s*\n+/g, "\n")
|
|
25
|
+
.trim();
|
|
26
|
+
}
|
|
27
|
+
function slimBug(bug, includeSteps = false) {
|
|
28
|
+
return {
|
|
29
|
+
id: bug.id,
|
|
30
|
+
title: bug.title,
|
|
31
|
+
status: bug.status,
|
|
32
|
+
severity: bug.severity,
|
|
33
|
+
pri: bug.pri,
|
|
34
|
+
product: bug.product,
|
|
35
|
+
branch: bug.branch,
|
|
36
|
+
assignedTo: pickAccount(bug.assignedTo),
|
|
37
|
+
assignedToName: pickRealname(bug.assignedTo),
|
|
38
|
+
openedBy: pickAccount(bug.openedBy),
|
|
39
|
+
openedByName: pickRealname(bug.openedBy),
|
|
40
|
+
openedDate: bug.openedDate,
|
|
41
|
+
resolution: bug.resolution || "",
|
|
42
|
+
resolvedBy: pickAccount(bug.resolvedBy),
|
|
43
|
+
...(includeSteps ? { steps: stripHtml(String(bug.steps || "")) } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function jsonResult(obj) {
|
|
47
|
+
let text = JSON.stringify(obj, null, 2);
|
|
48
|
+
if (text.length > CHARACTER_LIMIT) {
|
|
49
|
+
text = text.slice(0, CHARACTER_LIMIT) + "\n…(已截断,请用分页/过滤缩小范围)";
|
|
50
|
+
}
|
|
51
|
+
return { content: [{ type: "text", text }], structuredContent: obj };
|
|
52
|
+
}
|
|
53
|
+
function errResult(e) {
|
|
54
|
+
return { content: [{ type: "text", text: formatZentaoError(e) }], isError: true };
|
|
55
|
+
}
|
|
56
|
+
export function registerReadTools(server, client) {
|
|
57
|
+
server.registerTool("zentao_get_bug", {
|
|
58
|
+
title: "获取禅道 Bug 详情",
|
|
59
|
+
description: `按 ID 获取单条禅道 Bug 的完整信息,含重现步骤(steps)。
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
- id (number): Bug 的数字 ID
|
|
63
|
+
- include_steps (boolean): 是否包含 steps 纯文本,默认 true
|
|
64
|
+
|
|
65
|
+
Returns: { id, title, status, severity, pri, product, assignedTo, openedBy, openedDate, resolution, steps? }
|
|
66
|
+
|
|
67
|
+
只读,不修改任何数据。`,
|
|
68
|
+
inputSchema: {
|
|
69
|
+
id: z.number().int().positive().describe("Bug 的数字 ID"),
|
|
70
|
+
include_steps: z.boolean().default(true).describe("是否包含重现步骤纯文本"),
|
|
71
|
+
},
|
|
72
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
73
|
+
}, async ({ id, include_steps }) => {
|
|
74
|
+
try {
|
|
75
|
+
const bug = await client.getBug(id);
|
|
76
|
+
return jsonResult(slimBug(bug, include_steps));
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
return errResult(e);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
server.registerTool("zentao_list_bugs", {
|
|
83
|
+
title: "列出产品下的 Bug",
|
|
84
|
+
description: `列出某产品下的 Bug,服务端分页。可选按状态/指派人过滤。
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
- product (number): 产品 ID
|
|
88
|
+
- limit (number): 每页条数 1-200,默认 50
|
|
89
|
+
- page (number): 页码,从 1 开始,默认 1
|
|
90
|
+
- status ('active'|'resolved'|'closed'): 可选
|
|
91
|
+
- assigned_to (string): 可选,按指派人 account 过滤当前页
|
|
92
|
+
|
|
93
|
+
Returns: { product, page, limit, total, count, bugs: [...], has_more }
|
|
94
|
+
|
|
95
|
+
只读。禅道返回的 total 可能不准(随 limit 变),以翻页实际列出为准。`,
|
|
96
|
+
inputSchema: {
|
|
97
|
+
product: z.number().int().positive().describe("产品 ID"),
|
|
98
|
+
limit: z.number().int().min(1).max(200).default(50).describe("每页条数"),
|
|
99
|
+
page: z.number().int().min(1).default(1).describe("页码,从 1 开始"),
|
|
100
|
+
status: z
|
|
101
|
+
.enum(["all", "active", "resolved", "closed"])
|
|
102
|
+
.default("all")
|
|
103
|
+
.describe("按状态过滤,默认 all。注意默认不传时禅道只返回很窄的子集"),
|
|
104
|
+
assigned_to: z.string().optional().describe("按指派人 account 过滤"),
|
|
105
|
+
},
|
|
106
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
107
|
+
}, async ({ product, limit, page, status, assigned_to }) => {
|
|
108
|
+
try {
|
|
109
|
+
const params = { status, limit, page };
|
|
110
|
+
if (assigned_to)
|
|
111
|
+
params.assignedTo = assigned_to;
|
|
112
|
+
const data = await client.listProductBugs(product, params);
|
|
113
|
+
let bugs = Array.isArray(data?.bugs) ? data.bugs : [];
|
|
114
|
+
if (assigned_to)
|
|
115
|
+
bugs = bugs.filter((b) => pickAccount(b.assignedTo) === assigned_to);
|
|
116
|
+
const total = Number(data?.total ?? bugs.length);
|
|
117
|
+
const out = {
|
|
118
|
+
product,
|
|
119
|
+
page,
|
|
120
|
+
limit,
|
|
121
|
+
total,
|
|
122
|
+
count: bugs.length,
|
|
123
|
+
bugs: bugs.map((b) => slimBug(b, false)),
|
|
124
|
+
has_more: total > page * limit,
|
|
125
|
+
...(total > page * limit ? { next_page: page + 1 } : {}),
|
|
126
|
+
};
|
|
127
|
+
return jsonResult(out);
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
return errResult(e);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
server.registerTool("zentao_search_bugs", {
|
|
134
|
+
title: "搜索产品下的 Bug",
|
|
135
|
+
description: `在某产品下按关键词搜索 Bug(匹配标题与 steps)。底层拉取分页列表后客户端过滤。
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
- product (number): 产品 ID
|
|
139
|
+
- keyword (string): 关键词
|
|
140
|
+
- scan_limit (number): 最多扫描多少条(分页累加),默认 200,上限 1000
|
|
141
|
+
|
|
142
|
+
Returns: { product, keyword, scanned, matched, bugs: [...] }
|
|
143
|
+
|
|
144
|
+
只读。命中范围受 scan_limit 限制,必要时调大。`,
|
|
145
|
+
inputSchema: {
|
|
146
|
+
product: z.number().int().positive().describe("产品 ID"),
|
|
147
|
+
keyword: z.string().min(1).describe("搜索关键词"),
|
|
148
|
+
scan_limit: z.number().int().min(1).max(1000).default(200).describe("最多扫描条数"),
|
|
149
|
+
},
|
|
150
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
151
|
+
}, async ({ product, keyword, scan_limit }) => {
|
|
152
|
+
try {
|
|
153
|
+
const pageSize = 100;
|
|
154
|
+
const collected = [];
|
|
155
|
+
let page = 1;
|
|
156
|
+
let total = Infinity;
|
|
157
|
+
while (collected.length < scan_limit && (page - 1) * pageSize < total) {
|
|
158
|
+
const data = await client.listProductBugs(product, { status: "all", limit: pageSize, page });
|
|
159
|
+
total = Number(data?.total ?? 0);
|
|
160
|
+
const bugs = Array.isArray(data?.bugs) ? data.bugs : [];
|
|
161
|
+
if (bugs.length === 0)
|
|
162
|
+
break;
|
|
163
|
+
collected.push(...bugs);
|
|
164
|
+
page += 1;
|
|
165
|
+
}
|
|
166
|
+
const scanned = collected.slice(0, scan_limit);
|
|
167
|
+
const kw = keyword.toLowerCase();
|
|
168
|
+
const matched = scanned.filter((b) => String(b.title || "").toLowerCase().includes(kw) ||
|
|
169
|
+
stripHtml(String(b.steps || "")).toLowerCase().includes(kw));
|
|
170
|
+
return jsonResult({
|
|
171
|
+
product,
|
|
172
|
+
keyword,
|
|
173
|
+
scanned: scanned.length,
|
|
174
|
+
total_in_product: total === Infinity ? scanned.length : total,
|
|
175
|
+
matched: matched.length,
|
|
176
|
+
bugs: matched.map((b) => slimBug(b, false)),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
return errResult(e);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=read.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 写 tools。分级把关:
|
|
3
|
+
* - 评论:v1 不支持,调用即报错
|
|
4
|
+
* - 状态流转:默认 dry-run,带 confirm=true 才真写
|
|
5
|
+
* 写操作在禅道可见。
|
|
6
|
+
*/
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { ZentaoClient } from "../zentao-client.js";
|
|
9
|
+
export declare function registerWriteTools(server: McpServer, client: ZentaoClient): void;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatZentaoError } from "../zentao-client.js";
|
|
3
|
+
import { RESOLUTIONS } from "../constants.js";
|
|
4
|
+
function errResult(e) {
|
|
5
|
+
return { content: [{ type: "text", text: formatZentaoError(e) }], isError: true };
|
|
6
|
+
}
|
|
7
|
+
function okResult(text, structured) {
|
|
8
|
+
return { content: [{ type: "text", text }], structuredContent: structured };
|
|
9
|
+
}
|
|
10
|
+
const HIGH_RISK = {
|
|
11
|
+
readOnlyHint: false,
|
|
12
|
+
destructiveHint: true,
|
|
13
|
+
idempotentHint: false,
|
|
14
|
+
openWorldHint: true,
|
|
15
|
+
};
|
|
16
|
+
async function highRiskOp(client, id, confirm, previewLine, exec, okLine) {
|
|
17
|
+
const bug = await client.getBug(id);
|
|
18
|
+
const preview = previewLine(bug);
|
|
19
|
+
if (!confirm) {
|
|
20
|
+
return okResult(`[预览·未执行]\n${preview}\n\n确认无误后带 confirm=true 再次调用以真正提交。⚠️ 该操作在禅道可见。`, { dryRun: true, bug: { id: bug.id, title: bug.title, status: bug.status }, preview });
|
|
21
|
+
}
|
|
22
|
+
const result = await exec();
|
|
23
|
+
return okResult(okLine, { dryRun: false, result });
|
|
24
|
+
}
|
|
25
|
+
export function registerWriteTools(server, client) {
|
|
26
|
+
server.registerTool("zentao_comment_bug", {
|
|
27
|
+
title: "给 Bug 加备注(禅道 v1 REST 不支持,会报错)",
|
|
28
|
+
description: `⚠️ 禅道 v1 REST API **不支持**为 Bug 添加独立备注(实测:editBug 的 comment 字段被静默丢弃,
|
|
29
|
+
且无 /comment 动作端点)。调用本工具会**直接返回错误**,不会假装成功。
|
|
30
|
+
如需加备注:请在禅道 Web UI 手工操作,或改用旧版 session API(?m=action&f=comment)。
|
|
31
|
+
Args: id (number), comment (string)`,
|
|
32
|
+
inputSchema: {
|
|
33
|
+
id: z.number().int().positive().describe("Bug ID"),
|
|
34
|
+
comment: z.string().min(1).describe("备注内容"),
|
|
35
|
+
},
|
|
36
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
37
|
+
}, async ({ id, comment }) => {
|
|
38
|
+
try {
|
|
39
|
+
const result = await client.commentBug(id, comment);
|
|
40
|
+
return okResult(`✅ 已给 Bug #${id} 添加备注。`, { dryRun: false, result });
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
return errResult(e);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
server.registerTool("zentao_resolve_bug", {
|
|
47
|
+
title: "解决 Bug(状态流转)",
|
|
48
|
+
description: `把 Bug 标记为已解决。高风险:默认只预览,带 confirm=true 才真正提交。
|
|
49
|
+
Args: id, resolution(${RESOLUTIONS.join("/")}), resolvedBuild?(默认 trunk), comment?, confirm(默认 false)`,
|
|
50
|
+
inputSchema: {
|
|
51
|
+
id: z.number().int().positive().describe("Bug ID"),
|
|
52
|
+
resolution: z
|
|
53
|
+
.enum(RESOLUTIONS)
|
|
54
|
+
.describe("解决方案:fixed/postponed/willnotfix/duplicate/external/notrepro/bydesign/tostory"),
|
|
55
|
+
resolvedBuild: z.string().optional().describe("解决版本,默认 trunk"),
|
|
56
|
+
comment: z.string().optional().describe("备注"),
|
|
57
|
+
confirm: z.boolean().default(false).describe("false=仅预览;true=真正提交"),
|
|
58
|
+
},
|
|
59
|
+
annotations: HIGH_RISK,
|
|
60
|
+
}, async ({ id, resolution, resolvedBuild, comment, confirm }) => {
|
|
61
|
+
try {
|
|
62
|
+
return await highRiskOp(client, id, confirm, (b) => `解决 Bug #${id}「${b.title}」:${b.status} → resolved,resolution=${resolution},build=${resolvedBuild ?? "trunk"}${comment ? `,备注:${comment}` : ""}`, () => client.resolveBug(id, { resolution, resolvedBuild: resolvedBuild ?? "trunk", comment }), `✅ 已解决 Bug #${id}(resolution=${resolution})。`);
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
return errResult(e);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
server.registerTool("zentao_close_bug", {
|
|
69
|
+
title: "关闭 Bug(状态流转)",
|
|
70
|
+
description: `关闭一个已解决的 Bug。高风险:默认预览,confirm=true 才执行。
|
|
71
|
+
Args: id, comment?, confirm(默认 false)`,
|
|
72
|
+
inputSchema: {
|
|
73
|
+
id: z.number().int().positive().describe("Bug ID"),
|
|
74
|
+
comment: z.string().optional().describe("备注"),
|
|
75
|
+
confirm: z.boolean().default(false).describe("false=仅预览;true=真正提交"),
|
|
76
|
+
},
|
|
77
|
+
annotations: HIGH_RISK,
|
|
78
|
+
}, async ({ id, comment, confirm }) => {
|
|
79
|
+
try {
|
|
80
|
+
return await highRiskOp(client, id, confirm, (b) => `关闭 Bug #${id}「${b.title}」:${b.status} → closed${comment ? `,备注:${comment}` : ""}`, () => client.closeBug(id, { comment }), `✅ 已关闭 Bug #${id}。`);
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
return errResult(e);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
server.registerTool("zentao_activate_bug", {
|
|
87
|
+
title: "激活 Bug(状态流转)",
|
|
88
|
+
description: `重新激活一个已解决/关闭的 Bug。高风险:默认预览,confirm=true 才执行。
|
|
89
|
+
Args: id, assignedTo?(重新指派给谁), comment?, confirm(默认 false)`,
|
|
90
|
+
inputSchema: {
|
|
91
|
+
id: z.number().int().positive().describe("Bug ID"),
|
|
92
|
+
assignedTo: z.string().optional().describe("重新指派给的 account"),
|
|
93
|
+
comment: z.string().optional().describe("备注"),
|
|
94
|
+
confirm: z.boolean().default(false).describe("false=仅预览;true=真正提交"),
|
|
95
|
+
},
|
|
96
|
+
annotations: HIGH_RISK,
|
|
97
|
+
}, async ({ id, assignedTo, comment, confirm }) => {
|
|
98
|
+
try {
|
|
99
|
+
return await highRiskOp(client, id, confirm, (b) => `激活 Bug #${id}「${b.title}」:${b.status} → active${assignedTo ? `,指派给 ${assignedTo}` : ""}${comment ? `,备注:${comment}` : ""}`, () => client.activateBug(id, { assignedTo, comment }), `✅ 已激活 Bug #${id}。`);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
return errResult(e);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
server.registerTool("zentao_confirm_bug", {
|
|
106
|
+
title: "确认 Bug(状态流转)",
|
|
107
|
+
description: `确认一个 Bug(is_confirmed=1)。高风险:默认预览,confirm=true 才执行。
|
|
108
|
+
Args: id, assignedTo?, confirm(默认 false)`,
|
|
109
|
+
inputSchema: {
|
|
110
|
+
id: z.number().int().positive().describe("Bug ID"),
|
|
111
|
+
assignedTo: z.string().optional().describe("确认后指派给的 account"),
|
|
112
|
+
confirm: z.boolean().default(false).describe("false=仅预览;true=真正提交"),
|
|
113
|
+
},
|
|
114
|
+
annotations: HIGH_RISK,
|
|
115
|
+
}, async ({ id, assignedTo, confirm }) => {
|
|
116
|
+
try {
|
|
117
|
+
return await highRiskOp(client, id, confirm, (b) => `确认 Bug #${id}「${b.title}」${assignedTo ? `,指派给 ${assignedTo}` : ""}`, () => client.confirmBug(id, { assignedTo }), `✅ 已确认 Bug #${id}。`);
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
return errResult(e);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
server.registerTool("zentao_assign_bug", {
|
|
124
|
+
title: "指派 Bug(状态流转)",
|
|
125
|
+
description: `把 Bug 指派给某人(走 PUT /bugs/{id} editBug)。高风险:默认预览,confirm=true 才执行。
|
|
126
|
+
⚠️ 禅道 v1 REST 无独立指派端点,comment 无法经此记录(会被丢弃),仅指派本身生效。
|
|
127
|
+
Args: id, assignedTo(account), comment?(不生效), confirm(默认 false)`,
|
|
128
|
+
inputSchema: {
|
|
129
|
+
id: z.number().int().positive().describe("Bug ID"),
|
|
130
|
+
assignedTo: z.string().min(1).describe("指派给的 account"),
|
|
131
|
+
comment: z.string().optional().describe("备注"),
|
|
132
|
+
confirm: z.boolean().default(false).describe("false=仅预览;true=真正提交"),
|
|
133
|
+
},
|
|
134
|
+
annotations: HIGH_RISK,
|
|
135
|
+
}, async ({ id, assignedTo, comment, confirm }) => {
|
|
136
|
+
try {
|
|
137
|
+
return await highRiskOp(client, id, confirm, (b) => `指派 Bug #${id}「${b.title}」给 ${assignedTo}(原指派 ${b.assignedTo?.account ?? b.assignedTo ?? "—"})${comment ? `,备注:${comment}` : ""}`, () => client.assignBug(id, { assignedTo, comment }), `✅ 已把 Bug #${id} 指派给 ${assignedTo}。`);
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
return errResult(e);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write.js","sourceRoot":"","sources":["../../src/tools/write.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface ZentaoConfig {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
account?: string;
|
|
4
|
+
password?: string;
|
|
5
|
+
/** 直接复用的现成 token(跳过 account/password 换取)。 */
|
|
6
|
+
token?: string;
|
|
7
|
+
}
|
|
8
|
+
/** 把任意错误转成对 agent 友好、可操作的中文消息。 */
|
|
9
|
+
export declare function formatZentaoError(error: unknown): string;
|
|
10
|
+
export declare class ZentaoClient {
|
|
11
|
+
private http;
|
|
12
|
+
private token;
|
|
13
|
+
private readonly account?;
|
|
14
|
+
private readonly password?;
|
|
15
|
+
constructor(cfg: ZentaoConfig);
|
|
16
|
+
private ensureToken;
|
|
17
|
+
request<T = any>(method: "GET" | "POST" | "PUT" | "DELETE", endpoint: string, data?: unknown, params?: Record<string, unknown>, _retried?: boolean): Promise<T>;
|
|
18
|
+
getBug(id: number): Promise<any>;
|
|
19
|
+
listProductBugs(productId: number, params: Record<string, unknown>): Promise<any>;
|
|
20
|
+
resolveBug(id: number, body: {
|
|
21
|
+
resolution: string;
|
|
22
|
+
resolvedBuild?: string;
|
|
23
|
+
comment?: string;
|
|
24
|
+
}): Promise<any>;
|
|
25
|
+
closeBug(id: number, body: {
|
|
26
|
+
comment?: string;
|
|
27
|
+
}): Promise<any>;
|
|
28
|
+
activateBug(id: number, body: {
|
|
29
|
+
comment?: string;
|
|
30
|
+
assignedTo?: string;
|
|
31
|
+
}): Promise<any>;
|
|
32
|
+
confirmBug(id: number, body: {
|
|
33
|
+
assignedTo?: string;
|
|
34
|
+
}): Promise<any>;
|
|
35
|
+
/** 指派:PUT /bugs/{id}。v1 无独立指派端点,comment 无法经此记录。 */
|
|
36
|
+
assignBug(id: number, body: {
|
|
37
|
+
assignedTo: string;
|
|
38
|
+
comment?: string;
|
|
39
|
+
}): Promise<any>;
|
|
40
|
+
/**
|
|
41
|
+
* 加备注:v1 REST 不支持独立备注(editBug 的 comment 被静默丢弃,无 /comment 端点)。
|
|
42
|
+
* 直接抛错,避免返回假成功。
|
|
43
|
+
*/
|
|
44
|
+
commentBug(_id: number, _comment: string): never;
|
|
45
|
+
}
|
|
46
|
+
export declare function clientFromEnv(): ZentaoClient;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 禅道 REST API 客户端。
|
|
3
|
+
*
|
|
4
|
+
* 直连 `{baseUrl}/api.php/v1`。
|
|
5
|
+
* 认证:优先用 ZENTAO_TOKEN,否则用 account+password 调 `POST /tokens`,
|
|
6
|
+
* 缓存并在 401 时自动刷新一次。
|
|
7
|
+
*/
|
|
8
|
+
import axios, { AxiosError } from "axios";
|
|
9
|
+
/** 把任意错误转成对 agent 友好、可操作的中文消息。 */
|
|
10
|
+
export function formatZentaoError(error) {
|
|
11
|
+
if (axios.isAxiosError(error)) {
|
|
12
|
+
const ax = error;
|
|
13
|
+
if (ax.response) {
|
|
14
|
+
const status = ax.response.status;
|
|
15
|
+
const body = ax.response.data;
|
|
16
|
+
const apiMsg = typeof body === "object" && body && "error" in body
|
|
17
|
+
? String(body.error)
|
|
18
|
+
: typeof body === "string"
|
|
19
|
+
? body.slice(0, 200)
|
|
20
|
+
: "";
|
|
21
|
+
switch (status) {
|
|
22
|
+
case 401:
|
|
23
|
+
return `Error 401:认证失败或 token 失效。检查 ZENTAO_ACCOUNT/ZENTAO_PASSWORD 是否正确。${apiMsg}`;
|
|
24
|
+
case 403:
|
|
25
|
+
return `Error 403:无权限。当前禅道账号对该资源没有操作权限。${apiMsg}`;
|
|
26
|
+
case 404:
|
|
27
|
+
return `Error 404:对象不存在。请检查 bug/产品 ID 是否正确。${apiMsg}`;
|
|
28
|
+
case 422:
|
|
29
|
+
return `Error 422:参数校验失败。${apiMsg || "检查必填字段(如 resolution)是否齐全。"}`;
|
|
30
|
+
default:
|
|
31
|
+
return `Error ${status}:请求失败。${apiMsg}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (ax.code === "ECONNABORTED")
|
|
35
|
+
return "Error:请求超时,请重试或检查网络。";
|
|
36
|
+
return `Error:网络错误 ${ax.code ?? ""} ${ax.message}`;
|
|
37
|
+
}
|
|
38
|
+
return `Error:${error instanceof Error ? error.message : String(error)}`;
|
|
39
|
+
}
|
|
40
|
+
export class ZentaoClient {
|
|
41
|
+
http;
|
|
42
|
+
token;
|
|
43
|
+
account;
|
|
44
|
+
password;
|
|
45
|
+
constructor(cfg) {
|
|
46
|
+
const baseUrl = cfg.baseUrl.replace(/\/$/, "");
|
|
47
|
+
this.account = cfg.account;
|
|
48
|
+
this.password = cfg.password;
|
|
49
|
+
this.token = cfg.token ?? null;
|
|
50
|
+
this.http = axios.create({
|
|
51
|
+
baseURL: `${baseUrl}/api.php/v1`,
|
|
52
|
+
timeout: 30000,
|
|
53
|
+
maxRedirects: 5,
|
|
54
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
55
|
+
validateStatus: (s) => s >= 200 && s < 500,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async ensureToken() {
|
|
59
|
+
if (this.token)
|
|
60
|
+
return this.token;
|
|
61
|
+
if (!this.account || !this.password) {
|
|
62
|
+
throw new Error("缺少认证:请设置 ZENTAO_TOKEN,或同时设置 ZENTAO_ACCOUNT 与 ZENTAO_PASSWORD。");
|
|
63
|
+
}
|
|
64
|
+
const resp = await this.http.post("/tokens", {
|
|
65
|
+
account: this.account,
|
|
66
|
+
password: this.password,
|
|
67
|
+
});
|
|
68
|
+
const token = resp.data?.token;
|
|
69
|
+
if (!token) {
|
|
70
|
+
throw new Error(`换取 token 失败:${resp.data?.error ?? JSON.stringify(resp.data)}`);
|
|
71
|
+
}
|
|
72
|
+
this.token = token;
|
|
73
|
+
return token;
|
|
74
|
+
}
|
|
75
|
+
async request(method, endpoint, data, params, _retried = false) {
|
|
76
|
+
const token = await this.ensureToken();
|
|
77
|
+
const resp = await this.http.request({
|
|
78
|
+
method,
|
|
79
|
+
url: endpoint,
|
|
80
|
+
data,
|
|
81
|
+
params,
|
|
82
|
+
headers: { Token: token },
|
|
83
|
+
});
|
|
84
|
+
const status = resp.status;
|
|
85
|
+
if (status === 401 && !_retried && this.account && this.password) {
|
|
86
|
+
this.token = null;
|
|
87
|
+
return this.request(method, endpoint, data, params, true);
|
|
88
|
+
}
|
|
89
|
+
if (status >= 400) {
|
|
90
|
+
const err = new AxiosError(`ZenTao API ${status}`, String(status), undefined, undefined, { status, data: resp.data, statusText: resp.statusText, headers: resp.headers, config: resp.config });
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
return resp.data;
|
|
94
|
+
}
|
|
95
|
+
getBug(id) {
|
|
96
|
+
return this.request("GET", `/bugs/${id}`);
|
|
97
|
+
}
|
|
98
|
+
listProductBugs(productId, params) {
|
|
99
|
+
return this.request("GET", `/products/${productId}/bugs`, undefined, params);
|
|
100
|
+
}
|
|
101
|
+
// 禅道 v1 REST 是精选端点集,不是通用 `{module}/{id}/{action}` 动作路由。
|
|
102
|
+
// 实测 `/bugs/{id}/assignto` 一类子路由会 404;指派走 PUT /bugs/{id}(字段合并,未传字段不清空)。
|
|
103
|
+
// resolve/close/activate/confirm 仍走动作子路由;若目标实例 404,改走 PUT 并先在测试单上验证必填字段。
|
|
104
|
+
resolveBug(id, body) {
|
|
105
|
+
return this.request("POST", `/bugs/${id}/resolve`, body);
|
|
106
|
+
}
|
|
107
|
+
closeBug(id, body) {
|
|
108
|
+
return this.request("POST", `/bugs/${id}/close`, body);
|
|
109
|
+
}
|
|
110
|
+
activateBug(id, body) {
|
|
111
|
+
return this.request("POST", `/bugs/${id}/activate`, body);
|
|
112
|
+
}
|
|
113
|
+
confirmBug(id, body) {
|
|
114
|
+
return this.request("POST", `/bugs/${id}/confirm`, body);
|
|
115
|
+
}
|
|
116
|
+
/** 指派:PUT /bugs/{id}。v1 无独立指派端点,comment 无法经此记录。 */
|
|
117
|
+
assignBug(id, body) {
|
|
118
|
+
return this.request("PUT", `/bugs/${id}`, { assignedTo: body.assignedTo });
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 加备注:v1 REST 不支持独立备注(editBug 的 comment 被静默丢弃,无 /comment 端点)。
|
|
122
|
+
* 直接抛错,避免返回假成功。
|
|
123
|
+
*/
|
|
124
|
+
commentBug(_id, _comment) {
|
|
125
|
+
throw new Error("禅道 v1 REST API 不支持为 Bug 添加独立备注/评论(editBug 的 comment 字段被静默丢弃,且无 /comment 动作端点)。请在禅道 Web UI 手工添加备注,或改用旧版 session API(?m=action&f=comment)。");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export function clientFromEnv() {
|
|
129
|
+
const baseUrl = process.env.ZENTAO_URL?.replace(/\/$/, "") ?? "";
|
|
130
|
+
if (!baseUrl) {
|
|
131
|
+
throw new Error("缺少 ZENTAO_URL。请设置为禅道地址,例如 https://example.com/zentao");
|
|
132
|
+
}
|
|
133
|
+
return new ZentaoClient({
|
|
134
|
+
baseUrl,
|
|
135
|
+
account: process.env.ZENTAO_ACCOUNT,
|
|
136
|
+
password: process.env.ZENTAO_PASSWORD,
|
|
137
|
+
token: process.env.ZENTAO_TOKEN,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=zentao-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zentao-client.js","sourceRoot":"","sources":["../src/zentao-client.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dannyvan/zentao-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "禅道 MCP + CLI,直连 REST API v1。只做 Bug,写操作默认 dry-run。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"zentao-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/dannyvan/zentao-mcp.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/dannyvan/zentao-mcp/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/dannyvan/zentao-mcp#readme",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"keywords": [
|
|
27
|
+
"mcp",
|
|
28
|
+
"zentao",
|
|
29
|
+
"bug"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"start": "node dist/index.js",
|
|
34
|
+
"prepublishOnly": "npm run build"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.6.1",
|
|
41
|
+
"axios": "^1.7.9",
|
|
42
|
+
"zod": "^3.23.8"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.10.0",
|
|
46
|
+
"typescript": "^5.7.2"
|
|
47
|
+
}
|
|
48
|
+
}
|