@bangdao-ai/zentao-mcp 1.1.2 → 1.1.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/index.js CHANGED
@@ -4,22 +4,70 @@
4
4
  * 通过 npm/npx 调用 Python MCP 服务器
5
5
  */
6
6
 
7
- const { spawn } = require('child_process');
7
+ const { spawn, execSync } = require('child_process');
8
8
  const path = require('path');
9
9
  const fs = require('fs');
10
+ const os = require('os');
10
11
 
11
- // 获取 Python 可执行文件路径
12
+ // 检测操作系统平台
13
+ const isWindows = process.platform === 'win32';
14
+
15
+ // 获取 Python 可执行文件路径(跨平台)
12
16
  function findPython() {
13
17
  const pythonCommands = ['python3', 'python'];
14
18
 
19
+ // 优先检查环境变量
20
+ const pythonEnv = process.env.PYTHON || process.env.PYTHON3;
21
+ if (pythonEnv && fs.existsSync(pythonEnv)) {
22
+ return pythonEnv;
23
+ }
24
+
25
+ // 尝试直接执行命令来查找 Python
15
26
  for (const cmd of pythonCommands) {
16
27
  try {
17
- const result = require('child_process').execSync(`which ${cmd}`, { encoding: 'utf-8' }).trim();
28
+ let result;
29
+ if (isWindows) {
30
+ // Windows 使用 where 命令
31
+ try {
32
+ result = execSync(`where ${cmd}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
33
+ // where 可能返回多行,取第一行
34
+ if (result) {
35
+ const lines = result.split('\n').filter(line => line.trim());
36
+ if (lines.length > 0) {
37
+ result = lines[0].trim();
38
+ }
39
+ }
40
+ } catch (e) {
41
+ // where 命令失败,尝试直接执行
42
+ try {
43
+ execSync(`${cmd} --version`, { encoding: 'utf-8', stdio: ['ignore', 'ignore', 'ignore'] });
44
+ // 如果能执行,返回命令名(spawn 会自动查找)
45
+ return cmd;
46
+ } catch (e2) {
47
+ continue;
48
+ }
49
+ }
50
+ } else {
51
+ // Unix/Linux/Mac 使用 which 命令
52
+ result = execSync(`which ${cmd}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
53
+ }
54
+
18
55
  if (result) {
19
56
  return result;
20
57
  }
21
58
  } catch (e) {
22
- // 继续尝试下一个
59
+ // 继续尝试下一个命令
60
+ continue;
61
+ }
62
+ }
63
+
64
+ // 如果都失败了,尝试直接使用命令名(让系统 PATH 处理)
65
+ for (const cmd of pythonCommands) {
66
+ try {
67
+ execSync(`${cmd} --version`, { encoding: 'utf-8', stdio: ['ignore', 'ignore', 'ignore'] });
68
+ return cmd;
69
+ } catch (e) {
70
+ continue;
23
71
  }
24
72
  }
25
73
 
@@ -66,10 +114,17 @@ function main() {
66
114
  // 启动 Python MCP 服务器
67
115
  // 注意:MCP 服务器通过 stdio 通信,不会输出到控制台
68
116
  // 这是正常行为,服务器会等待 MCP 协议消息
69
- const child = spawn(python, [scriptPath], {
117
+ const spawnOptions = {
70
118
  stdio: 'inherit',
71
119
  env: process.env
72
- });
120
+ };
121
+
122
+ // Windows 兼容性:如果 Python 路径包含空格或特殊字符,可能需要 shell
123
+ if (isWindows && (python.includes(' ') || python.includes('(') || python.includes(')'))) {
124
+ spawnOptions.shell = true;
125
+ }
126
+
127
+ const child = spawn(python, [scriptPath], spawnOptions);
73
128
 
74
129
  // 处理退出
75
130
  child.on('exit', (code) => {
@@ -91,27 +146,51 @@ function main() {
91
146
  process.exit(1);
92
147
  });
93
148
 
94
- // 处理信号
95
- process.on('SIGINT', () => {
96
- if (child && !child.killed) {
97
- child.kill('SIGINT');
98
- }
99
- process.exit(0);
100
- });
101
-
102
- process.on('SIGTERM', () => {
103
- if (child && !child.killed) {
104
- child.kill('SIGTERM');
105
- }
106
- process.exit(0);
107
- });
149
+ // 处理信号(Windows 兼容性)
150
+ if (!isWindows) {
151
+ process.on('SIGINT', () => {
152
+ if (child && !child.killed) {
153
+ child.kill('SIGINT');
154
+ }
155
+ process.exit(0);
156
+ });
157
+
158
+ process.on('SIGTERM', () => {
159
+ if (child && !child.killed) {
160
+ child.kill('SIGTERM');
161
+ }
162
+ process.exit(0);
163
+ });
164
+ } else {
165
+ // Windows 使用不同的信号处理
166
+ process.on('SIGINT', () => {
167
+ if (child && !child.killed) {
168
+ child.kill();
169
+ }
170
+ process.exit(0);
171
+ });
172
+
173
+ process.on('SIGTERM', () => {
174
+ if (child && !child.killed) {
175
+ child.kill();
176
+ }
177
+ process.exit(0);
178
+ });
179
+ }
108
180
 
109
181
  } catch (error) {
110
182
  console.error('错误:', error.message);
111
183
  console.error('\n故障排查:');
112
- console.error('1. 检查 Python 是否安装: python3 --version');
113
- console.error('2. 检查脚本是否存在: ls -la src/mcp_server.py');
114
- console.error('3. 检查依赖是否安装: pip3 list | grep mcp');
184
+ if (isWindows) {
185
+ console.error('1. 检查 Python 是否安装: python --version 或 python3 --version');
186
+ console.error('2. 检查脚本是否存在: dir src\\mcp_server.py');
187
+ console.error('3. 检查依赖是否安装: pip list | findstr mcp');
188
+ console.error('4. 确保 Python 已添加到系统 PATH 环境变量');
189
+ } else {
190
+ console.error('1. 检查 Python 是否安装: python3 --version');
191
+ console.error('2. 检查脚本是否存在: ls -la src/mcp_server.py');
192
+ console.error('3. 检查依赖是否安装: pip3 list | grep mcp');
193
+ }
115
194
  process.exit(1);
116
195
  }
117
196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bangdao-ai/zentao-mcp",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "禅道Bug管理系统MCP工具",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -19,7 +19,8 @@
19
19
  "license": "MIT",
20
20
  "files": [
21
21
  "index.js",
22
- "src/",
22
+ "src/mcp_server.py",
23
+ "src/zentao_nexus.py",
23
24
  "requirements.txt",
24
25
  "README.md",
25
26
  "package.json"
package/src/mcp_server.py CHANGED
@@ -203,22 +203,10 @@ class ZenTaoMCPServer:
203
203
  "enum": ["fixed", "bydesign", "reqchange", "duplicate", "external", "notrepro", "postponed", "willnotfix", "tostory", "history", "configchange"]
204
204
  },
205
205
  "resolved_by": {"type": "string", "description": "解决人(姓名或工号)"},
206
- "resolved_build": {"type": "string", "description": "解决版本ID(可选,如果未提供则默认使用trunk)"}
206
+ "resolved_build": {"type": "string", "description": "解决版本ID(如果解决方案是fixed时,解决版本必传)"}
207
207
  },
208
208
  "required": ["product_id", "bug_ids", "resolution", "resolved_by"]
209
209
  }
210
- ),
211
- Tool(
212
- name="close_bug",
213
- description="关闭BUG",
214
- inputSchema={
215
- "type": "object",
216
- "properties": {
217
- "bug_id": {"type": "string", "description": "BUG ID"},
218
- "comment": {"type": "string", "description": "关闭备注(可选,默认AI关闭)"}
219
- },
220
- "required": ["bug_id"]
221
- }
222
210
  )
223
211
  ]
