@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 +1 -1
- package/build/index.js +1 -1
- package/build/tools.js +27 -4
- package/build/zentaoClient.js +43 -23
- package/package.json +10 -1
package/README.md
CHANGED
package/build/index.js
CHANGED
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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 {
|
package/build/zentaoClient.js
CHANGED
|
@@ -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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|