@dyno181cm.nexsoft/zentao_mcp 1.2.1 → 1.2.3

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.1"
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
@@ -1,8 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import * as os from "os";
3
3
  import * as path from "path";
4
+ import fs from "fs";
4
5
  import { ZentaoClient } from "./zentaoClient.js";
5
6
  const client = new ZentaoClient();
7
+ const activeDownloads = new Map();
6
8
  /**
7
9
  * Registers Zentao-specific tools to the Model Context Protocol (MCP) server.
8
10
  *
@@ -129,10 +131,31 @@ export function registerTools(server) {
129
131
  const ext = extension ? (extension.startsWith('.') ? extension : `.${extension}`) : '';
130
132
  const targetPath = path.join(os.tmpdir(), `zentao_file_${fileId}${ext}`);
131
133
  try {
132
- const savedPath = await client.downloadFile(fileId, targetPath);
133
- return {
134
- content: [{ type: "text", text: `File downloaded successfully to: ${savedPath}` }],
135
- };
134
+ // If it already exists on disk and is not currently being downloaded
135
+ if (fs.existsSync(targetPath) && !activeDownloads.has(targetPath)) {
136
+ return {
137
+ content: [{ type: "text", text: `File already exists locally at: ${targetPath}` }],
138
+ };
139
+ }
140
+ // If it is currently being downloaded, await the active promise
141
+ if (activeDownloads.has(targetPath)) {
142
+ await activeDownloads.get(targetPath);
143
+ return {
144
+ content: [{ type: "text", text: `File downloaded successfully to: ${targetPath}` }],
145
+ };
146
+ }
147
+ // Start new download and track it
148
+ const downloadPromise = client.downloadFile(fileId, targetPath);
149
+ activeDownloads.set(targetPath, downloadPromise);
150
+ try {
151
+ const savedPath = await downloadPromise;
152
+ return {
153
+ content: [{ type: "text", text: `File downloaded successfully to: ${savedPath}` }],
154
+ };
155
+ }
156
+ finally {
157
+ activeDownloads.delete(targetPath);
158
+ }
136
159
  }
137
160
  catch (error) {
138
161
  return {
@@ -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',
@@ -58,45 +84,39 @@ export class ZentaoClient {
58
84
  * @throws {Error} If account/password env vars are missing or if the API request fails.
59
85
  */
60
86
  async login() {
61
- try {
62
- if (!this.account || !this.password) {
63
- throw new Error('ZENTAO_ACCOUNT or ZENTAO_PASSWORD is not set in environment variables');
64
- }
65
- const response = await this.client.post('/tokens', {
66
- account: this.account,
67
- password: this.password,
68
- });
69
- if (response.data && response.data.token) {
70
- this.token = response.data.token;
71
- }
72
- else {
73
- throw new Error('Login failed: Token not found in response');
74
- }
87
+ if (!this.account || !this.password) {
88
+ throw new Error('ZENTAO_ACCOUNT or ZENTAO_PASSWORD is not set in environment variables');
75
89
  }
76
- catch (error) {
77
- console.error('Failed to login to Zentao:', error.message);
78
- throw error;
90
+ const response = await this.client.post('/tokens', {
91
+ account: this.account,
92
+ password: this.password,
93
+ });
94
+ if (response.data && response.data.token) {
95
+ this.token = response.data.token;
96
+ }
97
+ else {
98
+ throw new Error('Login failed: Token not found in response');
79
99
  }
80
100
  }
81
101
  /**
82
102
  * Retrieves details of a specific task.
103
+ * Checks the cache first, otherwise fetches from API.
83
104
  *
84
105
  * @param taskId The ID of the task.
85
106
  * @returns Resolves with the task details.
86
107
  */
87
108
  async getTaskDetails(taskId) {
88
- const res = await this.client.get(`/tasks/${taskId}`);
89
- return res.data;
109
+ return this.get(`/tasks/${taskId}`);
90
110
  }
91
111
  /**
92
112
  * Retrieves details of a specific bug.
113
+ * Checks the cache first, otherwise fetches from API.
93
114
  *
94
115
  * @param bugId The ID of the bug.
95
116
  * @returns Resolves with the bug details.
96
117
  */
97
118
  async getBugDetails(bugId) {
98
- const res = await this.client.get(`/bugs/${bugId}`);
99
- return res.data;
119
+ return this.get(`/bugs/${bugId}`);
100
120
  }
101
121
  /**
102
122
  * 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.1",
3
+ "version": "1.2.3",
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": {