224
212
 
@@ -448,13 +436,6 @@ class ZenTaoMCPServer:
448
436
  result = self.nexus.resolve_bug(product_id, bug_ids, resolution, resolved_by, resolved_build)
449
437
  return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
450
438
 
451
- elif name == "close_bug":
452
- bug_id = arguments["bug_id"]
453
- comment = arguments.get("comment", "AI关闭")
454
-
455
- result = self.nexus.close_bug(bug_id, comment)
456
- return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
457
-
458
439
  else:
459
440
  raise ValueError(f"未知的工具: {name}")
460
441
 
@@ -133,7 +133,7 @@ class ZenTaoAPIClient:
133
133
  md5_hash.update(token_string.encode('utf-8'))
134
134
  return md5_hash.hexdigest().lower()
135
135
 
136
- def _call_new_api(self, function_name, params=None, form_data=None, files=None, m_param='allneedlist'):
136
+ def _call_new_api(self, function_name, params=None, form_data=None, files=None):
137
137
  """
138
138
  调用新的API接口(使用code、time、token认证)
139
139
 
@@ -142,7 +142,6 @@ class ZenTaoAPIClient:
142
142
  params: URL参数(字典)
143
143
  form_data: POST form-data数据(字典或列表,列表格式用于数组参数)
144
144
  files: 文件上传(字典,用于文件上传接口)
145
- m_param: m参数值(默认allneedlist,某些接口可能需要其他值如bug)
146
145
 
147
146
  Returns:
148
147
  dict: API响应结果
@@ -155,7 +154,7 @@ class ZenTaoAPIClient:
155
154
  'code': self.code,
156
155
  'time': current_timestamp,
157
156
  'token': token,
158
- 'm': m_param,
157
+ 'm': 'allneedlist',
159
158
  'f': function_name
160
159
  }
161
160
 
@@ -167,68 +166,67 @@ class ZenTaoAPIClient:
167
166
  'User-Agent': 'Python-ZenTao-Client/1.0'
168
167
  }
169
168
 
170
- # 打印请求信息(仅在非静默模式下)
171
- if not self.silent:
172
- print("\n" + "=" * 80)
173
- print("【禅道API请求信息】")
174
- print("=" * 80)
175
- print(f"API URL: {api_url}")
176
- print(f"\n【Headers】")
177
- for key, value in headers.items():
178
- print(f" {key}: {value}")
179
-
180
- print(f"\n【Token生成信息】")
181
- print(f" code: {self.code}")
182
- print(f" key: {self.key[:10]}...{self.key[-10:] if len(self.key) > 20 else self.key}") # 只显示部分key,保护隐私
183
- print(f" time: {current_timestamp}")
184
- print(f" token生成公式: md5(code + key + time)")
185
- print(f" token生成字符串: {self.code}{self.key}{current_timestamp}")
186
- print(f" token: {token}")
187
-
188
- print(f"\n【URL参数】")
189
- for key, value in url_params.items():
190
- print(f" {key}: {value}")
191
-
192
- # 处理form_data用于显示
193
- display_form_data = form_data
194
- if isinstance(form_data, dict):
195
- # 检查是否有数组类型的值
196
- processed_data = []
197
- for key, value in form_data.items():
198
- if isinstance(value, (list, tuple)):
199
- for item in value:
200
- processed_data.append((key, str(item)))
201
- else:
202
- processed_data.append((key, str(value)))
203
- display_form_data = processed_data
204
-
205
- if display_form_data:
206
- print(f"\n【Body (form-data)】")
207
- if isinstance(display_form_data, list):
208
- for item in display_form_data:
209
- if isinstance(item, tuple):
210
- key, value = item
211
- # 如果是文件,只显示文件名
212
- if hasattr(value, 'read'):
213
- print(f" {key}: <文件对象>")
214
- else:
215
- print(f" {key}: {value}")
169
+ # 打印请求信息
170
+ print("\n" + "=" * 80)
171
+ print("【禅道API请求信息】")
172
+ print("=" * 80)
173
+ print(f"API URL: {api_url}")
174
+ print(f"\n【Headers】")
175
+ for key, value in headers.items():
176
+ print(f" {key}: {value}")
177
+
178
+ print(f"\n【Token生成信息】")
179
+ print(f" code: {self.code}")
180
+ print(f" key: {self.key[:10]}...{self.key[-10:] if len(self.key) > 20 else self.key}") # 只显示部分key,保护隐私
181
+ print(f" time: {current_timestamp}")
182
+ print(f" token生成公式: md5(code + key + time)")
183
+ print(f" token生成字符串: {self.code}{self.key}{current_timestamp}")
184
+ print(f" token: {token}")
185
+
186
+ print(f"\n【URL参数】")
187
+ for key, value in url_params.items():
188
+ print(f" {key}: {value}")
189
+
190
+ # 处理form_data用于显示
191
+ display_form_data = form_data
192
+ if isinstance(form_data, dict):
193
+ # 检查是否有数组类型的值
194
+ processed_data = []
195
+ for key, value in form_data.items():
196
+ if isinstance(value, (list, tuple)):
197
+ for item in value:
198
+ processed_data.append((key, str(item)))
199
+ else:
200
+ processed_data.append((key, str(value)))
201
+ display_form_data = processed_data
202
+
203
+ if display_form_data:
204
+ print(f"\n【Body (form-data)】")
205
+ if isinstance(display_form_data, list):
206
+ for item in display_form_data:
207
+ if isinstance(item, tuple):
208
+ key, value = item
209
+ # 如果是文件,只显示文件名
210
+ if hasattr(value, 'read'):
211
+ print(f" {key}: <文件对象>")
216
212
  else:
217
- print(f" {item}")
218
- elif isinstance(display_form_data, dict):
219
- for key, value in display_form_data.items():
220
- print(f" {key}: {value}")
221
-
222
- if files:
223
- print(f"\n【Files】")
224
- for key, file_info in files.items():
225
- if isinstance(file_info, tuple):
226
- filename = file_info[0] if len(file_info) > 0 else "unknown"
227
- print(f" {key}: {filename}")
213
+ print(f" {key}: {value}")
228
214
  else:
229
- print(f" {key}: {file_info}")
230
-
231
- print("=" * 80)
215
+ print(f" {item}")
216
+ elif isinstance(display_form_data, dict):
217
+ for key, value in display_form_data.items():
218
+ print(f" {key}: {value}")
219
+
220
+ if files:
221
+ print(f"\n【Files】")
222
+ for key, file_info in files.items():
223
+ if isinstance(file_info, tuple):
224
+ filename = file_info[0] if len(file_info) > 0 else "unknown"
225
+ print(f" {key}: {filename}")
226
+ else:
227
+ print(f" {key}: {file_info}")
228
+
229
+ print("=" * 80)
232
230
 
233
231
  try:
234
232
  if files:
@@ -253,33 +251,24 @@ class ZenTaoAPIClient:
253
251
  else:
254
252
  response = requests.post(api_url, params=url_params, headers=headers, timeout=30)
255
253
 
256
- # 处理响应
254
+ # 打印响应信息
255
+ print("\n【禅道API响应信息】")
256
+ print("=" * 80)
257
+ print(f"状态码: {response.status_code}")
258
+ print(f"响应Headers:")
259
+ for key, value in response.headers.items():
260
+ print(f" {key}: {value}")
261
+
257
262
  try:
258
263
  response_json = response.json()
259
- # 打印响应信息(仅在非静默模式下)
260
- if not self.silent:
261
- print("\n【禅道API响应信息】")
262
- print("=" * 80)
263
- print(f"状态码: {response.status_code}")
264
- print(f"响应Headers:")
265
- for key, value in response.headers.items():
266
- print(f" {key}: {value}")
267
- print(f"\n响应Body (JSON):")
268
- print(json.dumps(response_json, ensure_ascii=False, indent=2))
269
- print("=" * 80 + "\n")
264
+ print(f"\n响应Body (JSON):")
265
+ print(json.dumps(response_json, ensure_ascii=False, indent=2))
266
+ print("=" * 80 + "\n")
270
267
  return response_json
271
268
  except json.JSONDecodeError:
272
- # 打印响应信息(仅在非静默模式下)
273
- if not self.silent:
274
- print("\n【禅道API响应信息】")
275
- print("=" * 80)
276
- print(f"状态码: {response.status_code}")
277
- print(f"响应Headers:")
278
- for key, value in response.headers.items():
279
- print(f" {key}: {value}")
280
- print(f"\n响应Body (非JSON):")
281
- print(response.text[:1000]) # 只显示前1000个字符
282
- print("=" * 80 + "\n")
269
+ print(f"\n响应Body (非JSON):")
270
+ print(response.text[:1000]) # 只显示前1000个字符
271
+ print("=" * 80 + "\n")
283
272
  return {
284
273
  "status": 0,
285
274
  "message": "fail",
@@ -288,11 +277,9 @@ class ZenTaoAPIClient:
288
277
  "raw_response": response.text
289
278
  }
290
279
  except requests.exceptions.RequestException as e:
291
- # 打印异常信息(仅在非静默模式下)
292
- if not self.silent:
293
- print(f"\n【请求异常】")
294
- print(f"错误信息: {str(e)}")
295
- print("=" * 80 + "\n")
280
+ print(f"\n【请求异常】")
281
+ print(f"错误信息: {str(e)}")
282
+ print("=" * 80 + "\n")
296
283
  return {
297
284
  "status": 0,
298
285
  "message": "fail",
@@ -401,13 +388,13 @@ class ZenTaoAPIClient:
401
388
  history: 历史遗留
402
389
  configchange: 调整配置
403
390
  resolved_by: 解决人(必传,姓名或工号)
404
- resolved_build: 解决版本ID(可选,如果未提供则默认使用"trunk")
391
+ resolved_build: 解决版本ID(如果解决方案是fixed时,默认使用"trunk")
405
392
 
406
393
  Returns:
407
394
  dict: 解决结果
408
395
  """
