@dyno181cm.nexsoft/zentao_mcp 1.2.0 → 1.2.2

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 CHANGED
@@ -20,7 +20,7 @@ A Model Context Protocol (MCP) server for integrating with the Zentao API. This
20
20
  1. Clone the repository:
21
21
 
22
22
  ```bash
23
- git clone <repository-url>
23
+ git clone https://github.com/dyno-nexsoft/zentao_mcp.git
24
24
  cd zentao_mcp
25
25
  ```
26
26
 
package/build/index.js CHANGED
@@ -8,7 +8,7 @@ dotenv.config();
8
8
  // Create the MCP server instance
9
9
  const server = new McpServer({
10
10
  name: "Zentao MCP Server",
11
- version: "1.2.0"
11
+ version: "1.2.2"
12
12
  });
13
13
  // Register all Zentao-specific tools onto the server
14
14
  registerTools(server);
package/build/tools.js CHANGED
@@ -13,6 +13,87 @@ const client = new ZentaoClient();
13
13
  *
14
14
  * @param server The MCP Server instance where the tools will be registered.
15
15
  */
16
+ function parseFiles(files) {
17
+ if (!files)
18
+ return [];
19
+ const mapper = (f) => ({
20
+ id: f.id,
21
+ title: f.title || f.name,
22
+ extension: f.extension,
23
+ size: f.size
24
+ });
25
+ if (Array.isArray(files)) {
26
+ return files.map(mapper);
27
+ }
28
+ if (typeof files === 'object') {
29
+ return Object.values(files).map(mapper);
30
+ }
31
+ return [];
32
+ }
33
+ function cleanTask(task) {
34
+ if (!task)
35
+ return null;
36
+ return {
37
+ id: task.id,
38
+ name: task.name,
39
+ status: task.status,
40
+ pri: task.pri,
41
+ desc: task.desc,
42
+ estimate: task.estimate,
43
+ consumed: task.consumed,
44
+ left: task.left,
45
+ progress: task.progress,
46
+ openedBy: task.openedBy ? {
47
+ account: task.openedBy.account,
48
+ realname: task.openedBy.realname
49
+ } : undefined,
50
+ assignedTo: task.assignedTo ? {
51
+ account: task.assignedTo.account,
52
+ realname: task.assignedTo.realname
53
+ } : undefined,
54
+ finishedBy: task.finishedBy ? {
55
+ account: task.finishedBy.account,
56
+ realname: task.finishedBy.realname
57
+ } : undefined,
58
+ closedBy: task.closedBy ? {
59
+ account: task.closedBy.account,
60
+ realname: task.closedBy.realname
61
+ } : undefined,
62
+ closedReason: task.closedReason,
63
+ files: parseFiles(task.files)
64
+ };
65
+ }
66
+ function cleanBug(bug) {
67
+ if (!bug)
68
+ return null;
69
+ return {
70
+ id: bug.id,
71
+ title: bug.title,
72
+ status: bug.status,
73
+ severity: bug.severity,
74
+ pri: bug.pri,
75
+ type: bug.type,
76
+ steps: bug.steps,
77
+ openedBy: bug.openedBy ? {
78
+ account: bug.openedBy.account,
79
+ realname: bug.openedBy.realname
80
+ } : undefined,
81
+ assignedTo: bug.assignedTo ? {
82
+ account: bug.assignedTo.account,
83
+ realname: bug.assignedTo.realname
84
+ } : undefined,
85
+ resolvedBy: bug.resolvedBy ? {
86
+ account: bug.resolvedBy.account,
87
+ realname: bug.resolvedBy.realname
88
+ } : undefined,
89
+ resolution: bug.resolution,
90
+ closedBy: bug.closedBy ? {
91
+ account: bug.closedBy.account,
92
+ realname: bug.closedBy.realname
93
+ } : undefined,
94
+ files: parseFiles(bug.files)
95
+ };
96
+ }
16
97
  export function registerTools(server) {
17
98
  server.registerTool("zentao_get_task_details", {
18
99
  description: "Get detailed information of a task",
@@ -21,8 +102,9 @@ export function registerTools(server) {
21
102
  }
22
103
  }, async ({ taskId }) => {
23
104
  const data = await client.getTaskDetails(taskId);
105
+ const cleaned = cleanTask(data);
24
106
  return {
25
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
107
+ content: [{ type: "text", text: JSON.stringify(cleaned, null, 2) }],
26
108
  };
27
109
  });
28
110
  server.registerTool("zentao_get_bug_details", {
@@ -32,8 +114,9 @@ export function registerTools(server) {
32
114
  }
33
115
  }, async ({ bugId }) => {
34
116
  const data = await client.getBugDetails(bugId);
117
+ const cleaned = cleanBug(data);
35
118
  return {
36
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
119
+ content: [{ type: "text", text: JSON.stringify(cleaned, null, 2) }],
37
120
  };
38
121
  });
39
122
  server.registerTool("zentao_download_attachment", {
@@ -13,12 +13,38 @@ export class ZentaoClient {
13
13
  account = process.env.ZENTAO_ACCOUNT || '';
14
14
  password = process.env.ZENTAO_PASSWORD || '';
15
15
  baseUrl = process.env.ZENTAO_BASE_URL || '';
16
+ getCache = new Map();
17
+ cacheTtlMs = 2 * 60 * 1000; // 2 minutes TTL
18
+ /**
19
+ * Clears the in-memory cache for GET requests. Helpful for testing.
20
+ */
21
+ clearCache() {
22
+ this.getCache.clear();
23
+ }
24
+ /**
25
+ * Helper method to perform GET requests with caching.
26
+ *
27
+ * @param url The API endpoint path.
28
+ * @returns The response data.
29
+ */
30
+ async get(url) {
31
+ const cached = this.getCache.get(url);
32
+ const now = Date.now();
33
+ if (cached && (now - cached.timestamp < this.cacheTtlMs)) {
34
+ return cached.data;
35
+ }
36
+ const res = await this.client.get(url);
37
+ this.getCache.set(url, { data: res.data, timestamp: now });
38
+ return res.data;
39
+ }
16
40
  /**
17
41
  * Initializes the Axios client with base configurations and registers interceptors
18
42
  * to automatically inject token headers and handle token expiration (401 errors).
43
+ *
44
+ * @param customClient Optional custom AxiosInstance for dependency injection/testing.
19
45
  */
20
- constructor() {
21
- this.client = axios.create({
46
+ constructor(customClient) {
47
+ this.client = customClient || axios.create({
22
48
  baseURL: this.baseUrl,
23
49
  headers: {
24
50
  'Content-Type': 'application/json',
@@ -80,23 +106,23 @@ export class ZentaoClient {
80
106
  }
81
107
  /**
82
108
  * Retrieves details of a specific task.
109
+ * Checks the cache first, otherwise fetches from API.
83
110
  *
84
111
  * @param taskId The ID of the task.
85
112
  * @returns Resolves with the task details.
86
113
  */
87
114
  async getTaskDetails(taskId) {
88
- const res = await this.client.get(`/tasks/${taskId}`);
89
- return res.data;
115
+ return this.get(`/tasks/${taskId}`);
90
116
  }
91
117
  /**
92
118
  * Retrieves details of a specific bug.
119
+ * Checks the cache first, otherwise fetches from API.
93
120
  *
94
121
  * @param bugId The ID of the bug.
95
122
  * @returns Resolves with the bug details.
96
123
  */
97
124
  async getBugDetails(bugId) {
98
- const res = await this.client.get(`/bugs/${bugId}`);
99
- return res.data;
125
+ return this.get(`/bugs/${bugId}`);
100
126
  }
101
127
  /**
102
128
  * Downloads a file from Zentao API and pipes it to a local file path.
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "@dyno181cm.nexsoft/zentao_mcp",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Zentao MCP Server",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/dyno-nexsoft/zentao_mcp.git"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/dyno-nexsoft/zentao_mcp/issues"
11
+ },
12
+ "homepage": "https://github.com/dyno-nexsoft/zentao_mcp#readme",
5
13
  "main": "build/index.js",
6
14
  "bin": {
7
15
  "zentao_mcp": "build/index.js"
@@ -14,6 +22,7 @@
14
22
  "build": "tsc",
15
23
  "start": "node build/index.js",
16
24
  "dev": "tsc --watch",
25
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
17
26
  "test:api": "tsx tests/testApi.ts"
18
27
  },
19
28
  "dependencies": {