@zzp123/mcp-zentao 1.18.7 → 1.18.8

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/README.md CHANGED
@@ -279,6 +279,70 @@ docker run -d \
279
279
  }
280
280
  ```
281
281
 
282
+ ## 🚦 传输方式:STDIO 与 HTTP 双支持
283
+
284
+ 默认使用 STDIO 传输(适合本地/CLI 场景)。如果需要通过 URL 与 MCP 客户端通信,可启用 HTTP 传输。两种配置方式任选其一:
285
+
286
+ **方式 1:环境变量**
287
+ - `MCP_TRANSPORT=http` 启用 HTTP(默认 `stdio`)
288
+ - `MCP_HTTP_PORT`(或 `MCP_PORT`):端口,默认 3000
289
+ - `MCP_HTTP_HOST`(或 `MCP_HOST`):监听地址,默认 `0.0.0.0`
290
+ - `MCP_HTTP_PATH`:路由前缀,默认 `/mcp`
291
+ - 安全可选:`MCP_ALLOWED_HOSTS`、`MCP_ALLOWED_ORIGINS`(逗号分隔),`MCP_DNS_PROTECTION=true`
292
+
293
+ 示例:
294
+ ```bash
295
+ MCP_TRANSPORT=http MCP_HTTP_PORT=3000 MCP_HTTP_PATH=/mcp zentao --config "{\"url\":\"http://your-zentao\",\"username\":\"u\",\"password\":\"p\",\"apiVersion\":\"v1\"}"
296
+ ```
297
+
298
+ **方式 2:CLI 参数**
299
+ - `--transport http`
300
+ - `--http-port 3000`(或 `--port`)
301
+ - `--http-host 0.0.0.0`(或 `--host`)
302
+ - `--http-path /mcp`
303
+ - 安全可选:`--allowed-hosts host1,host2`,`--allowed-origins http://a,http://b`,`--dns-protection true`
304
+
305
+ 示例:
306
+ ```bash
307
+ zentao --transport http --http-port 3000 --http-path /mcp --config "{\"url\":\"http://your-zentao\",\"username\":\"u\",\"password\":\"p\",\"apiVersion\":\"v1\"}"
308
+ ```
309
+
310
+ HTTP 模式下客户端需要连接到 `http://<host>:<port><path>`(默认 `http://localhost:3000/mcp`)。
311
+
312
+ **方式 3:交互式初始化(最便捷)**
313
+
314
+ 运行一次交互式向导,选择 STDIO/HTTP 并保存到 `~/.zentao/transport.json`,后续无需再带参数:
315
+ ```bash
316
+ zentao --init-transport
317
+ # 或 zentao-dev / zentao-pm / zentao-qa 均可
318
+ ```
319
+
320
+ 交互式示例:
321
+ ```
322
+ === MCP 传输方式初始化 ===
323
+ 选择传输方式 (1) stdio (2) http [1]: 2
324
+ HTTP 端口 [3000]: 4000
325
+ HTTP 路径前缀 (如 /mcp) [/mcp]: /zentao
326
+ 监听地址 [0.0.0.0]: 127.0.0.1
327
+ 允许的 Host 列表(逗号分隔,可留空): localhost:4000
328
+ 允许的 Origin 列表(逗号分隔,可留空): https://example.com
329
+ 启用 DNS Rebinding 防护? (y/N): y
330
+ ```
331
+
332
+ 最终配置会写入 `~/.zentao/transport.json`,示例:
333
+ ```json
334
+ {
335
+ "transport": "http",
336
+ "host": "127.0.0.1",
337
+ "port": 4000,
338
+ "path": "/zentao",
339
+ "allowedHosts": ["localhost:4000"],
340
+ "allowedOrigins": ["https://example.com"],
341
+ "dnsProtection": true
342
+ }
343
+ ```
344
+ 该文件优先级低于环境变量/CLI 参数,因此仍可通过临时参数覆盖。
345
+
282
346
  ## 基本使用
283
347
 
