@leeguoo/zentao-mcp 0.2.1 → 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 +133 -13
  2. package/package.json +4 -1
  3. package/src/index.js +164 -6
package/README.md CHANGED
@@ -2,9 +2,38 @@
2
2
 
3
3
  MCP server for ZenTao RESTful APIs (products + bugs).
4
4
 
5
- ## Quick Start (npx)
5
+ ## Quick Start
6
6
 
7
- Use this MCP server config in your client:
7
+ ### Cursor IDE
8
+
9
+ 1. Open Cursor Settings (⌘, on Mac or Ctrl+, on Windows/Linux)
10
+ 2. Navigate to **Features** → **Model Context Protocol**
11
+ 3. Click **Edit Config** to open `~/.cursor/mcp.json` (or create it)
12
+ 4. Add the following configuration:
13
+
14
+ ```json
15
+ {
16
+ "mcpServers": {
17
+ "zentao-mcp": {
18
+ "command": "npx",
19
+ "args": [
20
+ "-y",
21
+ "@leeguoo/zentao-mcp",
22
+ "--zentao-url=https://zentao.example.com/zentao",
23
+ "--zentao-account=leo",
24
+ "--zentao-password=***",
25
+ "--stdio"
26
+ ]
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ 5. Restart Cursor IDE
33
+
34
+ ### Other MCP Clients (Claude Desktop, etc.)
35
+
36
+ For clients using TOML configuration (e.g., Claude Desktop), add to your MCP config file:
8
37
 
9
38
  ```toml
10
39
  [mcp_servers."zentao-mcp"]
@@ -12,30 +41,103 @@ command = "npx"
12
41
  args = [
13
42
  "-y",
14
43
  "@leeguoo/zentao-mcp",
15
- "--stdio",
16
44
  "--zentao-url=https://zentao.example.com/zentao",
17
45
  "--zentao-account=leo",
18
- "--zentao-password=***"
46
+ "--zentao-password=***",
47
+ "--stdio"
19
48
  ]
20
49
  ```
21
50
 
51
+ **Config file locations:**
52
+ - Claude Desktop: `~/Library/Application Support/Claude/claude_desktop_config.toml` (Mac) or `%APPDATA%\Claude\claude_desktop_config.toml` (Windows)
53
+ - Cursor: `~/.cursor/mcp.json` (JSON format)
54
+
22
55
  ## Configuration
23
56
 
24
- Required:
25
- - `--zentao-url` / `ZENTAO_URL` (e.g. `https://zentao.example.com/zentao`)
26
- - `--zentao-account` / `ZENTAO_ACCOUNT`
27
- - `--zentao-password` / `ZENTAO_PASSWORD`
57
+ ### Required Parameters
58
+
59
+ You can configure the server using CLI arguments or environment variables:
28
60
 
29
- Tip: `ZENTAO_URL` should include the ZenTao base path (often `/zentao`).
61
+ **CLI Arguments:**
62
+ - `--zentao-url` (e.g. `https://zentao.example.com/zentao`)
63
+ - `--zentao-account`
64
+ - `--zentao-password`
65
+
66
+ **Environment Variables:**
67
+ - `ZENTAO_URL` (e.g. `https://zentao.example.com/zentao`)
68
+ - `ZENTAO_ACCOUNT`
69
+ - `ZENTAO_PASSWORD`
70
+
71
+ ### Using Environment Variables in Cursor
72
+
73
+ If you prefer to use environment variables instead of CLI args, you can configure them in Cursor:
74
+
75
+ ```json
76
+ {
77
+ "mcpServers": {
78
+ "zentao-mcp": {
79
+ "command": "npx",
80
+ "args": ["-y", "@leeguoo/zentao-mcp", "--stdio"],
81
+ "env": {
82
+ "ZENTAO_URL": "https://zentao.example.com/zentao",
83
+ "ZENTAO_ACCOUNT": "leo",
84
+ "ZENTAO_PASSWORD": "***"
85
+ }
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ **Tip:** `ZENTAO_URL` should include the ZenTao base path (often `/zentao`).
30
92
 
31
93
  ## Tools
32
94
 
33
- - `zentao_products_list` (page, limit)
34
- - `zentao_bugs_list` (product, page, limit)
35
- - `zentao_bugs_stats` (includeZero, limit)
95
+ The MCP server provides four tools that can be triggered by natural language in Cursor:
96
+
97
+ - **`zentao_products_list`** - List all products
98
+ - **`zentao_bugs_list`** - List bugs for a specific product
99
+ - **`zentao_bugs_stats`** - Get bug statistics across products
100
+ - **`zentao_bugs_mine`** - List my bugs by assignment or creator
36
101
 
37
- Example tool input:
102
+ ### Usage Examples
38
103
 
104
+ After configuring the MCP server in Cursor, you can use natural language to interact with ZenTao:
105
+
106
+ **English:**
107
+ - "Show me all products"
108
+ - "List bugs for product 1"
109
+ - "Show me bugs"
110
+ - "What's the bug statistics?"
111
+ - "Show my bugs"
112
+ - "List bugs assigned to me"
113
+ - "View bugs in product 2"
114
+
115
+ **Chinese (中文):**
116
+ - "看bug" / "查看bug" / "显示bug"
117
+ - "产品1的bug列表"
118
+ - "bug统计"
119
+ - "显示所有产品"
120
+ - "查看产品2的问题"
121
+ - "我的bug"
122
+ - "分配给我的bug"
123
+
124
+ The AI will automatically:
125
+ 1. Use `zentao_products_list` to get product IDs when needed
126
+ 2. Use `zentao_bugs_list` when you ask to see bugs
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
129
+
130
+ ### Tool Parameters
131
+
132
+ **zentao_products_list:**
133
+ ```json
134
+ {
135
+ "page": 1,
136
+ "limit": 1000
137
+ }
138
+ ```
139
+
140
+ **zentao_bugs_list:**
39
141
  ```json
