@zzp123/mcp-zentao 1.18.9 → 1.18.11

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/dist/index-pm.js CHANGED
@@ -17,15 +17,12 @@ if (configIndex !== -1 && configIndex + 1 < args.length) {
17
17
  // 获取 --config 后面的 JSON 字符串并解析
18
18
  const jsonStr = args[configIndex + 1];
19
19
  configData = JSON.parse(jsonStr);
20
- process.stdout.write('成功解析配置数据: ' + JSON.stringify(configData) + '\n');
21
20
  // 如果配置数据中包含 config 对象,则保存配置
22
21
  if (configData.config) {
23
- process.stdout.write('正在保存配置...\n');
24
22
  saveConfig(configData.config);
25
23
  }
26
24
  }
27
25
  catch (error) {
28
- process.stderr.write('配置解析失败: ' + String(error) + '\n');
29
26
  process.exit(1);
30
27
  }
31
28
  }
@@ -36,11 +33,10 @@ const server = new McpServer({
36
33
  });
37
34
  // Initialize ZentaoAPI instance
38
35
  let zentaoApi = null;
36
+ const getApi = () => zentaoApi;
39
37
  export default async function main(params) {
40
- process.stdout.write('接收到的参数: ' + JSON.stringify(params) + '\n');
41
38
  // 如果传入了配置信息,就保存它
42
39
  if (params.config) {
43
- process.stdout.write('保存新的配置信息...\n');
44
40
  saveConfig(params.config);
45
41
  }
46
42
  }
@@ -58,147 +54,6 @@ server.tool("initZentao", {}, async ({}) => {
58
54
  content: [{ type: "text", text: JSON.stringify(config, null, 2) }]
59
55
  };
60
56
  });
