@zzp123/mcp-zentao 1.18.2 → 1.18.4

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.js CHANGED
@@ -4,6 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { z } from "zod";
5
5
  import { ZentaoAPI } from './api/zentaoApi.js';
6
6
  import { loadConfig, saveConfig } from './config.js';
7
+ import { withApi } from './mcpHelpers.js';
7
8
  // 解析命令行参数
8
9
  const args = process.argv.slice(2);
9
10
  let configData = null;
@@ -14,15 +15,12 @@ if (configIndex !== -1 && configIndex + 1 < args.length) {
14
15
  // 获取 --config 后面的 JSON 字符串并解析
15
16
  const jsonStr = args[configIndex + 1];
16
17
  configData = JSON.parse(jsonStr);
17
- console.log('成功解析配置数据:', configData);
18
18
  // 如果配置数据中包含 config 对象,则保存配置
19
19
  if (configData.config) {
20
- console.log('正在保存配置...');
21
20
  saveConfig(configData.config);
22
21
  }
23
22
  }
24
23
  catch (error) {
25
- console.error('配置解析失败:', error);
26
24
  process.exit(1);
27
25
  }
28
26
  }
@@ -33,11 +31,10 @@ const server = new McpServer({
33
31
  });
34
32
  // Initialize ZentaoAPI instance
35
33
  let zentaoApi = null;
34
+ const getApi = () => zentaoApi;
36
35
  export default async function main(params) {
37
- console.log('接收到的参数:', params);
38
36
  // 如果传入了配置信息,就保存它
39
37
  if (params.config) {
40
- console.log('保存新的配置信息...');
41
38
  saveConfig(params.config);
42
39
  }
43
40
  }
@@ -58,25 +55,21 @@ server.tool("initZentao", {}, async ({}) => {
58
55
  // Add getMyTasks tool
59
56
  server.tool("getMyTasks", {
60
57
  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);
58
+ }, withApi(getApi, async (api, { status }) => {
59
+ const tasks = await api.getMyTasks(status);
65
60
  return {
66
61
  content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }]
67
62
  };
68
- });
63
+ }));
69
64
  // Add getTaskDetail tool
70
65
  server.tool("getTaskDetail", {
71
66
  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);
67
+ }, withApi(getApi, async (api, { taskId }) => {
68
+ const task = await api.getTaskDetail(taskId);
76
69
  return {
77
70
  content: [{ type: "text", text: JSON.stringify(task, null, 2) }]
78
71
  };
79
- });
72
+ }));
80
73
  // Add getProducts tool