40
142
  {
41
143
  "product": 1,
@@ -44,6 +146,24 @@ Example tool input:
44
146
  }
45
147
  ```
46
148
 
149
+ **zentao_bugs_stats:**
150
+ ```json
151
+ {
152
+ "includeZero": false,
153
+ "limit": 1000
154
+ }
155
+ ```
156
+
157
+ **zentao_bugs_mine:**
158
+ ```json
159
+ {
160
+ "scope": "assigned",
161
+ "includeZero": false,
162
+ "includeDetails": true,
163
+ "maxItems": 50
164
+ }
165
+ ```
166
+
47
167
  ## Local Development
48
168
 
49
169
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leeguoo/zentao-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for ZenTao RESTful APIs",
5
5
  "keywords": [
6
6
  "zentao",
@@ -26,6 +26,9 @@
26
26
  "src",
27
27
  "README.md"
28
28
  ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
29
32
  "scripts": {
30
33
  "start": "node src/index.js",
31
34
  "release": "./scripts/release.sh",
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: {
@@ -218,7 +349,7 @@ const server = new Server(
218
349
  const tools = [
219
350
  {
220
351
  name: "zentao_products_list",
221
- description: "List products (RESTful API).",
352
+ description: "List all products from ZenTao. Use this to get product IDs before querying bugs. Returns product information including ID, name, and bug counts.",
222
353
  inputSchema: {
223
354
  type: "object",
224
355
  properties: {
@@ -230,11 +361,11 @@ const tools = [
230
361
  },
231
362
  {
232
363
  name: "zentao_bugs_list",
233
- description: "List bugs for a product (RESTful API).",
364
+ description: "List bugs (缺陷/问题) for a specific product in ZenTao. Use this when user asks to 'see bugs', 'view bugs', 'show bugs', '看bug', '查看bug', '显示bug', or wants to check issues for a product. Requires product ID which can be obtained from zentao_products_list.",
234
365
  inputSchema: {
235
366
  type: "object",
236
367
  properties: {
237
- product: { type: "integer", description: "Product ID." },
368
+ product: { type: "integer", description: "Product ID (required). Get this from zentao_products_list first." },
238
369
  page: { type: "integer", description: "Page number (default 1)." },
239
370
  limit: { type: "integer", description: "Page size (default 20)." },
240
371
  },
@@ -244,16 +375,40 @@ const tools = [
244
375
  },
245
376
  {
246
377
  name: "zentao_bugs_stats",
247
- description: "Aggregate bug totals across products.",
378
+ description: "Get bug statistics (bug统计) across all products. Shows total bugs, unresolved bugs, closed bugs, and fixed bugs per product. Use when user asks for bug summary, statistics, overview, or 'bug统计'.",
248
379
  inputSchema: {
249
380
  type: "object",
250
381
  properties: {
251
- includeZero: { type: "boolean", description: "Include products with zero bugs." },
382
+ includeZero: { type: "boolean", description: "Include products with zero bugs (default false)." },
252
383
  limit: { type: "integer", description: "Max products to fetch (default 1000)." },
253
384
  },
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,