@dyno181cm.nexsoft/zentao_mcp 1.0.2 → 1.2.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 CHANGED
@@ -6,9 +6,9 @@ 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.
11
+ - `zentao_download_attachment`: Download file attachments from ZenTao (e.g., bug/task images or videos).
12
12
 
13
13
  ## Requirements
14
14
 
@@ -44,11 +44,7 @@ This server communicates via standard input/output (`stdio`), making it compatib
44
44
 
45
45
  ### Integrating with MCP Clients (Claude Desktop, Cursor, etc.)
46
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`):
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`):
52
48
 
53
49
  ```json
54
50
  {
@@ -69,39 +65,17 @@ Add the following to your MCP client configuration (e.g., `claude_desktop_config
69
65
  }
70
66
  ```
71
67
 
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
68
  ### Available Tools
93
69
 
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`**
70
+ - **`zentao_get_task_details`**
102
71
  - **Inputs:**
103
- - `productId` (string | number) - Required. Product ID.
72
+ - `taskId` (string | number) - Required. Task ID.
104
73
 
105
74
  - **`zentao_get_bug_details`**
106
75
  - **Inputs:**
107
76
  - `bugId` (string | number) - Required. Bug ID.
77
+
78
+ - **`zentao_download_attachment`**
79
+ - **Inputs:**
80
+ - `fileId` (string | number) - Required. File ID to download.
81
+ - `extension` (string) - Optional. File extension (e.g., mp4, png).
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.0"
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
@@ -1,28 +1,26 @@
1
1
  import { z } from "zod";
2
+ import * as os from "os";
3
+ import * as path from "path";
2
4
  import { ZentaoClient } from "./zentaoClient.js";
3
5
  const client = new ZentaoClient();
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
+ */
4
16
  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",
17
+ server.registerTool("zentao_get_task_details", {
18
+ description: "Get detailed information of a task",
7
19
  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"),
20
+ taskId: z.union([z.string(), z.number()]).describe("Task ID"),
12
21
  }
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);
22
+ }, async ({ taskId }) => {
23
+ const data = await client.getTaskDetails(taskId);
26
24
  return {
27
25
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
28
26
  };
@@ -38,4 +36,25 @@ export function registerTools(server) {
38
36
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
39
37
  };
40
38
  });
39
+ server.registerTool("zentao_download_attachment", {
40
+ description: "Download a file attachment from ZenTao and save it locally",
41
+ inputSchema: {
42
+ fileId: z.union([z.string(), z.number()]).describe("File ID to download"),
43
+ extension: z.string().optional().describe("Optional file extension (e.g., mp4, png)"),
44
+ }
45
+ }, async ({ fileId, extension }) => {
46
+ const ext = extension ? (extension.startsWith('.') ? extension : `.${extension}`) : '';
47
+ const targetPath = path.join(os.tmpdir(), `zentao_file_${fileId}${ext}`);
48
+ try {
49
+ const savedPath = await client.downloadFile(fileId, targetPath);
50
+ return {
51
+ content: [{ type: "text", text: `File downloaded successfully to: ${savedPath}` }],
52
+ };
53
+ }
54
+ catch (error) {
55
+ return {
56
+ content: [{ type: "text", text: `Failed to download file: ${error.message}` }],
57
+ };
58
+ }
59
+ });
41
60
  }
@@ -1,12 +1,22 @@
1
1
  import axios from 'axios';
2
2
  import * as dotenv from 'dotenv';
3
+ import * as fs from 'fs';
3
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
+ */
4
10
  export class ZentaoClient {
5
11
  client;
6
12
  token = null;
7
13
  account = process.env.ZENTAO_ACCOUNT || '';
8
14
  password = process.env.ZENTAO_PASSWORD || '';
9
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
+ */
10
20
  constructor() {
11
21
  this.client = axios.create({
12
22
  baseURL: this.baseUrl,
@@ -41,6 +51,12 @@ export class ZentaoClient {
41
51
  return Promise.reject(error);
42
52
  });
43
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
+ */
44
60
  async login() {
45
61
  try {
46
62
  if (!this.account || !this.password) {
@@ -62,40 +78,51 @@ export class ZentaoClient {
62
78
  throw error;
63
79
  }
64
80
  }
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`);
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}`);
95
89
  return res.data;
96
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
+ */
97
97
  async getBugDetails(bugId) {
98
98
  const res = await this.client.get(`/bugs/${bugId}`);
99
99
  return res.data;
100
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
+ */
108
+ async downloadFile(fileId, targetPath) {
109
+ const writer = fs.createWriteStream(targetPath);
110
+ const res = await this.client.get(`/files/${fileId}`, {
111
+ responseType: 'stream',
112
+ });
113
+ return new Promise((resolve, reject) => {
114
+ res.data.pipe(writer);
115
+ let error = null;
116
+ writer.on('error', err => {
117
+ error = err;
118
+ writer.close();
119
+ reject(err);
120
+ });
121
+ writer.on('close', () => {
122
+ if (!error) {
123
+ resolve(targetPath);
124
+ }
125
+ });
126
+ });
127
+ }
101
128
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dyno181cm.nexsoft/zentao_mcp",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "Zentao MCP Server",
5
5
  "main": "build/index.js",
6
6
  "bin": {