61
- server.tool("getMyTasks", {
62
- status: z.enum(['wait', 'doing', 'done', 'all']).optional()
63
- }, async ({ status }) => {
64
- if (!zentaoApi)
65
- throw new Error("Please initialize Zentao API first");
66
- const tasks = await zentaoApi.getMyTasks(status);
67
- return {
68
- content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }]
69
- };
70
- });
71
- server.tool("getTaskDetail", {
72
- taskId: z.number()
73
- }, async ({ taskId }) => {
74
- if (!zentaoApi)
75
- throw new Error("Please initialize Zentao API first");
76
- const task = await zentaoApi.getTaskDetail(taskId);
77
- return {
78
- content: [{ type: "text", text: JSON.stringify(task, null, 2) }]
79
- };
80
- });
81
- server.tool("getProducts", "获取产品列表 - 只返回核心字段(id, 名称, 负责人)", {
82
- fields: z.enum(['basic', 'default', 'full']).optional().describe("返回字段级别:'basic'(id/名称/状态)、'default'(+负责人/创建人,默认)、'full'(完整字段)")
83
- }, async ({ fields }) => {
84
- if (!zentaoApi)
85
- throw new Error("Please initialize Zentao API first");
86
- const products = await zentaoApi.getProducts(fields);
87
- return {
88
- content: [{ type: "text", text: JSON.stringify(products, null, 2) }]
89
- };
90
- });
91
- server.tool("getMyBugs", "获取我的Bug列表 - 固定查询指派给我的Bug(status='assigntome'),固定每页20条", {
92
- productId: z.number().optional().describe("产品ID,默认使用第二个产品"),
93
- page: z.number().optional().describe("页码,从1开始,默认为1"),
94
- branch: z.string().optional().describe("分支筛选:'all'(所有分支,默认)、'0'(主干分支)、分支ID"),
95
- order: z.string().optional().describe("排序方式,如 id_desc(ID降序), pri_desc(优先级降序), openedDate_desc(创建时间降序)等,默认id_desc"),
96
- fields: z.enum(['basic', 'default', 'full']).optional().describe("返回字段级别:'basic'(基本字段,默认)、'default'(默认字段)、'full'(完整字段),默认'basic'")
97
- }, async ({ productId, page, branch, order, fields }) => {
98
- if (!zentaoApi)
99
- throw new Error("Please initialize Zentao API first");
100
- const result = await zentaoApi.getMyBugs(productId, page, branch, order, fields);
101
- // 返回分页信息和数据
102
- const summary = {
103
- summary: `当前第 ${result.page} 页,共 ${result.total} 个Bug,本页显示 ${result.bugs.length} 个`,
104
- pagination: {
105
- page: result.page,
106
- limit: result.limit,
107
- total: result.total,
108
- totalPages: Math.ceil(result.total / result.limit)
109
- },
110
- bugs: result.bugs
111
- };
112
- return {
113
- content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
114
- };
115
- });
116
- server.tool("getProductBugs", "获取指定产品的Bug列表 - 支持多种状态筛选和分支筛选", {
117
- productId: z.number().describe("产品ID(必填)"),
118
- page: z.number().optional().describe("页码,从1开始,默认为1"),
119
- limit: z.number().optional().describe("每页数量,默认20,最大100"),
120
- status: z.enum([
121
- 'active', // 激活状态
122
- 'resolved', // 已解决
123
- 'closed', // 已关闭
124
- 'all', // 所有状态
125
- 'assigntome', // 指派给我的
126
- 'openedbyme', // 由我创建
127
- 'resolvedbyme', // 由我解决
128
- 'assignedbyme', // 由我指派
129
- 'assigntonull', // 未指派
130
- 'unconfirmed', // 未确认
131
- 'unclosed', // 未关闭
132
- 'unresolved', // 未解决(激活状态)
133
- 'toclosed', // 待关闭(已解决)
134
- 'postponedbugs', // 延期的Bug
135
- 'longlifebugs', // 长期未处理的Bug
136
- 'overduebugs', // 已过期的Bug
137
- 'review', // 待我审核
138
- 'feedback', // 用户反馈
139
- 'needconfirm', // 需求变更需确认
140
- 'bysearch' // 自定义搜索
141
- ]).optional().describe("Bug状态筛选,默认返回所有状态"),
142
- branch: z.string().optional().describe("分支筛选:'all'(所有分支,默认)、'0'(主干分支)、分支ID"),
143
- order: z.string().optional().describe("排序方式,如 id_desc(ID降序), pri_desc(优先级降序), openedDate_desc(创建时间降序)等,默认id_desc"),
144
- fields: z.enum(['basic', 'default', 'full']).optional().describe("返回字段级别:'basic'(基本字段,默认)、'default'(默认字段)、'full'(完整字段),默认'basic'")
145
- }, async ({ productId, page, limit, status, branch, order, fields }) => {
146
- if (!zentaoApi)
147
- throw new Error("Please initialize Zentao API first");
148
- const result = await zentaoApi.getProductBugs(productId, page, limit, status, branch, order, fields);
149
- // 返回分页信息和数据
150
- const summary = {
151
- summary: `当前第 ${result.page} 页,共 ${result.total} 个Bug,本页显示 ${result.bugs.length} 个`,
152
- pagination: {
153
- page: result.page,
154
- limit: result.limit,
155
- total: result.total,
156
- totalPages: Math.ceil(result.total / result.limit)
157
- },
158
- bugs: result.bugs
159
- };
160
- return {
161
- content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
162
- };
163
- });
164
- server.tool("getBugDetail", {
165
- bugId: z.number(),
166
- fields: z.enum(['basic', 'detail', 'full']).optional().describe(`
167
- 字段级别:
168
- - basic: 基本信息(id, title, status, severity, pri等核心字段)
169
- - detail: 详细信息(包含steps步骤,但不包含files/cases/linkBugs等关联数据)
170
- - full: 完整信息(包含所有字段,默认值)
171
- `.trim())
172
- }, async ({ bugId, fields }) => {
173
- if (!zentaoApi)
174
- throw new Error("Please initialize Zentao API first");
175
- const bug = await zentaoApi.getBugDetail(bugId, fields);
176
- // 添加说明信息
177
- const result = {
178
- fieldLevel: fields || 'full',
179
- bug: bug
180
- };
181
- // 根据字段级别添加提示
182
- if (fields === 'basic') {
183
- result.note = '仅显示基本信息,如需完整内容请使用 fields=detail 或 fields=full';
184
- }
185
- else if (fields === 'detail') {
186
- result.note = '显示详细信息但不包含关联数据(files/cases/linkBugs),如需完整内容请使用 fields=full';
187
- }
188
- return {
189
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
190
- };
191
- });
192
- server.tool("getPrograms", {
193
- order: z.string().optional()
194
- }, async ({ order }) => {
195
- if (!zentaoApi)
196
- throw new Error("Please initialize Zentao API first");
197
- const programs = await zentaoApi.getPrograms(order);
198
- return {
199
- content: [{ type: "text", text: JSON.stringify(programs, null, 2) }]
200
- };
201
- });
202
57
  server.tool("getProductPlans", {
203
58
  productId: z.number(),
204
59
  branch: z.number().optional(),
@@ -210,8 +65,19 @@ server.tool("getProductPlans", {
210
65
  if (!zentaoApi)
211
66
  throw new Error("Please initialize Zentao API first");
212
67
  const plans = await zentaoApi.getProductPlans(productId, branch, begin, end, status, parent);
68
+ // 精简返回字段
69
+ const simplifyPlan = (p) => ({
70
+ id: p.id,
71
+ title: p.title,
72
+ status: p.status,
73
+ begin: p.begin,
74
+ end: p.end,
75
+ stories: p.stories,
76
+ bugs: p.bugs
77
+ });
78
+ const simplified = plans.map(simplifyPlan);
213
79
  return {
214
- content: [{ type: "text", text: JSON.stringify(plans, null, 2) }]
80
+ content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
215
81
  };
216
82
  });
217
83
  server.tool("createStory", "创建需求 - 支持创建软件需求(story)或用户需求(requirement)", {
@@ -246,33 +112,6 @@ server.tool("createStory", "创建需求 - 支持创建软件需求(story)或用
246
112
  content: [{ type: "text", text: JSON.stringify(story, null, 2) }]
247
113
  };
248
114
  });
249
- server.tool("getPlanDetail", {
250
- planId: z.number()
251
- }, async ({ planId }) => {
252
- if (!zentaoApi)
253
- throw new Error("Please initialize Zentao API first");
254
- const plan = await zentaoApi.getPlanDetail(planId);
255
- return {
256
- content: [{ type: "text", text: JSON.stringify(plan, null, 2) }]
257
- };
258
- });
259
- server.tool("updatePlan", {
260
- planId: z.number(),
261
- update: z.object({
262
- branch: z.number().optional(),
263
- title: z.string().optional(),
264
- begin: z.string().optional(),
265
- end: z.string().optional(),
266
- desc: z.string().optional()
267
- })
268
- }, async ({ planId, update }) => {
269
- if (!zentaoApi)
270
- throw new Error("Please initialize Zentao API first");
271
- const plan = await zentaoApi.updatePlan(planId, update);
272
- return {
273
- content: [{ type: "text", text: JSON.stringify(plan, null, 2) }]
274
- };
275
- });
276
115
  server.tool("deletePlan", {
277
116
  planId: z.number()
278
117
  }, async ({ planId }) => {
@@ -294,88 +133,61 @@ server.tool("linkStoriesToPlan", {
294
133
  content: [{ type: "text", text: JSON.stringify(plan, null, 2) }]
295
134
  };
296
135
  });
297
- server.tool("unlinkStoriesFromPlan", {
298
- planId: z.number(),
299
- storyIds: z.array(z.number())
300
- }, async ({ planId, storyIds }) => {
301
- if (!zentaoApi)
302
- throw new Error("Please initialize Zentao API first");
303
- const plan = await zentaoApi.unlinkStoriesFromPlan(planId, storyIds);
304
- return {
305
- content: [{ type: "text", text: JSON.stringify(plan, null, 2) }]
306
- };
307
- });
308
- server.tool("linkBugsToPlan", {
309
- planId: z.number(),
310
- bugIds: z.array(z.number())
311
- }, async ({ planId, bugIds }) => {
136
+ server.tool("getStoryDetail", "获取需求详情 - 支持软件需求(story)和用户需求(requirement),自动识别类型", {
137
+ storyId: z.number().describe("需求ID(可以是story或requirement类型)")
138
+ }, async ({ storyId }) => {
312
139
  if (!zentaoApi)
313
140
  throw new Error("Please initialize Zentao API first");
314
- const plan = await zentaoApi.linkBugsToPlan(planId, bugIds);
315
- return {
316
- content: [{ type: "text", text: JSON.stringify(plan, null, 2) }]
141
+ const story = await zentaoApi.getStoryDetail(storyId, 'basic');
142
+ // 只返回基本信息
143
+ const result = {
144
+ id: story.id,
145
+ title: story.title,
146
+ status: story.status,
147
+ stage: story.stage,
148
+ pri: story.pri,
149
+ type: story.type,
150
+ category: story.category,
151
+ product: story.product,
152
+ productName: story.productName,
153
+ openedBy: story.openedBy,
154
+ openedDate: story.openedDate,
155
+ assignedTo: story.assignedTo
317
156
  };
318
- });
319
- server.tool("unlinkBugsFromPlan", {
320
- planId: z.number(),
321
- bugIds: z.array(z.number())
322
- }, async ({ planId, bugIds }) => {
323
- if (!zentaoApi)
324
- throw new Error("Please initialize Zentao API first");
325
- const plan = await zentaoApi.unlinkBugsFromPlan(planId, bugIds);
326
157
  return {
327
- content: [{ type: "text", text: JSON.stringify(plan, null, 2) }]
158
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
328
159
  };
329
160
  });
330
- server.tool("getStoryDetail", "获取需求详情 - 支持软件需求(story)和用户需求(requirement),自动识别类型", {
331
- storyId: z.number().describe("需求ID(可以是story或requirement类型)"),
332
- fields: z.enum(['basic', 'detail', 'full']).optional().describe(`
333
- 字段级别:
334
- - basic: 基本信息(id, title, status, stage, pri等核心字段)
335
- - detail: 详细信息(包含spec和verify,但不包含executions/tasks/stages/children等关联数据)
336
- - full: 完整信息(包含所有字段,默认值)
337
- `.trim())
338
- }, async ({ storyId, fields }) => {
161
+ server.tool("getProjectExecutions", {
162
+ projectId: z.number()
163
+ }, async ({ projectId }) => {
339
164
  if (!zentaoApi)
340
165
  throw new Error("Please initialize Zentao API first");
341
- const story = await zentaoApi.getStoryDetail(storyId, fields);
342
- // 添加说明信息
343
- const result = {
344
- fieldLevel: fields || 'full',
345
- story: story
346
- };
347
- // 根据字段级别添加提示
348
- if (fields === 'basic') {
349
- result.note = '仅显示基本信息,如需完整内容请使用 fields=detail 或 fields=full';
350
- }
351
- else if (fields === 'detail') {
352
- result.note = '显示详细信息但不包含关联数据(executions/tasks/stages/children),如需完整内容请使用 fields=full';
353
- }
166
+ const executions = await zentaoApi.getProjectExecutions(projectId);
167
+ // 只返回基本字段
168
+ const simplified = executions.map((e) => ({
169
+ id: e.id,
170
+ name: e.name,
171
+ status: e.status,
172
+ begin: e.begin,
173
+ end: e.end,
174
+ progress: e.progress
175
+ }));
354
176
  return {
355
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
177
+ content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
356
178
  };
357
179
  });
358
- server.tool("createPlan", {
359
- productId: z.number(),
360
- title: z.string(),
361
- branch: z.number().optional(),
362
- begin: z.string().optional(),
363
- end: z.string().optional(),
364
- desc: z.string().optional(),
365
- parent: z.number().optional()
366
- }, async ({ productId, title, branch, begin, end, desc, parent }) => {
180
+ server.tool("getProjects", {
181
+ page: z.number().optional(),
182
+ limit: z.number().optional()
183
+ }, async ({ page, limit }) => {
367
184
  if (!zentaoApi)
368
185
  throw new Error("Please initialize Zentao API first");
369
- const plan = await zentaoApi.createPlan(productId, {
370
- title,
371
- branch,
372
- begin,
373
- end,
374
- desc,
375
- parent
376
- });
186
+ const projects = await zentaoApi.getProjects(page, limit);
187
+ // 只返回 id 和 name 字段
188
+ const simplified = projects.map((p) => ({ id: p.id, name: p.name }));
377
189
  return {
378
- content: [{ type: "text", text: JSON.stringify(plan, null, 2) }]
190
+ content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
379
191
  };
380
192
  });
381
193
  server.tool("changeStory", "需求变更 - 支持软件需求(story)和用户需求(requirement),自动识别类型", {
@@ -421,10 +233,10 @@ server.tool("changeStory", "需求变更 - 支持软件需求(story)和用户需
421
233
  verify: z.string().optional(),
422
234
  // 备注
423
235
  comment: z.string().optional()
424
- }).describe(`更新需求(使用 PUT 接口)
425
-
426
- 此工具使用标准的"修改需求其他字段"接口(PUT /stories/:id),支持修改29个字段。
427
- 自动处理评审人问题,无需手动设置 needNotReview。
236
+ }).describe(`更新需求(使用 PUT 接口)
237
+
238
+ 此工具使用标准的"修改需求其他字段"接口(PUT /stories/:id),支持修改29个字段。
239
+ 自动处理评审人问题,无需手动设置 needNotReview。
428
240
  `.trim())
429
241
  }, async ({ storyId, update }) => {
430
242
  if (!zentaoApi)
@@ -434,94 +246,22 @@ server.tool("changeStory", "需求变更 - 支持软件需求(story)和用户需
434
246
  content: [{ type: "text", text: JSON.stringify(story, null, 2) }]
435
247
  };
436
248
  });
437
- server.tool("getProductStories", "获取产品需求列表 - 包含软件需求(story)和用户需求(requirement),默认只返回核心字段(id, 标题, 指派人, 创建人)", {
438
- productId: z.number(),
439
- page: z.number().optional().describe("页码,从1开始,默认为1"),
440
- limit: z.number().optional().describe("每页数量,默认20,最大100"),
441
- fields: z.enum(['basic', 'default', 'full']).optional().describe("字段级别:\n- basic: 基本信息(id, title, assignedTo, openedBy)\n- default: 常用信息(包含basic + status, stage, pri等)\n- full: 完整信息(所有字段)")
442
- }, async ({ productId, page, limit, fields }) => {
443
- if (!zentaoApi)
444
- throw new Error("Please initialize Zentao API first");
445
- const result = await zentaoApi.getProductStories(productId, page, limit, fields);
446
- // 返回分页信息和数据
447
- const summary = {
448
- summary: `当前第 ${result.page} 页,共 ${result.total} 条需求,本页显示 ${result.stories.length} 条`,
449
- pagination: {
450
- page: result.page,
451
- limit: result.limit,
452
- total: result.total,
453
- totalPages: Math.ceil(result.total / result.limit)
454
- },
455
- stories: result.stories
456
- };
457
- return {
458
- content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
459
- };
460
- });
461
- server.tool("getProgramDetail", { programId: z.number() }, async ({ programId }) => {
462
- if (!zentaoApi)
463
- throw new Error("Please initialize Zentao API first");
464
- return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.getProgramDetail(programId), null, 2) }] };
465
- });
466
- server.tool("createProgram", {
467
- name: z.string().optional(), parent: z.string().optional(), PM: z.string().optional(),
468
- budget: z.number().optional(), budgetUnit: z.string().optional(), desc: z.string().optional(),
469
- begin: z.string().optional(), end: z.string().optional(), acl: z.string().optional(),
470
- whitelist: z.array(z.string()).optional()
471
- }, async (params) => {
472
- if (!zentaoApi)
473
- throw new Error("Please initialize Zentao API first");
474
- return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.createProgram(params), null, 2) }] };
475
- });
476
- server.tool("updateProgram", {
477
- programId: z.number(),
478
- update: z.object({
479
- name: z.string().optional(), parent: z.string().optional(), PM: z.string().optional(),
480
- budget: z.number().optional(), budgetUnit: z.string().optional(), desc: z.string().optional(),
481
- begin: z.string().optional(), end: z.string().optional(), acl: z.string().optional(),
482
- whitelist: z.array(z.string()).optional()
483
- })
484
- }, async ({ programId, update }) => {
249
+ server.tool("createExecution", {
250
+ projectId: z.number(), project: z.number(), name: z.string(), code: z.string(),
251
+ begin: z.string(), end: z.string(), days: z.number().optional(), lifetime: z.string().optional(),
252
+ products: z.array(z.number()).describe("关联产品ID数组"),
253
+ PO: z.string().optional(), PM: z.string().optional(), QD: z.string().optional(),
254
+ RD: z.string().optional(), teamMembers: z.array(z.string()).optional(),
255
+ desc: z.string().optional(), acl: z.string().optional(), whitelist: z.array(z.string()).optional()
256
+ }, async ({ projectId, ...params }) => {
485
257
  if (!zentaoApi)
486
258
  throw new Error("Please initialize Zentao API first");
487
- return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.updateProgram(programId, update), null, 2) }] };
259
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.createExecution(projectId, params), null, 2) }] };
488
260
  });
489
- server.tool("deleteProgram", { programId: z.number() }, async ({ programId }) => {
261
+ server.tool("deleteExecution", { executionId: z.number() }, async ({ executionId }) => {
490
262
  if (!zentaoApi)
491
263
  throw new Error("Please initialize Zentao API first");
492
- return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.deleteProgram(programId), null, 2) }] };
493
- });
494
- server.tool("getProductDetail", { productId: z.number() }, async ({ productId }) => {
495
- if (!zentaoApi)
496
- throw new Error("Please initialize Zentao API first");
497
- return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.getProductDetail(productId), null, 2) }] };
498
- });
499
- server.tool("createProduct", {
500
- name: z.string(), code: z.string(), program: z.number().optional(), line: z.number().optional(),
501
- PO: z.string().optional(), QD: z.string().optional(), RD: z.string().optional(),
502
- type: z.string().optional(), desc: z.string().optional(), acl: z.string().optional(),
503
- whitelist: z.array(z.string()).optional()
504
- }, async (params) => {
505
- if (!zentaoApi)
506
- throw new Error("Please initialize Zentao API first");
507
- return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.createProduct(params), null, 2) }] };
508
- });
509
- server.tool("updateProduct", {
510
- productId: z.number(),
511
- update: z.object({
512
- name: z.string().optional(), code: z.string().optional(), type: z.string().optional(),
513
- line: z.number().optional(), program: z.number().optional(), status: z.string().optional(),
514
- desc: z.string().optional()
515
- })
516
- }, async ({ productId, update }) => {
517
- if (!zentaoApi)
518
- throw new Error("Please initialize Zentao API first");
519
- return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.updateProduct(productId, update), null, 2) }] };
520
- });
521
- server.tool("deleteProduct", { productId: z.number() }, async ({ productId }) => {
522
- if (!zentaoApi)
523
- throw new Error("Please initialize Zentao API first");
524
- return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.deleteProduct(productId), null, 2) }] };
264
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.deleteExecution(executionId), null, 2) }] };
525
265
  });
526
266
  server.tool("deleteStory", "删除需求 - 支持软件需求(story)和用户需求(requirement),自动识别类型", {
527
267
  storyId: z.number().describe("需求ID(可以是story或requirement类型)")
@@ -530,269 +270,18 @@ server.tool("deleteStory", "删除需求 - 支持软件需求(story)和用户需
530
270
  throw new Error("Please initialize Zentao API first");
531
271
  return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.deleteStory(storyId), null, 2) }] };
532
272
  });
533
- server.tool("getModules", {
534
- type: z.enum(['story', 'task', 'bug', 'case', 'feedback', 'product']),
535
- id: z.number(),
536
- fields: z.string().optional()
537
- }, async ({ type, id, fields }) => {
538
- if (!zentaoApi)
539
- throw new Error("Please initialize Zentao API first");
540
- const response = await zentaoApi.getModules(type, id, fields);
541
- return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] };
542
- });
543
- server.tool("uploadFile", {
544
- filePath: z.string().optional(),
545
- base64Data: z.string().optional(),
546
- filename: z.string().optional(),
547
- uid: z.string().optional()
548
- }, async ({ filePath, base64Data, filename, uid }) => {
273
+ server.tool("getMyProfile", {}, async () => {
549
274
  if (!zentaoApi)
550
275
  throw new Error("Please initialize Zentao API first");
551
- const fs = await import('fs');
552
- const path = await import('path');
553
- let fileBuffer;
554
- let finalFilename;
555
- let savedPath;
556
- // 如果提供了 base64 数据(复制的图片)
557
- if (base64Data) {
558
- // 创建 img 文件夹(如果不存在)
559
- const imgDir = path.join(process.cwd(), 'img');
560
- if (!fs.existsSync(imgDir)) {
561
- fs.mkdirSync(imgDir, { recursive: true });
562
- }
563
- // 处理 base64 数据
564
- const matches = base64Data.match(/^data:image\/(\w+);base64,(.+)$/);
565
- if (matches) {
566
- const ext = matches[1];
567
- const data = matches[2];
568
- fileBuffer = Buffer.from(data, 'base64');
569
- finalFilename = filename || `image_${Date.now()}.${ext}`;
570
- }
571
- else {
572
- // 直接的 base64 数据,没有 data URL 前缀
573
- fileBuffer = Buffer.from(base64Data, 'base64');
574
- finalFilename = filename || `image_${Date.now()}.png`;
575
- }
576
- // 保存到 img 文件夹
577
- savedPath = path.join(imgDir, finalFilename);
578
- fs.writeFileSync(savedPath, fileBuffer);
579
- }
580
- // 如果提供了文件路径
581
- else if (filePath) {
582
- fileBuffer = fs.readFileSync(filePath);
583
- finalFilename = path.basename(filePath);
584
- }
585
- else {
586
- throw new Error("必须提供 filePath 或 base64Data 参数");
587
- }
588
- const result = await zentaoApi.uploadFile({
589
- file: fileBuffer,
590
- filename: finalFilename,
591
- uid
592
- });
593
- // 生成禅道需要的 HTML 格式
594
- const fileId = result.id;
595
- const baseUrl = zentaoApi.getConfig().url;
596
- const imageUrl = `${baseUrl}/zentao/entao/api.php?m=file&f=read&t=png&fileID=${fileId}`;
597
- const imageHtml = `<p><img onload="setImageSize(this,0)" src="${imageUrl}" alt="${finalFilename}" /></p>`;
598
- const response = {
599
- upload: result,
600
- fileId: fileId,
601
- imageUrl: imageUrl,
602
- imageHtml: imageHtml,
603
- tip: `更新 Bug 描述时,请使用 imageHtml 字段中的 HTML 代码。图片会被包裹在 <p> 标签中以确保正确显示。`
604
- };
605
- if (savedPath) {
606
- response.savedPath = savedPath;
607
- }
608
- return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] };
609
- });
610
- server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动获取剪贴板中的图片并上传", {
611
- filename: z.string().optional().describe("自定义文件名,默认为 clipboard_时间戳.png"),
612
- uid: z.string().optional().describe("禅道文件UID,用于关联需求或Bug")
613
- }, async ({ filename, uid }) => {
614
- if (!zentaoApi) {
615
- return {
616
- content: [{
617
- type: "text",
618
- text: JSON.stringify({ error: "请先初始化禅道API配置" }, null, 2)
619
- }],
620
- isError: true
621
- };
622
- }
623
- const fs = await import('fs');
624
- const path = await import('path');
625
- const { execSync } = await import('child_process');
626
- try {
627
- process.stdout.write('[uploadImageFromClipboard] 开始执行...\n');
628
- // 读取系统剪贴板图片
629
- let fileBuffer;
630
- let finalFilename;
631
- // Windows: 使用 PowerShell 脚本读取剪贴板
632
- if (process.platform === 'win32') {
633
- process.stdout.write('[uploadImageFromClipboard] Windows平台,使用PowerShell脚本读取剪贴板...\n');
634
- // 内嵌 PowerShell 脚本,避免依赖外部文件
635
- const psScript = `Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); $bytes = $ms.ToArray(); $ms.Close(); $base64 = [Convert]::ToBase64String($bytes); Write-Output $base64 } else { Write-Output 'NoImage' }`;
636
- try {
637
- // 使用 Base64 编码 PowerShell 脚本以避免转义问题
638
- const psScriptBase64 = Buffer.from(psScript, 'utf16le').toString('base64');
639
- const result = execSync(`powershell -ExecutionPolicy Bypass -EncodedCommand ${psScriptBase64}`, {
640
- encoding: 'utf8',
641
- maxBuffer: 10 * 1024 * 1024,
642
- stdio: ['pipe', 'pipe', 'pipe']
643
- }).trim();
644
- if (!result || result.includes('NoImage') || result.includes('Error')) {
645
- process.stderr.write('[uploadImageFromClipboard] 剪贴板中没有图片\n');
646
- return {
647
- content: [{
648
- type: "text",
649
- text: JSON.stringify({
650
- error: "剪贴板中没有图片",
651
- tip: "请先复制图片,不要粘贴到输入框,直接告诉我'上传'"
652
- }, null, 2)
653
- }],
654
- isError: true
655
- };
656
- }
657
- fileBuffer = Buffer.from(result, 'base64');
658
- finalFilename = filename || `clipboard_${Date.now()}.png`;
659
- process.stdout.write(`[uploadImageFromClipboard] 已读取剪贴板图片,大小: ${fileBuffer.length} bytes\n`);
660
- }
661
- catch (err) {
662
- process.stderr.write('[uploadImageFromClipboard] Windows读取剪贴板失败: ' + String(err) + '\n');
663
- return {
664
- content: [{
665
- type: "text",
666
- text: JSON.stringify({
667
- error: "读取剪贴板失败",
668
- details: err.message,
669
- tip: "请确保已复制图片到剪贴板"
670
- }, null, 2)
671
- }],
672
- isError: true
673
- };
674
- }
675
- }
676
- // macOS: 使用 pngpaste
677
- else if (process.platform === 'darwin') {
678
- process.stdout.write('[uploadImageFromClipboard] macOS平台,使用pngpaste读取剪贴板...\n');
679
- const tempFile = path.join(process.cwd(), '.temp_clipboard.png');
680
- try {
681
- execSync(`pngpaste "${tempFile}"`);
682
- fileBuffer = fs.readFileSync(tempFile);
683
- fs.unlinkSync(tempFile);
684
- finalFilename = filename || `clipboard_${Date.now()}.png`;
685
- process.stdout.write(`[uploadImageFromClipboard] 已读取剪贴板图片,大小: ${fileBuffer.length} bytes\n`);
686
- }
687
- catch (err) {
688
- process.stderr.write('[uploadImageFromClipboard] macOS读取剪贴板失败: ' + String(err) + '\n');
689
- return {
690
- content: [{
691
- type: "text",
692
- text: JSON.stringify({
693
- error: "剪贴板中没有图片或pngpaste未安装",
694
- tip: "请先安装 pngpaste: brew install pngpaste"
695
- }, null, 2)
696
- }],
697
- isError: true
698
- };
699
- }
700
- }
701
- // Linux: 使用 xclip
702
- else {
703
- process.stdout.write('[uploadImageFromClipboard] Linux平台,使用xclip读取剪贴板...\n');
704
- try {
705
- const buffer = execSync('xclip -selection clipboard -t image/png -o', {
706
- maxBuffer: 10 * 1024 * 1024
707
- });
708
- fileBuffer = buffer;
709
- finalFilename = filename || `clipboard_${Date.now()}.png`;
710
- process.stdout.write(`[uploadImageFromClipboard] 已读取剪贴板图片,大小: ${fileBuffer.length} bytes\n`);
711
- }
712
- catch (err) {
713
- process.stderr.write('[uploadImageFromClipboard] Linux读取剪贴板失败: ' + String(err) + '\n');
714
- return {
715
- content: [{
716
- type: "text",
717
- text: JSON.stringify({
718
- error: "剪贴板中没有图片或xclip未安装",
719
- tip: "请先安装 xclip: sudo apt-get install xclip"
720
- }, null, 2)
721
- }],
722
- isError: true
723
- };
724
- }
725
- }
726
- // 创建 img 文件夹
727
- const imgDir = path.join(process.cwd(), 'img');
728
- if (!fs.existsSync(imgDir)) {
729
- process.stdout.write(`[uploadImageFromClipboard] 创建目录: ${imgDir}\n`);
730
- fs.mkdirSync(imgDir, { recursive: true });
731
- }
732
- // 保存到 img 文件夹
733
- const savedPath = path.join(imgDir, finalFilename);
734
- fs.writeFileSync(savedPath, fileBuffer);
735
- process.stdout.write(`[uploadImageFromClipboard] 图片已保存到: ${savedPath}\n`);
736
- // 上传到禅道
737
- process.stdout.write('[uploadImageFromClipboard] 开始上传到禅道...\n');
738
- const uploadResult = await zentaoApi.uploadFile({
739
- file: fileBuffer,
740
- filename: finalFilename,
741
- uid
742
- });
743
- process.stdout.write('[uploadImageFromClipboard] 上传成功,结果: ' + JSON.stringify(uploadResult) + '\n');
744
- // 生成禅道需要的 HTML 格式
745
- const fileId = uploadResult.id;
746
- const baseUrl = zentaoApi.getConfig().url;
747
- const imageUrl = `${baseUrl}/zentao/file-read-${fileId}.png`;
748
- const imageHtml = `<p><img onload="setImageSize(this,0)" src="${imageUrl}" alt="${finalFilename}" /></p>`;
749
- const response = {
750
- success: true,
751
- upload: uploadResult,
752
- savedPath: savedPath,
753
- filename: finalFilename,
754
- fileSize: fileBuffer.length,
755
- fileId: fileId,
756
- imageUrl: imageUrl,
757
- imageHtml: imageHtml,
758
- message: `图片已保存到本地并上传到禅道`,
759
- tip: `更新 Bug 描述时,请使用 imageHtml 字段中的 HTML 代码。图片会被包裹在 <p> 标签中以确保正确显示。`
760
- };
761
- process.stdout.write('[uploadImageFromClipboard] 返回结果: ' + JSON.stringify(response) + '\n');
762
- return {
763
- content: [{
764
- type: "text",
765
- text: JSON.stringify(response, null, 2)
766
- }]
767
- };
768
- }
769
- catch (error) {
770
- process.stderr.write('[uploadImageFromClipboard] 发生错误: ' + String(error) + '\n');
771
- const errorResponse = {
772
- success: false,
773
- error: error.message || String(error),
774
- stack: error.stack,
775
- tip: "请检查:\n1. 确保已复制图片到剪贴板\n2. 不要粘贴到输入框,直接说'上传剪贴板图片'\n3. 或使用命令行: upload.bat"
776
- };
777
- return {
778
- content: [{
779
- type: "text",
780
- text: JSON.stringify(errorResponse, null, 2)
781
- }],
782
- isError: true
783
- };
784
- }
276
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.getMyProfile(), null, 2) }] };
785
277
  });
786
- server.tool("downloadFile", {
787
- fileId: z.number(),
788
- savePath: z.string()
789
- }, async ({ fileId, savePath }) => {
278
+ server.tool("getUsers", {
279
+ page: z.number().optional(),
280
+ limit: z.number().optional()
281
+ }, async (params) => {
790
282
  if (!zentaoApi)
791
283
  throw new Error("Please initialize Zentao API first");
792
- const fs = await import('fs');
793
- const fileBuffer = await zentaoApi.downloadFile(fileId);
794
- fs.writeFileSync(savePath, fileBuffer);
795
- return { content: [{ type: "text", text: `文件已下载到: ${savePath}` }] };
284
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.getUsers(params), null, 2) }] };
796
285
  });
797
286
  server.tool("getComments", "获取评论列表 - 获取指定对象的所有评论", {
798
287
  objectType: z.enum([
@@ -825,19 +314,6 @@ server.tool("getComments", "获取评论列表 - 获取指定对象的所有评
825
314
  }]
826
315
  };
827
316
  });
828
- server.tool("getCommentDetail", "获取单条评论详情", {
829
- commentId: z.number().describe("评论ID")
830
- }, async ({ commentId }) => {
831
- if (!zentaoApi)
832
- throw new Error("Please initialize Zentao API first");
833
- const comment = await zentaoApi.getCommentDetail(commentId);
834
- return {
835
- content: [{
836
- type: "text",
837
- text: JSON.stringify(comment, null, 2)
838
- }]
839
- };
840
- });
841
317
  server.tool("addComment", "添加评论 - 为需求、任务、Bug等对象添加评论", {
842
318
  objectType: z.enum([
843
319
  'story', // 软件需求
@@ -874,43 +350,6 @@ server.tool("addComment", "添加评论 - 为需求、任务、Bug等对象添
874
350
  }]
875
351
  };
876
352
  });
877
- server.tool("updateComment", "更新评论 - 只能更新自己的评论", {
878
- commentId: z.number().describe("评论ID"),
879
- comment: z.string().describe("更新后的评论内容,支持HTML格式"),
880
- uid: z.string().optional().describe("唯一标识符(可选)")
881
- }, async ({ commentId, comment, uid }) => {
882
- if (!zentaoApi)
883
- throw new Error("Please initialize Zentao API first");
884
- const result = await zentaoApi.updateComment(commentId, {
885
- comment,
886
- uid
887
- });
888
- return {
889
- content: [{
890
- type: "text",
891
- text: JSON.stringify({
892
- message: '评论更新成功',
893
- comment: result
894
- }, null, 2)
895
- }]
896
- };
897
- });
898
- server.tool("deleteComment", "删除评论 - 只能删除自己的评论,管理员可以删除所有评论", {
899
- commentId: z.number().describe("评论ID")
900
- }, async ({ commentId }) => {
901
- if (!zentaoApi)
902
- throw new Error("Please initialize Zentao API first");
903
- const result = await zentaoApi.deleteComment(commentId);
904
- return {
905
- content: [{
906
- type: "text",
907
- text: JSON.stringify({
908
- status: 'success',
909
- message: result.message || '评论删除成功'
910
- }, null, 2)
911
- }]
912
- };
913
- });
914
353
  await startServerTransport(server, { args }).catch(err => {
915
354
  process.stderr.write('[FATAL] MCP server failed: ' + String(err) + '\n');
916
355
  process.exit(1);