284
348
  ```typescript
package/dist/index-dev.js CHANGED
@@ -1,12 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
3
  import { z } from "zod";
5
4
  import { ZentaoAPI } from './api/zentaoApi.js';
6
5
  import { loadConfig, saveConfig } from './config.js';
6
+ import { interactiveInitTransport, startServerTransport } from './serverTransport.js';
7
7
  // 解析命令行参数
8
8
  const args = process.argv.slice(2);
9
9
  let configData = null;
10
+ if (await interactiveInitTransport({ args })) {
11
+ process.exit(0);
12
+ }
10
13
  // 查找 --config 参数
11
14
  const configIndex = args.indexOf('--config');
12
15
  if (configIndex !== -1 && configIndex + 1 < args.length) {
@@ -783,9 +786,7 @@ server.tool("deleteComment", "删除评论 - 只能删除自己的评论,管
783
786
  }]
784
787
  };
785
788
  });
786
- // Start receiving messages on stdin and sending messages on stdout
787
- const transport = new StdioServerTransport();
788
- await server.connect(transport).catch(err => {
789
+ await startServerTransport(server, { args }).catch(err => {
789
790
  process.stderr.write('[FATAL] MCP server failed: ' + String(err) + '\n');
790
791
  process.exit(1);
791
792
  });
package/dist/index-pm.js CHANGED
@@ -1,12 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
3
  import { z } from "zod";
5
4
  import { ZentaoAPI } from './api/zentaoApi.js';
6
5
  import { loadConfig, saveConfig } from './config.js';
6
+ import { interactiveInitTransport, startServerTransport } from './serverTransport.js';
7
7
  // 解析命令行参数
8
8
  const args = process.argv.slice(2);
9
9
  let configData = null;
10
+ if (await interactiveInitTransport({ args })) {
11
+ process.exit(0);
12
+ }
10
13
  // 查找 --config 参数
11
14
  const configIndex = args.indexOf('--config');
12
15
  if (configIndex !== -1 && configIndex + 1 < args.length) {
@@ -908,9 +911,7 @@ server.tool("deleteComment", "删除评论 - 只能删除自己的评论,管
908
911
  }]
909
912
  };
910
913
  });
911
- // Start receiving messages on stdin and sending messages on stdout
912
- const transport = new StdioServerTransport();
913
- await server.connect(transport).catch(err => {
914
+ await startServerTransport(server, { args }).catch(err => {
914
915
  process.stderr.write('[FATAL] MCP server failed: ' + String(err) + '\n');
915
916
  process.exit(1);
916
917
  });
package/dist/index-qa.js CHANGED
@@ -1,12 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
3
  import { z } from "zod";
5
4
  import { ZentaoAPI } from './api/zentaoApi.js';
6
5
  import { loadConfig, saveConfig } from './config.js';
6
+ import { interactiveInitTransport, startServerTransport } from './serverTransport.js';
7
7
  // 解析命令行参数
8
8
  const args = process.argv.slice(2);
9
9
  let configData = null;
10
+ if (await interactiveInitTransport({ args })) {
11
+ process.exit(0);
12
+ }
10
13
  // 查找 --config 参数
11
14
  const configIndex = args.indexOf('--config');
12
15
  if (configIndex !== -1 && configIndex + 1 < args.length) {
@@ -720,6 +723,7 @@ server.tool("deleteComment", "删除评论 - 只能删除自己的评论,管
720
723
  }]
721
724
  };
722
725
  });
723
- // Start receiving messages on stdin and sending messages on stdout
724
- const transport = new StdioServerTransport();
725
- await server.connect(transport).catch(err => { process.stderr.write('[FATAL] MCP server failed: ' + String(err) + '\n'); process.exit(1); });
726
+ await startServerTransport(server, { args }).catch(err => {
727
+ process.stderr.write('[FATAL] MCP server failed: ' + String(err) + '\n');
728
+ process.exit(1);
729
+ });
package/dist/index.js CHANGED
@@ -1,13 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
3
  import { z } from "zod";
5
4
  import { ZentaoAPI } from './api/zentaoApi.js';
6
5
  import { loadConfig, saveConfig } from './config.js';
7
6
  import { withApi } from './mcpHelpers.js';
7
+ import { interactiveInitTransport, startServerTransport } from './serverTransport.js';
8
8
  // 解析命令行参数
9
9
  const args = process.argv.slice(2);
10
10
  let configData = null;
11
+ if (await interactiveInitTransport({ args })) {
12
+ process.exit(0);
13
+ }
11
14
  // 查找 --config 参数
12
15
  const configIndex = args.indexOf('--config');
13
16
  if (configIndex !== -1 && configIndex + 1 < args.length) {
@@ -1540,9 +1543,7 @@ server.tool("deleteComment", "删除评论 - 只能删除自己的评论,管
1540
1543
  }]
1541
1544
  };
1542
1545
  });
1543
- // Start receiving messages on stdin and sending messages on stdout
1544
- const transport = new StdioServerTransport();
1545
- await server.connect(transport).catch(err => {
1546
+ await startServerTransport(server, { args }).catch(err => {
1546
1547
  process.stderr.write('[FATAL] MCP server failed: ' + String(err) + '\n');
1547
1548
  process.exit(1);
1548
1549
  });
@@ -0,0 +1,20 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ interface TransportLogger {
3
+ info?: (message: string) => void;
4
+ error?: (message: string) => void;
5
+ }
6
+ export interface StartTransportOptions {
7
+ args?: string[];
8
+ logger?: TransportLogger;
9
+ }
10
+ export interface InteractiveInitOptions {
11
+ args?: string[];
12
+ logger?: TransportLogger;
13
+ }
14
+ export declare function startServerTransport(server: McpServer, options?: StartTransportOptions): Promise<void>;
15
+ /**
16
+ * 交互式初始化:让用户选择 STDIO / HTTP 及 HTTP 参数,并将结果写入 ~/.zentao/transport.json
17
+ * 返回 true 表示已处理(应在入口处直接 exit)
18
+ */
19
+ export declare function interactiveInitTransport(options?: InteractiveInitOptions): Promise<boolean>;
20
+ export {};
@@ -0,0 +1,151 @@
1
+ import { createServer } from 'node:http';
2
+ import { randomUUID } from 'node:crypto';
3
+ import readline from 'node:readline';
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6
+ import { loadTransportConfig, saveTransportConfig } from './transportConfig.js';
7
+ const DEFAULT_HTTP_PORT = 3000;
8
+ const DEFAULT_HTTP_PATH = '/mcp';
9
+ const getArgValue = (args, flag) => {
10
+ for (let i = 0; i < args.length; i++) {
11
+ const arg = args[i];
12
+ if (arg === flag) {
13
+ return args[i + 1];
14
+ }
15
+ if (arg.startsWith(`${flag}=`)) {
16
+ return arg.substring(flag.length + 1);
17
+ }
18
+ }
19
+ return undefined;
20
+ };
21
+ const parsePort = (value) => {
22
+ if (!value)
23
+ return undefined;
24
+ const parsed = Number(value);
25
+ if (Number.isFinite(parsed) && parsed > 0 && parsed <= 65535) {
26
+ return parsed;
27
+ }
28
+ return undefined;
29
+ };
30
+ export async function startServerTransport(server, options = {}) {
31
+ const args = options.args ?? [];
32
+ const logger = options.logger ?? {
33
+ info: (msg) => process.stdout.write(msg + '\n'),
34
+ error: (msg) => process.stderr.write(msg + '\n')
35
+ };
36
+ const fileConfig = loadTransportConfig();
37
+ const mode = (process.env.MCP_TRANSPORT || getArgValue(args, '--transport') || fileConfig?.transport || 'stdio').toLowerCase();
38
+ if (mode !== 'http') {
39
+ const transport = new StdioServerTransport();
40
+ await server.connect(transport);
41
+ return;
42
+ }
43
+ const portStr = process.env.MCP_HTTP_PORT
44
+ || process.env.MCP_PORT
45
+ || getArgValue(args, '--http-port')
46
+ || getArgValue(args, '--port')
47
+ || (fileConfig?.port ? String(fileConfig.port) : undefined);
48
+ const host = process.env.MCP_HTTP_HOST
49
+ || process.env.MCP_HOST
50
+ || getArgValue(args, '--http-host')
51
+ || getArgValue(args, '--host')
52
+ || fileConfig?.host
53
+ || '0.0.0.0';
54
+ const pathSetting = process.env.MCP_HTTP_PATH
55
+ || getArgValue(args, '--http-path')
56
+ || fileConfig?.path
57
+ || DEFAULT_HTTP_PATH;
58
+ const port = parsePort(portStr) ?? DEFAULT_HTTP_PORT;
59
+ const normalizedPath = pathSetting.startsWith('/') ? pathSetting : `/${pathSetting}`;
60
+ const allowedHosts = (process.env.MCP_ALLOWED_HOSTS || getArgValue(args, '--allowed-hosts'))?.split(',').map(h => h.trim()).filter(Boolean)
61
+ || fileConfig?.allowedHosts;
62
+ const allowedOrigins = (process.env.MCP_ALLOWED_ORIGINS || getArgValue(args, '--allowed-origins'))?.split(',').map(o => o.trim()).filter(Boolean)
63
+ || fileConfig?.allowedOrigins;
64
+ const enableDnsProtection = (process.env.MCP_DNS_PROTECTION || getArgValue(args, '--dns-protection')) === 'true'
65
+ || Boolean(fileConfig?.dnsProtection);
66
+ const httpTransport = new StreamableHTTPServerTransport({
67
+ sessionIdGenerator: () => randomUUID(),
68
+ enableJsonResponse: true,
69
+ allowedHosts,
70
+ allowedOrigins,
71
+ enableDnsRebindingProtection: enableDnsProtection
72
+ });
73
+ await server.connect(httpTransport);
74
+ const httpServer = createServer(async (req, res) => {
75
+ const reqPath = new URL(req.url || '/', 'http://localhost').pathname;
76
+ if (reqPath !== normalizedPath) {
77
+ res.writeHead(404).end('Not Found');
78
+ return;
79
+ }
80
+ try {
81
+ await httpTransport.handleRequest(req, res);
82
+ }
83
+ catch (error) {
84
+ logger.error?.(`[ERROR] HTTP transport request failed: ${String(error)}`);
85
+ res.writeHead(500).end('Internal Server Error');
86
+ }
87
+ });
88
+ httpServer.listen(port, host, () => {
89
+ const displayHost = host === '0.0.0.0' ? 'localhost' : host;
90
+ logger.info?.(`[INFO] MCP HTTP server listening at http://${displayHost}:${port}${normalizedPath}`);
91
+ });
92
+ httpServer.on('error', (err) => {
93
+ logger.error?.(`[ERROR] HTTP server error: ${String(err)}`);
94
+ });
95
+ }
96
+ /**
97
+ * 交互式初始化:让用户选择 STDIO / HTTP 及 HTTP 参数,并将结果写入 ~/.zentao/transport.json
98
+ * 返回 true 表示已处理(应在入口处直接 exit)
99
+ */
100
+ export async function interactiveInitTransport(options = {}) {
101
+ const args = options.args ?? [];
102
+ const logger = options.logger ?? {
103
+ info: (msg) => process.stdout.write(msg + '\n'),
104
+ error: (msg) => process.stderr.write(msg + '\n')
105
+ };
106
+ if (!args.includes('--init-transport')) {
107
+ return false;
108
+ }
109
+ const rl = readline.createInterface({
110
+ input: process.stdin,
111
+ output: process.stdout
112
+ });
113
+ const ask = (query) => new Promise(resolve => rl.question(query, resolve));
114
+ try {
115
+ logger.info?.('=== MCP 传输方式初始化 ===');
116
+ const modeInput = await ask('选择传输方式 (1) stdio (2) http [1]: ');
117
+ const chosenMode = modeInput.trim() === '2' ? 'http' : 'stdio';
118
+ if (chosenMode === 'stdio') {
119
+ saveTransportConfig({ transport: 'stdio' });
120
+ logger.info?.('已保存:使用 STDIO 传输');
121
+ return true;
122
+ }
123
+ const portInput = await ask('HTTP 端口 [3000]: ');
124
+ const pathInput = await ask('HTTP 路径前缀 (如 /mcp) [/mcp]: ');
125
+ const hostInput = await ask('监听地址 [0.0.0.0]: ');
126
+ const allowedHostsInput = await ask('允许的 Host 列表(逗号分隔,可留空): ');
127
+ const allowedOriginsInput = await ask('允许的 Origin 列表(逗号分隔,可留空): ');
128
+ const dnsInput = await ask('启用 DNS Rebinding 防护? (y/N): ');
129
+ const port = parsePort(portInput?.trim()) ?? DEFAULT_HTTP_PORT;
130
+ const pathSetting = pathInput?.trim() || '/mcp';
131
+ const host = hostInput?.trim() || '0.0.0.0';
132
+ const allowedHosts = allowedHostsInput.split(',').map(s => s.trim()).filter(Boolean);
133
+ const allowedOrigins = allowedOriginsInput.split(',').map(s => s.trim()).filter(Boolean);
134
+ const dnsProtection = dnsInput.trim().toLowerCase() === 'y';
135
+ const config = {
136
+ transport: 'http',
137
+ host,
138
+ port,
139
+ path: pathSetting.startsWith('/') ? pathSetting : `/${pathSetting}`,
140
+ allowedHosts: allowedHosts.length ? allowedHosts : undefined,
141
+ allowedOrigins: allowedOrigins.length ? allowedOrigins : undefined,
142
+ dnsProtection
143
+ };
144
+ saveTransportConfig(config);
145
+ logger.info?.(`已保存:HTTP 模式 http://${host}:${port}${config.path}`);
146
+ return true;
147
+ }
148
+ finally {
149
+ rl.close();
150
+ }
151
+ }
@@ -0,0 +1,12 @@
1
+ export type TransportMode = 'stdio' | 'http';
2
+ export interface TransportConfig {
3
+ transport: TransportMode;
4
+ host?: string;
5
+ port?: number;
6
+ path?: string;
7
+ allowedHosts?: string[];
8
+ allowedOrigins?: string[];
9
+ dnsProtection?: boolean;
10
+ }
11
+ export declare function saveTransportConfig(config: TransportConfig): void;
12
+ export declare function loadTransportConfig(): TransportConfig | null;
@@ -0,0 +1,23 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ const CONFIG_DIR = path.join(os.homedir(), '.zentao');
5
+ const TRANSPORT_FILE = path.join(CONFIG_DIR, 'transport.json');
6
+ export function saveTransportConfig(config) {
7
+ if (!fs.existsSync(CONFIG_DIR)) {
8
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
9
+ }
10
+ fs.writeFileSync(TRANSPORT_FILE, JSON.stringify(config, null, 2));
11
+ }
12
+ export function loadTransportConfig() {
13
+ try {
14
+ if (fs.existsSync(TRANSPORT_FILE)) {
15
+ const config = JSON.parse(fs.readFileSync(TRANSPORT_FILE, 'utf-8'));
16
+ return config;
17
+ }
18
+ }
19
+ catch {
20
+ // ignore parse errors, return null
21
+ }
22
+ return null;
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zzp123/mcp-zentao",
3
- "version": "1.18.7",
3
+ "version": "1.18.8",
4
4
  "description": "禅道项目管理系统的高级API集成包 - 完整版,包含所有94个工具。另有产品经理、测试工程师、开发工程师专用精简版本可选",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",