81
74
  server.tool("getProducts", "获取产品列表 - 只返回核心字段(id, 名称, 负责人)", {
82
75
  fields: z.enum(['basic', 'default', 'full']).optional().describe("返回字段级别:'basic'(id/名称/状态)、'default'(+负责人/创建人,默认)、'full'(完整字段)")
@@ -564,76 +557,58 @@ server.tool("getProjects", {
564
557
  };
565
558
  });
566
559
  // Add changeStory tool
567
- server.tool("changeStory", "需求变更 - 支持软件需求(story)和用户需求(requirement),自动识别类型。支持对象或JSON字符串格式", {
560
+ server.tool("changeStory", "需求变更 - 支持软件需求(story)和用户需求(requirement),自动识别类型", {
568
561
  storyId: z.number().describe("需求ID(可以是story或requirement类型)"),
569
- update: z.union([
570
- z.object({
571
- // 基本信息
572
- title: z.string().optional(),
573
- product: z.number().optional(),
574
- parent: z.number().optional(),
575
- module: z.number().optional(),
576
- branch: z.number().optional(),
577
- plan: z.union([z.number(), z.array(z.number())]).optional(),
578
- type: z.string().optional(),
579
- // 来源信息
580
- source: z.string().optional(),
581
- sourceNote: z.string().optional(),
582
- // 分类与优先级
583
- category: z.string().optional(),
584
- pri: z.number().optional(),
585
- estimate: z.number().optional(),
586
- // 状态与阶段
587
- stage: z.string().optional(),
588
- status: z.string().optional(),
589
- // 关键词与标识
590
- keywords: z.string().optional(),
591
- color: z.string().optional(),
592
- grade: z.number().optional(),
593
- // 人员相关
594
- mailto: z.array(z.string()).optional(),
595
- reviewer: z.array(z.string()).optional().describe("评审人员列表(通常为必填,除非设置needNotReview=true跳过评审)"),
596
- assignedTo: z.string().optional(),
597
- closedBy: z.string().optional(),
598
- feedbackBy: z.string().optional(),
599
- // 关闭相关
600
- closedReason: z.enum(['done', 'subdivided', 'duplicate', 'postponed', 'willnotdo', 'cancel', 'bydesign']).optional(),
601
- duplicateStory: z.number().optional(),
602
- // 评审相关
603
- needNotReview: z.boolean().optional().describe("是否跳过评审,如果不提供reviewer则应设置为true"),
604
- // 通知相关
605
- notifyEmail: z.string().optional(),
606
- // 描述内容
607
- spec: z.string().optional(),
608
- verify: z.string().optional(),
609
- // 备注
610
- comment: z.string().optional()
611
- }),
612
- z.string()
613
- ]).describe(`更新需求(使用 PUT 接口)
562
+ update: z.object({
563
+ // 基本信息
564
+ title: z.string().optional(),
565
+ product: z.number().optional(),
566
+ parent: z.number().optional(),
567
+ module: z.number().optional(),
568
+ branch: z.number().optional(),
569
+ plan: z.union([z.number(), z.array(z.number())]).optional(),
570
+ type: z.string().optional(),
571
+ // 来源信息
572
+ source: z.string().optional(),
573
+ sourceNote: z.string().optional(),
574
+ // 分类与优先级
575
+ category: z.string().optional(),
576
+ pri: z.number().optional(),
577
+ estimate: z.number().optional(),
578
+ // 状态与阶段
579
+ stage: z.string().optional(),
580
+ status: z.string().optional(),
581
+ // 关键词与标识
582
+ keywords: z.string().optional(),
583
+ color: z.string().optional(),
584
+ grade: z.number().optional(),
585
+ // 人员相关
586
+ mailto: z.array(z.string()).optional(),
587
+ reviewer: z.array(z.string()).optional().describe("评审人员列表(通常为必填,除非设置needNotReview=true跳过评审)"),
588
+ assignedTo: z.string().optional(),
589
+ closedBy: z.string().optional(),
590
+ feedbackBy: z.string().optional(),
591
+ // 关闭相关
592
+ closedReason: z.enum(['done', 'subdivided', 'duplicate', 'postponed', 'willnotdo', 'cancel', 'bydesign']).optional(),
593
+ duplicateStory: z.number().optional(),
594
+ // 评审相关
595
+ needNotReview: z.boolean().optional().describe("是否跳过评审,如果不提供reviewer则应设置为true"),
596
+ // 通知相关
597
+ notifyEmail: z.string().optional(),
598
+ // 描述内容
599
+ spec: z.string().optional(),
600
+ verify: z.string().optional(),
601
+ // 备注
602
+ comment: z.string().optional()
603
+ }).describe(`更新需求(使用 PUT 接口)
614
604
 
615
605
  此工具使用标准的"修改需求其他字段"接口(PUT /stories/:id),支持修改29个字段。
616
606
  自动处理评审人问题,无需手动设置 needNotReview。
617
-
618
- 支持对象或JSON字符串格式。
619
607
  `.trim())
620
608
  }, async ({ storyId, update }) => {
621
609
  if (!zentaoApi)
622
610
  throw new Error("Please initialize Zentao API first");
623
- // 如果 update 是字符串,尝试解析为对象
624
- let updateData;
625
- if (typeof update === 'string') {
626
- try {
627
- updateData = JSON.parse(update);
628
- }
629
- catch (error) {
630
- throw new Error(`Invalid JSON string: ${error instanceof Error ? error.message : 'Unknown error'}`);
631
- }
632
- }
633
- else {
634
- updateData = update;
635
- }
636
- const story = await zentaoApi.changeStory(storyId, updateData);
611
+ const story = await zentaoApi.changeStory(storyId, update);
637
612
  return {
638
613
  content: [{ type: "text", text: JSON.stringify(story, null, 2) }]
639
614
  };
@@ -878,49 +853,32 @@ server.tool("getRequirementDetail", "获取用户需求详情 - 用户需求的
878
853
  });
879
854
  server.tool("changeRequirement", "用户需求变更 - 用户需求的专用接口,提供更好的语义化", {
880
855
  requirementId: z.number().describe("用户需求ID"),
881
- update: z.union([
882
- z.object({
883
- title: z.string().optional(),
884
- product: z.number().optional(),
885
- parent: z.number().optional(),
886
- module: z.number().optional(),
887
- pri: z.number().optional(),
888
- category: z.string().optional(),
889
- spec: z.string().optional(),
890
- verify: z.string().optional(),
891
- source: z.string().optional(),
892
- sourceNote: z.string().optional(),
893
- estimate: z.number().optional(),
894
- keywords: z.string().optional(),
895
- assignedTo: z.string().optional(),
896
- reviewer: z.array(z.string()).optional(),
897
- comment: z.string().optional()
898
- }),
899
- z.string().describe("JSON字符串格式的更新内容")
900
- ]).describe(`
856
+ update: z.object({
857
+ title: z.string().optional(),
858
+ product: z.number().optional(),
859
+ parent: z.number().optional(),
860
+ module: z.number().optional(),
861
+ pri: z.number().optional(),
862
+ category: z.string().optional(),
863
+ spec: z.string().optional(),
864
+ verify: z.string().optional(),
865
+ source: z.string().optional(),
866
+ sourceNote: z.string().optional(),
867
+ estimate: z.number().optional(),
868
+ keywords: z.string().optional(),
869
+ assignedTo: z.string().optional(),
870
+ reviewer: z.array(z.string()).optional(),
871
+ comment: z.string().optional()
872
+ }).describe(`
901
873
  更新用户需求(使用 PUT /stories/:id 接口)
902
874
 
903
875
  此工具使用标准的"修改需求其他字段"接口(PUT /stories/:id),支持修改多个字段。
904
876
  自动处理评审人问题,无需手动设置 needNotReview。
905
-
906
- 支持对象或JSON字符串格式。
907
877
  `.trim())
908
878
  }, async ({ requirementId, update }) => {
909
879
  if (!zentaoApi)
910
880
  throw new Error("Please initialize Zentao API first");
911
- let updateData;
912
- if (typeof update === 'string') {
913
- try {
914
- updateData = JSON.parse(update);
915
- }
916
- catch (e) {
917
- throw new Error('Invalid JSON string for update parameter');
918
- }
919
- }
920
- else {
921
- updateData = update;
922
- }
923
- const requirement = await zentaoApi.changeStory(requirementId, updateData);
881
+ const requirement = await zentaoApi.changeStory(requirementId, update);
924
882
  return {
925
883
  content: [{ type: "text", text: JSON.stringify(requirement, null, 2) }]
926
884
  };
@@ -1311,13 +1269,11 @@ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动
1311
1269
  const path = await import('path');
1312
1270
  const { execSync } = await import('child_process');
1313
1271
  try {
1314
- console.log('[uploadImageFromClipboard] 开始执行...');
1315
1272
  // 读取系统剪贴板图片
1316
1273
  let fileBuffer;
1317
1274
  let finalFilename;
1318
1275
  // Windows: 使用 PowerShell 脚本读取剪贴板
1319
1276
  if (process.platform === 'win32') {
1320
- console.log('[uploadImageFromClipboard] Windows平台,使用PowerShell脚本读取剪贴板...');
1321
1277
  // 内嵌 PowerShell 脚本,避免依赖外部文件
1322
1278
  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' }`;
1323
1279
  try {
@@ -1329,7 +1285,6 @@ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动
1329
1285
  stdio: ['pipe', 'pipe', 'pipe']
1330
1286
  }).trim();
1331
1287
  if (!result || result.includes('NoImage') || result.includes('Error')) {
1332
- console.error('[uploadImageFromClipboard] 剪贴板中没有图片');
1333
1288
  return {
1334
1289
  content: [{
1335
1290
  type: "text",
@@ -1343,10 +1298,8 @@ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动
1343
1298
  }
1344
1299
  fileBuffer = Buffer.from(result, 'base64');
1345
1300
  finalFilename = filename || `clipboard_${Date.now()}.png`;
1346
- console.log(`[uploadImageFromClipboard] 已读取剪贴板图片,大小: ${fileBuffer.length} bytes`);
1347
1301
  }
1348
1302
  catch (err) {
1349
- console.error('[uploadImageFromClipboard] Windows读取剪贴板失败:', err);
1350
1303
  return {
1351
1304
  content: [{
1352
1305
  type: "text",
@@ -1362,17 +1315,14 @@ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动
1362
1315
  }
1363
1316
  // macOS: 使用 pngpaste
1364
1317
  else if (process.platform === 'darwin') {
1365
- console.log('[uploadImageFromClipboard] macOS平台,使用pngpaste读取剪贴板...');
1366
1318
  const tempFile = path.join(process.cwd(), '.temp_clipboard.png');
1367
1319
  try {
1368
1320
  execSync(`pngpaste "${tempFile}"`);
1369
1321
  fileBuffer = fs.readFileSync(tempFile);
1370
1322
  fs.unlinkSync(tempFile);
1371
1323
  finalFilename = filename || `clipboard_${Date.now()}.png`;
1372
- console.log(`[uploadImageFromClipboard] 已读取剪贴板图片,大小: ${fileBuffer.length} bytes`);
1373
1324
  }
1374
1325
  catch (err) {
1375
- console.error('[uploadImageFromClipboard] macOS读取剪贴板失败:', err);
1376
1326
  return {
1377
1327
  content: [{
1378
1328
  type: "text",
@@ -1387,17 +1337,14 @@ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动
1387
1337
  }
1388
1338
  // Linux: 使用 xclip
1389
1339
  else {
1390
- console.log('[uploadImageFromClipboard] Linux平台,使用xclip读取剪贴板...');
1391
1340
  try {
1392
1341
  const buffer = execSync('xclip -selection clipboard -t image/png -o', {
1393
1342
  maxBuffer: 10 * 1024 * 1024
1394
1343
  });
1395
1344
  fileBuffer = buffer;
1396
1345
  finalFilename = filename || `clipboard_${Date.now()}.png`;
1397
- console.log(`[uploadImageFromClipboard] 已读取剪贴板图片,大小: ${fileBuffer.length} bytes`);
1398
1346
  }
1399
1347
  catch (err) {
1400
- console.error('[uploadImageFromClipboard] Linux读取剪贴板失败:', err);
1401
1348
  return {
1402
1349
  content: [{
1403
1350
  type: "text",
@@ -1413,21 +1360,17 @@ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动
1413
1360
  // 创建 img 文件夹
1414
1361
  const imgDir = path.join(process.cwd(), 'img');
1415
1362
  if (!fs.existsSync(imgDir)) {
1416
- console.log(`[uploadImageFromClipboard] 创建目录: ${imgDir}`);
1417
1363
  fs.mkdirSync(imgDir, { recursive: true });
1418
1364
  }
1419
1365
  // 保存到 img 文件夹
1420
1366
  const savedPath = path.join(imgDir, finalFilename);
1421
1367
  fs.writeFileSync(savedPath, fileBuffer);
1422
- console.log(`[uploadImageFromClipboard] 图片已保存到: ${savedPath}`);
1423
1368
  // 上传到禅道
1424
- console.log('[uploadImageFromClipboard] 开始上传到禅道...');
1425
1369
  const uploadResult = await zentaoApi.uploadFile({
1426
1370
  file: fileBuffer,
1427
1371
  filename: finalFilename,
1428
1372
  uid
1429
1373
  });
1430
- console.log('[uploadImageFromClipboard] 上传成功,结果:', uploadResult);
1431
1374
  // 生成禅道需要的 HTML 格式
1432
1375
  const fileId = uploadResult.id;
1433
1376
  const baseUrl = zentaoApi.getConfig().url;
@@ -1445,7 +1388,6 @@ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动
1445
1388
  message: `图片已保存到本地并上传到禅道`,
1446
1389
  tip: `更新 Bug 描述时,请使用 imageHtml 字段中的 HTML 代码。图片会被包裹在 <p> 标签中以确保正确显示。`
1447
1390
  };
1448
- console.log('[uploadImageFromClipboard] 返回结果:', response);
1449
1391
  return {
1450
1392
  content: [{
1451
1393
  type: "text",
@@ -1454,7 +1396,6 @@ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动
1454
1396
  };
1455
1397
  }
1456
1398
  catch (error) {
1457
- console.error('[uploadImageFromClipboard] 发生错误:', error);
1458
1399
  const errorResponse = {
1459
1400
  success: false,
1460
1401
  error: error.message || String(error),
@@ -1601,4 +1542,7 @@ server.tool("deleteComment", "删除评论 - 只能删除自己的评论,管
1601
1542
  });
1602
1543
  // Start receiving messages on stdin and sending messages on stdout
1603
1544
  const transport = new StdioServerTransport();
1604
- await server.connect(transport).catch(console.error);
1545
+ await server.connect(transport).catch(err => {
1546
+ process.stderr.write('[FATAL] MCP server failed: ' + String(err) + '\n');
1547
+ process.exit(1);
1548
+ });
@@ -0,0 +1,13 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ZentaoAPI } from "./api/zentaoApi.js";
3
+ export type ApiGetter = () => ZentaoAPI | null;
4
+ export declare function ensureApi(getApi: ApiGetter): ZentaoAPI;
5
+ export declare function withApi<TArgs, TResult>(getApi: ApiGetter, handler: (api: ZentaoAPI, args: TArgs) => Promise<TResult>): (args: TArgs) => Promise<TResult>;
6
+ export type ToolHandler<TArgs = any> = (args: TArgs) => Promise<any>;
7
+ export interface ToolDefinition<TArgs = any> {
8
+ name: string;
9
+ description?: string;
10
+ schema: any;
11
+ handler: ToolHandler<TArgs>;
12
+ }
13
+ export declare function registerTools(server: McpServer, tools: ToolDefinition[]): void;
@@ -0,0 +1,24 @@
1
+ export function ensureApi(getApi) {
2
+ const api = getApi();
3
+ if (!api) {
4
+ throw new Error("Please initialize Zentao API first");
5
+ }
6
+ return api;
7
+ }
8
+ export function withApi(getApi, handler) {
9
+ return async (args) => {
10
+ const api = ensureApi(getApi);
11
+ return handler(api, args);
12
+ };
13
+ }
14
+ export function registerTools(server, tools) {
15
+ for (const tool of tools) {
16
+ const { name, description, schema, handler } = tool;
17
+ if (description) {
18
+ server.tool(name, description, schema, handler);
19
+ }
20
+ else {
21
+ server.tool(name, schema, handler);
22
+ }
23
+ }
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zzp123/mcp-zentao",
3
- "version": "1.18.2",
3
+ "version": "1.18.4",
4
4
  "description": "禅道项目管理系统的高级API集成包 - 完整版,包含所有94个工具。另有产品经理、测试工程师、开发工程师专用精简版本可选",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",