@dyno181cm.nexsoft/zentao_mcp 1.3.0 → 1.4.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 +167 -47
- package/build/formatters/actionFormatter.js +94 -0
- package/build/formatters/bugFormatter.js +11 -3
- package/build/formatters/taskFormatter.js +11 -3
- package/build/tools/commentTool.js +34 -0
- package/build/tools/statusTool.js +91 -0
- package/build/tools.js +7 -0
- package/build/zentaoClient.js +33 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,62 +1,93 @@
|
|
|
1
1
|
# Zentao MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@dyno181cm.nexsoft/zentao_mcp)
|
|
4
|
+
[](https://www.npmjs.com/package/@dyno181cm.nexsoft/zentao_mcp)
|
|
5
|
+
[](https://github.com/dyno-nexsoft/zentao_mcp/releases)
|
|
6
|
+
[](https://github.com/dyno-nexsoft/zentao_mcp/blob/master/LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
[](https://www.typescriptlang.org)
|
|
9
|
+
[](https://modelcontextprotocol.io)
|
|
4
10
|
|
|
5
|
-
|
|
11
|
+
A **Model Context Protocol (MCP)** server for integrating AI assistants (Claude, Cursor, etc.) with the [ZenTao](https://www.zentao.net) project management API.
|
|
12
|
+
Fetch task details, bug reports, and attachments — all directly inside your AI chat.
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
- **MCP Tools**:
|
|
9
|
-
- `zentao_get_task_details`: Get detailed information about a specific task by ID.
|
|
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).
|
|
14
|
+
---
|
|
12
15
|
|
|
13
|
-
##
|
|
16
|
+
## ✨ Features
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
| Tool | Description |
|
|
19
|
+
|---|---|
|
|
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 |
|
|
23
|
+
| `zentao_get_comments` | Fetch only the history and comments timeline of a task or bug |
|
|
24
|
+
| `zentao_update_task_status` | Update task status (start, finish, close, pause, cancel, restart) and add optional comments/hours |
|
|
25
|
+
| `zentao_update_bug_status` | Update bug status (resolve, close, activate) and add optional comments/resolutions |
|
|
17
26
|
|
|
18
|
-
## Installation
|
|
19
27
|
|
|
20
|
-
|
|
28
|
+
**Under the hood:**
|
|
29
|
+
- 🔐 Auto login & token refresh — no manual auth needed
|
|
30
|
+
- 📦 In-memory cache (2 min TTL) + in-flight request dedup — avoids redundant API calls
|
|
31
|
+
- 🖼️ Inline HTML images are automatically downloaded and served as local `file://` links
|
|
32
|
+
- 📎 Attachments are downloaded and embedded as clickable local links
|
|
33
|
+
- 🛡️ Corrupt partial downloads are auto-cleaned on error
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
git clone https://github.com/dyno-nexsoft/zentao_mcp.git
|
|
24
|
-
cd zentao_mcp
|
|
25
|
-
```
|
|
35
|
+
---
|
|
26
36
|
|
|
27
|
-
|
|
37
|
+
## 📦 Installation
|
|
28
38
|
|
|
29
|
-
|
|
30
|
-
npm install
|
|
31
|
-
```
|
|
39
|
+
### Option 1 — `npx` (recommended, no install required)
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
```bash
|
|
42
|
+
npx @dyno181cm.nexsoft/zentao_mcp
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Option 2 — Global install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install -g @dyno181cm.nexsoft/zentao_mcp
|
|
49
|
+
zentao_mcp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Option 3 — Clone & build
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git clone https://github.com/dyno-nexsoft/zentao_mcp.git
|
|
56
|
+
cd zentao_mcp
|
|
57
|
+
npm install
|
|
58
|
+
npm run build
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## ⚙️ Configuration
|
|
40
64
|
|
|
41
|
-
|
|
65
|
+
Create a `.env` file in the project root (or pass via MCP client `env` block):
|
|
42
66
|
|
|
43
|
-
|
|
67
|
+
```env
|
|
68
|
+
ZENTAO_BASE_URL=https://your-zentao-url.com/zentao/api.php/v1
|
|
69
|
+
ZENTAO_ACCOUNT=your_username
|
|
70
|
+
ZENTAO_PASSWORD=your_password
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 🔌 MCP Client Integration
|
|
44
76
|
|
|
45
|
-
|
|
77
|
+
This server communicates via **stdio transport** — compatible with any MCP client.
|
|
46
78
|
|
|
47
|
-
|
|
79
|
+
### Claude Desktop / Cursor / Windsurf
|
|
80
|
+
|
|
81
|
+
Add to your MCP client config (e.g. `claude_desktop_config.json`):
|
|
48
82
|
|
|
49
83
|
```json
|
|
50
84
|
{
|
|
51
85
|
"mcpServers": {
|
|
52
86
|
"zentao": {
|
|
53
87
|
"command": "npx",
|
|
54
|
-
"args": [
|
|
55
|
-
"-y",
|
|
56
|
-
"@dyno181cm.nexsoft/zentao_mcp"
|
|
57
|
-
],
|
|
88
|
+
"args": ["-y", "@dyno181cm.nexsoft/zentao_mcp"],
|
|
58
89
|
"env": {
|
|
59
|
-
"ZENTAO_BASE_URL": "https://your-zentao-url.com/api.php/v1",
|
|
90
|
+
"ZENTAO_BASE_URL": "https://your-zentao-url.com/zentao/api.php/v1",
|
|
60
91
|
"ZENTAO_ACCOUNT": "your_username",
|
|
61
92
|
"ZENTAO_PASSWORD": "your_password"
|
|
62
93
|
}
|
|
@@ -65,17 +96,106 @@ Once published to npm or GitHub, users can integrate this server into their MCP
|
|
|
65
96
|
}
|
|
66
97
|
```
|
|
67
98
|
|
|
68
|
-
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 🛠️ Available Tools
|
|
102
|
+
|
|
103
|
+
### `zentao_get_task_details`
|
|
104
|
+
Get full details of a ZenTao task.
|
|
105
|
+
|
|
106
|
+
| Parameter | Type | Required | Description |
|
|
107
|
+
|---|---|---|---|
|
|
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 |
|
|
116
|
+
|
|
117
|
+
### `zentao_download_attachment`
|
|
118
|
+
Download a ZenTao file attachment to local disk.
|
|
119
|
+
|
|
120
|
+
| Parameter | Type | Required | Description |
|
|
121
|
+
|---|---|---|---|
|
|
122
|
+
| `fileId` | `string \| number` | ✅ | File ID to download |
|
|
123
|
+
| `extension` | `string` | ❌ | File extension hint (e.g. `mp4`, `png`) |
|
|
124
|
+
|
|
125
|
+
### `zentao_get_comments`
|
|
126
|
+
Get only the history and comments timeline of a task or bug.
|
|
127
|
+
|
|
128
|
+
| Parameter | Type | Required | Description |
|
|
129
|
+
|---|---|---|---|
|
|
130
|
+
| `type` | `"task" \| "bug"` | ✅ | Type of the object |
|
|
131
|
+
| `id` | `string \| number` | ✅ | Task or Bug ID |
|
|
132
|
+
|
|
133
|
+
### `zentao_update_task_status`
|
|
134
|
+
Update a task's status with optional comments, actual start/finish dates, and consumed hours.
|
|
135
|
+
|
|
136
|
+
| Parameter | Type | Required | Description |
|
|
137
|
+
|---|---|---|---|
|
|
138
|
+
| `taskId` | `string \| number` | ✅ | Task ID |
|
|
139
|
+
| `action` | `"start" \| "finish" \| "close" \| "pause" \| "cancel" \| "restart"` | ✅ | Status transition action |
|
|
140
|
+
| `comment` | `string` | ❌ | Optional comment to attach |
|
|
141
|
+
| `consumed` | `number` | ❌ | Optional hours consumed |
|
|
142
|
+
| `finishedDate` | `string` | ❌ | Optional actual finish date (YYYY-MM-DD) |
|
|
143
|
+
| `realStarted` | `string` | ❌ | Optional actual start date (YYYY-MM-DD) |
|
|
144
|
+
| `assignedTo` | `string` | ❌ | Optional user account to assign to next |
|
|
145
|
+
| `left` | `number` | ❌ | Optional left hours (for `restart`) |
|
|
146
|
+
|
|
147
|
+
### `zentao_update_bug_status`
|
|
148
|
+
Update a bug's status with optional comments and resolutions.
|
|
149
|
+
|
|
150
|
+
| Parameter | Type | Required | Description |
|
|
151
|
+
|---|---|---|---|
|
|
152
|
+
| `bugId` | `string \| number` | ✅ | Bug ID |
|
|
153
|
+
| `action` | `"resolve" \| "close" \| "activate"` | ✅ | Status transition action |
|
|
154
|
+
| `comment` | `string` | ❌ | Optional comment to attach |
|
|
155
|
+
| `resolution` | `"fixed" \| "bydesign" \| "postponed" \| "external" \| "notrepro" \| "willnotfix"` | ❌ | Optional resolution type |
|
|
156
|
+
| `resolvedBuild` | `string` | ❌ | Optional build ID where bug was resolved |
|
|
157
|
+
| `resolvedDate` | `string` | ❌ | Optional resolution date (YYYY-MM-DD) |
|
|
158
|
+
| `assignedTo` | `string` | ❌ | Optional user account to assign to next |
|
|
159
|
+
| `openedBuild` | `string` | ❌ | Optional build ID where bug was found (for `activate`) |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 🧑💻 Development
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npm run build # Compile TypeScript
|
|
167
|
+
npm run dev # Watch mode
|
|
168
|
+
npm test # Unit tests (Jest)
|
|
169
|
+
npm run test:api # Live API integration test
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Project structure
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
src/
|
|
176
|
+
├── index.ts # MCP server entry point
|
|
177
|
+
├── zentaoClient.ts # Axios client: auth, cache, dedup, status updates
|
|
178
|
+
├── tools.ts # Thin orchestrator + backward-compat exports
|
|
179
|
+
├── utils/
|
|
180
|
+
│ ├── fileUtils.ts # toFileUrl · getMimeType · formatSize
|
|
181
|
+
│ ├── markdownUtils.ts # htmlToMarkdown · parseFiles · formatUser
|
|
182
|
+
│ └── mcpResponse.ts # mcpText · buildMcpResponse
|
|
183
|
+
├── formatters/
|
|
184
|
+
│ ├── imageLocalizer.ts # Inline <img> → local file:// link
|
|
185
|
+
│ ├── attachmentRenderer.ts # Attachments → Markdown ## Files section
|
|
186
|
+
│ ├── actionFormatter.ts # renderHistoryAndComments
|
|
187
|
+
│ ├── taskFormatter.ts # taskToMarkdown (includes comments)
|
|
188
|
+
│ └── bugFormatter.ts # bugToMarkdown (includes comments)
|
|
189
|
+
└── 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
|
|
195
|
+
```
|
|
69
196
|
|
|
70
|
-
|
|
71
|
-
- **Inputs:**
|
|
72
|
-
- `taskId` (string | number) - Required. Task ID.
|
|
197
|
+
---
|
|
73
198
|
|
|
74
|
-
|
|
75
|
-
- **Inputs:**
|
|
76
|
-
- `bugId` (string | number) - Required. Bug ID.
|
|
199
|
+
## 📄 License
|
|
77
200
|
|
|
78
|
-
-
|
|
79
|
-
- **Inputs:**
|
|
80
|
-
- `fileId` (string | number) - Required. File ID to download.
|
|
81
|
-
- `extension` (string) - Optional. File extension (e.g., mp4, png).
|
|
201
|
+
[MIT](./LICENSE) © [dyno-nexsoft](https://github.com/dyno-nexsoft)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { htmlToMarkdown, parseFiles } from "../utils/markdownUtils.js";
|
|
2
|
+
import { localizeImages } from "./imageLocalizer.js";
|
|
3
|
+
import { toFileUrl, formatSize } from "../utils/fileUtils.js";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
/**
|
|
8
|
+
* Normalizes and formats the history/comments/actions list into a clean Markdown block.
|
|
9
|
+
*
|
|
10
|
+
* @param actions The raw actions array from Zentao task or bug response.
|
|
11
|
+
* @param client Authenticated ZentaoClient used for image/attachment downloads.
|
|
12
|
+
* @param downloadedImages Optional collector for local image paths.
|
|
13
|
+
*/
|
|
14
|
+
export async function renderHistoryAndComments(actions, client, downloadedImages) {
|
|
15
|
+
if (!actions || !Array.isArray(actions) || actions.length === 0) {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
const lines = [];
|
|
19
|
+
for (const act of actions) {
|
|
20
|
+
// 1. Localize and convert the action description (desc)
|
|
21
|
+
let desc = "";
|
|
22
|
+
if (act.desc) {
|
|
23
|
+
const localizedDesc = await localizeImages(act.desc, client, downloadedImages);
|
|
24
|
+
desc = htmlToMarkdown(localizedDesc).trim();
|
|
25
|
+
// Remove date prefix from desc if it exists to avoid redundancy
|
|
26
|
+
if (act.date) {
|
|
27
|
+
const prefixes = [act.date + ",", act.date + " "];
|
|
28
|
+
for (const prefix of prefixes) {
|
|
29
|
+
if (desc.startsWith(prefix)) {
|
|
30
|
+
desc = desc.substring(prefix.length).trim();
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Fallback if description is empty
|
|
37
|
+
if (!desc) {
|
|
38
|
+
desc = `${act.actor || "User"} did action "${act.action || "unknown"}"`;
|
|
39
|
+
}
|
|
40
|
+
// 2. Format comment if present
|
|
41
|
+
let commentStr = "";
|
|
42
|
+
if (act.comment) {
|
|
43
|
+
const localizedComment = await localizeImages(act.comment, client, downloadedImages);
|
|
44
|
+
const commentMd = htmlToMarkdown(localizedComment).trim();
|
|
45
|
+
if (commentMd) {
|
|
46
|
+
// Blockquote the comment
|
|
47
|
+
commentStr = `\n > ${commentMd.replace(/\n/g, '\n > ')}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// 3. Format files if present in action
|
|
51
|
+
let filesStr = "";
|
|
52
|
+
if (act.files && Array.isArray(act.files) && act.files.length > 0) {
|
|
53
|
+
const parsedFiles = parseFiles(act.files);
|
|
54
|
+
if (parsedFiles.length > 0) {
|
|
55
|
+
const fileLinks = [];
|
|
56
|
+
for (const f of parsedFiles) {
|
|
57
|
+
const ext = (f.extension || '').toLowerCase();
|
|
58
|
+
const targetPath = path.join(os.tmpdir(), `zentao_file_${f.id}.${ext}`);
|
|
59
|
+
let localPath = null;
|
|
60
|
+
try {
|
|
61
|
+
localPath = fs.existsSync(targetPath)
|
|
62
|
+
? targetPath
|
|
63
|
+
: await client.downloadFile(f.id, targetPath);
|
|
64
|
+
if (localPath && downloadedImages && !downloadedImages.includes(localPath)) {
|
|
65
|
+
downloadedImages.push(localPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// ignore download error
|
|
70
|
+
}
|
|
71
|
+
if (localPath) {
|
|
72
|
+
fileLinks.push(`[${f.title}](${toFileUrl(localPath)}) *(Size: ${formatSize(f.size)})*`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
fileLinks.push(`[${f.title}] *(download failed)*`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (fileLinks.length > 0) {
|
|
79
|
+
filesStr = `\n - *Attachments:* ${fileLinks.join(", ")}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// 4. Combine into a timeline item
|
|
84
|
+
lines.push(`- **[${act.date || "N/A"}]** ${desc}${commentStr}${filesStr}`);
|
|
85
|
+
}
|
|
86
|
+
if (lines.length === 0)
|
|
87
|
+
return "";
|
|
88
|
+
return [
|
|
89
|
+
"",
|
|
90
|
+
"## History & Comments",
|
|
91
|
+
"",
|
|
92
|
+
...lines,
|
|
93
|
+
].join("\n");
|
|
94
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { htmlToMarkdown, parseFiles, formatUser } from "../utils/markdownUtils.js";
|
|
2
2
|
import { localizeImages } from "./imageLocalizer.js";
|
|
3
3
|
import { renderAttachments } from "./attachmentRenderer.js";
|
|
4
|
+
import { renderHistoryAndComments } from "./actionFormatter.js";
|
|
4
5
|
/**
|
|
5
6
|
* Format a raw ZenTao bug object into a human-readable Markdown document.
|
|
6
7
|
*
|
|
@@ -32,13 +33,20 @@ export async function bugToMarkdown(rawBug, client, downloadedImages) {
|
|
|
32
33
|
const localizedSteps = await localizeImages(rawBug.steps, client, downloadedImages);
|
|
33
34
|
const steps = htmlToMarkdown(localizedSteps);
|
|
34
35
|
const attachments = await renderAttachments(parseFiles(rawBug.files), client, downloadedImages);
|
|
35
|
-
|
|
36
|
+
const historySection = await renderHistoryAndComments(rawBug.actions, client, downloadedImages);
|
|
37
|
+
const parts = [
|
|
36
38
|
`# Bug #${rawBug.id}: ${rawBug.title}`,
|
|
37
39
|
'',
|
|
38
40
|
...meta.map((m) => `- ${m}`),
|
|
39
41
|
'',
|
|
40
42
|
'## Repro Steps',
|
|
41
43
|
steps || '*No steps provided.*',
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
];
|
|
45
|
+
if (attachments) {
|
|
46
|
+
parts.push(attachments);
|
|
47
|
+
}
|
|
48
|
+
if (historySection) {
|
|
49
|
+
parts.push(historySection);
|
|
50
|
+
}
|
|
51
|
+
return parts.join('\n');
|
|
44
52
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { htmlToMarkdown, parseFiles, formatUser } from "../utils/markdownUtils.js";
|
|
2
2
|
import { localizeImages } from "./imageLocalizer.js";
|
|
3
3
|
import { renderAttachments } from "./attachmentRenderer.js";
|
|
4
|
+
import { renderHistoryAndComments } from "./actionFormatter.js";
|
|
4
5
|
/**
|
|
5
6
|
* Format a raw ZenTao task object into a human-readable Markdown document.
|
|
6
7
|
*
|
|
@@ -35,13 +36,20 @@ export async function taskToMarkdown(rawTask, client, downloadedImages) {
|
|
|
35
36
|
const localizedDesc = await localizeImages(rawTask.desc, client, downloadedImages);
|
|
36
37
|
const desc = htmlToMarkdown(localizedDesc);
|
|
37
38
|
const attachments = await renderAttachments(parseFiles(rawTask.files), client, downloadedImages);
|
|
38
|
-
|
|
39
|
+
const historySection = await renderHistoryAndComments(rawTask.actions, client, downloadedImages);
|
|
40
|
+
const parts = [
|
|
39
41
|
`# Task #${rawTask.id}: ${rawTask.name}`,
|
|
40
42
|
'',
|
|
41
43
|
...meta.map((m) => `- ${m}`),
|
|
42
44
|
'',
|
|
43
45
|
'## Description',
|
|
44
46
|
desc || '*No description provided.*',
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
];
|
|
48
|
+
if (attachments) {
|
|
49
|
+
parts.push(attachments);
|
|
50
|
+
}
|
|
51
|
+
if (historySection) {
|
|
52
|
+
parts.push(historySection);
|
|
53
|
+
}
|
|
54
|
+
return parts.join('\n');
|
|
47
55
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { renderHistoryAndComments } from "../formatters/actionFormatter.js";
|
|
3
|
+
import { buildMcpResponse } from "../utils/mcpResponse.js";
|
|
4
|
+
/**
|
|
5
|
+
* Register the `zentao_get_comments` 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 registerCommentTool(server, client) {
|
|
11
|
+
server.registerTool("zentao_get_comments", {
|
|
12
|
+
description: "Get only the history and comments timeline of a task or a bug",
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: z.enum(["task", "bug"]).describe("Type of the object ('task' or 'bug')"),
|
|
15
|
+
id: z.union([z.string(), z.number()]).describe("Task or Bug ID"),
|
|
16
|
+
},
|
|
17
|
+
}, async ({ type, id }) => {
|
|
18
|
+
let data;
|
|
19
|
+
if (type === "task") {
|
|
20
|
+
data = await client.getTaskDetails(id);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
data = await client.getBugDetails(id);
|
|
24
|
+
}
|
|
25
|
+
const downloadedImages = [];
|
|
26
|
+
const markdown = await renderHistoryAndComments(data.actions, client, downloadedImages);
|
|
27
|
+
const title = type === "task" ? `Task #${data.id || id}: ${data.name || ""}` : `Bug #${data.id || id}: ${data.title || ""}`;
|
|
28
|
+
const output = [
|
|
29
|
+
`# History & Comments for ${title}`,
|
|
30
|
+
markdown.trim() || "*No history or comments found.*"
|
|
31
|
+
].join("\n\n");
|
|
32
|
+
return buildMcpResponse(output, downloadedImages);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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 task and bug status update MCP tools on the given server.
|
|
7
|
+
*
|
|
8
|
+
* @param server The MCP Server instance.
|
|
9
|
+
* @param client Authenticated ZentaoClient used to update data.
|
|
10
|
+
*/
|
|
11
|
+
export function registerStatusTools(server, client) {
|
|
12
|
+
// ─── Task Status Update Tool ───────────────────────────────────────────────
|
|
13
|
+
server.registerTool("zentao_update_task_status", {
|
|
14
|
+
description: "Update the status of a task and optionally add a comment/remark",
|
|
15
|
+
inputSchema: {
|
|
16
|
+
taskId: z.union([z.string(), z.number()]).describe("Task ID"),
|
|
17
|
+
action: z.enum(["start", "finish", "close", "pause", "cancel", "restart"]).describe("Status transition action"),
|
|
18
|
+
comment: z.string().optional().describe("Optional comment/remark to attach to this action"),
|
|
19
|
+
consumed: z.number().optional().describe("Optional hours consumed (used for 'start' or 'finish')"),
|
|
20
|
+
finishedDate: z.string().optional().describe("Optional actual finish date YYYY-MM-DD (used for 'finish', defaults to today)"),
|
|
21
|
+
realStarted: z.string().optional().describe("Optional actual start date YYYY-MM-DD (used for 'start' or 'finish')"),
|
|
22
|
+
assignedTo: z.string().optional().describe("Optional user account to assign to next (used for 'finish' or 'restart')"),
|
|
23
|
+
left: z.number().optional().describe("Optional left hours (used for 'restart')"),
|
|
24
|
+
},
|
|
25
|
+
}, async ({ taskId, action, comment, consumed, finishedDate, realStarted, assignedTo, left }) => {
|
|
26
|
+
const payload = {};
|
|
27
|
+
if (comment !== undefined)
|
|
28
|
+
payload.comment = comment;
|
|
29
|
+
if (action === "finish") {
|
|
30
|
+
// Fetch task details to get realStarted / estStarted if not provided (Zentao requires them)
|
|
31
|
+
const task = await client.getTaskDetails(taskId);
|
|
32
|
+
payload.realStarted = realStarted || task.realStarted || task.estStarted || new Date().toISOString().split("T")[0];
|
|
33
|
+
payload.finishedDate = finishedDate || new Date().toISOString().split("T")[0];
|
|
34
|
+
payload.consumed = consumed !== undefined ? consumed : (task.consumed || 0);
|
|
35
|
+
if (assignedTo)
|
|
36
|
+
payload.assignedTo = assignedTo;
|
|
37
|
+
}
|
|
38
|
+
else if (action === "start") {
|
|
39
|
+
payload.realStarted = realStarted || new Date().toISOString().split("T")[0];
|
|
40
|
+
if (consumed !== undefined)
|
|
41
|
+
payload.consumed = consumed;
|
|
42
|
+
}
|
|
43
|
+
else if (action === "restart") {
|
|
44
|
+
if (assignedTo)
|
|
45
|
+
payload.assignedTo = assignedTo;
|
|
46
|
+
if (left !== undefined)
|
|
47
|
+
payload.left = left;
|
|
48
|
+
}
|
|
49
|
+
const updatedTask = await client.updateTaskStatus(taskId, action, payload);
|
|
50
|
+
const downloadedImages = [];
|
|
51
|
+
const markdown = await taskToMarkdown(updatedTask, client, downloadedImages);
|
|
52
|
+
const output = `### Successfully updated task status to '${updatedTask.status || "updated"}'!\n\n${markdown}`;
|
|
53
|
+
return buildMcpResponse(output, downloadedImages);
|
|
54
|
+
});
|
|
55
|
+
// ─── Bug Status Update Tool ────────────────────────────────────────────────
|
|
56
|
+
server.registerTool("zentao_update_bug_status", {
|
|
57
|
+
description: "Update the status of a bug and optionally add a comment/remark",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
bugId: z.union([z.string(), z.number()]).describe("Bug ID"),
|
|
60
|
+
action: z.enum(["resolve", "close", "activate"]).describe("Status transition action"),
|
|
61
|
+
comment: z.string().optional().describe("Optional comment/remark to attach to this action"),
|
|
62
|
+
resolution: z.enum(["fixed", "bydesign", "postponed", "external", "notrepro", "willnotfix"]).optional().describe("Resolution type (required for 'resolve', defaults to 'fixed')"),
|
|
63
|
+
resolvedBuild: z.string().optional().describe("Build ID/name in which the bug was resolved (used for 'resolve', defaults to 'trunk')"),
|
|
64
|
+
resolvedDate: z.string().optional().describe("Optional date of resolution YYYY-MM-DD (used for 'resolve', defaults to today)"),
|
|
65
|
+
assignedTo: z.string().optional().describe("Optional user account to assign to next (used for 'resolve' or 'activate')"),
|
|
66
|
+
openedBuild: z.string().optional().describe("Optional build ID/name where bug was found (used for 'activate')"),
|
|
67
|
+
},
|
|
68
|
+
}, async ({ bugId, action, comment, resolution, resolvedBuild, resolvedDate, assignedTo, openedBuild }) => {
|
|
69
|
+
const payload = {};
|
|
70
|
+
if (comment !== undefined)
|
|
71
|
+
payload.comment = comment;
|
|
72
|
+
if (action === "resolve") {
|
|
73
|
+
payload.resolution = resolution || "fixed";
|
|
74
|
+
payload.resolvedBuild = resolvedBuild || "trunk";
|
|
75
|
+
payload.resolvedDate = resolvedDate || new Date().toISOString().split("T")[0];
|
|
76
|
+
if (assignedTo)
|
|
77
|
+
payload.assignedTo = assignedTo;
|
|
78
|
+
}
|
|
79
|
+
else if (action === "activate") {
|
|
80
|
+
if (assignedTo)
|
|
81
|
+
payload.assignedTo = assignedTo;
|
|
82
|
+
if (openedBuild)
|
|
83
|
+
payload.openedBuild = openedBuild;
|
|
84
|
+
}
|
|
85
|
+
const updatedBug = await client.updateBugStatus(bugId, action, payload);
|
|
86
|
+
const downloadedImages = [];
|
|
87
|
+
const markdown = await bugToMarkdown(updatedBug, client, downloadedImages);
|
|
88
|
+
const output = `### Successfully updated bug status to '${updatedBug.status || "updated"}'!\n\n${markdown}`;
|
|
89
|
+
return buildMcpResponse(output, downloadedImages);
|
|
90
|
+
});
|
|
91
|
+
}
|
package/build/tools.js
CHANGED
|
@@ -16,6 +16,8 @@ import { bugToMarkdown as _bugToMarkdown, } from "./formatters/bugFormatter.js";
|
|
|
16
16
|
import { registerTaskTool } from "./tools/taskTool.js";
|
|
17
17
|
import { registerBugTool } from "./tools/bugTool.js";
|
|
18
18
|
import { registerDownloadTool } from "./tools/downloadTool.js";
|
|
19
|
+
import { registerCommentTool } from "./tools/commentTool.js";
|
|
20
|
+
import { registerStatusTools } from "./tools/statusTool.js";
|
|
19
21
|
// ─── Shared singleton ────────────────────────────────────────────────────────
|
|
20
22
|
const client = new ZentaoClient();
|
|
21
23
|
// ─── Backward-compatible re-exports ──────────────────────────────────────────
|
|
@@ -37,6 +39,9 @@ export function bugToMarkdown(rawBug, downloadedImages) {
|
|
|
37
39
|
* - `zentao_get_task_details` — Fetch full details of a task by ID.
|
|
38
40
|
* - `zentao_get_bug_details` — Fetch full details of a bug by ID.
|
|
39
41
|
* - `zentao_download_attachment` — Download an attachment and save locally.
|
|
42
|
+
* - `zentao_get_comments` — Fetch history and comments timeline for task/bug.
|
|
43
|
+
* - `zentao_update_task_status` — Update a task's status with comments.
|
|
44
|
+
* - `zentao_update_bug_status` — Update a bug's status with comments.
|
|
40
45
|
*
|
|
41
46
|
* @param server The MCP Server instance where the tools will be registered.
|
|
42
47
|
*/
|
|
@@ -44,4 +49,6 @@ export function registerTools(server) {
|
|
|
44
49
|
registerTaskTool(server, client);
|
|
45
50
|
registerBugTool(server, client);
|
|
46
51
|
registerDownloadTool(server, client);
|
|
52
|
+
registerCommentTool(server, client);
|
|
53
|
+
registerStatusTools(server, client);
|
|
47
54
|
}
|
package/build/zentaoClient.js
CHANGED
|
@@ -191,4 +191,37 @@ export class ZentaoClient {
|
|
|
191
191
|
});
|
|
192
192
|
});
|
|
193
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Helper method to perform POST requests.
|
|
196
|
+
* Clears the cache to ensure subsequent GET requests fetch the updated state.
|
|
197
|
+
*
|
|
198
|
+
* @param url The API endpoint path.
|
|
199
|
+
* @param data The payload data.
|
|
200
|
+
* @returns The response data.
|
|
201
|
+
*/
|
|
202
|
+
async post(url, data) {
|
|
203
|
+
this.clearCache();
|
|
204
|
+
const res = await this.client.post(url, data);
|
|
205
|
+
return res.data;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Updates task status by calling the action endpoint.
|
|
209
|
+
*
|
|
210
|
+
* @param taskId The ID of the task.
|
|
211
|
+
* @param action The status transition action (e.g. 'start', 'finish', 'close', 'pause', 'cancel').
|
|
212
|
+
* @param payload The request body payload.
|
|
213
|
+
*/
|
|
214
|
+
async updateTaskStatus(taskId, action, payload) {
|
|
215
|
+
return this.post(`/tasks/${taskId}/${action}`, payload);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Updates bug status by calling the action endpoint.
|
|
219
|
+
*
|
|
220
|
+
* @param bugId The ID of the bug.
|
|
221
|
+
* @param action The status transition action (e.g. 'resolve', 'close', 'activate').
|
|
222
|
+
* @param payload The request body payload.
|
|
223
|
+
*/
|
|
224
|
+
async updateBugStatus(bugId, action, payload) {
|
|
225
|
+
return this.post(`/bugs/${bugId}/${action}`, payload);
|
|
226
|
+
}
|
|
194
227
|
}
|