@dyno181cm.nexsoft/zentao_mcp 1.1.0 → 1.2.1
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 +4 -36
- package/build/index.js +10 -1
- package/build/tools.js +100 -21
- package/build/zentaoClient.js +36 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,8 +6,7 @@ A Model Context Protocol (MCP) server for integrating with the Zentao API. This
|
|
|
6
6
|
|
|
7
7
|
- **Authentication**: Automatically handles login and token management for Zentao.
|
|
8
8
|
- **MCP Tools**:
|
|
9
|
-
- `
|
|
10
|
-
- `zentao_get_product_bugs`: Fetch a list of bugs associated with a specific product.
|
|
9
|
+
- `zentao_get_task_details`: Get detailed information about a specific task by ID.
|
|
11
10
|
- `zentao_get_bug_details`: Get detailed information about a specific bug.
|
|
12
11
|
- `zentao_download_attachment`: Download file attachments from ZenTao (e.g., bug/task images or videos).
|
|
13
12
|
|
|
@@ -45,11 +44,7 @@ This server communicates via standard input/output (`stdio`), making it compatib
|
|
|
45
44
|
|
|
46
45
|
### Integrating with MCP Clients (Claude Desktop, Cursor, etc.)
|
|
47
46
|
|
|
48
|
-
Once published to npm or GitHub, users can integrate this server into their MCP clients easily.
|
|
49
|
-
|
|
50
|
-
#### Option 1: Running via `npx` (Recommended if published to NPM)
|
|
51
|
-
|
|
52
|
-
Add the following to your MCP client configuration (e.g., `claude_desktop_config.json`):
|
|
47
|
+
Once published to npm or GitHub, users can integrate this server into their MCP clients easily. Add the following to your MCP client configuration (e.g., `claude_desktop_config.json`):
|
|
53
48
|
|
|
54
49
|
```json
|
|
55
50
|
{
|
|
@@ -70,38 +65,11 @@ Add the following to your MCP client configuration (e.g., `claude_desktop_config
|
|
|
70
65
|
}
|
|
71
66
|
```
|
|
72
67
|
|
|
73
|
-
#### Option 2: Running from a Local Clone
|
|
74
|
-
|
|
75
|
-
If a user clones the repository locally, they can configure their client to run it directly:
|
|
76
|
-
|
|
77
|
-
```json
|
|
78
|
-
{
|
|
79
|
-
"mcpServers": {
|
|
80
|
-
"zentao": {
|
|
81
|
-
"command": "node",
|
|
82
|
-
"args": ["/absolute/path/to/zentao_mcp/build/index.js"],
|
|
83
|
-
"env": {
|
|
84
|
-
"ZENTAO_BASE_URL": "https://your-zentao-url.com/api.php/v1",
|
|
85
|
-
"ZENTAO_ACCOUNT": "your_username",
|
|
86
|
-
"ZENTAO_PASSWORD": "your_password"
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
68
|
### Available Tools
|
|
94
69
|
|
|
95
|
-
- **`
|
|
96
|
-
- **Inputs:**
|
|
97
|
-
- `executionId` (string | number) - Required. Execution ID.
|
|
98
|
-
- `page` (number) - Optional. Current page (default 1).
|
|
99
|
-
- `limit` (number) - Optional. Items per page (default 500).
|
|
100
|
-
- `moduleId` (string | number) - Optional. Filter by module ID.
|
|
101
|
-
|
|
102
|
-
- **`zentao_get_product_bugs`**
|
|
70
|
+
- **`zentao_get_task_details`**
|
|
103
71
|
- **Inputs:**
|
|
104
|
-
- `
|
|
72
|
+
- `taskId` (string | number) - Required. Task ID.
|
|
105
73
|
|
|
106
74
|
- **`zentao_get_bug_details`**
|
|
107
75
|
- **Inputs:**
|
package/build/index.js
CHANGED
|
@@ -3,17 +3,26 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { registerTools } from "./tools.js";
|
|
5
5
|
import * as dotenv from "dotenv";
|
|
6
|
+
// Initialize environment variables from .env file
|
|
6
7
|
dotenv.config();
|
|
8
|
+
// Create the MCP server instance
|
|
7
9
|
const server = new McpServer({
|
|
8
10
|
name: "Zentao MCP Server",
|
|
9
|
-
version: "1.
|
|
11
|
+
version: "1.2.1"
|
|
10
12
|
});
|
|
13
|
+
// Register all Zentao-specific tools onto the server
|
|
11
14
|
registerTools(server);
|
|
15
|
+
/**
|
|
16
|
+
* Main entrypoint of the Zentao MCP Server.
|
|
17
|
+
* Connects the server to a standard input/output (stdio) transport
|
|
18
|
+
* so that MCP clients can communicate with it.
|
|
19
|
+
*/
|
|
12
20
|
async function main() {
|
|
13
21
|
const transport = new StdioServerTransport();
|
|
14
22
|
await server.connect(transport);
|
|
15
23
|
console.error("Zentao MCP Server started via stdio.");
|
|
16
24
|
}
|
|
25
|
+
// Start the server and handle any initialization errors
|
|
17
26
|
main().catch(error => {
|
|
18
27
|
console.error("Fatal error starting server:", error);
|
|
19
28
|
process.exit(1);
|
package/build/tools.js
CHANGED
|
@@ -3,30 +3,108 @@ import * as os from "os";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { ZentaoClient } from "./zentaoClient.js";
|
|
5
5
|
const client = new ZentaoClient();
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Registers Zentao-specific tools to the Model Context Protocol (MCP) server.
|
|
8
|
+
*
|
|
9
|
+
* Available Tools:
|
|
10
|
+
* - `zentao_get_task_details`: Fetch full details of a specific task by ID.
|
|
11
|
+
* - `zentao_get_bug_details`: Fetch full details of a specific bug by ID.
|
|
12
|
+
* - `zentao_download_attachment`: Download attachments (e.g. bug reproduction videos/images) and save locally.
|
|
13
|
+
*
|
|
14
|
+
* @param server The MCP Server instance where the tools will be registered.
|
|
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
|
|
20
24
|
});
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
}
|
|
97
|
+
export function registerTools(server) {
|
|
98
|
+
server.registerTool("zentao_get_task_details", {
|
|
99
|
+
description: "Get detailed information of a task",
|
|
23
100
|
inputSchema: {
|
|
24
|
-
|
|
101
|
+
taskId: z.union([z.string(), z.number()]).describe("Task ID"),
|
|
25
102
|
}
|
|
26
|
-
}, async ({
|
|
27
|
-
const data = await client.
|
|
103
|
+
}, async ({ taskId }) => {
|
|
104
|
+
const data = await client.getTaskDetails(taskId);
|
|
105
|
+
const cleaned = cleanTask(data);
|
|
28
106
|
return {
|
|
29
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
107
|
+
content: [{ type: "text", text: JSON.stringify(cleaned, null, 2) }],
|
|
30
108
|
};
|
|
31
109
|
});
|
|
32
110
|
server.registerTool("zentao_get_bug_details", {
|
|
@@ -36,8 +114,9 @@ export function registerTools(server) {
|
|
|
36
114
|
}
|
|
37
115
|
}, async ({ bugId }) => {
|
|
38
116
|
const data = await client.getBugDetails(bugId);
|
|
117
|
+
const cleaned = cleanBug(data);
|
|
39
118
|
return {
|
|
40
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
119
|
+
content: [{ type: "text", text: JSON.stringify(cleaned, null, 2) }],
|
|
41
120
|
};
|
|
42
121
|
});
|
|
43
122
|
server.registerTool("zentao_download_attachment", {
|
package/build/zentaoClient.js
CHANGED
|
@@ -2,12 +2,21 @@ import axios from 'axios';
|
|
|
2
2
|
import * as dotenv from 'dotenv';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
dotenv.config();
|
|
5
|
+
/**
|
|
6
|
+
* Client for communicating with the Zentao API.
|
|
7
|
+
* Handles authentication, automatic token injection, session expiration retries,
|
|
8
|
+
* and standard API requests for projects, executions, tasks, bugs, and attachments.
|
|
9
|
+
*/
|
|
5
10
|
export class ZentaoClient {
|
|
6
11
|
client;
|
|
7
12
|
token = null;
|
|
8
13
|
account = process.env.ZENTAO_ACCOUNT || '';
|
|
9
14
|
password = process.env.ZENTAO_PASSWORD || '';
|
|
10
15
|
baseUrl = process.env.ZENTAO_BASE_URL || '';
|
|
16
|
+
/**
|
|
17
|
+
* Initializes the Axios client with base configurations and registers interceptors
|
|
18
|
+
* to automatically inject token headers and handle token expiration (401 errors).
|
|
19
|
+
*/
|
|
11
20
|
constructor() {
|
|
12
21
|
this.client = axios.create({
|
|
13
22
|
baseURL: this.baseUrl,
|
|
@@ -42,6 +51,12 @@ export class ZentaoClient {
|
|
|
42
51
|
return Promise.reject(error);
|
|
43
52
|
});
|
|
44
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Performs authentication request to Zentao API to obtain a token.
|
|
56
|
+
* Updates the internal token state upon successful login.
|
|
57
|
+
*
|
|
58
|
+
* @throws {Error} If account/password env vars are missing or if the API request fails.
|
|
59
|
+
*/
|
|
45
60
|
async login() {
|
|
46
61
|
try {
|
|
47
62
|
if (!this.account || !this.password) {
|
|
@@ -63,42 +78,33 @@ export class ZentaoClient {
|
|
|
63
78
|
throw error;
|
|
64
79
|
}
|
|
65
80
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
async getProjectExecutions(projectId) {
|
|
75
|
-
const res = await this.client.get(`/projects/${projectId}/executions`);
|
|
76
|
-
return res.data;
|
|
77
|
-
}
|
|
78
|
-
async getExecutionDetails(executionId) {
|
|
79
|
-
const res = await this.client.get(`/executions/${executionId}`);
|
|
80
|
-
return res.data;
|
|
81
|
-
}
|
|
82
|
-
async getExecutionTasks(executionId, page = 1, limit = 500, moduleId) {
|
|
83
|
-
let url = `/executions/${executionId}/tasks?page=${page}&limit=${limit}`;
|
|
84
|
-
if (moduleId) {
|
|
85
|
-
url += `&moduleID=${moduleId}`;
|
|
86
|
-
}
|
|
87
|
-
const res = await this.client.get(url);
|
|
88
|
-
return res.data;
|
|
89
|
-
}
|
|
90
|
-
async getTaskModules(executionId) {
|
|
91
|
-
const res = await this.client.get(`/modules?type=task&id=${executionId}`);
|
|
92
|
-
return res.data;
|
|
93
|
-
}
|
|
94
|
-
async getProductBugs(productId) {
|
|
95
|
-
const res = await this.client.get(`/products/${productId}/bugs`);
|
|
81
|
+
/**
|
|
82
|
+
* Retrieves details of a specific task.
|
|
83
|
+
*
|
|
84
|
+
* @param taskId The ID of the task.
|
|
85
|
+
* @returns Resolves with the task details.
|
|
86
|
+
*/
|
|
87
|
+
async getTaskDetails(taskId) {
|
|
88
|
+
const res = await this.client.get(`/tasks/${taskId}`);
|
|
96
89
|
return res.data;
|
|
97
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Retrieves details of a specific bug.
|
|
93
|
+
*
|
|
94
|
+
* @param bugId The ID of the bug.
|
|
95
|
+
* @returns Resolves with the bug details.
|
|
96
|
+
*/
|
|
98
97
|
async getBugDetails(bugId) {
|
|
99
98
|
const res = await this.client.get(`/bugs/${bugId}`);
|
|
100
99
|
return res.data;
|
|
101
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Downloads a file from Zentao API and pipes it to a local file path.
|
|
103
|
+
*
|
|
104
|
+
* @param fileId The ID of the file to download.
|
|
105
|
+
* @param targetPath The local file path where the file should be saved.
|
|
106
|
+
* @returns A promise that resolves to the targetPath upon success, or rejects with an error.
|
|
107
|
+
*/
|
|
102
108
|
async downloadFile(fileId, targetPath) {
|
|
103
109
|
const writer = fs.createWriteStream(targetPath);
|
|
104
110
|
const res = await this.client.get(`/files/${fileId}`, {
|