409
- # 如果没有提供 resolved_build,使用默认值 "trunk"
410
- if not resolved_build:
396
+ # 如果 resolution 是 "fixed" 且没有提供 resolved_build,使用默认值 "trunk"
397
+ if resolution == 'fixed' and not resolved_build:
411
398
  resolved_build = "trunk"
412
399
 
413
400
  params = {'productID': product_id}
@@ -427,31 +414,6 @@ class ZenTaoAPIClient:
427
414
 
428
415
  return self._call_new_api('resolveBug', params=params, form_data=form_data)
429
416
 
430
- def close_bug(self, bug_id, comment="AI关闭"):
431
- """
432
- 关闭BUG
433
-
434
- Args:
435
- bug_id: BUG ID(必传)
436
- comment: 关闭备注(可选,默认"AI关闭")
437
-
438
- Returns:
439
- dict: 关闭结果
440
- """
441
- params = {'bugID': bug_id}
442
- form_data = {'comment': comment}
443
- result = self._call_new_api('close', params=params, form_data=form_data, m_param='bug')
444
-
445
- # 如果返回的是"Array"字符串,表示操作成功(禅道某些接口的特殊返回格式)
446
- if isinstance(result, dict) and result.get("raw_response") == "Array":
447
- return {
448
- "status": 1,
449
- "message": "success",
450
- "info": "success"
451
- }
452
-
453
- return result
454
-
455
417
  def create_bug_by_api(self, bug_data, use_form_data=True):
456
418
  """通过API创建bug"""
457
419
  return self._call_new_api('createbugbyapi', form_data=bug_data)
@@ -921,10 +883,6 @@ class ZenTaoNexus:
921
883
  def resolve_bug(self, product_id, bug_ids, resolution="fixed", resolved_by=None, resolved_build=None):
922
884
  """批量解决BUG"""
923
885
  return self.client.resolve_bug(product_id, bug_ids, resolution, resolved_by, resolved_build)
924
-
925
- def close_bug(self, bug_id, comment="AI关闭"):
926
- """关闭BUG"""
927
- return self.client.close_bug(bug_id, comment)
928
886
 
929
887
 
930
888
  # 便捷函数