@leeguoo/zentao-mcp 0.2.2 → 0.3.1
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/README.md +21 -5
- package/package.json +1 -1
- package/src/index.js +180 -1
package/README.md
CHANGED
|
@@ -19,10 +19,10 @@ MCP server for ZenTao RESTful APIs (products + bugs).
|
|
|
19
19
|
"args": [
|
|
20
20
|
"-y",
|
|
21
21
|
"@leeguoo/zentao-mcp",
|
|
22
|
-
"--stdio",
|
|
23
22
|
"--zentao-url=https://zentao.example.com/zentao",
|
|
24
23
|
"--zentao-account=leo",
|
|
25
|
-
"--zentao-password=***"
|
|
24
|
+
"--zentao-password=***",
|
|
25
|
+
"--stdio"
|
|
26
26
|
]
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -41,10 +41,10 @@ command = "npx"
|
|
|
41
41
|
args = [
|
|
42
42
|
"-y",
|
|
43
43
|
"@leeguoo/zentao-mcp",
|
|
44
|
-
"--stdio",
|
|
45
44
|
"--zentao-url=https://zentao.example.com/zentao",
|
|
46
45
|
"--zentao-account=leo",
|
|
47
|
-
"--zentao-password=***"
|
|
46
|
+
"--zentao-password=***",
|
|
47
|
+
"--stdio"
|
|
48
48
|
]
|
|
49
49
|
```
|
|
50
50
|
|
|
@@ -92,11 +92,12 @@ If you prefer to use environment variables instead of CLI args, you can configur
|
|
|
92
92
|
|
|
93
93
|
## Tools
|
|
94
94
|
|
|
95
|
-
The MCP server provides
|
|
95
|
+
The MCP server provides four tools that can be triggered by natural language in Cursor:
|
|
96
96
|
|
|
97
97
|
- **`zentao_products_list`** - List all products
|
|
98
98
|
- **`zentao_bugs_list`** - List bugs for a specific product
|
|
99
99
|
- **`zentao_bugs_stats`** - Get bug statistics across products
|
|
100
|
+
- **`zentao_bugs_mine`** - List my bugs by assignment or creator
|
|
100
101
|
|
|
101
102
|
### Usage Examples
|
|
102
103
|
|
|
@@ -107,6 +108,8 @@ After configuring the MCP server in Cursor, you can use natural language to inte
|
|
|
107
108
|
- "List bugs for product 1"
|
|
108
109
|
- "Show me bugs"
|
|
109
110
|
- "What's the bug statistics?"
|
|
111
|
+
- "Show my bugs"
|
|
112
|
+
- "List bugs assigned to me"
|
|
110
113
|
- "View bugs in product 2"
|
|
111
114
|
|
|
112
115
|
**Chinese (中文):**
|
|
@@ -115,11 +118,14 @@ After configuring the MCP server in Cursor, you can use natural language to inte
|
|
|
115
118
|
- "bug统计"
|
|
116
119
|
- "显示所有产品"
|
|
117
120
|
- "查看产品2的问题"
|
|
121
|
+
- "我的bug"
|
|
122
|
+
- "分配给我的bug"
|
|
118
123
|
|
|
119
124
|
The AI will automatically:
|
|
120
125
|
1. Use `zentao_products_list` to get product IDs when needed
|
|
121
126
|
2. Use `zentao_bugs_list` when you ask to see bugs
|
|
122
127
|
3. Use `zentao_bugs_stats` when you ask for statistics or overview
|
|
128
|
+
4. Use `zentao_bugs_mine` when you ask for your own bugs
|
|
123
129
|
|
|
124
130
|
### Tool Parameters
|
|
125
131
|
|
|
@@ -148,6 +154,16 @@ The AI will automatically:
|
|
|
148
154
|
}
|
|
149
155
|
```
|
|
150
156
|
|
|
157
|
+
**zentao_bugs_mine:**
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"scope": "assigned",
|
|
161
|
+
"includeZero": false,
|
|
162
|
+
"includeDetails": true,
|
|
163
|
+
"maxItems": 50
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
151
167
|
## Local Development
|
|
152
168
|
|
|
153
169
|
```bash
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -50,6 +50,35 @@ function normalizeError(message, payload) {
|
|
|
50
50
|
return { status: 0, msg: message || "error", result: payload ?? [] };
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function normalizeAccountValue(value) {
|
|
54
|
+
return String(value || "").trim().toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function extractAccounts(value) {
|
|
58
|
+
if (value === undefined || value === null) return [];
|
|
59
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
60
|
+
const normalized = normalizeAccountValue(value);
|
|
61
|
+
return normalized ? [normalized] : [];
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
return value.flatMap((item) => extractAccounts(item));
|
|
65
|
+
}
|
|
66
|
+
if (typeof value === "object") {
|
|
67
|
+
const candidates = [];
|
|
68
|
+
if (value.account) candidates.push(...extractAccounts(value.account));
|
|
69
|
+
if (value.realname) candidates.push(...extractAccounts(value.realname));
|
|
70
|
+
if (value.name) candidates.push(...extractAccounts(value.name));
|
|
71
|
+
if (value.user) candidates.push(...extractAccounts(value.user));
|
|
72
|
+
return candidates.filter(Boolean);
|
|
73
|
+
}
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function matchesAccount(value, matchAccount) {
|
|
78
|
+
const candidates = extractAccounts(value);
|
|
79
|
+
return candidates.includes(matchAccount);
|
|
80
|
+
}
|
|
81
|
+
|
|
53
82
|
class ZentaoClient {
|
|
54
83
|
constructor({ baseUrl, account, password }) {
|
|
55
84
|
this.baseUrl = normalizeBaseUrl(baseUrl);
|
|
@@ -156,6 +185,49 @@ class ZentaoClient {
|
|
|
156
185
|
return normalizeResult(payload);
|
|
157
186
|
}
|
|
158
187
|
|
|
188
|
+
async fetchAllBugsForProduct({ product, perPage, maxItems }) {
|
|
189
|
+
const bugs = [];
|
|
190
|
+
let page = 1;
|
|
191
|
+
let total = null;
|
|
192
|
+
const pageSize = toInt(perPage, 100);
|
|
193
|
+
const cap = toInt(maxItems, 0);
|
|
194
|
+
|
|
195
|
+
while (true) {
|
|
196
|
+
const payload = await this.request({
|
|
197
|
+
method: "GET",
|
|
198
|
+
path: "/api.php/v1/bugs",
|
|
199
|
+
query: {
|
|
200
|
+
product,
|
|
201
|
+
page,
|
|
202
|
+
limit: pageSize,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (payload.error) {
|
|
207
|
+
throw new Error(payload.error);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const pageBugs = Array.isArray(payload.bugs) ? payload.bugs : [];
|
|
211
|
+
total = payload.total ?? total;
|
|
212
|
+
for (const bug of pageBugs) {
|
|
213
|
+
bugs.push(bug);
|
|
214
|
+
if (cap > 0 && bugs.length >= cap) {
|
|
215
|
+
return { bugs, total };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (total !== null && payload.limit) {
|
|
220
|
+
if (page * payload.limit >= total) break;
|
|
221
|
+
} else if (pageBugs.length < pageSize) {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
page += 1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { bugs, total };
|
|
229
|
+
}
|
|
230
|
+
|
|
159
231
|
async bugStats({ includeZero, limit }) {
|
|
160
232
|
const productsResponse = await this.listProducts({ page: 1, limit: toInt(limit, 1000) });
|
|
161
233
|
if (productsResponse.status !== 1) return productsResponse;
|
|
@@ -183,6 +255,86 @@ class ZentaoClient {
|
|
|
183
255
|
products: rows,
|
|
184
256
|
});
|
|
185
257
|
}
|
|
258
|
+
|
|
259
|
+
async bugsMine({
|
|
260
|
+
account,
|
|
261
|
+
scope,
|
|
262
|
+
productIds,
|
|
263
|
+
includeZero,
|
|
264
|
+
perPage,
|
|
265
|
+
maxItems,
|
|
266
|
+
includeDetails,
|
|
267
|
+
}) {
|
|
268
|
+
const matchAccount = normalizeAccountValue(account || this.account);
|
|
269
|
+
const targetScope = (scope || "assigned").toLowerCase();
|
|
270
|
+
|
|
271
|
+
const productsResponse = await this.listProducts({ page: 1, limit: 1000 });
|
|
272
|
+
if (productsResponse.status !== 1) return productsResponse;
|
|
273
|
+
const products = productsResponse.result.products || [];
|
|
274
|
+
|
|
275
|
+
const productSet = Array.isArray(productIds) && productIds.length
|
|
276
|
+
? new Set(productIds.map((id) => Number(id)))
|
|
277
|
+
: null;
|
|
278
|
+
|
|
279
|
+
const rows = [];
|
|
280
|
+
const bugs = [];
|
|
281
|
+
let totalMatches = 0;
|
|
282
|
+
const maxCollect = toInt(maxItems, 200);
|
|
283
|
+
|
|
284
|
+
for (const product of products) {
|
|
285
|
+
if (productSet && !productSet.has(Number(product.id))) continue;
|
|
286
|
+
const { bugs: productBugs } = await this.fetchAllBugsForProduct({
|
|
287
|
+
product: product.id,
|
|
288
|
+
perPage,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const matches = productBugs.filter((bug) => {
|
|
292
|
+
const assigned = matchesAccount(bug.assignedTo, matchAccount);
|
|
293
|
+
const opened = matchesAccount(bug.openedBy, matchAccount);
|
|
294
|
+
const resolved = matchesAccount(bug.resolvedBy, matchAccount);
|
|
295
|
+
if (targetScope === "assigned") return assigned;
|
|
296
|
+
if (targetScope === "opened") return opened;
|
|
297
|
+
if (targetScope === "resolved") return resolved;
|
|
298
|
+
return assigned || opened || resolved;
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (!includeZero && matches.length === 0) continue;
|
|
302
|
+
totalMatches += matches.length;
|
|
303
|
+
|
|
304
|
+
rows.push({
|
|
305
|
+
id: product.id,
|
|
306
|
+
name: product.name,
|
|
307
|
+
totalBugs: toInt(product.totalBugs, 0),
|
|
308
|
+
myBugs: matches.length,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
if (includeDetails && bugs.length < maxCollect) {
|
|
312
|
+
for (const bug of matches) {
|
|
313
|
+
if (bugs.length >= maxCollect) break;
|
|
314
|
+
bugs.push({
|
|
315
|
+
id: bug.id,
|
|
316
|
+
title: bug.title,
|
|
317
|
+
product: bug.product,
|
|
318
|
+
status: bug.status,
|
|
319
|
+
pri: bug.pri,
|
|
320
|
+
severity: bug.severity,
|
|
321
|
+
assignedTo: bug.assignedTo,
|
|
322
|
+
openedBy: bug.openedBy,
|
|
323
|
+
resolvedBy: bug.resolvedBy,
|
|
324
|
+
openedDate: bug.openedDate,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return normalizeResult({
|
|
331
|
+
account: matchAccount,
|
|
332
|
+
scope: targetScope,
|
|
333
|
+
total: totalMatches,
|
|
334
|
+
products: rows,
|
|
335
|
+
bugs: includeDetails ? bugs : [],
|
|
336
|
+
});
|
|
337
|
+
}
|
|
186
338
|
}
|
|
187
339
|
|
|
188
340
|
function createClient() {
|
|
@@ -206,7 +358,7 @@ function getClient() {
|
|
|
206
358
|
const server = new Server(
|
|
207
359
|
{
|
|
208
360
|
name: "zentao-mcp",
|
|
209
|
-
version: "0.
|
|
361
|
+
version: "0.3.1",
|
|
210
362
|
},
|
|
211
363
|
{
|
|
212
364
|
capabilities: {
|
|
@@ -254,6 +406,30 @@ const tools = [
|
|
|
254
406
|
additionalProperties: false,
|
|
255
407
|
},
|
|
256
408
|
},
|
|
409
|
+
{
|
|
410
|
+
name: "zentao_bugs_mine",
|
|
411
|
+
description: "List my bugs (我的Bug) by assignment or creator. Default scope is assigned. Use when user asks for 'my bugs', '我的bug', '分配给我', or personal bug list.",
|
|
412
|
+
inputSchema: {
|
|
413
|
+
type: "object",
|
|
414
|
+
properties: {
|
|
415
|
+
account: { type: "string", description: "Account to match (default: login account)." },
|
|
416
|
+
scope: {
|
|
417
|
+
type: "string",
|
|
418
|
+
description: "Filter scope: assigned|opened|resolved|all (default assigned).",
|
|
419
|
+
},
|
|
420
|
+
productIds: {
|
|
421
|
+
type: "array",
|
|
422
|
+
items: { type: "integer" },
|
|
423
|
+
description: "Optional product IDs to limit search.",
|
|
424
|
+
},
|
|
425
|
+
includeZero: { type: "boolean", description: "Include products with zero matches (default false)." },
|
|
426
|
+
perPage: { type: "integer", description: "Page size when scanning products (default 100)." },
|
|
427
|
+
maxItems: { type: "integer", description: "Max bug items to return (default 200)." },
|
|
428
|
+
includeDetails: { type: "boolean", description: "Include bug details list (default false)." },
|
|
429
|
+
},
|
|
430
|
+
additionalProperties: false,
|
|
431
|
+
},
|
|
432
|
+
},
|
|
257
433
|
];
|
|
258
434
|
|
|
259
435
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
@@ -273,6 +449,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
273
449
|
case "zentao_bugs_stats":
|
|
274
450
|
result = await api.bugStats(args);
|
|
275
451
|
break;
|
|
452
|
+
case "zentao_bugs_mine":
|
|
453
|
+
result = await api.bugsMine(args);
|
|
454
|
+
break;
|
|
276
455
|
default:
|
|
277
456
|
return {
|
|
278
457
|
isError: true,
|