@zzp123/mcp-zentao 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 bigtian
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,294 @@
1
+ # MCP-Zentao
2
+
3
+ 禅道项目管理系统的高级API集成包,提供任务管理、Bug跟踪等功能的完整封装,专为Cursor IDE设计的MCP扩展。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install @bigtian/mcp-zentao -g
9
+ ```
10
+
11
+ ## 使用方法
12
+
13
+ ### 首次使用(配置禅道信息)
14
+
15
+ 首次使用时,需要提供禅道的配置信息:
16
+
17
+ ```bash
18
+ zentao '{"config":{"url":"https://your-zentao-url","username":"your-username","password":"your-password","apiVersion":"v1"},"name":"张三","age":25,"skills":["编程","设计"]}'
19
+ ```
20
+
21
+ 配置信息会被保存在用户目录下的 `.zentao/config.json` 文件中,后续使用时无需再次提供。
22
+
23
+ ### 后续使用
24
+
25
+ 配置完成后,只需要提供任务相关的信息即可:
26
+
27
+ ```bash
28
+ zentao '{"name":"张三","age":25,"skills":["编程","设计"]}'
29
+ ```
30
+
31
+ ### 更新配置
32
+
33
+ 如果需要更新禅道配置信息,只需要再次提供 config 参数即可:
34
+
35
+ ```bash
36
+ zentao '{"config":{"url":"https://new-zentao-url","username":"new-username","password":"new-password","apiVersion":"v1"},"name":"张三","age":25,"skills":["编程","设计"]}'
37
+ ```
38
+
39
+ ## 配置文件位置
40
+
41
+ 配置文件保存在用户目录下的 `.zentao/config.json` 文件中:
42
+
43
+ - Windows: `C:\Users\你的用户名\.zentao\config.json`
44
+ - macOS/Linux: `~/.zentao/config.json`
45
+
46
+ ## 功能特性
47
+
48
+ - 支持配置信息的持久化存储
49
+ - 自动管理禅道API的认证信息
50
+ - 提供任务创建、更新、完成等功能
51
+ - 支持Bug跟踪和处理
52
+ - 完整的类型定义支持
53
+
54
+ ## 注意事项
55
+
56
+ - 配置文件中包含敏感信息,请确保文件权限设置正确
57
+ - 建议定期更新密码,以确保安全性
58
+ - 如遇到问题,可以删除配置文件重新配置
59
+
60
+ ## 许可证
61
+
62
+ MIT
63
+
64
+ ## 特点
65
+
66
+ - 完整的禅道API封装
67
+ - 简单易用的接口设计
68
+ - 类型安全(TypeScript支持)
69
+ - 完善的错误处理
70
+ - 自动化的认证管理
71
+
72
+ ## 与其他项目的区别
73
+
74
+ 不同于通用的数据库操作工具(如 mcp-mysql-server),本项目专注于提供:
75
+
76
+ 1. 禅道系统特定的业务功能
77
+ 2. 高级别的API抽象
78
+ 3. 完整的禅道工作流支持
79
+ 4. 开箱即用的禅道集成方案
80
+
81
+ ## 本地开发
82
+
83
+ 1. 克隆仓库
84
+ ```bash
85
+ git clone https://github.com/bigtian/mcp-zentao.git
86
+ cd mcp-zentao
87
+ ```
88
+
89
+ 2. 安装依赖
90
+ ```bash
91
+ npm install
92
+ ```
93
+
94
+ 3. 运行测试
95
+ ```bash
96
+ npm test
97
+ ```
98
+
99
+ 4. 构建项目
100
+ ```bash
101
+ npm run build
102
+ ```
103
+
104
+ ## Docker 使用
105
+
106
+ ### 使用 docker-compose(推荐)
107
+
108
+ 1. 复制环境变量模板并修改配置
109
+ ```bash
110
+ cp .env.example .env
111
+ # 编辑 .env 文件,填入你的禅道系统配置
112
+ ```
113
+
114
+ 2. 启动服务
115
+ ```bash
116
+ docker-compose up -d
117
+ ```
118
+
119
+ 3. 查看日志
120
+ ```bash
121
+ docker-compose logs -f
122
+ ```
123
+
124
+ ### 手动使用 Docker
125
+
126
+ 1. 构建镜像
127
+ ```bash
128
+ docker build -t mcp-zentao .
129
+ ```
130
+
131
+ 2. 运行容器
132
+ ```bash
133
+ docker run -d \
134
+ --name mcp-zentao \
135
+ -p 3000:3000 \
136
+ -e ZENTAO_URL=your-zentao-url \
137
+ -e ZENTAO_USERNAME=your-username \
138
+ -e ZENTAO_PASSWORD=your-password \
139
+ -e ZENTAO_API_VERSION=v1 \
140
+ -v $(pwd)/logs:/app/logs \
141
+ mcp-zentao
142
+ ```
143
+
144
+ ### 在 Cursor IDE 中配置
145
+
146
+ 在 Cursor IDE 的配置文件中添加以下配置:
147
+
148
+ ```json
149
+ {
150
+ "mcpServers": {
151
+ "zentao": {
152
+ "url": "http://localhost:3000"
153
+ }
154
+ }
155
+ }
156
+ ```
157
+
158
+ ## 基本使用
159
+
160
+ ```typescript
161
+ import { ZentaoAPI } from '@bigtian/mcp-zentao';
162
+
163
+ // 创建API实例
164
+ const api = new ZentaoAPI({
165
+ url: 'https://your-zentao-url', // 你的禅道系统URL
166
+ username: 'your-username', // 用户名
167
+ password: 'your-password', // 密码
168
+ apiVersion: 'v1' // API版本,默认为v1
169
+ });
170
+
171
+ // 获取我的任务列表
172
+ async function getMyTasks() {
173
+ try {
174
+ const tasks = await api.getMyTasks();
175
+ console.log('我的任务:', tasks);
176
+ } catch (error) {
177
+ console.error('获取任务失败:', error);
178
+ }
179
+ }
180
+
181
+ // 获取我的Bug列表
182
+ async function getMyBugs() {
183
+ try {
184
+ const bugs = await api.getMyBugs();
185
+ console.log('我的Bug:', bugs);
186
+ } catch (error) {
187
+ console.error('获取Bug失败:', error);
188
+ }
189
+ }
190
+
191
+ // 完成任务
192
+ async function finishTask(taskId: number) {
193
+ try {
194
+ await api.finishTask(taskId);
195
+ console.log('任务已完成');
196
+ } catch (error) {
197
+ console.error('完成任务失败:', error);
198
+ }
199
+ }
200
+
201
+ // 解决Bug
202
+ async function resolveBug(bugId: number) {
203
+ try {
204
+ await api.resolveBug(bugId, {
205
+ resolution: 'fixed',
206
+ resolvedBuild: 'trunk',
207
+ comment: '问题已修复'
208
+ });
209
+ console.log('Bug已解决');
210
+ } catch (error) {
211
+ console.error('解决Bug失败:', error);
212
+ }
213
+ }
214
+ ```
215
+
216
+ ## API文档
217
+
218
+ ### ZentaoAPI 类
219
+
220
+ #### 构造函数
221
+
222
+ ```typescript
223
+ constructor(config: {
224
+ url: string; // 禅道系统URL
225
+ username: string; // 用户名
226
+ password: string; // 密码
227
+ apiVersion?: string; // API版本,默认为v1
228
+ })
229
+ ```
230
+
231
+ #### 方法
232
+
233
+ 1. `getMyTasks(): Promise<Task[]>`
234
+ - 获取当前用户的任务列表
235
+ - 返回: Promise<Task[]>
236
+
237
+ 2. `getMyBugs(): Promise<Bug[]>`
238
+ - 获取当前用户的Bug列表
239
+ - 返回: Promise<Bug[]>
240
+
241
+ 3. `finishTask(taskId: number): Promise<void>`
242
+ - 完成指定ID的任务
243
+ - 参数: taskId - 任务ID
244
+ - 返回: Promise<void>
245
+
246
+ 4. `resolveBug(bugId: number, resolution: BugResolution): Promise<void>`
247
+ - 解决指定ID的Bug
248
+ - 参数:
249
+ - bugId - Bug ID
250
+ - resolution - Bug解决方案
251
+ - 返回: Promise<void>
252
+
253
+ ### 类型定义
254
+
255
+ ```typescript
256
+ interface Task {
257
+ id: number;
258
+ name: string;
259
+ status: string;
260
+ pri: number;
261
+ // ... 其他任务属性
262
+ }
263
+
264
+ interface Bug {
265
+ id: number;
266
+ title: string;
267
+ status: string;
268
+ severity: number;
269
+ // ... 其他Bug属性
270
+ }
271
+
272
+ interface BugResolution {
273
+ resolution: string; // 解决方案类型
274
+ resolvedBuild?: string; // 解决版本
275
+ duplicateBug?: number; // 重复Bug ID
276
+ comment?: string; // 备注
277
+ }
278
+ ```
279
+
280
+ ## 注意事项
281
+
282
+ 1. 确保提供正确的禅道系统URL和API版本
283
+ 2. 用户名和密码需要有相应的API访问权限
284
+ 3. 所有API调用都是异步的,需要使用async/await或Promise处理
285
+ 4. 错误处理建议使用try/catch进行捕获
286
+
287
+ ## 开发环境
288
+
289
+ - Node.js >= 14.0.0
290
+ - TypeScript >= 4.0.0
291
+
292
+ ## 贡献
293
+
294
+ 欢迎提交Issue和Pull Request!
@@ -0,0 +1,38 @@
1
+ import { Bug, BugStatus, CreateTaskRequest, Task, TaskStatus, ZentaoConfig } from '../types/zentao';
2
+ export interface Product {
3
+ id: number;
4
+ name: string;
5
+ code: string;
6
+ status: string;
7
+ desc: string;
8
+ }
9
+ export interface TaskUpdate {
10
+ consumed?: number;
11
+ left?: number;
12
+ status?: TaskStatus;
13
+ finishedDate?: string;
14
+ comment?: string;
15
+ }
16
+ export interface BugResolution {
17
+ resolution: 'fixed' | 'notrepro' | 'duplicate' | 'bydesign' | 'willnotfix' | 'tostory' | 'external';
18
+ resolvedBuild?: string;
19
+ duplicateBug?: number;
20
+ comment?: string;
21
+ }
22
+ export declare class ZentaoAPI {
23
+ private config;
24
+ private client;
25
+ private token;
26
+ constructor(config: ZentaoConfig);
27
+ private getToken;
28
+ private request;
29
+ getMyTasks(status?: TaskStatus): Promise<Task[]>;
30
+ getTaskDetail(taskId: number): Promise<Task>;
31
+ getProducts(): Promise<Product[]>;
32
+ getMyBugs(status?: BugStatus, productId?: number): Promise<Bug[]>;
33
+ getBugDetail(bugId: number): Promise<Bug>;
34
+ updateTask(taskId: number, update: TaskUpdate): Promise<Task>;
35
+ finishTask(taskId: number, update?: TaskUpdate): Promise<Task>;
36
+ resolveBug(bugId: number, resolution: BugResolution): Promise<Bug>;
37
+ createTask(task: CreateTaskRequest): Promise<Task>;
38
+ }
@@ -0,0 +1,230 @@
1
+ import axios from 'axios';
2
+ import { createHash } from 'crypto';
3
+ export class ZentaoAPI {
4
+ config;
5
+ client;
6
+ token = null;
7
+ constructor(config) {
8
+ this.config = config;
9
+ this.client = axios.create({
10
+ baseURL: `${this.config.url}/api.php/${this.config.apiVersion}`,
11
+ timeout: 10000,
12
+ });
13
+ }
14
+ async getToken() {
15
+ if (this.token)
16
+ return this.token;
17
+ const password = createHash('md5')
18
+ .update(this.config.password)
19
+ .digest('hex');
20
+ try {
21
+ console.log('正在请求token...');
22
+ console.log('请求URL:', `${this.config.url}/api.php/${this.config.apiVersion}/tokens`);
23
+ const response = await this.client.post('/tokens', {
24
+ account: this.config.username,
25
+ password,
26
+ });
27
+ console.log('服务器响应:', response.data);
28
+ if (response.status === 200 || response.status === 201) {
29
+ if (typeof response.data === 'object' && response.data.token) {
30
+ this.token = response.data.token;
31
+ return this.token;
32
+ }
33
+ throw new Error(`获取token失败: 响应格式不正确 ${JSON.stringify(response.data)}`);
34
+ }
35
+ throw new Error(`获取token失败: 状态码 ${response.status}`);
36
+ }
37
+ catch (error) {
38
+ if (axios.isAxiosError(error)) {
39
+ const errorMessage = error.response
40
+ ? `状态码: ${error.response.status}, 响应: ${JSON.stringify(error.response.data)}`
41
+ : error.message;
42
+ throw new Error(`登录失败: ${errorMessage}`);
43
+ }
44
+ throw error;
45
+ }
46
+ }
47
+ async request(method, url, params, data) {
48
+ const token = await this.getToken();
49
+ try {
50
+ console.log(`正在请求 ${method} ${url}`, { params, data });
51
+ const response = await this.client.request({
52
+ method,
53
+ url,
54
+ params,
55
+ data,
56
+ headers: { Token: token },
57
+ });
58
+ console.log(`响应状态码: ${response.status}`);
59
+ console.log('响应数据:', response.data);
60
+ return response.data;
61
+ }
62
+ catch (error) {
63
+ if (axios.isAxiosError(error)) {
64
+ console.error('请求失败:', {
65
+ status: error.response?.status,
66
+ data: error.response?.data,
67
+ message: error.message
68
+ });
69
+ throw new Error(`请求失败: ${error.response?.data?.message || error.message}`);
70
+ }
71
+ throw error;
72
+ }
73
+ }
74
+ async getMyTasks(status) {
75
+ const params = {
76
+ assignedTo: this.config.username,
77
+ status: status || 'all',
78
+ };
79
+ const response = await this.request('GET', '/tasks', params);
80
+ return response.tasks;
81
+ }
82
+ async getTaskDetail(taskId) {
83
+ console.log(`正在获取任务 ${taskId} 的详情`);
84
+ const response = await this.request('GET', `/tasks/${taskId}`);
85
+ console.log('任务详情响应:', response);
86
+ if (!response) {
87
+ throw new Error(`获取任务详情失败: 响应为空`);
88
+ }
89
+ // 检查响应格式
90
+ if (response && typeof response === 'object') {
91
+ if ('task' in response) {
92
+ return response.task;
93
+ }
94
+ else {
95
+ // 如果响应本身就是任务对象
96
+ return response;
97
+ }
98
+ }
99
+ throw new Error(`获取任务详情失败: 响应格式不正确 ${JSON.stringify(response)}`);
100
+ }
101
+ async getProducts() {
102
+ try {
103
+ console.log('正在获取产品列表...');
104
+ const response = await this.request('GET', '/products');
105
+ console.log('产品列表响应:', response);
106
+ if (Array.isArray(response)) {
107
+ return response;
108
+ }
109
+ else if (response && typeof response === 'object') {
110
+ if (Array.isArray(response.products)) {
111
+ return response.products;
112
+ }
113
+ }
114
+ throw new Error(`获取产品列表失败: 响应格式不正确 ${JSON.stringify(response)}`);
115
+ }
116
+ catch (error) {
117
+ console.error('获取产品列表失败:', error);
118
+ throw error;
119
+ }
120
+ }
121
+ async getMyBugs(status, productId) {
122
+ if (!productId) {
123
+ // 如果没有提供产品ID,获取第一个可用的产品
124
+ const products = await this.getProducts();
125
+ if (products.length === 0) {
126
+ throw new Error('没有可用的产品');
127
+ }
128
+ productId = products[0].id;
129
+ console.log(`使用第一个可用的产品ID: ${productId}`);
130
+ }
131
+ const params = {
132
+ assignedTo: this.config.username,
133
+ status: status || 'all',
134
+ product: productId
135
+ };
136
+ try {
137
+ console.log('正在获取Bug列表,参数:', params);
138
+ const response = await this.request('GET', '/bugs', params);
139
+ console.log('Bug列表响应:', response);
140
+ if (Array.isArray(response)) {
141
+ return response;
142
+ }
143
+ else if (response && typeof response === 'object' && Array.isArray(response.bugs)) {
144
+ return response.bugs;
145
+ }
146
+ throw new Error(`获取Bug列表失败: 响应格式不正确 ${JSON.stringify(response)}`);
147
+ }
148
+ catch (error) {
149
+ if (error instanceof Error && error.message.includes('Need product id')) {
150
+ throw new Error('获取Bug列表失败: 请提供产品ID');
151
+ }
152
+ console.error('获取Bug列表失败:', error);
153
+ throw error;
154
+ }
155
+ }
156
+ async getBugDetail(bugId) {
157
+ const response = await this.request('GET', `/bugs/${bugId}`);
158
+ return response.bug;
159
+ }
160
+ async updateTask(taskId, update) {
161
+ try {
162
+ console.log(`正在更新任务 ${taskId}...`);
163
+ const response = await this.request('PUT', `/tasks/${taskId}`, undefined, {
164
+ ...update,
165
+ assignedTo: this.config.username,
166
+ });
167
+ console.log('任务更新响应:', response);
168
+ return response;
169
+ }
170
+ catch (error) {
171
+ console.error('更新任务失败:', error);
172
+ throw error;
173
+ }
174
+ }
175
+ async finishTask(taskId, update = {}) {
176
+ try {
177
+ console.log(`正在完成任务 ${taskId}...`);
178
+ const finalUpdate = {
179
+ status: 'done',
180
+ finishedDate: new Date().toISOString(),
181
+ ...update,
182
+ };
183
+ return await this.updateTask(taskId, finalUpdate);
184
+ }
185
+ catch (error) {
186
+ console.error('完成任务失败:', error);
187
+ throw error;
188
+ }
189
+ }
190
+ async resolveBug(bugId, resolution) {
191
+ try {
192
+ console.log(`正在解决Bug ${bugId}...`);
193
+ const response = await this.request('PUT', `/bugs/${bugId}`, undefined, {
194
+ status: 'resolved',
195
+ assignedTo: this.config.username,
196
+ ...resolution,
197
+ resolvedDate: new Date().toISOString(),
198
+ });
199
+ console.log('Bug解决响应:', response);
200
+ return response;
201
+ }
202
+ catch (error) {
203
+ console.error('解决Bug失败:', error);
204
+ throw error;
205
+ }
206
+ }
207
+ async createTask(task) {
208
+ try {
209
+ console.log('正在创建新任务...');
210
+ if (!task.execution) {
211
+ throw new Error('创建任务需要指定执行ID');
212
+ }
213
+ // 将数据转换为表单格式
214
+ const formData = new URLSearchParams();
215
+ Object.entries(task).forEach(([key, value]) => {
216
+ if (value !== undefined && value !== null) {
217
+ formData.append(key, value.toString());
218
+ }
219
+ });
220
+ // 在URL中添加执行ID
221
+ const response = await this.request('POST', `/executions/${task.execution}/tasks`, undefined, formData);
222
+ console.log('创建任务响应:', response);
223
+ return response;
224
+ }
225
+ catch (error) {
226
+ console.error('创建任务失败:', error);
227
+ throw error;
228
+ }
229
+ }
230
+ }
@@ -0,0 +1,9 @@
1
+ export interface ZentaoConfig {
2
+ url: string;
3
+ username: string;
4
+ password: string;
5
+ apiVersion: string;
6
+ }
7
+ export declare function saveConfig(config: ZentaoConfig): void;
8
+ export declare function loadConfig(): ZentaoConfig | null;
9
+ export declare function isConfigured(): boolean;
package/dist/config.js ADDED
@@ -0,0 +1,32 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ // 定义配置文件路径
5
+ const CONFIG_DIR = path.join(os.homedir(), '.zentao');
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
+ // 保存配置
8
+ export function saveConfig(config) {
9
+ // 确保配置目录存在
10
+ if (!fs.existsSync(CONFIG_DIR)) {
11
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
12
+ }
13
+ // 写入配置文件
14
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
15
+ }
16
+ // 读取配置
17
+ export function loadConfig() {
18
+ try {
19
+ if (fs.existsSync(CONFIG_FILE)) {
20
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
21
+ return config;
22
+ }
23
+ }
24
+ catch (error) {
25
+ console.error('读取配置文件失败:', error);
26
+ }
27
+ return null;
28
+ }
29
+ // 检查是否已配置
30
+ export function isConfigured() {
31
+ return fs.existsSync(CONFIG_FILE);
32
+ }
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { ZentaoConfig } from './config.js';
3
+ interface UserParams {
4
+ config?: ZentaoConfig;
5
+ name: string;
6
+ age: number;
7
+ skills: string[];
8
+ }
9
+ export default function main(params: UserParams): Promise<void>;
10
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,165 @@
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 { z } from "zod";
5
+ import { ZentaoAPI } from './api/zentaoApi.js';
6
+ import { loadConfig, saveConfig } from './config.js';
7
+ // 解析命令行参数
8
+ const args = process.argv.slice(2);
9
+ let configData = null;
10
+ // 查找 --config 参数
11
+ const configIndex = args.indexOf('--config');
12
+ if (configIndex !== -1 && configIndex + 1 < args.length) {
13
+ try {
14
+ // 获取 --config 后面的 JSON 字符串并解析
15
+ const jsonStr = args[configIndex + 1];
16
+ configData = JSON.parse(jsonStr);
17
+ console.log('成功解析配置数据:', configData);
18
+ // 如果配置数据中包含 config 对象,则保存配置
19
+ if (configData.config) {
20
+ console.log('正在保存配置...');
21
+ saveConfig(configData.config);
22
+ }
23
+ }
24
+ catch (error) {
25
+ console.error('配置解析失败:', error);
26
+ process.exit(1);
27
+ }
28
+ }
29
+ // Create an MCP server
30
+ const server = new McpServer({
31
+ name: "Zentao API",
32
+ version: "1.0.0"
33
+ });
34
+ // Initialize ZentaoAPI instance
35
+ let zentaoApi = null;
36
+ export default async function main(params) {
37
+ console.log('接收到的参数:', params);
38
+ // 如果传入了配置信息,就保存它
39
+ if (params.config) {
40
+ console.log('保存新的配置信息...');
41
+ saveConfig(params.config);
42
+ }
43
+ }
44
+ // Add Zentao configuration tool
45
+ server.tool("initZentao", {}, async ({}) => {
46
+ let config;
47
+ // 尝试从配置文件加载配置
48
+ const savedConfig = loadConfig();
49
+ if (!savedConfig) {
50
+ throw new Error("No configuration found. Please provide complete Zentao configuration.");
51
+ }
52
+ config = savedConfig;
53
+ zentaoApi = new ZentaoAPI(config);
54
+ return {
55
+ content: [{ type: "text", text: JSON.stringify(config, null, 2) }]
56
+ };
57
+ });
58
+ // Add getMyTasks tool
59
+ server.tool("getMyTasks", {
60
+ status: z.enum(['wait', 'doing', 'done', 'all']).optional()
61
+ }, async ({ status }) => {
62
+ if (!zentaoApi)
63
+ throw new Error("Please initialize Zentao API first");
64
+ const tasks = await zentaoApi.getMyTasks(status);
65
+ return {
66
+ content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }]
67
+ };
68
+ });
69
+ // Add getTaskDetail tool
70
+ server.tool("getTaskDetail", {
71
+ taskId: z.number()
72
+ }, async ({ taskId }) => {
73
+ if (!zentaoApi)
74
+ throw new Error("Please initialize Zentao API first");
75
+ const task = await zentaoApi.getTaskDetail(taskId);
76
+ return {
77
+ content: [{ type: "text", text: JSON.stringify(task, null, 2) }]
78
+ };
79
+ });
80
+ // Add getProducts tool
81
+ server.tool("getProducts", {}, async () => {
82
+ if (!zentaoApi)
83
+ throw new Error("Please initialize Zentao API first");
84
+ const products = await zentaoApi.getProducts();
85
+ return {
86
+ content: [{ type: "text", text: JSON.stringify(products, null, 2) }]
87
+ };
88
+ });
89
+ // Add getMyBugs tool
90
+ server.tool("getMyBugs", {
91
+ status: z.enum(['active', 'resolved', 'closed', 'all']).optional(),
92
+ productId: z.number().optional()
93
+ }, async ({ status, productId }) => {
94
+ if (!zentaoApi)
95
+ throw new Error("Please initialize Zentao API first");
96
+ const bugs = await zentaoApi.getMyBugs(status, productId);
97
+ return {
98
+ content: [{ type: "text", text: JSON.stringify(bugs, null, 2) }]
99
+ };
100
+ });
101
+ // Add getBugDetail tool
102
+ server.tool("getBugDetail", {
103
+ bugId: z.number()
104
+ }, async ({ bugId }) => {
105
+ if (!zentaoApi)
106
+ throw new Error("Please initialize Zentao API first");
107
+ const bug = await zentaoApi.getBugDetail(bugId);
108
+ return {
109
+ content: [{ type: "text", text: JSON.stringify(bug, null, 2) }]
110
+ };
111
+ });
112
+ // Add updateTask tool
113
+ server.tool("updateTask", {
114
+ taskId: z.number(),
115
+ update: z.object({
116
+ consumed: z.number().optional(),
117
+ left: z.number().optional(),
118
+ status: z.enum(['wait', 'doing', 'done']).optional(),
119
+ finishedDate: z.string().optional(),
120
+ comment: z.string().optional()
121
+ })
122
+ }, async ({ taskId, update }) => {
123
+ if (!zentaoApi)
124
+ throw new Error("Please initialize Zentao API first");
125
+ const task = await zentaoApi.updateTask(taskId, update);
126
+ return {
127
+ content: [{ type: "text", text: JSON.stringify(task, null, 2) }]
128
+ };
129
+ });
130
+ // Add finishTask tool
131
+ server.tool("finishTask", {
132
+ taskId: z.number(),
133
+ update: z.object({
134
+ consumed: z.number().optional(),
135
+ left: z.number().optional(),
136
+ comment: z.string().optional()
137
+ }).optional()
138
+ }, async ({ taskId, update }) => {
139
+ if (!zentaoApi)
140
+ throw new Error("Please initialize Zentao API first");
141
+ const task = await zentaoApi.finishTask(taskId, update);
142
+ return {
143
+ content: [{ type: "text", text: JSON.stringify(task, null, 2) }]
144
+ };
145
+ });
146
+ // Add resolveBug tool
147
+ server.tool("resolveBug", {
148
+ bugId: z.number(),
149
+ resolution: z.object({
150
+ resolution: z.enum(['fixed', 'notrepro', 'duplicate', 'bydesign', 'willnotfix', 'tostory', 'external']),
151
+ resolvedBuild: z.string().optional(),
152
+ duplicateBug: z.number().optional(),
153
+ comment: z.string().optional()
154
+ })
155
+ }, async ({ bugId, resolution }) => {
156
+ if (!zentaoApi)
157
+ throw new Error("Please initialize Zentao API first");
158
+ const bug = await zentaoApi.resolveBug(bugId, resolution);
159
+ return {
160
+ content: [{ type: "text", text: JSON.stringify(bug, null, 2) }]
161
+ };
162
+ });
163
+ // Start receiving messages on stdin and sending messages on stdout
164
+ const transport = new StdioServerTransport();
165
+ await server.connect(transport).catch(console.error);
@@ -0,0 +1,6 @@
1
+ import { BugStatus, TaskStatus, ZentaoConfig } from './types/zentao';
2
+ export declare function mcp_zentao_connect(config: ZentaoConfig): Promise<string>;
3
+ export declare function mcp_zentao_tasks(status?: TaskStatus): Promise<string>;
4
+ export declare function mcp_zentao_task(taskId: number): Promise<string>;
5
+ export declare function mcp_zentao_bugs(status?: BugStatus): Promise<string>;
6
+ export declare function mcp_zentao_bug(bugId: number): Promise<string>;
@@ -0,0 +1,70 @@
1
+ import { ZentaoAPI } from './api/zentaoApi';
2
+ import { BugService } from './services/bugService';
3
+ import { TaskService } from './services/taskService';
4
+ import { formatBugDetail, formatBugsTable, formatTaskDetail, formatTasksTable } from './utils/displayUtils';
5
+ let api = null;
6
+ let taskService = null;
7
+ let bugService = null;
8
+ function initializeServices(config) {
9
+ api = new ZentaoAPI(config);
10
+ taskService = new TaskService(api);
11
+ bugService = new BugService(api);
12
+ }
13
+ export async function mcp_zentao_connect(config) {
14
+ try {
15
+ initializeServices(config);
16
+ await api?.getMyTasks(); // 测试连接
17
+ return '禅道连接成功';
18
+ }
19
+ catch (error) {
20
+ return `连接失败: ${error instanceof Error ? error.message : '未知错误'}`;
21
+ }
22
+ }
23
+ export async function mcp_zentao_tasks(status) {
24
+ try {
25
+ if (!taskService)
26
+ throw new Error('请先连接禅道');
27
+ const tasks = await taskService.getMyTasks(status);
28
+ if (tasks.length === 0)
29
+ return '没有找到任何任务';
30
+ return formatTasksTable(tasks);
31
+ }
32
+ catch (error) {
33
+ return `获取任务失败: ${error instanceof Error ? error.message : '未知错误'}`;
34
+ }
35
+ }
36
+ export async function mcp_zentao_task(taskId) {
37
+ try {
38
+ if (!taskService)
39
+ throw new Error('请先连接禅道');
40
+ const task = await taskService.getTaskDetail(taskId);
41
+ return formatTaskDetail(task);
42
+ }
43
+ catch (error) {
44
+ return `获取任务详情失败: ${error instanceof Error ? error.message : '未知错误'}`;
45
+ }
46
+ }
47
+ export async function mcp_zentao_bugs(status) {
48
+ try {
49
+ if (!bugService)
50
+ throw new Error('请先连接禅道');
51
+ const bugs = await bugService.getMyBugs(status);
52
+ if (bugs.length === 0)
53
+ return '没有找到任何Bug';
54
+ return formatBugsTable(bugs);
55
+ }
56
+ catch (error) {
57
+ return `获取Bug失败: ${error instanceof Error ? error.message : '未知错误'}`;
58
+ }
59
+ }
60
+ export async function mcp_zentao_bug(bugId) {
61
+ try {
62
+ if (!bugService)
63
+ throw new Error('请先连接禅道');
64
+ const bug = await bugService.getBugDetail(bugId);
65
+ return formatBugDetail(bug);
66
+ }
67
+ catch (error) {
68
+ return `获取Bug详情失败: ${error instanceof Error ? error.message : '未知错误'}`;
69
+ }
70
+ }
@@ -0,0 +1,9 @@
1
+ import { ZentaoAPI } from '../api/zentaoApi';
2
+ import { Bug, BugStatus } from '../types/zentao';
3
+ export declare class BugService {
4
+ private api;
5
+ constructor(api: ZentaoAPI);
6
+ getMyBugs(status?: BugStatus): Promise<Bug[]>;
7
+ getBugDetail(bugId: number): Promise<Bug>;
8
+ private enrichBugData;
9
+ }
@@ -0,0 +1,47 @@
1
+ import { calculateDaysDifference } from '../utils/dateUtils';
2
+ export class BugService {
3
+ api;
4
+ constructor(api) {
5
+ this.api = api;
6
+ }
7
+ async getMyBugs(status) {
8
+ const bugs = await this.api.getMyBugs(status);
9
+ return bugs.map(bug => this.enrichBugData(bug));
10
+ }
11
+ async getBugDetail(bugId) {
12
+ const bug = await this.api.getBugDetail(bugId);
13
+ return this.enrichBugData(bug);
14
+ }
15
+ enrichBugData(bug) {
16
+ const enrichedBug = { ...bug };
17
+ // 添加严重程度标识
18
+ const severity = bug.severity;
19
+ if (severity >= 3) {
20
+ enrichedBug.severity_level = '严重';
21
+ }
22
+ else if (severity >= 2) {
23
+ enrichedBug.severity_level = '一般';
24
+ }
25
+ else {
26
+ enrichedBug.severity_level = '轻微';
27
+ }
28
+ // 计算处理时间
29
+ if (bug.openedDate) {
30
+ const daysOpen = calculateDaysDifference(bug.openedDate);
31
+ enrichedBug.days_open = daysOpen;
32
+ if (daysOpen > 7) {
33
+ enrichedBug.aging_status = '已超过7天';
34
+ enrichedBug.aging_description = `已开启${daysOpen}天,请尽快处理`;
35
+ }
36
+ else if (daysOpen > 3) {
37
+ enrichedBug.aging_status = '已超过3天';
38
+ enrichedBug.aging_description = `已开启${daysOpen}天`;
39
+ }
40
+ else {
41
+ enrichedBug.aging_status = '新建';
42
+ enrichedBug.aging_description = '新建Bug';
43
+ }
44
+ }
45
+ return enrichedBug;
46
+ }
47
+ }
@@ -0,0 +1,9 @@
1
+ import { ZentaoAPI } from '../api/zentaoApi';
2
+ import { Task, TaskStatus } from '../types/zentao';
3
+ export declare class TaskService {
4
+ private api;
5
+ constructor(api: ZentaoAPI);
6
+ getMyTasks(status?: TaskStatus): Promise<Task[]>;
7
+ getTaskDetail(taskId: number): Promise<Task>;
8
+ private enrichTaskData;
9
+ }
@@ -0,0 +1,44 @@
1
+ import { calculateRemainingDays } from '../utils/dateUtils';
2
+ export class TaskService {
3
+ api;
4
+ constructor(api) {
5
+ this.api = api;
6
+ }
7
+ async getMyTasks(status) {
8
+ const tasks = await this.api.getMyTasks(status);
9
+ return tasks.map(task => this.enrichTaskData(task));
10
+ }
11
+ async getTaskDetail(taskId) {
12
+ const task = await this.api.getTaskDetail(taskId);
13
+ return this.enrichTaskData(task);
14
+ }
15
+ enrichTaskData(task) {
16
+ const enrichedTask = { ...task };
17
+ // 添加优先级标识
18
+ const priority = task.pri;
19
+ if (priority >= 4) {
20
+ enrichedTask.priority_level = '高';
21
+ }
22
+ else if (priority >= 2) {
23
+ enrichedTask.priority_level = '中';
24
+ }
25
+ else {
26
+ enrichedTask.priority_level = '低';
27
+ }
28
+ // 计算剩余时间
29
+ if (task.deadline) {
30
+ const remainingDays = calculateRemainingDays(task.deadline);
31
+ enrichedTask.remaining_days = remainingDays;
32
+ if (remainingDays < 0) {
33
+ enrichedTask.status_description = '已逾期';
34
+ }
35
+ else if (remainingDays === 0) {
36
+ enrichedTask.status_description = '今日到期';
37
+ }
38
+ else {
39
+ enrichedTask.status_description = `还剩${remainingDays}天`;
40
+ }
41
+ }
42
+ return enrichedTask;
43
+ }
44
+ }
@@ -0,0 +1,58 @@
1
+ export interface ZentaoConfig {
2
+ url: string;
3
+ username: string;
4
+ password: string;
5
+ apiVersion: string;
6
+ }
7
+ export interface CreateTaskRequest {
8
+ name: string;
9
+ desc?: string;
10
+ pri?: number;
11
+ estimate?: number;
12
+ project?: number;
13
+ execution?: number;
14
+ module?: number;
15
+ story?: number;
16
+ type?: string;
17
+ assignedTo?: string;
18
+ estStarted?: string;
19
+ deadline?: string;
20
+ }
21
+ export interface Task {
22
+ id: number;
23
+ name: string;
24
+ status: string;
25
+ pri: number;
26
+ deadline?: string;
27
+ desc?: string;
28
+ priority_level?: '高' | '中' | '低';
29
+ remaining_days?: number;
30
+ status_description?: string;
31
+ }
32
+ export interface Bug {
33
+ id: number;
34
+ title: string;
35
+ status: string;
36
+ severity: number;
37
+ steps?: string;
38
+ openedDate?: string;
39
+ severity_level?: '严重' | '一般' | '轻微';
40
+ days_open?: number;
41
+ aging_status?: string;
42
+ aging_description?: string;
43
+ }
44
+ export interface TaskUpdate {
45
+ consumed?: number;
46
+ left?: number;
47
+ status?: TaskStatus;
48
+ finishedDate?: string;
49
+ comment?: string;
50
+ }
51
+ export interface BugResolution {
52
+ resolution: 'fixed' | 'notrepro' | 'duplicate' | 'bydesign' | 'willnotfix' | 'tostory' | 'external';
53
+ resolvedBuild?: string;
54
+ duplicateBug?: number;
55
+ comment?: string;
56
+ }
57
+ export type TaskStatus = 'wait' | 'doing' | 'done' | 'all';
58
+ export type BugStatus = 'active' | 'resolved' | 'closed' | 'all';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export declare function formatDate(date: Date): string;
2
+ export declare function calculateRemainingDays(deadline: string): number;
3
+ export declare function calculateDaysDifference(startDate: string): number;
@@ -0,0 +1,19 @@
1
+ export function formatDate(date) {
2
+ return date.toISOString().split('T')[0];
3
+ }
4
+ export function calculateRemainingDays(deadline) {
5
+ const today = new Date();
6
+ today.setHours(0, 0, 0, 0);
7
+ const deadlineDate = new Date(deadline);
8
+ deadlineDate.setHours(0, 0, 0, 0);
9
+ const diffTime = deadlineDate.getTime() - today.getTime();
10
+ return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
11
+ }
12
+ export function calculateDaysDifference(startDate) {
13
+ const today = new Date();
14
+ today.setHours(0, 0, 0, 0);
15
+ const start = new Date(startDate);
16
+ start.setHours(0, 0, 0, 0);
17
+ const diffTime = today.getTime() - start.getTime();
18
+ return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
19
+ }
@@ -0,0 +1,5 @@
1
+ import { Bug, Task } from '../types/zentao';
2
+ export declare function formatTasksTable(tasks: Task[]): string;
3
+ export declare function formatBugsTable(bugs: Bug[]): string;
4
+ export declare function formatTaskDetail(task: Task): string;
5
+ export declare function formatBugDetail(bug: Bug): string;
@@ -0,0 +1,83 @@
1
+ import chalk from 'chalk';
2
+ import { table } from 'table';
3
+ const tableConfig = {
4
+ border: {
5
+ topBody: `─`,
6
+ topJoin: `┬`,
7
+ topLeft: `┌`,
8
+ topRight: `┐`,
9
+ bottomBody: `─`,
10
+ bottomJoin: `┴`,
11
+ bottomLeft: `└`,
12
+ bottomRight: `┘`,
13
+ bodyLeft: `│`,
14
+ bodyRight: `│`,
15
+ bodyJoin: `│`,
16
+ joinBody: `─`,
17
+ joinLeft: `├`,
18
+ joinRight: `┤`,
19
+ joinJoin: `┼`
20
+ }
21
+ };
22
+ export function formatTasksTable(tasks) {
23
+ const header = ['ID', '标题', '优先级', '状态', '剩余时间'];
24
+ const rows = tasks.map(task => {
25
+ const priorityColor = {
26
+ '高': chalk.red,
27
+ '中': chalk.yellow,
28
+ '低': chalk.green
29
+ }[task.priority_level || '低'];
30
+ return [
31
+ task.id.toString(),
32
+ task.name,
33
+ priorityColor(task.priority_level || '低'),
34
+ task.status,
35
+ `${task.remaining_days || '-'}天`
36
+ ];
37
+ });
38
+ return table([header, ...rows], tableConfig);
39
+ }
40
+ export function formatBugsTable(bugs) {
41
+ const header = ['ID', '标题', '严重程度', '状态', '处理时间'];
42
+ const rows = bugs.map(bug => {
43
+ const severityColor = {
44
+ '严重': chalk.red,
45
+ '一般': chalk.yellow,
46
+ '轻微': chalk.green
47
+ }[bug.severity_level || '轻微'];
48
+ return [
49
+ bug.id.toString(),
50
+ bug.title,
51
+ severityColor(bug.severity_level || '轻微'),
52
+ bug.status,
53
+ bug.aging_status || '-'
54
+ ];
55
+ });
56
+ return table([header, ...rows], tableConfig);
57
+ }
58
+ export function formatTaskDetail(task) {
59
+ const lines = [
60
+ chalk.blue.bold(`任务详情 #${task.id}`),
61
+ `标题: ${task.name}`,
62
+ `状态: ${task.status}`,
63
+ `优先级: ${task.priority_level || '-'}`,
64
+ task.status_description ? `时间状态: ${task.status_description}` : '',
65
+ '',
66
+ '描述:',
67
+ task.desc || '无'
68
+ ];
69
+ return lines.filter(Boolean).join('\n');
70
+ }
71
+ export function formatBugDetail(bug) {
72
+ const lines = [
73
+ chalk.red.bold(`Bug详情 #${bug.id}`),
74
+ `标题: ${bug.title}`,
75
+ `状态: ${bug.status}`,
76
+ `严重程度: ${bug.severity_level || '-'}`,
77
+ bug.aging_description ? `处理时间: ${bug.aging_description}` : '',
78
+ '',
79
+ '描述:',
80
+ bug.steps || '无'
81
+ ];
82
+ return lines.filter(Boolean).join('\n');
83
+ }
package/json-args.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+
3
+ // 获取命令行参数中的 JSON 字符串
4
+ const jsonArg = process.argv[2];
5
+
6
+ if (!jsonArg) {
7
+ console.error('请提供 JSON 参数');
8
+ process.exit(1);
9
+ }
10
+
11
+ try {
12
+ // 解析 JSON 字符串
13
+ const params = JSON.parse(jsonArg);
14
+
15
+ // 导入并执行主程序
16
+ import('./dist/index.js')
17
+ .then(module => {
18
+ module.default(params);
19
+ })
20
+ .catch(error => {
21
+ console.error('执行失败:', error);
22
+ process.exit(1);
23
+ });
24
+ } catch (error) {
25
+ console.error('JSON 解析失败:', error);
26
+ process.exit(1);
27
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@zzp123/mcp-zentao",
3
+ "version": "1.0.0",
4
+ "description": "禅道项目管理系统的高级API集成包,提供任务管理、Bug跟踪等功能的完整封装,专为Cursor IDE设计的MCP扩展",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "zentao": "json-args.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "json-args.js"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "watch": "tsc -w",
19
+ "lint": "eslint src/**/*.ts",
20
+ "test": "jest",
21
+ "test:watch": "jest --watch",
22
+ "test:manual": "ts-node test/manual-test.ts",
23
+ "prepublishOnly": "npm run test && npm run build",
24
+ "start": "node json-args.js"
25
+ },
26
+ "keywords": [
27
+ "mcp",
28
+ "zentao",
29
+ "禅道",
30
+ "任务管理",
31
+ "项目管理",
32
+ "bug跟踪",
33
+ "api集成",
34
+ "cursor-ide",
35
+ "typescript"
36
+ ],
37
+ "author": "yourname",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/yourusername/mcp-zentao.git"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.6.1",
45
+ "axios": "^1.6.7",
46
+ "chalk": "^4.1.2",
47
+ "fs": "^0.0.1-security",
48
+ "table": "^6.8.1",
49
+ "yargs": "^17.7.2"
50
+ },
51
+ "devDependencies": {
52
+ "@types/jest": "^29.5.12",
53
+ "@types/node": "^20.11.19",
54
+ "@types/table": "^6.3.2",
55
+ "@typescript-eslint/eslint-plugin": "^7.0.1",
56
+ "@typescript-eslint/parser": "^7.0.1",
57
+ "eslint": "^8.56.0",
58
+ "jest": "^29.7.0",
59
+ "ts-jest": "^29.1.2",
60
+ "ts-node": "^10.9.2",
61
+ "typescript": "^5.9.3"
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ }
66
+ }