@zentao-hub/mcp 0.3.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/.env.example +13 -0
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/README.zh-CN.md +225 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/bugs.d.ts +2 -0
- package/dist/tools/bugs.js +387 -0
- package/dist/tools/bugs.js.map +1 -0
- package/dist/tools/builds.d.ts +2 -0
- package/dist/tools/builds.js +54 -0
- package/dist/tools/builds.js.map +1 -0
- package/dist/tools/cases.d.ts +2 -0
- package/dist/tools/cases.js +121 -0
- package/dist/tools/cases.js.map +1 -0
- package/dist/tools/docs.d.ts +2 -0
- package/dist/tools/docs.js +75 -0
- package/dist/tools/docs.js.map +1 -0
- package/dist/tools/efforts.d.ts +2 -0
- package/dist/tools/efforts.js +65 -0
- package/dist/tools/efforts.js.map +1 -0
- package/dist/tools/executions.d.ts +2 -0
- package/dist/tools/executions.js +118 -0
- package/dist/tools/executions.js.map +1 -0
- package/dist/tools/helpers.d.ts +36 -0
- package/dist/tools/helpers.js +255 -0
- package/dist/tools/helpers.js.map +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +44 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/issues.d.ts +2 -0
- package/dist/tools/issues.js +89 -0
- package/dist/tools/issues.js.map +1 -0
- package/dist/tools/products.d.ts +2 -0
- package/dist/tools/products.js +110 -0
- package/dist/tools/products.js.map +1 -0
- package/dist/tools/programs.d.ts +2 -0
- package/dist/tools/programs.js +46 -0
- package/dist/tools/programs.js.map +1 -0
- package/dist/tools/projects.d.ts +2 -0
- package/dist/tools/projects.js +80 -0
- package/dist/tools/projects.js.map +1 -0
- package/dist/tools/releases.d.ts +2 -0
- package/dist/tools/releases.js +46 -0
- package/dist/tools/releases.js.map +1 -0
- package/dist/tools/repos.d.ts +2 -0
- package/dist/tools/repos.js +42 -0
- package/dist/tools/repos.js.map +1 -0
- package/dist/tools/request.d.ts +2 -0
- package/dist/tools/request.js +41 -0
- package/dist/tools/request.js.map +1 -0
- package/dist/tools/risks.d.ts +2 -0
- package/dist/tools/risks.js +69 -0
- package/dist/tools/risks.js.map +1 -0
- package/dist/tools/stories.d.ts +2 -0
- package/dist/tools/stories.js +153 -0
- package/dist/tools/stories.js.map +1 -0
- package/dist/tools/tasks.d.ts +2 -0
- package/dist/tools/tasks.js +205 -0
- package/dist/tools/tasks.js.map +1 -0
- package/dist/tools/testreports.d.ts +2 -0
- package/dist/tools/testreports.js +47 -0
- package/dist/tools/testreports.js.map +1 -0
- package/dist/tools/todos.d.ts +2 -0
- package/dist/tools/todos.js +70 -0
- package/dist/tools/todos.js.map +1 -0
- package/dist/tools/users.d.ts +2 -0
- package/dist/tools/users.js +59 -0
- package/dist/tools/users.js.map +1 -0
- package/package.json +59 -0
package/.env.example
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# ZenTao instance base URL (no trailing slash). The /api.php/v1 path is appended automatically.
|
|
2
|
+
ZENTAO_URL=https://zentao.example.com
|
|
3
|
+
|
|
4
|
+
# Account used to authenticate. The MCP server exchanges these for a token at startup
|
|
5
|
+
# and silently re-logs in on 401.
|
|
6
|
+
ZENTAO_USERNAME=your-account
|
|
7
|
+
ZENTAO_PASSWORD=your-password
|
|
8
|
+
|
|
9
|
+
# Optional: pass a pre-issued token instead of username/password. Takes precedence when set.
|
|
10
|
+
# ZENTAO_TOKEN=
|
|
11
|
+
|
|
12
|
+
# Optional: HTTP request timeout in milliseconds (default 15000).
|
|
13
|
+
# ZENTAO_TIMEOUT_MS=15000
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Deven Liu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# @zentao-hub/mcp
|
|
2
|
+
|
|
3
|
+
English | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server for **ZenTao 20.x (open source edition)**. Covers the everyday entities of the ZenTao v1 REST API (products, projects, plans, executions, stories, tasks, bugs, test cases, builds, releases, repos, users, docs) plus a generic `zentao_request` escape hatch for endpoints that are not explicitly wrapped.
|
|
6
|
+
|
|
7
|
+
> Looking for the hub directory, slash commands, and commit-msg hook? See the sister package [`@zentao-hub/cli`](../cli). This package is the MCP server itself, nothing more.
|
|
8
|
+
|
|
9
|
+
## Tool overview (129 tools)
|
|
10
|
+
|
|
11
|
+
| Entity | Tools |
|
|
12
|
+
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| **Users / Depts** | `whoami` · `list_users` · `get_user` · `list_departments` · `list_department_users` |
|
|
14
|
+
| **Products** | `list_products` · `get_product` · `create_product` · `update_product` · `delete_product` · `list_product_branches` · `list_product_modules` · `list_product_plans` · `list_product_executions` |
|
|
15
|
+
| **Projects** | `list_projects` · `get_project` · `create_project` · `update_project` · `delete_project` · `list_project_executions` · `list_project_products` |
|
|
16
|
+
| **Programs** | `list_programs` · `get_program` · `create_program` · `delete_program` |
|
|
17
|
+
| **Executions** | `list_executions` · `get_execution` · `create_execution` · `update_execution` · `delete_execution` · `start_execution` · `close_execution` · `list_execution_tasks` · `list_execution_stories` · `list_execution_team` |
|
|
18
|
+
| **Stories** | `list_product_stories` · `get_story` · `create_story` · `update_story` · `delete_story` · `change_story` · `review_story` · `close_story` · `activate_story` · `assign_story` · `add_story_comment` |
|
|
19
|
+
| **Tasks** | `get_task` · `list_my_tasks` · `create_task` · `update_task` · `delete_task` · `start_task` · `finish_task` · `pause_task` · `activate_task` · `close_task` · `cancel_task` · `assign_task` · `add_task_comment` |
|
|
20
|
+
| **Bugs** | `list_my_bugs` · `list_product_bugs` · `list_execution_bugs` · `get_bug` · `create_bug` · `update_bug` · `delete_bug` · `resolve_bug` · `close_bug` · `confirm_bug` · `activate_bug` · `assign_bug` · `add_bug_comment` · `get_bug_history` · `download_bug_attachments` |
|
|
21
|
+
| **Test cases** | `list_product_cases` · `get_case` · `create_case` · `update_case` · `delete_case` · `create_case_result` · `list_case_results` · `list_product_testsuites` · `list_product_testtasks` |
|
|
22
|
+
| **Test reports** | `list_product_testreports` · `get_testreport` · `create_testreport` · `delete_testreport` |
|
|
23
|
+
| **Builds** | `list_product_builds` · `list_execution_builds` · `get_build` · `create_build` · `delete_build` |
|
|
24
|
+
| **Releases** | `list_product_releases` · `get_release` · `create_release` · `delete_release` |
|
|
25
|
+
| **Repos** | `list_repos` · `get_repo` · `list_repo_branches` · `list_repo_commits` |
|
|
26
|
+
| **Docs** | `list_doclibs` · `list_doclib_docs` · `get_doc` · `create_doc` · `update_doc` · `delete_doc` |
|
|
27
|
+
| **Effort** | `log_effort` · `list_efforts` · `update_effort` · `delete_effort` |
|
|
28
|
+
| **Todos** | `list_todos` · `get_todo` · `create_todo` · `update_todo` · `finish_todo` · `delete_todo` |
|
|
29
|
+
| **Issues** | `list_project_issues` · `get_issue` · `create_issue` · `update_issue` · `resolve_issue` · `close_issue` · `delete_issue` |
|
|
30
|
+
| **Risks** | `list_project_risks` · `get_risk` · `create_risk` · `update_risk` · `delete_risk` |
|
|
31
|
+
| **Escape hatch** | `zentao_request` — arbitrary `GET / POST / PUT / DELETE` against `/api.php/v1/...`, for endpoints with no explicit wrapper |
|
|
32
|
+
|
|
33
|
+
**On the product scope.** Most ZenTao v1 REST endpoints for bugs / stories / cases / builds / releases are product-scoped. The usual first step is `list_products` to grab a productId.
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- Node.js ≥ 18
|
|
38
|
+
- A ZenTao 20.x instance with the v1 REST API enabled, reachable from this machine
|
|
39
|
+
- A developer account (username + password), or a pre-issued token
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install -g @zentao-hub/mcp
|
|
45
|
+
# Or, inside this monorepo: `pnpm install && pnpm run build` from the repo root.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Executable entry point: `zentao-hub-mcp` (i.e. `dist/index.js`).
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
The recommended path is to log in once via the sister CLI
|
|
53
|
+
[`@zentao-hub/cli`](../cli) and let the MCP server pick the credentials
|
|
54
|
+
up automatically:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install -g @zentao-hub/cli @zentao-hub/mcp
|
|
58
|
+
zentao-hub login # default profile
|
|
59
|
+
zentao-hub login --profile personal # additional ZenTao instance
|
|
60
|
+
zentao-hub profiles # list and pick a default
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The CLI stores the token + URL + username in `~/.zentao-hub/credentials.json`
|
|
64
|
+
(mode 0600) and caches the password in the OS keychain (macOS Keychain /
|
|
65
|
+
libsecret on Linux / Credential Manager on Windows). The MCP server reads
|
|
66
|
+
that file when the env vars below are not set and silently refreshes
|
|
67
|
+
expired tokens using the cached password.
|
|
68
|
+
|
|
69
|
+
### Environment variables
|
|
70
|
+
|
|
71
|
+
Env vars take precedence over the credentials file, so CI/CD setups stay
|
|
72
|
+
explicit.
|
|
73
|
+
|
|
74
|
+
| Env var | When to use | Description |
|
|
75
|
+
| ----------------------- | -------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
76
|
+
| `ZENTAO_PROFILE` | Multiple profiles configured | Which profile to use. Defaults to the file's `default`, or to the sole profile if only one is configured. |
|
|
77
|
+
| `ZENTAO_URL` | Skipping the credentials file | ZenTao base URL, e.g. `https://zentao.example.com` (no trailing `/`). |
|
|
78
|
+
| `ZENTAO_TOKEN` | One of: token, or username+password | Pre-issued token. Auto-refresh disabled in this mode. |
|
|
79
|
+
| `ZENTAO_USERNAME` | Together with password | Account; exchanged for a token at startup, silently re-logged in on 401. |
|
|
80
|
+
| `ZENTAO_PASSWORD` | Together with username | Password. |
|
|
81
|
+
| `ZENTAO_TIMEOUT_MS` | Optional | Per-request timeout in milliseconds, default `15000`. |
|
|
82
|
+
| `ZENTAO_HUB_CREDENTIALS`| Optional | Override credentials file location (default: `~/.zentao-hub/credentials.json`). |
|
|
83
|
+
|
|
84
|
+
## Wiring it up to an MCP client
|
|
85
|
+
|
|
86
|
+
`@zentao-hub/mcp` is a vanilla stdio MCP server, so it drops into any
|
|
87
|
+
MCP-capable client. The sister CLI knows the config file format and location
|
|
88
|
+
for the major agents and writes the entry for you:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm install -g @zentao-hub/cli
|
|
92
|
+
zentao-hub login # cache credentials once (shared)
|
|
93
|
+
zentao-hub install mcp --agent claude # claude | copilot | copilot-cli | codex
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| `--agent` | Tool | Writes to |
|
|
97
|
+
| ------------- | --------------------------- | ---------------------------------------------------------------------------------------- |
|
|
98
|
+
| `claude` | Claude Code / Desktop | `~/.claude.json` (user) or `<repo>/.mcp.json` (workspace) |
|
|
99
|
+
| `copilot` | GitHub Copilot (VS Code) | `User/mcp.json` (user) or `<repo>/.vscode/mcp.json` (workspace) |
|
|
100
|
+
| `copilot-cli` | GitHub Copilot CLI | `~/.copilot/mcp-config.json` (user) or `<repo>/.mcp.json` (workspace, shared with Claude)|
|
|
101
|
+
| `codex` | OpenAI Codex CLI | `~/.codex/config.toml` (user-only) |
|
|
102
|
+
|
|
103
|
+
For everything else (Cline, Continue, custom clients) hand-edit the config —
|
|
104
|
+
the shape is `command: "zentao-hub-mcp"` with no required args, see the
|
|
105
|
+
Claude Code example below.
|
|
106
|
+
|
|
107
|
+
### Claude Code / Claude Desktop — using `zentao-hub login`
|
|
108
|
+
|
|
109
|
+
After `zentao-hub login` the env block can be omitted entirely:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"mcpServers": {
|
|
114
|
+
"zentao-hub": {
|
|
115
|
+
"command": "zentao-hub-mcp"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
With multiple profiles, pin one per MCP server entry:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"mcpServers": {
|
|
126
|
+
"zentao-hub": { "command": "zentao-hub-mcp" },
|
|
127
|
+
"zentao-hub-personal": { "command": "zentao-hub-mcp", "env": { "ZENTAO_PROFILE": "personal" } }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Claude Code / Claude Desktop — env-only (no CLI, e.g. for CI)
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"mcpServers": {
|
|
137
|
+
"zentao-hub": {
|
|
138
|
+
"command": "zentao-hub-mcp",
|
|
139
|
+
"env": {
|
|
140
|
+
"ZENTAO_URL": "https://zentao.example.com",
|
|
141
|
+
"ZENTAO_USERNAME": "your-account",
|
|
142
|
+
"ZENTAO_PASSWORD": "your-password"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Claude Code CLI (env-only example):
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
claude mcp add zentao-hub \
|
|
153
|
+
--env ZENTAO_URL=https://zentao.example.com \
|
|
154
|
+
--env ZENTAO_USERNAME=your-account \
|
|
155
|
+
--env ZENTAO_PASSWORD=your-password \
|
|
156
|
+
-- zentao-hub-mcp
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Usage conventions
|
|
160
|
+
|
|
161
|
+
- **List tools default to `limit=20`**, max 200. `page` is 1-based.
|
|
162
|
+
- **`list_my_bugs` / `list_my_tasks`** filter on the server side by `assignedTo.account == <current account>`, bypassing ZenTao v1's unreliable query parameters. The scan cap is `maxScan` (default 1000); the result includes a `truncated` flag.
|
|
163
|
+
- **State transitions use dedicated action tools** (`resolve_bug`, `finish_task`, `close_story`, ...) instead of `update_*`. ZenTao relies on `/{entity}/{id}/{action}` endpoints to drive its state machine and history records. `update_*` only changes fields and does not trigger state transitions.
|
|
164
|
+
- **`zentao_request` is the escape hatch.** Any endpoint without a typical wrapper (custom workflows, third-party extensions, endpoints added in newer versions) can be called directly:
|
|
165
|
+
```json
|
|
166
|
+
{"method": "GET", "path": "/products/27/testreports", "query": {"limit": 10}}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Behavior details
|
|
170
|
+
|
|
171
|
+
- **Automatic token refresh.** Any tool that gets a 401 clears the cached token, re-logs in once, and retries. In `ZENTAO_TOKEN`-only mode (no credentials to refresh with), the 401 is re-thrown as-is.
|
|
172
|
+
- **Compatible list shapes.** List tools auto-adapt to ZenTao's various wrappers (`{bugs:[...]}`, `{products:[...]}`, `{data:[...]}`, `{list:[...]}`, or a bare array) and try to extract `total`.
|
|
173
|
+
- **Summary field trimming.** List tools return only commonly used fields by default (id / title / status / assignedTo / openedDate / ...). Use the corresponding `get_*` for full details.
|
|
174
|
+
- **`download_bug_attachments`** additionally parses `<img src>` tags inside `bug.steps` to download inline screenshots into `destDir/inline/`. A single failure does not abort the rest.
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# From the monorepo root (uses pnpm)
|
|
180
|
+
corepack enable
|
|
181
|
+
pnpm install
|
|
182
|
+
pnpm run build # build everything
|
|
183
|
+
pnpm run dev # watch everything
|
|
184
|
+
|
|
185
|
+
# Only this package
|
|
186
|
+
pnpm --filter @zentao-hub/mcp run build
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Smoke test (no real ZenTao instance required):
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
ZENTAO_URL=http://example.invalid ZENTAO_TOKEN=fake \
|
|
193
|
+
node dist/index.js
|
|
194
|
+
# Send JSON-RPC `initialize` + `tools/list` over stdin to verify.
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Source layout
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
src/
|
|
201
|
+
├── index.ts entry point + stdio transport + tool registration
|
|
202
|
+
├── zentao-client.ts HTTP client: login, token refresh, timeout, download
|
|
203
|
+
└── tools/
|
|
204
|
+
├── index.ts aggregator: buildTools(client)
|
|
205
|
+
├── helpers.ts shared schema fragments, summary-key sets, JSON extraction
|
|
206
|
+
├── request.ts zentao_request escape hatch
|
|
207
|
+
├── users.ts whoami / users / departments
|
|
208
|
+
├── products.ts products / branches / modules / plans
|
|
209
|
+
├── projects.ts projects / project executions / project products
|
|
210
|
+
├── programs.ts programs
|
|
211
|
+
├── executions.ts executions + execution tasks/stories/team
|
|
212
|
+
├── stories.ts stories CRUD + change/review/close/activate/assign/comment
|
|
213
|
+
├── tasks.ts tasks CRUD + start/finish/pause/activate/close/cancel/assign/comment
|
|
214
|
+
├── bugs.ts bugs CRUD + resolve/close/confirm/activate/assign/comment/history/attachments
|
|
215
|
+
├── cases.ts cases / test results / testsuites / testtasks
|
|
216
|
+
├── testreports.ts test reports
|
|
217
|
+
├── builds.ts product/execution builds
|
|
218
|
+
├── releases.ts product releases
|
|
219
|
+
├── repos.ts code repos + branches + commits
|
|
220
|
+
├── docs.ts doclibs / docs
|
|
221
|
+
├── efforts.ts effort (work-hour) logs
|
|
222
|
+
├── todos.ts personal todos
|
|
223
|
+
├── issues.ts project issues
|
|
224
|
+
└── risks.ts project risks
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Adding a new tool: drop a `defineTool({ description, schema, handler })` into the matching entity module — it gets picked up automatically by the `build*Tools(ctx)` calls in `tools/index.ts`.
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
[MIT](../../LICENSE) © Deven Liu
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# @zentao-hub/mcp
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 简体中文
|
|
4
|
+
|
|
5
|
+
面向 **禅道 20.x 开源版** 的 [Model Context Protocol](https://modelcontextprotocol.io) 服务器,覆盖禅道 v1 REST API 的常用实体(产品 / 项目 / 计划 / 迭代 / 需求 / 任务 / Bug / 测试用例 / 构建 / 发布 / 代码库 / 用户 / 文档),加一个通用的 `zentao_request` 逃生口兜底未包装的端点。
|
|
6
|
+
|
|
7
|
+
> 想要 hub 目录、slash command、commit-msg hook 这些自动化协作工件,看姐妹包 [`@zentao-hub/cli`](../cli)。本包只做 MCP server 本身。
|
|
8
|
+
|
|
9
|
+
## 工具总览(129 个)
|
|
10
|
+
|
|
11
|
+
| 实体 | 工具 |
|
|
12
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| **用户/部门** | `whoami` · `list_users` · `get_user` · `list_departments` · `list_department_users` |
|
|
14
|
+
| **产品** | `list_products` · `get_product` · `create_product` · `update_product` · `delete_product` · `list_product_branches` · `list_product_modules` · `list_product_plans` · `list_product_executions` |
|
|
15
|
+
| **项目** | `list_projects` · `get_project` · `create_project` · `update_project` · `delete_project` · `list_project_executions` · `list_project_products` |
|
|
16
|
+
| **项目集** | `list_programs` · `get_program` · `create_program` · `delete_program` |
|
|
17
|
+
| **执行/迭代** | `list_executions` · `get_execution` · `create_execution` · `update_execution` · `delete_execution` · `start_execution` · `close_execution` · `list_execution_tasks` · `list_execution_stories` · `list_execution_team` |
|
|
18
|
+
| **需求** | `list_product_stories` · `get_story` · `create_story` · `update_story` · `delete_story` · `change_story` · `review_story` · `close_story` · `activate_story` · `assign_story` · `add_story_comment` |
|
|
19
|
+
| **任务** | `get_task` · `list_my_tasks` · `create_task` · `update_task` · `delete_task` · `start_task` · `finish_task` · `pause_task` · `activate_task` · `close_task` · `cancel_task` · `assign_task` · `add_task_comment` |
|
|
20
|
+
| **Bug** | `list_my_bugs` · `list_product_bugs` · `list_execution_bugs` · `get_bug` · `create_bug` · `update_bug` · `delete_bug` · `resolve_bug` · `close_bug` · `confirm_bug` · `activate_bug` · `assign_bug` · `add_bug_comment` · `get_bug_history` · `download_bug_attachments` |
|
|
21
|
+
| **测试用例** | `list_product_cases` · `get_case` · `create_case` · `update_case` · `delete_case` · `create_case_result` · `list_case_results` · `list_product_testsuites` · `list_product_testtasks` |
|
|
22
|
+
| **测试报告** | `list_product_testreports` · `get_testreport` · `create_testreport` · `delete_testreport` |
|
|
23
|
+
| **构建** | `list_product_builds` · `list_execution_builds` · `get_build` · `create_build` · `delete_build` |
|
|
24
|
+
| **发布** | `list_product_releases` · `get_release` · `create_release` · `delete_release` |
|
|
25
|
+
| **代码库** | `list_repos` · `get_repo` · `list_repo_branches` · `list_repo_commits` |
|
|
26
|
+
| **文档** | `list_doclibs` · `list_doclib_docs` · `get_doc` · `create_doc` · `update_doc` · `delete_doc` |
|
|
27
|
+
| **工时(effort)** | `log_effort` · `list_efforts` · `update_effort` · `delete_effort` |
|
|
28
|
+
| **待办** | `list_todos` · `get_todo` · `create_todo` · `update_todo` · `finish_todo` · `delete_todo` |
|
|
29
|
+
| **问题** | `list_project_issues` · `get_issue` · `create_issue` · `update_issue` · `resolve_issue` · `close_issue` · `delete_issue` |
|
|
30
|
+
| **风险** | `list_project_risks` · `get_risk` · `create_risk` · `update_risk` · `delete_risk` |
|
|
31
|
+
| **逃生口** | `zentao_request` — 任意 `GET / POST / PUT / DELETE` 到 `/api.php/v1/...`,覆盖没显式包装的端点 |
|
|
32
|
+
|
|
33
|
+
**关于 product 作用域**:禅道 v1 REST API 大多 bug / story / case / build / release 接口都按产品作用域。第一步通常先 `list_products` 拿 productId。
|
|
34
|
+
|
|
35
|
+
## 环境要求
|
|
36
|
+
|
|
37
|
+
- Node.js ≥ 18
|
|
38
|
+
- 一台开启了 v1 REST API 的禅道 20.x 实例,并能从本机访问
|
|
39
|
+
- 一个开发者账号(账号 / 密码),或一个已签发的 token
|
|
40
|
+
|
|
41
|
+
## 安装
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install -g @zentao-hub/mcp
|
|
45
|
+
# 或在 monorepo 内:从根目录 `pnpm install && pnpm run build`
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
可执行入口:`zentao-hub-mcp`(即 `dist/index.js`)。
|
|
49
|
+
|
|
50
|
+
## 配置
|
|
51
|
+
|
|
52
|
+
推荐做法:通过姊妹 CLI [`@zentao-hub/cli`](../cli) 登录一次,MCP 服务自动读取:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install -g @zentao-hub/cli @zentao-hub/mcp
|
|
56
|
+
zentao-hub login # 默认 profile
|
|
57
|
+
zentao-hub login --profile personal # 多个禅道实例时再加一个
|
|
58
|
+
zentao-hub profiles # 列出并切换默认 profile
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
CLI 把 URL + 用户名 + token 存到 `~/.zentao-hub/credentials.json`(权限 0600),
|
|
62
|
+
密码缓存在系统 keychain(macOS Keychain / Linux libsecret / Windows
|
|
63
|
+
Credential Manager)。MCP server 在环境变量未设置时自动读取,token 过期时
|
|
64
|
+
用缓存密码静默续期。
|
|
65
|
+
|
|
66
|
+
### 环境变量
|
|
67
|
+
|
|
68
|
+
环境变量优先级高于凭据文件,CI/CD 场景仍可显式注入。
|
|
69
|
+
|
|
70
|
+
| 环境变量 | 何时使用 | 说明 |
|
|
71
|
+
| ----------------------- | ------------------------------------- | -------------------------------------------------------------------- |
|
|
72
|
+
| `ZENTAO_PROFILE` | 配置了多个 profile | 选用哪个 profile。默认使用文件中的 `default`,若只有 1 个 profile 直接用它。 |
|
|
73
|
+
| `ZENTAO_URL` | 不使用凭据文件时 | 禅道地址,例如 `https://zentao.example.com`(结尾不要 `/`)。 |
|
|
74
|
+
| `ZENTAO_TOKEN` | 与(用户名+密码)二选一 | 已签发的 token。此模式下自动续期关闭。 |
|
|
75
|
+
| `ZENTAO_USERNAME` | 与密码同时提供 | 账号;启动时换取 token,401 时自动重新登录。 |
|
|
76
|
+
| `ZENTAO_PASSWORD` | 与账号同时提供 | 密码。 |
|
|
77
|
+
| `ZENTAO_TIMEOUT_MS` | 可选 | 单次请求超时,毫秒,默认 `15000`。 |
|
|
78
|
+
| `ZENTAO_HUB_CREDENTIALS`| 可选 | 覆盖凭据文件路径(默认 `~/.zentao-hub/credentials.json`)。 |
|
|
79
|
+
|
|
80
|
+
## 接入 MCP 客户端
|
|
81
|
+
|
|
82
|
+
`@zentao-hub/mcp` 是一个标准的 stdio MCP server,可接入任何 MCP-capable client。
|
|
83
|
+
姊妹 CLI 内置了主流 agent 的配置格式与位置,可一条命令注册:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm install -g @zentao-hub/cli
|
|
87
|
+
zentao-hub login # 凭据缓存一次,所有 agent 共用
|
|
88
|
+
zentao-hub install mcp --agent claude # claude | copilot | copilot-cli | codex
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
| `--agent` | 对应工具 | 写入文件 |
|
|
92
|
+
| ------------- | ------------------------- | ------------------------------------------------------------------------------------- |
|
|
93
|
+
| `claude` | Claude Code / Desktop | `~/.claude.json`(user)或 `<repo>/.mcp.json`(workspace) |
|
|
94
|
+
| `copilot` | GitHub Copilot(VS Code) | `User/mcp.json`(user)或 `<repo>/.vscode/mcp.json`(workspace) |
|
|
95
|
+
| `copilot-cli` | GitHub Copilot CLI | `~/.copilot/mcp-config.json`(user)或 `<repo>/.mcp.json`(workspace,与 Claude 共用)|
|
|
96
|
+
| `codex` | OpenAI Codex CLI | `~/.codex/config.toml`(仅 user) |
|
|
97
|
+
|
|
98
|
+
其他 client(Cline、Continue、自研客户端等)手工编辑 config 即可——形如
|
|
99
|
+
`command: "zentao-hub-mcp"`,无需必填参数,参考下面的 Claude Code 示例。
|
|
100
|
+
|
|
101
|
+
### Claude Code / Claude Desktop — 使用 `zentao-hub login`
|
|
102
|
+
|
|
103
|
+
`zentao-hub login` 完成后 env 块完全可省略:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"mcpServers": {
|
|
108
|
+
"zentao-hub": {
|
|
109
|
+
"command": "zentao-hub-mcp"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
配置了多个 profile 时,给每个 MCP server 条目指定一个:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"mcpServers": {
|
|
120
|
+
"zentao-hub": { "command": "zentao-hub-mcp" },
|
|
121
|
+
"zentao-hub-personal": { "command": "zentao-hub-mcp", "env": { "ZENTAO_PROFILE": "personal" } }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Claude Code / Claude Desktop — 纯环境变量(不依赖 CLI,例如 CI)
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"mcpServers": {
|
|
131
|
+
"zentao-hub": {
|
|
132
|
+
"command": "zentao-hub-mcp",
|
|
133
|
+
"env": {
|
|
134
|
+
"ZENTAO_URL": "https://zentao.example.com",
|
|
135
|
+
"ZENTAO_USERNAME": "你的账号",
|
|
136
|
+
"ZENTAO_PASSWORD": "你的密码"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Claude Code CLI(纯环境变量示例):
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
claude mcp add zentao-hub \
|
|
147
|
+
--env ZENTAO_URL=https://zentao.example.com \
|
|
148
|
+
--env ZENTAO_USERNAME=你的账号 \
|
|
149
|
+
--env ZENTAO_PASSWORD=你的密码 \
|
|
150
|
+
-- zentao-hub-mcp
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 用法约定
|
|
154
|
+
|
|
155
|
+
- **列表工具默认 `limit=20`**,最大 200。`page` 1-based。
|
|
156
|
+
- **list_my_bugs / list_my_tasks** 在服务端按 `assignedTo.account == <当前账号>` 过滤,绕开禅道 v1 不可靠的查询参数;扫描上限由 `maxScan`(默认 1000)控制,结果中 `truncated` 标记是否截断。
|
|
157
|
+
- **状态转移用专用动作工具**(`resolve_bug`、`finish_task`、`close_story` …),而不是 `update_*` —— 禅道靠 `/{entity}/{id}/{action}` 端点驱动状态机和历史记录。`update_*` 只改字段,不触发状态变更。
|
|
158
|
+
- **`zentao_request` 是逃生口**:任何没典型 wrapper 的端点(自定义工作流、第三方扩展、新版本里新增端点)都可以直接调,例如:
|
|
159
|
+
```json
|
|
160
|
+
{"method": "GET", "path": "/products/27/testreports", "query": {"limit": 10}}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## 行为细节
|
|
164
|
+
|
|
165
|
+
- **Token 自动刷新**:任何工具收到 401 都会清掉缓存 token、重登一次、重试。仅 `ZENTAO_TOKEN` 模式(没账号密码可刷新)下 401 会原样抛回。
|
|
166
|
+
- **列表结构兼容**:list 工具自动适配 `{bugs:[...]}` / `{products:[...]}` / `{data:[...]}` / `{list:[...]}` / 裸数组等多种禅道返回包装,并尽量提取 `total`。
|
|
167
|
+
- **summary 字段裁剪**:list 工具默认只回常用字段(id / title / status / assignedTo / openedDate …)。要全字段就用对应的 `get_*`。
|
|
168
|
+
- **`download_bug_attachments`** 还会解析 bug.steps 里的 `<img src>` 把内联截图也下载到 `destDir/inline/`,单个失败不中断。
|
|
169
|
+
|
|
170
|
+
## 开发
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# 在 monorepo 根目录(使用 pnpm)
|
|
174
|
+
corepack enable
|
|
175
|
+
pnpm install
|
|
176
|
+
pnpm run build # 全部构建
|
|
177
|
+
pnpm run dev # 全部 watch
|
|
178
|
+
|
|
179
|
+
# 仅本包
|
|
180
|
+
pnpm --filter @zentao-hub/mcp run build
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
冒烟测试(不需要真实禅道实例):
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
ZENTAO_URL=http://example.invalid ZENTAO_TOKEN=fake \
|
|
187
|
+
node dist/index.js
|
|
188
|
+
# 通过 stdin 发 JSON-RPC `initialize` + `tools/list` 验证。
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## 源码组织
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
src/
|
|
195
|
+
├── index.ts 入口 + stdio transport + 工具注册
|
|
196
|
+
├── zentao-client.ts HTTP 客户端:登录、token 刷新、超时、下载
|
|
197
|
+
└── tools/
|
|
198
|
+
├── index.ts 聚合 buildTools(client)
|
|
199
|
+
├── helpers.ts 共用 schema 片段、summary key 集合、JSON 提取
|
|
200
|
+
├── request.ts zentao_request 逃生口
|
|
201
|
+
├── users.ts whoami / users / departments
|
|
202
|
+
├── products.ts products / branches / modules / plans
|
|
203
|
+
├── projects.ts projects / project executions / project products
|
|
204
|
+
├── programs.ts programs
|
|
205
|
+
├── executions.ts executions + execution tasks/stories/team
|
|
206
|
+
├── stories.ts stories CRUD + change/review/close/activate/assign/comment
|
|
207
|
+
├── tasks.ts tasks CRUD + start/finish/pause/activate/close/cancel/assign/comment
|
|
208
|
+
├── bugs.ts bugs CRUD + resolve/close/confirm/activate/assign/comment/history/attachments
|
|
209
|
+
├── cases.ts cases / test results / testsuites / testtasks
|
|
210
|
+
├── testreports.ts test reports
|
|
211
|
+
├── builds.ts product/execution builds
|
|
212
|
+
├── releases.ts product releases
|
|
213
|
+
├── repos.ts code repos + branches + commits
|
|
214
|
+
├── docs.ts doclibs / docs
|
|
215
|
+
├── efforts.ts effort (work-hour) logs
|
|
216
|
+
├── todos.ts personal todos
|
|
217
|
+
├── issues.ts project issues
|
|
218
|
+
└── risks.ts project risks
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
加新工具:在对应实体模块里加一个 `defineTool({ description, schema, handler })`,然后会被 `tools/index.ts` 的 `build*Tools(ctx)` 自动带进来。
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
[MIT](../../LICENSE) © Deven Liu
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
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 { ZentaoApiError, createClientFromConfig, loadZentaoConfig, } from "@zentao-hub/sdk";
|
|
5
|
+
import { buildTools } from "./tools/index.js";
|
|
6
|
+
function toToolResult(value) {
|
|
7
|
+
return {
|
|
8
|
+
content: [
|
|
9
|
+
{
|
|
10
|
+
type: "text",
|
|
11
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2),
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function toErrorResult(err) {
|
|
17
|
+
const msg = err instanceof ZentaoApiError
|
|
18
|
+
? `${err.message}${err.body ? `\n${JSON.stringify(err.body, null, 2).slice(0, 1000)}` : ""}`
|
|
19
|
+
: err instanceof Error
|
|
20
|
+
? err.message
|
|
21
|
+
: String(err);
|
|
22
|
+
return {
|
|
23
|
+
isError: true,
|
|
24
|
+
content: [{ type: "text", text: msg }],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async function main() {
|
|
28
|
+
const config = loadZentaoConfig();
|
|
29
|
+
const client = createClientFromConfig(config);
|
|
30
|
+
const tools = buildTools(client);
|
|
31
|
+
const server = new McpServer({
|
|
32
|
+
name: "zentao-hub",
|
|
33
|
+
version: "0.3.0",
|
|
34
|
+
});
|
|
35
|
+
for (const [name, def] of Object.entries(tools)) {
|
|
36
|
+
server.registerTool(name, {
|
|
37
|
+
description: def.description,
|
|
38
|
+
inputSchema: def.schema.shape,
|
|
39
|
+
}, async (args) => {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = def.schema.parse(args);
|
|
42
|
+
// The handler signatures are tied to each tool's own schema; the cast
|
|
43
|
+
// bridges the heterogeneous union back to the per-tool argument type.
|
|
44
|
+
const result = await def.handler(parsed);
|
|
45
|
+
return toToolResult(result);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
return toErrorResult(err);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const transport = new StdioServerTransport();
|
|
53
|
+
await server.connect(transport);
|
|
54
|
+
// stdio transport keeps the process alive via stdin; nothing else to do here.
|
|
55
|
+
}
|
|
56
|
+
main().catch((err) => {
|
|
57
|
+
// stderr is safe — stdio transport reserves stdout for the JSON-RPC channel.
|
|
58
|
+
console.error("zentao-hub-mcp failed to start:", err);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
});
|
|
61
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|