@dyno181cm.nexsoft/zentao_mcp 1.5.0 → 1.7.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
@@ -7,6 +7,7 @@
7
7
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square&logo=node.js)](https://nodejs.org)
8
8
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6?style=flat-square&logo=typescript)](https://www.typescriptlang.org)
9
9
  [![MCP](https://img.shields.io/badge/MCP-compatible-8A2BE2?style=flat-square)](https://modelcontextprotocol.io)
10
+ [![Tests](https://img.shields.io/badge/tests-13%20passed-brightgreen?style=flat-square&logo=jest)](https://github.com/dyno-nexsoft/zentao_mcp/tree/master/tests)
10
11
 
11
12
  A **Model Context Protocol (MCP)** server for integrating AI assistants (Claude, Cursor, etc.) with the [ZenTao](https://www.zentao.net) project management API.
12
13
  Fetch task details, bug reports, and attachments — all directly inside your AI chat.
@@ -17,20 +18,22 @@ Fetch task details, bug reports, and attachments — all directly inside your AI
17
18
 
18
19
  | Tool | Description |
19
20
  |---|---|
20
- | `zentao_get_task_details` | Fetch full details of a task by ID (status, assignee, description, history & comments, attachments) |
21
- | `zentao_get_bug_details` | Fetch full details of a bug by ID (severity, repro steps, history & comments, inline images, attachments) |
22
- | `zentao_download_attachment` | Download any ZenTao file attachment to local disk |
21
+ | `zentao_get_details` | Fetch full details of a task or bug by ID |
23
22
  | `zentao_get_comments` | Fetch only the history and comments timeline of a task or bug |
23
+ | `zentao_add_comment` | Add a comment/remark to a task or bug |
24
24
  | `zentao_update_task_status` | Update task status (start, finish, close, pause, cancel, restart) and add optional comments/hours |
25
25
  | `zentao_update_bug_status` | Update bug status (resolve, close, activate) and add optional comments/resolutions |
26
+ | `zentao_get_assigned_to_me` | Get tasks and bugs currently assigned to you (configured via `ZENTAO_ACCOUNT`) |
26
27
 
27
28
 
28
29
  **Under the hood:**
29
30
  - 🔐 Auto login & token refresh — no manual auth needed
30
31
  - 📦 In-memory cache (2 min TTL) + in-flight request dedup — avoids redundant API calls
32
+ - 🔁 Classic JSON API fallback — seamlessly retries via the web API when the REST endpoint returns empty or errors
31
33
  - 🖼️ Inline HTML images are automatically downloaded and served as local `file://` links
32
34
  - 📎 Attachments are downloaded and embedded as clickable local links
33
35
  - 🛡️ Corrupt partial downloads are auto-cleaned on error
36
+ - ⚡ Non-blocking async image I/O — base64 encoding uses `fs.promises` + `Promise.all`
34
37
 
35
38
  ---
36
39
 
@@ -100,35 +103,30 @@ Add to your MCP client config (e.g. `claude_desktop_config.json`):
100
103
 
101
104
  ## 🛠️ Available Tools
102
105
 
103
- ### `zentao_get_task_details`
104
- Get full details of a ZenTao task.
106
+ ### `zentao_get_details`
107
+ Get full details of a ZenTao task or bug, including repro steps (for bugs), inline images, attachments, history, and comments.
105
108
 
106
109
  | Parameter | Type | Required | Description |
107
110
  |---|---|---|---|
108
- | `taskId` | `string \| number` | ✅ | Task ID |
109
-
110
- ### `zentao_get_bug_details`
111
- Get full details of a ZenTao bug, including repro steps and inline images.
112
-
113
- | Parameter | Type | Required | Description |
114
- |---|---|---|---|
115
- | `bugId` | `string \| number` | ✅ | Bug ID |
111
+ | `type` | `"task" \| "bug"` | ✅ | Type of the object |
112
+ | `id` | `string \| number` | ✅ | Task or Bug ID |
116
113
 
117
- ### `zentao_download_attachment`
118
- Download a ZenTao file attachment to local disk.
114
+ ### `zentao_get_comments`
115
+ Get only the history and comments timeline of a task or bug.
119
116
 
120
117
  | Parameter | Type | Required | Description |
121
118
  |---|---|---|---|
122
- | `fileId` | `string \| number` | ✅ | File ID to download |
123
- | `extension` | `string` | | File extension hint (e.g. `mp4`, `png`) |
119
+ | `type` | `"task" \| "bug"` | ✅ | Type of the object |
120
+ | `id` | `string \| number` | | Task or Bug ID |
124
121
 
125
- ### `zentao_get_comments`
126
- Get only the history and comments timeline of a task or bug.
122
+ ### `zentao_add_comment`
123
+ Add a comment/remark to a task or bug.
127
124
 
128
125
  | Parameter | Type | Required | Description |
129
126
  |---|---|---|---|
130
127
  | `type` | `"task" \| "bug"` | ✅ | Type of the object |
131
128
  | `id` | `string \| number` | ✅ | Task or Bug ID |
129
+ | `comment` | `string` | ✅ | Comment content |
132
130
 
133
131
  ### `zentao_update_task_status`
134
132
  Update a task's status with optional comments, actual start/finish dates, and consumed hours.
@@ -158,6 +156,9 @@ Update a bug's status with optional comments and resolutions.
158
156
  | `assignedTo` | `string` | ❌ | Optional user account to assign to next |
159
157
  | `openedBuild` | `string` | ❌ | Optional build ID where bug was found (for `activate`) |
160
158
 
159
+ ### `zentao_get_assigned_to_me`
160
+ Get tasks and bugs currently assigned to the configured user account. Takes no arguments.
161
+
161
162
  ---
162
163
 
163
164
  ## 🧑‍💻 Development
@@ -174,24 +175,31 @@ npm run test:api # Live API integration test
174
175
  ```
175
176
  src/
176
177
  ├── index.ts # MCP server entry point
177
- ├── zentaoClient.ts # Axios client: auth, cache, dedup, status updates
178
+ ├── zentaoClient.ts # Axios client: auth, cache, dedup, fallback, stream helpers
178
179
  ├── tools.ts # Thin orchestrator + backward-compat exports
179
180
  ├── utils/
180
181
  │ ├── fileUtils.ts # toFileUrl · getMimeType · formatSize
181
182
  │ ├── markdownUtils.ts # htmlToMarkdown · parseFiles · formatUser
182
- │ └── mcpResponse.ts # mcpText · buildMcpResponse
183
+ │ └── mcpResponse.ts # mcpText · buildMcpResponse (async)
183
184
  ├── formatters/
184
- │ ├── imageLocalizer.ts # Inline <img> → local file:// link
185
+ │ ├── imageLocalizer.ts # Inline <img> → local file:// link (deduped, concurrent)
185
186
  │ ├── attachmentRenderer.ts # Attachments → Markdown ## Files section
186
187
  │ ├── actionFormatter.ts # renderHistoryAndComments
187
188
  │ ├── taskFormatter.ts # taskToMarkdown (includes comments)
188
- └── bugFormatter.ts # bugToMarkdown (includes comments)
189
+ ├── bugFormatter.ts # bugToMarkdown (includes comments)
190
+ │ └── myWorkFormatter.ts # myWorkToMarkdown (includes comments)
189
191
  └── tools/
190
- ├── taskTool.ts # zentao_get_task_details registration
191
- ├── bugTool.ts # zentao_get_bug_details registration
192
- ├── downloadTool.ts # zentao_download_attachment registration
193
- ├── commentTool.ts # zentao_get_comments registration
194
- └── statusTool.ts # status update tools registration
192
+ ├── detailTool.ts # zentao_get_details
193
+ ├── commentTool.ts # zentao_get_comments · zentao_add_comment
194
+ ├── statusTool.ts # zentao_update_task_status · zentao_update_bug_status
195
+ └── myWorkTool.ts # zentao_get_assigned_to_me
196
+ ```
197
+
198
+ ### Running tests
199
+
200
+ ```bash
201
+ npm test # Unit tests — 13 tests across ZentaoClient
202
+ npm run test:api # Live API integration test (requires .env)
195
203
  ```
196
204
 
197
205
  ---
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Format a raw ZenTao "my work" response object into a human-readable Markdown document.
3
+ *
4
+ * @param myWork The raw "my work" object containing profile, task, and bug lists.
5
+ * @param client Authenticated ZentaoClient to get baseUrl for linking.
6
+ */
7
+ export function myWorkToMarkdown(myWork, client) {
8
+ if (!myWork)
9
+ return "No work items found.";
10
+ const profile = myWork.profile || {};
11
+ const taskData = myWork.task || { total: 0, tasks: [] };
12
+ const bugData = myWork.bug || { total: 0, bugs: [] };
13
+ const webUrl = client.webUrl || "";
14
+ const header = [
15
+ `# Work Items Assigned to ${profile.realname || profile.account || "Me"}`,
16
+ `- **Account:** ${profile.account || "N/A"}`,
17
+ `- **Role:** ${profile.role?.name || "N/A"}`,
18
+ `- **Total Tasks:** ${taskData.total}`,
19
+ `- **Total Bugs:** ${bugData.total}`,
20
+ ""
21
+ ];
22
+ const sections = [...header];
23
+ // ── Tasks Section ──
24
+ sections.push("## Tasks Assigned to Me");
25
+ if (taskData.total === 0 || !taskData.tasks || taskData.tasks.length === 0) {
26
+ sections.push("*No tasks assigned to you.*", "");
27
+ }
28
+ else {
29
+ sections.push("| ID | Task Name | Pri | Status | Execution | Deadline | Progress |", "| :--- | :--- | :---: | :---: | :--- | :---: | :---: |");
30
+ for (const t of taskData.tasks) {
31
+ const taskLink = webUrl ? `[${t.id}](${webUrl}/task-view-${t.id}.html)` : `${t.id}`;
32
+ const execution = t.executionName || "N/A";
33
+ const prog = t.progress ?? 0;
34
+ const deadline = t.deadline || "N/A";
35
+ sections.push(`| ${taskLink} | ${t.name} | ${t.pri} | ${t.status} | ${execution} | ${deadline} | ${prog}% |`);
36
+ }
37
+ sections.push("");
38
+ }
39
+ // ── Bugs Section ──
40
+ sections.push("## Bugs Assigned to Me");
41
+ if (bugData.total === 0 || !bugData.bugs || bugData.bugs.length === 0) {
42
+ sections.push("*No bugs assigned to you.*", "");
43
+ }
44
+ else {
45
+ sections.push("| ID | Bug Title | Pri | Status | Product | Deadline |", "| :--- | :--- | :---: | :---: | :--- | :---: |");
46
+ for (const b of bugData.bugs) {
47
+ const bugLink = webUrl ? `[${b.id}](${webUrl}/bug-view-${b.id}.html)` : `${b.id}`;
48
+ const product = b.productName || "N/A";
49
+ const deadline = b.deadline || "N/A";
50
+ sections.push(`| ${bugLink} | ${b.title} | ${b.pri} | ${b.status} | ${product} | ${deadline} |`);
51
+ }
52
+ sections.push("");
53
+ }
54
+ return sections.join("\n");
55
+ }
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ import { taskToMarkdown } from "../formatters/taskFormatter.js";
3
+ import { bugToMarkdown } from "../formatters/bugFormatter.js";
4
+ import { buildMcpResponse } from "../utils/mcpResponse.js";
5
+ /**
6
+ * Register the `zentao_get_details` MCP tool on the given server.
7
+ *
8
+ * @param server The MCP Server instance.
9
+ * @param client Authenticated ZentaoClient used to fetch and process data.
10
+ */
11
+ export function registerDetailTool(server, client) {
12
+ server.registerTool("zentao_get_details", {
13
+ description: "Get detailed information of a task or a bug",
14
+ inputSchema: {
15
+ type: z.enum(["task", "bug"]).describe("Type of the object ('task' or 'bug')"),
16
+ id: z.union([z.string(), z.number()]).describe("Task or Bug ID"),
17
+ },
18
+ }, async ({ type, id }) => {
19
+ let data;
20
+ let markdown;
21
+ const downloadedImages = [];
22
+ if (type === "task") {
23
+ data = await client.getTaskDetails(id);
24
+ markdown = await taskToMarkdown(data, client, downloadedImages);
25
+ }
26
+ else {
27
+ data = await client.getBugDetails(id);
28
+ markdown = await bugToMarkdown(data, client, downloadedImages);
29
+ }
30
+ return await buildMcpResponse(markdown, downloadedImages);
31
+ });
32
+ }
@@ -0,0 +1,18 @@
1
+ import { myWorkToMarkdown } from "../formatters/myWorkFormatter.js";
2
+ import { mcpText } from "../utils/mcpResponse.js";
3
+ /**
4
+ * Register the `zentao_get_assigned_to_me` MCP tool on the given server.
5
+ *
6
+ * @param server The MCP Server instance.
7
+ * @param client Authenticated ZentaoClient used to fetch and process data.
8
+ */
9
+ export function registerMyWorkTool(server, client) {
10
+ server.registerTool("zentao_get_assigned_to_me", {
11
+ description: "Get tasks and bugs assigned to the current user (configured in environment variables)",
12
+ inputSchema: {},
13
+ }, async () => {
14
+ const data = await client.getMyWork();
15
+ const markdown = myWorkToMarkdown(data, client);
16
+ return mcpText(markdown);
17
+ });
18
+ }
package/build/tools.js CHANGED
@@ -13,11 +13,10 @@
13
13
  import { ZentaoClient } from "./zentaoClient.js";
14
14
  import { taskToMarkdown as _taskToMarkdown, } from "./formatters/taskFormatter.js";
15
15
  import { bugToMarkdown as _bugToMarkdown, } from "./formatters/bugFormatter.js";
16
- import { registerTaskTool } from "./tools/taskTool.js";
17
- import { registerBugTool } from "./tools/bugTool.js";
18
- import { registerDownloadTool } from "./tools/downloadTool.js";
16
+ import { registerDetailTool } from "./tools/detailTool.js";
19
17
  import { registerCommentTool } from "./tools/commentTool.js";
20
18
  import { registerStatusTools } from "./tools/statusTool.js";
19
+ import { registerMyWorkTool } from "./tools/myWorkTool.js";
21
20
  // ─── Shared singleton ────────────────────────────────────────────────────────
22
21
  const client = new ZentaoClient();
23
22
  // ─── Backward-compatible re-exports ──────────────────────────────────────────
@@ -36,9 +35,7 @@ export function bugToMarkdown(rawBug, downloadedImages) {
36
35
  * Registers all Zentao MCP tools on the provided server instance.
37
36
  *
38
37
  * Available tools:
39
- * - `zentao_get_task_details` — Fetch full details of a task by ID.
40
- * - `zentao_get_bug_details` — Fetch full details of a bug by ID.
41
- * - `zentao_download_attachment` — Download an attachment and save locally.
38
+ * - `zentao_get_details` — Fetch full details of a task or a bug by ID.
42
39
  * - `zentao_get_comments` — Fetch history and comments timeline for task/bug.
43
40
  * - `zentao_update_task_status` — Update a task's status with comments.
44
41
  * - `zentao_update_bug_status` — Update a bug's status with comments.
@@ -46,9 +43,8 @@ export function bugToMarkdown(rawBug, downloadedImages) {
46
43
  * @param server The MCP Server instance where the tools will be registered.
47
44
  */
48
45
  export function registerTools(server) {
49
- registerTaskTool(server, client);
50
- registerBugTool(server, client);
51
- registerDownloadTool(server, client);
46
+ registerDetailTool(server, client);
52
47
  registerCommentTool(server, client);
53
48
  registerStatusTools(server, client);
49
+ registerMyWorkTool(server, client);
54
50
  }
@@ -52,6 +52,12 @@ export class ZentaoClient {
52
52
  }
53
53
  return this.baseUrl.split('/api/v1')[0];
54
54
  }
55
+ /**
56
+ * Public getter exposing the web base URL.
57
+ */
58
+ get webUrl() {
59
+ return this.webBaseUrl;
60
+ }
55
61
  // ─── Cache helpers ────────────────────────────────────────────────────────────
56
62
  /**
57
63
  * Clears the in-memory cache for GET requests. Helpful for testing.
@@ -304,6 +310,14 @@ export class ZentaoClient {
304
310
  });
305
311
  }
306
312
  // ─── Public API ───────────────────────────────────────────────────────────────
313
+ /**
314
+ * Retrieves tasks and bugs assigned to the current user (my work).
315
+ *
316
+ * @returns Resolves with the user's work items (tasks and bugs).
317
+ */
318
+ async getMyWork() {
319
+ return this.get('/user?fields=task,bug&type=assignedTo');
320
+ }
307
321
  /**
308
322
  * Retrieves details of a specific task.
309
323
  * Checks the cache first, otherwise fetches from API.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dyno181cm.nexsoft/zentao_mcp",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Zentao MCP Server",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,22 +0,0 @@
1
- import { z } from "zod";
2
- import { bugToMarkdown } from "../formatters/bugFormatter.js";
3
- import { buildMcpResponse } from "../utils/mcpResponse.js";
4
- /**
5
- * Register the `zentao_get_bug_details` MCP tool on the given server.
6
- *
7
- * @param server The MCP Server instance.
8
- * @param client Authenticated ZentaoClient used to fetch and process data.
9
- */
10
- export function registerBugTool(server, client) {
11
- server.registerTool("zentao_get_bug_details", {
12
- description: "Get detailed information of a bug",
13
- inputSchema: {
14
- bugId: z.union([z.string(), z.number()]).describe("Bug ID"),
15
- },
16
- }, async ({ bugId }) => {
17
- const data = await client.getBugDetails(bugId);
18
- const downloadedImages = [];
19
- const markdown = await bugToMarkdown(data, client, downloadedImages);
20
- return await buildMcpResponse(markdown, downloadedImages);
21
- });
22
- }
@@ -1,56 +0,0 @@
1
- import { z } from "zod";
2
- import * as os from "os";
3
- import * as path from "path";
4
- import fs from "fs";
5
- import { mcpText } from "../utils/mcpResponse.js";
6
- /** Tracks in-flight downloads to deduplicate concurrent requests for the same file. */
7
- const activeDownloads = new Map();
8
- /**
9
- * Download a ZenTao attachment to a local path.
10
- * Deduplicates concurrent requests for the same target file.
11
- */
12
- async function downloadAttachment(client, fileId, targetPath) {
13
- if (fs.existsSync(targetPath) && !activeDownloads.has(targetPath)) {
14
- return `File already exists locally at: ${targetPath}`;
15
- }
16
- if (activeDownloads.has(targetPath)) {
17
- await activeDownloads.get(targetPath);
18
- return `File downloaded successfully to: ${targetPath}`;
19
- }
20
- const downloadPromise = client.downloadFile(fileId, targetPath);
21
- activeDownloads.set(targetPath, downloadPromise);
22
- try {
23
- const savedPath = await downloadPromise;
24
- return `File downloaded successfully to: ${savedPath}`;
25
- }
26
- finally {
27
- activeDownloads.delete(targetPath);
28
- }
29
- }
30
- /**
31
- * Register the `zentao_download_attachment` MCP tool on the given server.
32
- *
33
- * @param server The MCP Server instance.
34
- * @param client Authenticated ZentaoClient used for downloading files.
35
- */
36
- export function registerDownloadTool(server, client) {
37
- server.registerTool("zentao_download_attachment", {
38
- description: "Download a file attachment from ZenTao and save it locally",
39
- inputSchema: {
40
- fileId: z.union([z.string(), z.number()]).describe("File ID to download"),
41
- extension: z.string().optional().describe("Optional file extension (e.g., mp4, png)"),
42
- },
43
- }, async ({ fileId, extension }) => {
44
- const ext = extension
45
- ? extension.startsWith('.') ? extension : `.${extension}`
46
- : '';
47
- const targetPath = path.join(os.tmpdir(), `zentao_file_${fileId}${ext}`);
48
- try {
49
- const message = await downloadAttachment(client, fileId, targetPath);
50
- return mcpText(message);
51
- }
52
- catch (error) {
53
- return mcpText(`Failed to download file: ${error.message}`);
54
- }
55
- });
56
- }
@@ -1,22 +0,0 @@
1
- import { z } from "zod";
2
- import { taskToMarkdown } from "../formatters/taskFormatter.js";
3
- import { buildMcpResponse } from "../utils/mcpResponse.js";
4
- /**
5
- * Register the `zentao_get_task_details` MCP tool on the given server.
6
- *
7
- * @param server The MCP Server instance.
8
- * @param client Authenticated ZentaoClient used to fetch and process data.
9
- */
10
- export function registerTaskTool(server, client) {
11
- server.registerTool("zentao_get_task_details", {
12
- description: "Get detailed information of a task",
13
- inputSchema: {
14
- taskId: z.union([z.string(), z.number()]).describe("Task ID"),
15
- },
16
- }, async ({ taskId }) => {
17
- const data = await client.getTaskDetails(taskId);
18
- const downloadedImages = [];
19
- const markdown = await taskToMarkdown(data, client, downloadedImages);
20
- return await buildMcpResponse(markdown, downloadedImages);
21
- });
22
- }