@leeguoo/zentao-mcp 0.2.2 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +21 -5
  2. package/package.json +1 -1
  3. package/src/index.js +159 -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 three tools that can be triggered by natural language in Cursor:
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leeguoo/zentao-mcp",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for ZenTao RESTful APIs",
5
5
  "keywords": [
6
6
  "zentao",
package/src/index.js CHANGED
@@ -50,6 +50,10 @@ function normalizeError(message, payload) {
50
50
  return { status: 0, msg: message || "error", result: payload ?? [] };
51
51
  }
52
52
 
53
+ function normalizeAccount(value) {
54
+ return String(value || "").trim().toLowerCase();
55
+ }
56
+
53
57
  class ZentaoClient {
54
58
  constructor({ baseUrl, account, password }) {
55
59
  this.baseUrl = normalizeBaseUrl(baseUrl);
@@ -156,6 +160,49 @@ class ZentaoClient {
156
160
  return normalizeResult(payload);
157
161
  }
158
162
 
163
+ async fetchAllBugsForProduct({ product, perPage, maxItems }) {
164
+ const bugs = [];
165
+ let page = 1;
166
+ let total = null;
167
+ const pageSize = toInt(perPage, 100);
168
+ const cap = toInt(maxItems, 0);
169
+
170
+ while (true) {
171
+ const payload = await this.request({
172
+ method: "GET",
173
+ path: "/api.php/v1/bugs",
174
+ query: {
175
+ product,
176
+ page,
177
+ limit: pageSize,
178
+ },
179
+ });
180
+
181
+ if (payload.error) {
182
+ throw new Error(payload.error);
183
+ }
184
+
185
+ const pageBugs = Array.isArray(payload.bugs) ? payload.bugs : [];
186
+ total = payload.total ?? total;
187
+ for (const bug of pageBugs) {
188
+ bugs.push(bug);
189
+ if (cap > 0 && bugs.length >= cap) {
190
+ return { bugs, total };
191
+ }
192
+ }
193
+
194
+ if (total !== null && payload.limit) {
195
+ if (page * payload.limit >= total) break;
196
+ } else if (pageBugs.length < pageSize) {
197
+ break;
198
+ }
199
+
200
+ page += 1;
201
+ }
202
+
203
+ return { bugs, total };
204
+ }
205
+
159
206
  async bugStats({ includeZero, limit }) {
160
207
  const productsResponse = await this.listProducts({ page: 1, limit: toInt(limit, 1000) });
161
208
  if (productsResponse.status !== 1) return productsResponse;
@@ -183,6 +230,90 @@ class ZentaoClient {
183
230
  products: rows,
184
231
  });
185
232
  }
233
+
234
+ async bugsMine({
235
+ account,
236
+ scope,
237
+ productIds,
238
+ includeZero,
239
+ perPage,
240
+ maxItems,
241
+ includeDetails,
242
+ }) {
243
+ const matchAccount = normalizeAccount(account || this.account);
244
+ const targetScope = (scope || "assigned").toLowerCase();
245
+
246
+ const productsResponse = await this.listProducts({ page: 1, limit: 1000 });
247
+ if (productsResponse.status !== 1) return productsResponse;
248
+ const products = productsResponse.result.products || [];
249
+
250
+ const productSet = Array.isArray(productIds) && productIds.length
251
+ ? new Set(productIds.map((id) => Number(id)))
252
+ : null;
253
+
254
+ const rows = [];
255
+ const bugs = [];
256
+ let totalMatches = 0;
257
+ const maxCollect = toInt(maxItems, 200);
258
+
259
+ for (const product of products) {
260
+ if (productSet && !productSet.has(Number(product.id))) continue;
261
+ const { bugs: productBugs } = await this.fetchAllBugsForProduct({
262
+ product: product.id,
263
+ perPage,
264
+ });
265
+
266
+ const matches = productBugs.filter((bug) => {
267
+ const assigned = normalizeAccount(bug.assignedTo);
268
+ const opened = normalizeAccount(bug.openedBy);
269
+ const resolved = normalizeAccount(bug.resolvedBy);
270
+ if (targetScope === "assigned") return assigned === matchAccount;
271
+ if (targetScope === "opened") return opened === matchAccount;
272
+ if (targetScope === "resolved") return resolved === matchAccount;
273
+ return (
274
+ assigned === matchAccount ||
275
+ opened === matchAccount ||
276
+ resolved === matchAccount
277
+ );
278
+ });
279
+
280
+ if (!includeZero && matches.length === 0) continue;
281
+ totalMatches += matches.length;
282
+
283
+ rows.push({
284
+ id: product.id,
285
+ name: product.name,
286
+ totalBugs: toInt(product.totalBugs, 0),
287
+ myBugs: matches.length,
288
+ });
289
+
290
+ if (includeDetails && bugs.length < maxCollect) {
291
+ for (const bug of matches) {
292
+ if (bugs.length >= maxCollect) break;
293
+ bugs.push({
294
+ id: bug.id,
295
+ title: bug.title,
296
+ product: bug.product,
297
+ status: bug.status,
298
+ pri: bug.pri,
299
+ severity: bug.severity,
300
+ assignedTo: bug.assignedTo,
301
+ openedBy: bug.openedBy,
302
+ resolvedBy: bug.resolvedBy,
303
+ openedDate: bug.openedDate,
304
+ });
305
+ }
306
+ }
307
+ }
308
+
309
+ return normalizeResult({
310
+ account: matchAccount,
311
+ scope: targetScope,
312
+ total: totalMatches,
313
+ products: rows,
314
+ bugs: includeDetails ? bugs : [],
315
+ });
316
+ }
186
317
  }
187
318
 
188
319
  function createClient() {
@@ -206,7 +337,7 @@ function getClient() {
206
337
  const server = new Server(
207
338
  {
208
339
  name: "zentao-mcp",
209
- version: "0.2.0",
340
+ version: "0.2.2",
210
341
  },
211
342
  {
212
343
  capabilities: {
@@ -254,6 +385,30 @@ const tools = [
254
385
  additionalProperties: false,
255
386
  },
256
387
  },
388
+ {
389
+ name: "zentao_bugs_mine",
390
+ 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.",
391
+ inputSchema: {
392
+ type: "object",
393
+ properties: {
394
+ account: { type: "string", description: "Account to match (default: login account)." },
395
+ scope: {
396
+ type: "string",
397
+ description: "Filter scope: assigned|opened|resolved|all (default assigned).",
398
+ },
399
+ productIds: {
400
+ type: "array",
401
+ items: { type: "integer" },
402
+ description: "Optional product IDs to limit search.",
403
+ },
404
+ includeZero: { type: "boolean", description: "Include products with zero matches (default false)." },
405
+ perPage: { type: "integer", description: "Page size when scanning products (default 100)." },
406
+ maxItems: { type: "integer", description: "Max bug items to return (default 200)." },
407
+ includeDetails: { type: "boolean", description: "Include bug details list (default false)." },
408
+ },
409
+ additionalProperties: false,
410
+ },
411
+ },
257
412
  ];
258
413
 
259
414
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
@@ -273,6 +428,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
273
428
  case "zentao_bugs_stats":
274
429
  result = await api.bugStats(args);
275
430
  break;
431
+ case "zentao_bugs_mine":
432
+ result = await api.bugsMine(args);
433
+ break;
276
434
  default:
277
435
  return {
278
436
  isError: true,