@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 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
- - `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.
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
- - **`zentao_get_execution_tasks`**
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
- - `productId` (string | number) - Required. Product ID.
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.0.0"
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
- export function registerTools(server) {
7
- server.registerTool("zentao_get_execution_tasks", {
8
- description: "Get a list of tasks in an execution (sprint), optionally filtered by moduleID",
9
- inputSchema: {
10
- executionId: z.union([z.string(), z.number()]).describe("Execution ID"),
11
- page: z.number().optional().describe("Current page (default 1)"),
12
- limit: z.number().optional().describe("Number of items per page (default 500)"),
13
- moduleId: z.union([z.string(), z.number()]).optional().describe("Filter by module ID"),
14
- }
15
- }, async ({ executionId, page = 1, limit = 500, moduleId }) => {
16
- const data = await client.getExecutionTasks(executionId, page, limit, moduleId);
17
- return {
18
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
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
- server.registerTool("zentao_get_product_bugs", {
22
- description: "Get a list of bugs for a product",
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
- productId: z.union([z.string(), z.number()]).describe("Product ID"),
101
+ taskId: z.union([z.string(), z.number()]).describe("Task ID"),
25
102
  }
26
- }, async ({ productId }) => {
27
- const data = await client.getProductBugs(productId);
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(data, null, 2) }],
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(data, null, 2) }],
119
+ content: [{ type: "text", text: JSON.stringify(cleaned, null, 2) }],
41
120
  };
42
121
  });
43
122
  server.registerTool("zentao_download_attachment", {
@@ -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
- async getUsers(page = 1, limit = 100) {
67
- const res = await this.client.get(`/users?page=${page}&limit=${limit}`);
68
- return res.data;
69
- }
70
- async getProjects() {
71
- const res = await this.client.get('/projects');
72
- return res.data;
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}`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dyno181cm.nexsoft/zentao_mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Zentao MCP Server",
5
5
  "main": "build/index.js",
6
6
  "bin": {