@dyno181cm.nexsoft/zentao_mcp 1.0.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.
- package/README.md +107 -0
- package/build/index.js +20 -0
- package/build/testApi.js +31 -0
- package/build/tools.js +41 -0
- package/build/zentaoClient.js +101 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Zentao MCP Server
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server for integrating with the Zentao API. This server provides tools to access project management data from Zentao directly through MCP-compatible interfaces.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Authentication**: Automatically handles login and token management for Zentao.
|
|
8
|
+
- **MCP Tools**:
|
|
9
|
+
- `zentao_get_execution_tasks`: Retrieve tasks in an execution (sprint), with optional filtering by module ID.
|
|
10
|
+
- `zentao_get_product_bugs`: Fetch a list of bugs associated with a specific product.
|
|
11
|
+
- `zentao_get_bug_details`: Get detailed information about a specific bug.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Node.js (v18 or higher recommended)
|
|
16
|
+
- A Zentao instance accessible via API
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
1. Clone the repository:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone <repository-url>
|
|
24
|
+
cd zentao_mcp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
2. Install dependencies:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
3. Configure environment variables:
|
|
34
|
+
Create a `.env` file in the root directory and add the following configurations:
|
|
35
|
+
```env
|
|
36
|
+
ZENTAO_BASE_URL=https://your-zentao-url.com/api.php/v1
|
|
37
|
+
ZENTAO_ACCOUNT=your_username
|
|
38
|
+
ZENTAO_PASSWORD=your_password
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
This server communicates via standard input/output (`stdio`), making it compatible with any MCP client that supports stdio transport.
|
|
44
|
+
|
|
45
|
+
### Integrating with MCP Clients (Claude Desktop, Cursor, etc.)
|
|
46
|
+
|
|
47
|
+
Once published to npm or GitHub, users can integrate this server into their MCP clients easily.
|
|
48
|
+
|
|
49
|
+
#### Option 1: Running via `npx` (Recommended if published to NPM)
|
|
50
|
+
|
|
51
|
+
Add the following to your MCP client configuration (e.g., `claude_desktop_config.json`):
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"zentao": {
|
|
57
|
+
"command": "npx",
|
|
58
|
+
"args": [
|
|
59
|
+
"-y",
|
|
60
|
+
"@dyno181cm.nexsoft/zentao_mcp"
|
|
61
|
+
],
|
|
62
|
+
"env": {
|
|
63
|
+
"ZENTAO_BASE_URL": "https://your-zentao-url.com/api.php/v1",
|
|
64
|
+
"ZENTAO_ACCOUNT": "your_username",
|
|
65
|
+
"ZENTAO_PASSWORD": "your_password"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### Option 2: Running from a Local Clone
|
|
73
|
+
|
|
74
|
+
If a user clones the repository locally, they can configure their client to run it directly:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"mcpServers": {
|
|
79
|
+
"zentao": {
|
|
80
|
+
"command": "node",
|
|
81
|
+
"args": ["/absolute/path/to/zentao_mcp/build/index.js"],
|
|
82
|
+
"env": {
|
|
83
|
+
"ZENTAO_BASE_URL": "https://your-zentao-url.com/api.php/v1",
|
|
84
|
+
"ZENTAO_ACCOUNT": "your_username",
|
|
85
|
+
"ZENTAO_PASSWORD": "your_password"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Available Tools
|
|
93
|
+
|
|
94
|
+
- **`zentao_get_execution_tasks`**
|
|
95
|
+
- **Inputs:**
|
|
96
|
+
- `executionId` (string | number) - Required. Execution ID.
|
|
97
|
+
- `page` (number) - Optional. Current page (default 1).
|
|
98
|
+
- `limit` (number) - Optional. Items per page (default 500).
|
|
99
|
+
- `moduleId` (string | number) - Optional. Filter by module ID.
|
|
100
|
+
|
|
101
|
+
- **`zentao_get_product_bugs`**
|
|
102
|
+
- **Inputs:**
|
|
103
|
+
- `productId` (string | number) - Required. Product ID.
|
|
104
|
+
|
|
105
|
+
- **`zentao_get_bug_details`**
|
|
106
|
+
- **Inputs:**
|
|
107
|
+
- `bugId` (string | number) - Required. Bug ID.
|
package/build/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { registerTools } from "./tools.js";
|
|
5
|
+
import * as dotenv from "dotenv";
|
|
6
|
+
dotenv.config();
|
|
7
|
+
const server = new McpServer({
|
|
8
|
+
name: "Zentao MCP Server",
|
|
9
|
+
version: "1.0.0"
|
|
10
|
+
});
|
|
11
|
+
registerTools(server);
|
|
12
|
+
async function main() {
|
|
13
|
+
const transport = new StdioServerTransport();
|
|
14
|
+
await server.connect(transport);
|
|
15
|
+
console.error("Zentao MCP Server started via stdio.");
|
|
16
|
+
}
|
|
17
|
+
main().catch(error => {
|
|
18
|
+
console.error("Fatal error starting server:", error);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
package/build/testApi.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ZentaoClient } from './zentaoClient.js';
|
|
2
|
+
import * as dotenv from 'dotenv';
|
|
3
|
+
dotenv.config();
|
|
4
|
+
async function main() {
|
|
5
|
+
try {
|
|
6
|
+
const client = new ZentaoClient();
|
|
7
|
+
console.log('Logging in to Zentao...');
|
|
8
|
+
await client.login();
|
|
9
|
+
console.log('Successfully logged in.');
|
|
10
|
+
console.log('\n--- Fetching execution tasks (Execution ID: 2) ---');
|
|
11
|
+
try {
|
|
12
|
+
const tasks = await client.getExecutionTasks(2, 1, 5);
|
|
13
|
+
console.log('Tasks result:', JSON.stringify(tasks, null, 2));
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
console.error('Error fetching tasks:', e.response?.data || e.message);
|
|
17
|
+
}
|
|
18
|
+
console.log('\n--- Fetching bug details (Bug ID: 2) ---');
|
|
19
|
+
try {
|
|
20
|
+
const bug = await client.getBugDetails(2);
|
|
21
|
+
console.log('Bug details:', JSON.stringify(bug, null, 2));
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
console.error('Error fetching bug:', e.response?.data || e.message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error('Fatal Error:', error.message || error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
main();
|
package/build/tools.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ZentaoClient } from "./zentaoClient.js";
|
|
3
|
+
const client = new ZentaoClient();
|
|
4
|
+
export function registerTools(server) {
|
|
5
|
+
server.registerTool("zentao_get_execution_tasks", {
|
|
6
|
+
description: "Get a list of tasks in an execution (sprint), optionally filtered by moduleID",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
executionId: z.union([z.string(), z.number()]).describe("Execution ID"),
|
|
9
|
+
page: z.number().optional().describe("Current page (default 1)"),
|
|
10
|
+
limit: z.number().optional().describe("Number of items per page (default 500)"),
|
|
11
|
+
moduleId: z.union([z.string(), z.number()]).optional().describe("Filter by module ID"),
|
|
12
|
+
}
|
|
13
|
+
}, async ({ executionId, page = 1, limit = 500, moduleId }) => {
|
|
14
|
+
const data = await client.getExecutionTasks(executionId, page, limit, moduleId);
|
|
15
|
+
return {
|
|
16
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
server.registerTool("zentao_get_product_bugs", {
|
|
20
|
+
description: "Get a list of bugs for a product",
|
|
21
|
+
inputSchema: {
|
|
22
|
+
productId: z.union([z.string(), z.number()]).describe("Product ID"),
|
|
23
|
+
}
|
|
24
|
+
}, async ({ productId }) => {
|
|
25
|
+
const data = await client.getProductBugs(productId);
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
server.registerTool("zentao_get_bug_details", {
|
|
31
|
+
description: "Get detailed information of a bug",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
bugId: z.union([z.string(), z.number()]).describe("Bug ID"),
|
|
34
|
+
}
|
|
35
|
+
}, async ({ bugId }) => {
|
|
36
|
+
const data = await client.getBugDetails(bugId);
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import * as dotenv from 'dotenv';
|
|
3
|
+
dotenv.config();
|
|
4
|
+
export class ZentaoClient {
|
|
5
|
+
client;
|
|
6
|
+
token = null;
|
|
7
|
+
account = process.env.ZENTAO_ACCOUNT || '';
|
|
8
|
+
password = process.env.ZENTAO_PASSWORD || '';
|
|
9
|
+
baseUrl = process.env.ZENTAO_BASE_URL || '';
|
|
10
|
+
constructor() {
|
|
11
|
+
this.client = axios.create({
|
|
12
|
+
baseURL: this.baseUrl,
|
|
13
|
+
headers: {
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
// Add a request interceptor to automatically add the token and handle retries
|
|
18
|
+
this.client.interceptors.request.use(async (config) => {
|
|
19
|
+
// Don't add token if we are requesting a token
|
|
20
|
+
if (config.url === '/tokens')
|
|
21
|
+
return config;
|
|
22
|
+
if (!this.token) {
|
|
23
|
+
await this.login();
|
|
24
|
+
}
|
|
25
|
+
if (this.token) {
|
|
26
|
+
config.headers['Token'] = this.token;
|
|
27
|
+
}
|
|
28
|
+
return config;
|
|
29
|
+
});
|
|
30
|
+
// Add response interceptor to handle 401 Unauthorized (token expiry)
|
|
31
|
+
this.client.interceptors.response.use((response) => response, async (error) => {
|
|
32
|
+
const originalRequest = error.config;
|
|
33
|
+
if (error.response?.status === 401 && !originalRequest._retry && originalRequest.url !== '/tokens') {
|
|
34
|
+
originalRequest._retry = true;
|
|
35
|
+
await this.login();
|
|
36
|
+
if (this.token) {
|
|
37
|
+
originalRequest.headers['Token'] = this.token;
|
|
38
|
+
return this.client(originalRequest);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return Promise.reject(error);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async login() {
|
|
45
|
+
try {
|
|
46
|
+
if (!this.account || !this.password) {
|
|
47
|
+
throw new Error('ZENTAO_ACCOUNT or ZENTAO_PASSWORD is not set in environment variables');
|
|
48
|
+
}
|
|
49
|
+
const response = await this.client.post('/tokens', {
|
|
50
|
+
account: this.account,
|
|
51
|
+
password: this.password,
|
|
52
|
+
});
|
|
53
|
+
if (response.data && response.data.token) {
|
|
54
|
+
this.token = response.data.token;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
throw new Error('Login failed: Token not found in response');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.error('Failed to login to Zentao:', error.message);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async getUsers(page = 1, limit = 100) {
|
|
66
|
+
const res = await this.client.get(`/users?page=${page}&limit=${limit}`);
|
|
67
|
+
return res.data;
|
|
68
|
+
}
|
|
69
|
+
async getProjects() {
|
|
70
|
+
const res = await this.client.get('/projects');
|
|
71
|
+
return res.data;
|
|
72
|
+
}
|
|
73
|
+
async getProjectExecutions(projectId) {
|
|
74
|
+
const res = await this.client.get(`/projects/${projectId}/executions`);
|
|
75
|
+
return res.data;
|
|
76
|
+
}
|
|
77
|
+
async getExecutionDetails(executionId) {
|
|
78
|
+
const res = await this.client.get(`/executions/${executionId}`);
|
|
79
|
+
return res.data;
|
|
80
|
+
}
|
|
81
|
+
async getExecutionTasks(executionId, page = 1, limit = 500, moduleId) {
|
|
82
|
+
let url = `/executions/${executionId}/tasks?page=${page}&limit=${limit}`;
|
|
83
|
+
if (moduleId) {
|
|
84
|
+
url += `&moduleID=${moduleId}`;
|
|
85
|
+
}
|
|
86
|
+
const res = await this.client.get(url);
|
|
87
|
+
return res.data;
|
|
88
|
+
}
|
|
89
|
+
async getTaskModules(executionId) {
|
|
90
|
+
const res = await this.client.get(`/modules?type=task&id=${executionId}`);
|
|
91
|
+
return res.data;
|
|
92
|
+
}
|
|
93
|
+
async getProductBugs(productId) {
|
|
94
|
+
const res = await this.client.get(`/products/${productId}/bugs`);
|
|
95
|
+
return res.data;
|
|
96
|
+
}
|
|
97
|
+
async getBugDetails(bugId) {
|
|
98
|
+
const res = await this.client.get(`/bugs/${bugId}`);
|
|
99
|
+
return res.data;
|
|
100
|
+
}
|
|
101
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dyno181cm.nexsoft/zentao_mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zentao MCP Server",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"zentao_mcp": "build/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"files": [
|
|
11
|
+
"build"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"start": "node build/index.js",
|
|
16
|
+
"dev": "tsc --watch",
|
|
17
|
+
"test:api": "tsx tests/testApi.ts"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.1.0",
|
|
21
|
+
"axios": "^1.7.9",
|
|
22
|
+
"dotenv": "^16.4.7"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/jest": "^30.0.0",
|
|
26
|
+
"@types/node": "^22.10.2",
|
|
27
|
+
"jest": "^30.4.2",
|
|
28
|
+
"ts-jest": "^29.4.11",
|
|
29
|
+
"tsx": "^4.23.0",
|
|
30
|
+
"typescript": "^5.7.2"
|
|
31
|
+
}
|
|
32
|
+
}
|