@maplezzk/mcps 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/README.md +269 -0
- package/dist/commands/daemon.js +229 -45
- package/dist/commands/server.js +43 -12
- package/dist/core/client.js +37 -7
- package/dist/core/pool.js +27 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -308,6 +308,275 @@ mcps 通过以下方式优化性能:
|
|
|
308
308
|
- 查看状态:~200ms
|
|
309
309
|
- 调用工具:~50-100ms
|
|
310
310
|
|
|
311
|
+
## 开发工作流
|
|
312
|
+
|
|
313
|
+
欢迎贡献代码!以下是参与项目开发的完整流程。
|
|
314
|
+
|
|
315
|
+
### 环境准备
|
|
316
|
+
|
|
317
|
+
**前置要求:**
|
|
318
|
+
- Node.js >= 20
|
|
319
|
+
- npm >= 9
|
|
320
|
+
- Git
|
|
321
|
+
|
|
322
|
+
**克隆项目:**
|
|
323
|
+
```bash
|
|
324
|
+
git clone https://github.com/a13835614623/mcps.git
|
|
325
|
+
cd mcps
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**安装依赖:**
|
|
329
|
+
```bash
|
|
330
|
+
npm install
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### 本地开发
|
|
334
|
+
|
|
335
|
+
**开发模式(使用 ts-node 直接运行):**
|
|
336
|
+
```bash
|
|
337
|
+
npm run dev -- <command>
|
|
338
|
+
# 例如
|
|
339
|
+
npm run dev -- ls
|
|
340
|
+
npm run dev -- start
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**构建项目:**
|
|
344
|
+
```bash
|
|
345
|
+
npm run build
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**运行构建后的版本:**
|
|
349
|
+
```bash
|
|
350
|
+
npm start -- <command>
|
|
351
|
+
# 或者
|
|
352
|
+
node dist/index.js <command>
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### 测试
|
|
356
|
+
|
|
357
|
+
**运行测试:**
|
|
358
|
+
```bash
|
|
359
|
+
# 运行所有测试
|
|
360
|
+
npm test
|
|
361
|
+
|
|
362
|
+
# 监听模式(开发时推荐)
|
|
363
|
+
npm run test:watch
|
|
364
|
+
|
|
365
|
+
# 启动测试 UI 界面
|
|
366
|
+
npm run test:ui
|
|
367
|
+
|
|
368
|
+
# 生成测试覆盖率报告
|
|
369
|
+
npm run test:coverage
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**测试要求:**
|
|
373
|
+
- 所有测试必须通过
|
|
374
|
+
- 新功能需要添加相应的测试
|
|
375
|
+
- 保持测试覆盖率在合理水平
|
|
376
|
+
|
|
377
|
+
### 提交规范
|
|
378
|
+
|
|
379
|
+
**提交信息格式:**
|
|
380
|
+
```
|
|
381
|
+
<type>: <description>
|
|
382
|
+
|
|
383
|
+
[optional body]
|
|
384
|
+
|
|
385
|
+
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**提交类型(type):**
|
|
389
|
+
- `feat`: 新功能
|
|
390
|
+
- `fix`: 修复 bug
|
|
391
|
+
- `chore`: 构建过程或辅助工具的变动
|
|
392
|
+
- `docs`: 文档更新
|
|
393
|
+
- `refactor`: 重构(既不是新增功能,也不是修复 bug)
|
|
394
|
+
- `style`: 代码格式调整(不影响代码运行的变动)
|
|
395
|
+
- `test`: 增加测试
|
|
396
|
+
- `perf`: 性能优化
|
|
397
|
+
|
|
398
|
+
**示例:**
|
|
399
|
+
```bash
|
|
400
|
+
feat: 支持可配置的 daemon 启动超时时间
|
|
401
|
+
|
|
402
|
+
新增功能:
|
|
403
|
+
- 支持通过命令行参数 --timeout/-t 设置超时
|
|
404
|
+
- 支持通过环境变量 MCPS_DAEMON_TIMEOUT 设置超时
|
|
405
|
+
- 支持通过配置文件 daemonTimeout 字段设置超时
|
|
406
|
+
|
|
407
|
+
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### PR 流程
|
|
411
|
+
|
|
412
|
+
**1. 创建功能分支:**
|
|
413
|
+
```bash
|
|
414
|
+
git checkout -b feature/your-feature-name
|
|
415
|
+
# 或
|
|
416
|
+
git checkout -b fix/your-bug-fix
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**分支命名规范:**
|
|
420
|
+
- `feature/` - 新功能
|
|
421
|
+
- `fix/` - bug 修复
|
|
422
|
+
- `refactor/` - 重构
|
|
423
|
+
- `docs/` - 文档更新
|
|
424
|
+
- `chore/` - 构建/工具更新
|
|
425
|
+
|
|
426
|
+
**2. 开发并提交:**
|
|
427
|
+
```bash
|
|
428
|
+
# 进行开发...
|
|
429
|
+
npm run build # 确保构建成功
|
|
430
|
+
npm test # 确保测试通过
|
|
431
|
+
|
|
432
|
+
# 提交代码
|
|
433
|
+
git add .
|
|
434
|
+
git commit -m "feat: 你的功能描述"
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**3. 更新版本号(如需要):**
|
|
438
|
+
```bash
|
|
439
|
+
# Patch 版本(bug 修复)
|
|
440
|
+
npm version patch
|
|
441
|
+
|
|
442
|
+
# Minor 版本(新功能)
|
|
443
|
+
npm version minor
|
|
444
|
+
|
|
445
|
+
# Major 版本(破坏性变更)
|
|
446
|
+
npm version major
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
**4. 推送并创建 PR:**
|
|
450
|
+
```bash
|
|
451
|
+
git push origin feature/your-feature-name
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
然后访问 GitHub 创建 Pull Request,或在命令行使用:
|
|
455
|
+
```bash
|
|
456
|
+
gh pr create --title "feat: 功能标题" --body "PR 描述"
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
**5. PR 检查清单:**
|
|
460
|
+
- ✅ CI 测试通过(GitHub Actions)
|
|
461
|
+
- ✅ 代码通过所有测试
|
|
462
|
+
- ✅ 新功能有对应的测试
|
|
463
|
+
- ✅ 提交信息符合规范
|
|
464
|
+
- ✅ PR 描述清晰说明了变更内容
|
|
465
|
+
- ✅ 版本号已正确更新(如需要)
|
|
466
|
+
|
|
467
|
+
**6. 解决冲突(如有):**
|
|
468
|
+
```bash
|
|
469
|
+
# 如果 main 分支有更新,先合并最新代码
|
|
470
|
+
git fetch origin
|
|
471
|
+
git merge origin/main
|
|
472
|
+
|
|
473
|
+
# 解决冲突后
|
|
474
|
+
git add .
|
|
475
|
+
git commit -m "chore: merge main and resolve conflicts"
|
|
476
|
+
git push origin feature/your-feature-name
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### 发布流程
|
|
480
|
+
|
|
481
|
+
项目采用**自动化发布**流程:
|
|
482
|
+
|
|
483
|
+
**1. 版本管理:**
|
|
484
|
+
- 修改 `package.json` 中的版本号
|
|
485
|
+
- 或使用 `npm version` 命令
|
|
486
|
+
|
|
487
|
+
**2. 发布触发:**
|
|
488
|
+
- 当 PR 合并到 `main` 分支时
|
|
489
|
+
- 如果版本号发生变化
|
|
490
|
+
- GitHub Actions 自动发布到 npm
|
|
491
|
+
|
|
492
|
+
**3. 版本号规则:**
|
|
493
|
+
- `1.0.0` → `1.0.1` (Patch): bug 修复
|
|
494
|
+
- `1.0.1` → `1.1.0` (Minor): 新功能
|
|
495
|
+
- `1.1.0` → `2.0.0` (Major): 破坏性变更
|
|
496
|
+
|
|
497
|
+
**4. 预发布版本(可选):**
|
|
498
|
+
```bash
|
|
499
|
+
npm version prerelease --preid beta
|
|
500
|
+
# 生成 1.0.0-beta.0
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
预发布版本会发布到 npm 的 `beta` tag。
|
|
504
|
+
|
|
505
|
+
**5. 跳过发布:**
|
|
506
|
+
如果 PR 不需要发布,在标题中添加 `[skip release]`:
|
|
507
|
+
```
|
|
508
|
+
[skip release] chore: 更新文档
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### CI/CD
|
|
512
|
+
|
|
513
|
+
**CI 检查(.github/workflows/ci.yml):**
|
|
514
|
+
- 每次 PR 和 push 都会触发
|
|
515
|
+
- 运行测试套件
|
|
516
|
+
- 构建项目
|
|
517
|
+
- 确保代码质量
|
|
518
|
+
|
|
519
|
+
**Release 自动化(.github/workflows/release.yml):**
|
|
520
|
+
- PR 合并后触发
|
|
521
|
+
- 检测版本号变化
|
|
522
|
+
- 自动发布到 npm
|
|
523
|
+
- 创建 GitHub Release
|
|
524
|
+
|
|
525
|
+
### 代码规范
|
|
526
|
+
|
|
527
|
+
**TypeScript:**
|
|
528
|
+
- 使用 TypeScript 进行类型检查
|
|
529
|
+
- 运行 `npm run build` 检查类型错误
|
|
530
|
+
|
|
531
|
+
**代码风格:**
|
|
532
|
+
- 遵循项目现有代码风格
|
|
533
|
+
- 使用有意义的变量和函数名
|
|
534
|
+
- 添加必要的注释
|
|
535
|
+
|
|
536
|
+
**项目结构:**
|
|
537
|
+
```
|
|
538
|
+
mcps/
|
|
539
|
+
├── src/
|
|
540
|
+
│ ├── commands/ # 命令实现
|
|
541
|
+
│ ├── core/ # 核心功能
|
|
542
|
+
│ ├── types/ # 类型定义
|
|
543
|
+
│ └── index.ts # 入口文件
|
|
544
|
+
├── test/ # 测试文件
|
|
545
|
+
├── dist/ # 构建输出
|
|
546
|
+
└── package.json
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### 常见问题(开发)
|
|
550
|
+
|
|
551
|
+
**Q: 如何调试代码?**
|
|
552
|
+
```bash
|
|
553
|
+
# 使用开发模式运行
|
|
554
|
+
npm run dev -- start --verbose
|
|
555
|
+
|
|
556
|
+
# 或构建后直接运行
|
|
557
|
+
npm run build
|
|
558
|
+
node --inspect dist/index.js <command>
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
**Q: 测试失败了怎么办?**
|
|
562
|
+
```bash
|
|
563
|
+
# 运行特定测试文件
|
|
564
|
+
npm test -- <test-file>
|
|
565
|
+
|
|
566
|
+
# 查看详细输出
|
|
567
|
+
npm test -- --reporter=verbose
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
**Q: 如何本地测试 npm 包?**
|
|
571
|
+
```bash
|
|
572
|
+
# 在项目根目录
|
|
573
|
+
npm link
|
|
574
|
+
|
|
575
|
+
# 在其他项目中使用
|
|
576
|
+
npm link @maplezzk/mcps
|
|
577
|
+
mcps ls
|
|
578
|
+
```
|
|
579
|
+
|
|
311
580
|
## 常见问题
|
|
312
581
|
|
|
313
582
|
**Q: 如何查看所有服务器的运行状态?**
|
package/dist/commands/daemon.js
CHANGED
|
@@ -7,6 +7,42 @@ import { createRequire } from 'module';
|
|
|
7
7
|
import { DAEMON_PORT } from '../core/constants.js';
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
9
|
const pkg = require('../../package.json');
|
|
10
|
+
// Helper function to make HTTP requests to daemon (bypassing proxy)
|
|
11
|
+
function daemonRequest(method, path, body) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const port = parseInt(process.env.MCPS_PORT || String(DAEMON_PORT));
|
|
14
|
+
const options = {
|
|
15
|
+
method,
|
|
16
|
+
hostname: '127.0.0.1',
|
|
17
|
+
port,
|
|
18
|
+
path,
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
const req = http.request(options, (res) => {
|
|
24
|
+
let data = '';
|
|
25
|
+
res.on('data', chunk => { data += chunk; });
|
|
26
|
+
res.on('end', () => {
|
|
27
|
+
try {
|
|
28
|
+
resolve({
|
|
29
|
+
status: res.statusCode || 500,
|
|
30
|
+
ok: (res.statusCode || 500) >= 200 && (res.statusCode || 500) < 300,
|
|
31
|
+
data: data ? JSON.parse(data) : {},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
reject(e);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
req.on('error', reject);
|
|
40
|
+
if (body) {
|
|
41
|
+
req.write(body);
|
|
42
|
+
}
|
|
43
|
+
req.end();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
10
46
|
// Check if a port is in use
|
|
11
47
|
function isPortInUse(port) {
|
|
12
48
|
return new Promise((resolve) => {
|
|
@@ -39,8 +75,8 @@ const startAction = async (options) => {
|
|
|
39
75
|
if (portInUse) {
|
|
40
76
|
// Try to check if it's our daemon via HTTP
|
|
41
77
|
try {
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
78
|
+
const { ok } = await daemonRequest('GET', '/status');
|
|
79
|
+
if (ok) {
|
|
44
80
|
console.log(chalk.yellow(`Daemon is already running on port ${port}.`));
|
|
45
81
|
process.exit(0);
|
|
46
82
|
return;
|
|
@@ -61,40 +97,41 @@ const startAction = async (options) => {
|
|
|
61
97
|
// Otherwise, spawn a detached process
|
|
62
98
|
console.log(chalk.cyan('Starting daemon in background...'));
|
|
63
99
|
let childFailed = false;
|
|
100
|
+
// Create log file paths
|
|
101
|
+
const logDir = '/tmp/mcps-daemon';
|
|
102
|
+
const stdoutLog = `${logDir}/stdout.log`;
|
|
103
|
+
const stderrLog = `${logDir}/stderr.log`;
|
|
104
|
+
// Ensure log directory exists
|
|
105
|
+
const fs = await import('fs');
|
|
106
|
+
if (!fs.existsSync(logDir)) {
|
|
107
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
// Clear old log files for fresh start
|
|
110
|
+
if (fs.existsSync(stdoutLog)) {
|
|
111
|
+
fs.truncateSync(stdoutLog, 0);
|
|
112
|
+
}
|
|
113
|
+
if (fs.existsSync(stderrLog)) {
|
|
114
|
+
fs.truncateSync(stderrLog, 0);
|
|
115
|
+
}
|
|
116
|
+
// Open file descriptors for stdout and stderr
|
|
117
|
+
const stdoutFd = fs.openSync(stdoutLog, 'a');
|
|
118
|
+
const stderrFd = fs.openSync(stderrLog, 'a');
|
|
64
119
|
const subprocess = spawn(process.execPath, [process.argv[1], 'daemon', 'start'], {
|
|
65
120
|
detached: true,
|
|
66
|
-
// Pipe stdout/stderr
|
|
67
|
-
stdio: ['ignore',
|
|
121
|
+
// Pipe stdout/stderr to log files
|
|
122
|
+
stdio: ['ignore', stdoutFd, stderrFd],
|
|
68
123
|
env: {
|
|
69
124
|
...process.env,
|
|
70
125
|
MCPS_DAEMON_DETACHED: 'true',
|
|
71
126
|
MCPS_VERBOSE: options.verbose ? 'true' : 'false'
|
|
72
127
|
}
|
|
73
128
|
});
|
|
74
|
-
// Stream logs to current console while waiting for ready
|
|
75
|
-
if (subprocess.stdout) {
|
|
76
|
-
subprocess.stdout.on('data', (data) => {
|
|
77
|
-
process.stdout.write(`${data}`);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
if (subprocess.stderr) {
|
|
81
|
-
subprocess.stderr.on('data', (data) => {
|
|
82
|
-
const msg = data.toString();
|
|
83
|
-
// Detect port conflict in child process
|
|
84
|
-
if (msg.includes('Port') && msg.includes('is already in use')) {
|
|
85
|
-
childFailed = true;
|
|
86
|
-
}
|
|
87
|
-
// Only show error output if it contains critical errors
|
|
88
|
-
if (msg.includes('Error') || msg.includes('EADDRINUSE')) {
|
|
89
|
-
process.stderr.write(chalk.red(`[Daemon] ${msg}`));
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
129
|
subprocess.unref();
|
|
94
130
|
// Wait briefly to ensure it started (optional but good UX)
|
|
95
131
|
// We can poll status for a second
|
|
96
132
|
const start = Date.now();
|
|
97
133
|
// Use timeout from option/env (convert to ms)
|
|
134
|
+
let lastLogSize = 0; // Track last read position to avoid duplicate logs
|
|
98
135
|
while (Date.now() - start < timeout * 1000) {
|
|
99
136
|
// If child reported port conflict, check if daemon is actually running
|
|
100
137
|
if (childFailed) {
|
|
@@ -106,26 +143,46 @@ const startAction = async (options) => {
|
|
|
106
143
|
return;
|
|
107
144
|
}
|
|
108
145
|
}
|
|
146
|
+
// Show only new logs (avoid duplicates)
|
|
109
147
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
148
|
+
if (fs.existsSync(stdoutLog)) {
|
|
149
|
+
const { size } = fs.statSync(stdoutLog);
|
|
150
|
+
if (size > lastLogSize) {
|
|
151
|
+
// Read new logs only
|
|
152
|
+
const buffer = Buffer.alloc(size - lastLogSize);
|
|
153
|
+
const fd = fs.openSync(stdoutLog, 'r');
|
|
154
|
+
fs.readSync(fd, buffer, 0, size - lastLogSize, lastLogSize);
|
|
155
|
+
fs.closeSync(fd);
|
|
156
|
+
const newLogs = buffer.toString('utf-8');
|
|
157
|
+
newLogs.split('\n').forEach(line => {
|
|
158
|
+
if (line.trim())
|
|
159
|
+
process.stdout.write(line + '\n');
|
|
160
|
+
});
|
|
161
|
+
lastLogSize = size;
|
|
116
162
|
}
|
|
117
163
|
}
|
|
118
164
|
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
// Ignore log read errors
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const { ok, data } = await daemonRequest('GET', '/status');
|
|
170
|
+
if (ok && data.initialized) {
|
|
171
|
+
console.log(chalk.green(`Daemon started successfully on port ${port}.`));
|
|
172
|
+
console.log(chalk.gray(`Logs: ${stdoutLog}`));
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
119
176
|
catch { }
|
|
120
177
|
await new Promise(r => setTimeout(r, 200));
|
|
121
178
|
}
|
|
122
179
|
console.log(chalk.yellow('Daemon started (async check timeout, but likely running).'));
|
|
180
|
+
console.log(chalk.gray(`Logs: ${stdoutLog}`));
|
|
123
181
|
process.exit(0);
|
|
124
182
|
};
|
|
125
183
|
const stopAction = async (options) => {
|
|
126
184
|
try {
|
|
127
|
-
|
|
128
|
-
await fetch(`http://localhost:${port}/stop`, { method: 'POST' });
|
|
185
|
+
await daemonRequest('POST', '/stop');
|
|
129
186
|
console.log(chalk.green('Daemon stopped successfully.'));
|
|
130
187
|
}
|
|
131
188
|
catch (e) {
|
|
@@ -134,9 +191,7 @@ const stopAction = async (options) => {
|
|
|
134
191
|
};
|
|
135
192
|
const statusAction = async (options) => {
|
|
136
193
|
try {
|
|
137
|
-
const
|
|
138
|
-
const res = await fetch(`http://localhost:${port}/status`);
|
|
139
|
-
const data = await res.json();
|
|
194
|
+
const { data } = await daemonRequest('GET', '/status');
|
|
140
195
|
console.log('');
|
|
141
196
|
console.log(chalk.green(`Daemon is running (v${data.version})`));
|
|
142
197
|
if (data.connections && data.connections.length > 0) {
|
|
@@ -195,16 +250,98 @@ const statusAction = async (options) => {
|
|
|
195
250
|
};
|
|
196
251
|
const restartAction = async (serverName, options) => {
|
|
197
252
|
try {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
253
|
+
const body = serverName ? JSON.stringify({ server: serverName }) : JSON.stringify({});
|
|
254
|
+
// 启动日志显示(在后台读取守护进程日志)
|
|
255
|
+
const logPath = '/tmp/mcps-daemon/stdout.log';
|
|
256
|
+
const fs = await import('fs');
|
|
257
|
+
// 发送 restart 请求
|
|
258
|
+
const requestPromise = daemonRequest('POST', '/restart', body);
|
|
259
|
+
// 在后台显示日志
|
|
260
|
+
const showLogs = async () => {
|
|
261
|
+
try {
|
|
262
|
+
if (!fs.existsSync(logPath)) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
// 获取当前日志文件大小
|
|
266
|
+
let lastSize = fs.statSync(logPath).size;
|
|
267
|
+
// 等待请求完成,同时显示新日志
|
|
268
|
+
const startTime = Date.now();
|
|
269
|
+
const timeout = 30000; // 30秒超时
|
|
270
|
+
while (Date.now() - startTime < timeout) {
|
|
271
|
+
await new Promise(r => setTimeout(r, 200)); // 每200ms检查一次
|
|
272
|
+
try {
|
|
273
|
+
const { size: currentSize } = fs.statSync(logPath);
|
|
274
|
+
if (currentSize > lastSize) {
|
|
275
|
+
// 读取新增的日志内容
|
|
276
|
+
const buffer = Buffer.alloc(currentSize - lastSize);
|
|
277
|
+
const fd = fs.openSync(logPath, 'r');
|
|
278
|
+
fs.readSync(fd, buffer, 0, currentSize - lastSize, lastSize);
|
|
279
|
+
fs.closeSync(fd);
|
|
280
|
+
const newLogs = buffer.toString('utf-8');
|
|
281
|
+
// 只显示关闭和重启相关的日志
|
|
282
|
+
const relevantLogs = newLogs.split('\n').filter(line => {
|
|
283
|
+
return line.includes('Closing connection to') ||
|
|
284
|
+
line.includes('Connected ✓') ||
|
|
285
|
+
line.includes('Connecting to') ||
|
|
286
|
+
line.includes('Connected: ') ||
|
|
287
|
+
line.trim().startsWith('- '); // 包含服务名行
|
|
288
|
+
});
|
|
289
|
+
if (relevantLogs.length > 0) {
|
|
290
|
+
// 检查是否已经完成
|
|
291
|
+
const hasCompletion = newLogs.includes('All servers reinitialized successfully');
|
|
292
|
+
relevantLogs.forEach(log => {
|
|
293
|
+
if (log.trim()) {
|
|
294
|
+
if (log.includes('Closing')) {
|
|
295
|
+
console.log(chalk.yellow(log));
|
|
296
|
+
}
|
|
297
|
+
else if (log.includes('Connected ✓')) {
|
|
298
|
+
console.log(chalk.green(log));
|
|
299
|
+
}
|
|
300
|
+
else if (log.includes('Connected:')) {
|
|
301
|
+
// 最终连接统计
|
|
302
|
+
console.log(chalk.green(log));
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
console.log(log);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
if (hasCompletion) {
|
|
310
|
+
break; // 完成后退出
|
|
311
|
+
}
|
|
312
|
+
// 更新起始位置
|
|
313
|
+
lastSize = currentSize;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch (e) {
|
|
318
|
+
// 忽略读取错误
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch (e) {
|
|
323
|
+
// 忽略日志显示错误
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
// 同时执行请求和日志显示
|
|
327
|
+
const [{ status, ok, data }] = await Promise.all([
|
|
328
|
+
requestPromise,
|
|
329
|
+
showLogs()
|
|
330
|
+
]);
|
|
331
|
+
if (ok) {
|
|
332
|
+
console.log(chalk.green(data.message));
|
|
333
|
+
}
|
|
334
|
+
else if (status === 404) {
|
|
335
|
+
console.error(chalk.yellow(data.error || 'Server not found'));
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
console.error(chalk.red(data.error || 'Failed to restart'));
|
|
339
|
+
}
|
|
205
340
|
}
|
|
206
341
|
catch (e) {
|
|
207
|
-
console.error(chalk.red('Failed to restart.
|
|
342
|
+
console.error(chalk.red('Failed to restart.'));
|
|
343
|
+
console.error(chalk.red(`Error: ${e.message}`));
|
|
344
|
+
console.error(chalk.gray(`Stack: ${e.stack}`));
|
|
208
345
|
}
|
|
209
346
|
};
|
|
210
347
|
export const registerDaemonCommand = (program) => {
|
|
@@ -279,18 +416,57 @@ const startDaemon = (port) => {
|
|
|
279
416
|
req.on('end', async () => {
|
|
280
417
|
try {
|
|
281
418
|
const { server: serverName } = JSON.parse(body || '{}');
|
|
419
|
+
const verbose = process.env.MCPS_VERBOSE === 'true';
|
|
420
|
+
if (verbose) {
|
|
421
|
+
console.log(`[Daemon] Received restart request for: ${serverName || 'all servers'}`);
|
|
422
|
+
}
|
|
282
423
|
if (serverName) {
|
|
424
|
+
// Restart specific server connection
|
|
425
|
+
if (verbose) {
|
|
426
|
+
console.log(`[Daemon] Closing server: ${serverName}`);
|
|
427
|
+
}
|
|
283
428
|
const closed = await connectionPool.closeClient(serverName);
|
|
284
|
-
|
|
285
|
-
|
|
429
|
+
if (closed) {
|
|
430
|
+
// Reconnect to the specific server
|
|
431
|
+
try {
|
|
432
|
+
if (verbose) {
|
|
433
|
+
console.log(`[Daemon] Reconnecting to: ${serverName}`);
|
|
434
|
+
}
|
|
435
|
+
await connectionPool.getClient(serverName);
|
|
436
|
+
if (verbose) {
|
|
437
|
+
console.log(`[Daemon] Successfully restarted: ${serverName}`);
|
|
438
|
+
}
|
|
439
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
440
|
+
res.end(JSON.stringify({ message: `Server "${serverName}" restarted successfully.` }));
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
console.error(`[Daemon] Failed to reconnect to ${serverName}:`, error);
|
|
444
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
445
|
+
res.end(JSON.stringify({ error: `Failed to reconnect to "${serverName}": ${error.message}` }));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
if (verbose) {
|
|
450
|
+
console.log(`[Daemon] Server not found: ${serverName}`);
|
|
451
|
+
}
|
|
452
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
453
|
+
res.end(JSON.stringify({ error: `Server "${serverName}" not found or not connected.` }));
|
|
454
|
+
}
|
|
286
455
|
}
|
|
287
456
|
else {
|
|
457
|
+
// Restart all connections
|
|
458
|
+
console.log('Closing all connections...');
|
|
288
459
|
await connectionPool.closeAll();
|
|
460
|
+
console.log('All connections closed. Reinitializing...');
|
|
461
|
+
// Reinitialize all servers
|
|
462
|
+
await connectionPool.initializeAll();
|
|
463
|
+
console.log('All servers reinitialized successfully');
|
|
289
464
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
290
|
-
res.end(JSON.stringify({ message: 'All
|
|
465
|
+
res.end(JSON.stringify({ message: 'All servers restarted successfully.' }));
|
|
291
466
|
}
|
|
292
467
|
}
|
|
293
468
|
catch (error) {
|
|
469
|
+
console.error('[Daemon] Error during restart:', error);
|
|
294
470
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
295
471
|
res.end(JSON.stringify({ error: error.message }));
|
|
296
472
|
}
|
|
@@ -342,6 +518,14 @@ const startDaemon = (port) => {
|
|
|
342
518
|
res.end(JSON.stringify({ error: 'Missing server name' }));
|
|
343
519
|
return;
|
|
344
520
|
}
|
|
521
|
+
// Try to use cached tools first
|
|
522
|
+
const cachedTools = connectionPool.getCachedTools(serverName);
|
|
523
|
+
if (cachedTools !== null) {
|
|
524
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
525
|
+
res.end(JSON.stringify({ tools: cachedTools }));
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
// Fallback: fetch from client if not cached
|
|
345
529
|
const client = await connectionPool.getClient(serverName);
|
|
346
530
|
const toolsResult = await client.listTools();
|
|
347
531
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
package/dist/commands/server.js
CHANGED
|
@@ -1,7 +1,45 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import http from 'http';
|
|
2
3
|
import { configManager } from '../core/config.js';
|
|
3
4
|
import { DaemonClient } from '../core/daemon-client.js';
|
|
4
5
|
import { detectServerType } from '../types/config.js';
|
|
6
|
+
import { DAEMON_PORT } from '../core/constants.js';
|
|
7
|
+
// Helper function to make HTTP requests to daemon (bypassing proxy)
|
|
8
|
+
function daemonRequest(method, path, body) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const port = parseInt(process.env.MCPS_PORT || String(DAEMON_PORT));
|
|
11
|
+
const options = {
|
|
12
|
+
method,
|
|
13
|
+
hostname: '127.0.0.1',
|
|
14
|
+
port,
|
|
15
|
+
path,
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
const req = http.request(options, (res) => {
|
|
21
|
+
let data = '';
|
|
22
|
+
res.on('data', chunk => { data += chunk; });
|
|
23
|
+
res.on('end', () => {
|
|
24
|
+
try {
|
|
25
|
+
resolve({
|
|
26
|
+
status: res.statusCode || 500,
|
|
27
|
+
ok: (res.statusCode || 500) >= 200 && (res.statusCode || 500) < 300,
|
|
28
|
+
data: data ? JSON.parse(data) : {},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
reject(e);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
req.on('error', reject);
|
|
37
|
+
if (body) {
|
|
38
|
+
req.write(body);
|
|
39
|
+
}
|
|
40
|
+
req.end();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
5
43
|
export const registerServerCommands = (program) => {
|
|
6
44
|
const listServersAction = () => {
|
|
7
45
|
const servers = configManager.listServers();
|
|
@@ -116,24 +154,17 @@ export const registerServerCommands = (program) => {
|
|
|
116
154
|
if (!name) {
|
|
117
155
|
try {
|
|
118
156
|
await DaemonClient.ensureDaemon();
|
|
119
|
-
// Call daemon restart API to
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
method: 'POST',
|
|
123
|
-
headers: { 'Content-Type': 'application/json' },
|
|
124
|
-
body: JSON.stringify({})
|
|
125
|
-
});
|
|
126
|
-
if (res.ok) {
|
|
127
|
-
const data = await res.json();
|
|
157
|
+
// Call daemon restart API to restart all connections
|
|
158
|
+
const { ok, data } = await daemonRequest('POST', '/restart', JSON.stringify({}));
|
|
159
|
+
if (ok) {
|
|
128
160
|
console.log(chalk.green(data.message));
|
|
129
|
-
console.log(chalk.gray('All servers will be reconnected on next use.'));
|
|
130
161
|
}
|
|
131
162
|
else {
|
|
132
|
-
throw new Error('Failed to
|
|
163
|
+
throw new Error(data.error || 'Failed to restart connections');
|
|
133
164
|
}
|
|
134
165
|
}
|
|
135
166
|
catch (error) {
|
|
136
|
-
console.error(chalk.red(`Failed to
|
|
167
|
+
console.error(chalk.red(`Failed to restart all servers: ${error.message}`));
|
|
137
168
|
console.error(chalk.yellow('Make sure the daemon is running (use: mcps start)'));
|
|
138
169
|
}
|
|
139
170
|
return;
|
package/dist/core/client.js
CHANGED
|
@@ -112,7 +112,7 @@ export class McpClientService {
|
|
|
112
112
|
});
|
|
113
113
|
await this.client.connect(this.transport);
|
|
114
114
|
// 连接成功后,立即查找并保存子进程 PID
|
|
115
|
-
if (
|
|
115
|
+
if ('command' in config) {
|
|
116
116
|
await this.recordChildPids();
|
|
117
117
|
}
|
|
118
118
|
}
|
|
@@ -160,6 +160,10 @@ export class McpClientService {
|
|
|
160
160
|
McpClientService.globalPidsBeforeConnection = afterSnapshot;
|
|
161
161
|
// 过滤出匹配我们命令的进程
|
|
162
162
|
const commandBaseName = this.serverCommand.split('/').pop() || this.serverCommand;
|
|
163
|
+
const verbose = process.env.MCPS_VERBOSE === 'true';
|
|
164
|
+
if (verbose) {
|
|
165
|
+
log(`[Daemon] ${this.serverName}: Found ${newPids.size} new PIDs: [${Array.from(newPids).join(', ')}]`);
|
|
166
|
+
}
|
|
163
167
|
for (const pid of newPids) {
|
|
164
168
|
try {
|
|
165
169
|
const { stdout: cmdOutput } = await execAsync(`ps -p ${pid} -o command=`);
|
|
@@ -173,13 +177,18 @@ export class McpClientService {
|
|
|
173
177
|
}));
|
|
174
178
|
if (isMatch) {
|
|
175
179
|
this.childPids.add(pid);
|
|
176
|
-
|
|
180
|
+
if (verbose) {
|
|
181
|
+
log(`[Daemon] ${this.serverName}: Added PID ${pid} to childPids (cmd: ${cmdLine.substring(0, 50)}...)`);
|
|
182
|
+
}
|
|
177
183
|
}
|
|
178
184
|
}
|
|
179
185
|
catch {
|
|
180
186
|
// 进程可能已经不存在了
|
|
181
187
|
}
|
|
182
188
|
}
|
|
189
|
+
if (verbose) {
|
|
190
|
+
log(`[Daemon] ${this.serverName}: Total childPids after collection: ${this.childPids.size}`);
|
|
191
|
+
}
|
|
183
192
|
}
|
|
184
193
|
catch (e) {
|
|
185
194
|
// 记录失败,不影响主流程
|
|
@@ -200,23 +209,44 @@ export class McpClientService {
|
|
|
200
209
|
});
|
|
201
210
|
}
|
|
202
211
|
close() {
|
|
212
|
+
const verbose = process.env.MCPS_VERBOSE === 'true';
|
|
203
213
|
// 对于 stdio 类型的服务器,先杀掉子进程(在关闭 transport 之前)
|
|
204
214
|
if (this.serverType === 'stdio' && this.childPids.size > 0) {
|
|
205
|
-
|
|
215
|
+
const pidList = Array.from(this.childPids).join(', ');
|
|
216
|
+
if (verbose) {
|
|
217
|
+
log(`[Daemon] Closing ${this.serverName}, killing PIDs: [${pidList}]`);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// 简短版本,始终显示
|
|
221
|
+
log(`[Daemon] Closing ${this.serverName} (${this.childPids.size} process(es))`);
|
|
222
|
+
}
|
|
206
223
|
// 直接使用 SIGKILL,确保进程被终止
|
|
207
224
|
for (const pid of this.childPids) {
|
|
208
225
|
try {
|
|
209
226
|
process.kill(pid, 'SIGKILL');
|
|
210
|
-
|
|
227
|
+
if (verbose) {
|
|
228
|
+
log(`[Daemon] SIGKILLED child process ${pid} (${this.serverName})`);
|
|
229
|
+
}
|
|
211
230
|
}
|
|
212
231
|
catch (e) {
|
|
213
|
-
|
|
232
|
+
if (verbose) {
|
|
233
|
+
log(`[Daemon] Failed to kill ${pid}: ${e.message}`);
|
|
234
|
+
}
|
|
214
235
|
}
|
|
215
236
|
}
|
|
216
237
|
this.childPids.clear();
|
|
217
|
-
|
|
238
|
+
if (verbose) {
|
|
239
|
+
log(`[Daemon] All child processes cleared for ${this.serverName}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
if (verbose) {
|
|
244
|
+
log(`[Daemon] No child PIDs to kill for ${this.serverName} (childPids.size: ${this.childPids.size})`);
|
|
245
|
+
}
|
|
218
246
|
}
|
|
219
247
|
// 暂时不关闭 transport,避免卡住
|
|
220
|
-
|
|
248
|
+
if (verbose) {
|
|
249
|
+
log(`[Daemon] ${this.serverName} close() completed`);
|
|
250
|
+
}
|
|
221
251
|
}
|
|
222
252
|
}
|
package/dist/core/pool.js
CHANGED
|
@@ -2,7 +2,7 @@ import { McpClientService } from './client.js';
|
|
|
2
2
|
import { configManager } from './config.js';
|
|
3
3
|
export class ConnectionPool {
|
|
4
4
|
clients = new Map();
|
|
5
|
-
toolsCache = new Map();
|
|
5
|
+
toolsCache = new Map(); // Cache full tools result
|
|
6
6
|
initializing = false;
|
|
7
7
|
initialized = false;
|
|
8
8
|
async getClient(serverName, options) {
|
|
@@ -25,17 +25,20 @@ export class ConnectionPool {
|
|
|
25
25
|
await connectPromise;
|
|
26
26
|
}
|
|
27
27
|
this.clients.set(serverName, client);
|
|
28
|
-
// Cache tools
|
|
28
|
+
// Cache full tools result after connection
|
|
29
29
|
try {
|
|
30
30
|
const result = await client.listTools();
|
|
31
|
-
this.toolsCache.set(serverName, result.tools
|
|
31
|
+
this.toolsCache.set(serverName, result.tools || []);
|
|
32
32
|
}
|
|
33
33
|
catch (e) {
|
|
34
|
-
// Connection succeeded but listTools failed, cache as
|
|
35
|
-
this.toolsCache.set(serverName,
|
|
34
|
+
// Connection succeeded but listTools failed, cache as empty array
|
|
35
|
+
this.toolsCache.set(serverName, []);
|
|
36
36
|
}
|
|
37
37
|
return client;
|
|
38
38
|
}
|
|
39
|
+
getCachedTools(serverName) {
|
|
40
|
+
return this.toolsCache.has(serverName) ? this.toolsCache.get(serverName) : null;
|
|
41
|
+
}
|
|
39
42
|
async closeClient(serverName) {
|
|
40
43
|
if (this.clients.has(serverName)) {
|
|
41
44
|
console.log(`[Daemon] Closing connection to ${serverName}...`);
|
|
@@ -52,17 +55,24 @@ export class ConnectionPool {
|
|
|
52
55
|
return false;
|
|
53
56
|
}
|
|
54
57
|
async closeAll() {
|
|
58
|
+
const verbose = process.env.MCPS_VERBOSE === 'true';
|
|
59
|
+
if (verbose) {
|
|
60
|
+
console.log('closeAll() called');
|
|
61
|
+
}
|
|
55
62
|
for (const [name, client] of this.clients) {
|
|
56
|
-
console.log(`
|
|
63
|
+
console.log(`Closing connection to ${name}...`);
|
|
57
64
|
try {
|
|
58
|
-
client.close();
|
|
65
|
+
client.close();
|
|
59
66
|
}
|
|
60
67
|
catch (e) {
|
|
61
|
-
console.error(`
|
|
68
|
+
console.error(`Error closing ${name}:`, e);
|
|
62
69
|
}
|
|
63
70
|
}
|
|
64
71
|
this.clients.clear();
|
|
65
72
|
this.toolsCache.clear();
|
|
73
|
+
if (verbose) {
|
|
74
|
+
console.log('Connection pools cleared');
|
|
75
|
+
}
|
|
66
76
|
}
|
|
67
77
|
async initializeAll() {
|
|
68
78
|
const servers = configManager.listServers();
|
|
@@ -73,20 +83,20 @@ export class ConnectionPool {
|
|
|
73
83
|
const enabledServers = servers.filter(server => {
|
|
74
84
|
const disabled = server.disabled === true;
|
|
75
85
|
if (verbose && disabled) {
|
|
76
|
-
console.log(`
|
|
86
|
+
console.log(`Skipping disabled server: ${server.name}`);
|
|
77
87
|
}
|
|
78
88
|
return !disabled;
|
|
79
89
|
});
|
|
80
90
|
if (enabledServers.length === 0) {
|
|
81
|
-
console.log('
|
|
91
|
+
console.log('No enabled servers to initialize.');
|
|
82
92
|
this.initializing = false;
|
|
83
93
|
this.initialized = true;
|
|
84
94
|
return;
|
|
85
95
|
}
|
|
86
|
-
console.log(`
|
|
96
|
+
console.log(`Connecting to ${enabledServers.length} server(s)...`);
|
|
87
97
|
const results = [];
|
|
88
98
|
for (const server of enabledServers) {
|
|
89
|
-
process.stdout.write(
|
|
99
|
+
process.stdout.write(`- ${server.name}... `);
|
|
90
100
|
try {
|
|
91
101
|
await this.getClient(server.name, { timeoutMs: 8000 });
|
|
92
102
|
results.push({ name: server.name, success: true });
|
|
@@ -108,16 +118,16 @@ export class ConnectionPool {
|
|
|
108
118
|
results.push({ name: server.name, success: false, error: errorMsg });
|
|
109
119
|
console.log('Failed ✗');
|
|
110
120
|
if (verbose) {
|
|
111
|
-
console.error(`
|
|
121
|
+
console.error(`Error: ${errorMsg}`);
|
|
112
122
|
}
|
|
113
123
|
}
|
|
114
124
|
}
|
|
115
125
|
// Print summary
|
|
116
126
|
const successCount = results.filter(r => r.success).length;
|
|
117
127
|
const failed = results.filter(r => !r.success);
|
|
118
|
-
console.log(`
|
|
128
|
+
console.log(`Connected: ${successCount}/${enabledServers.length}`);
|
|
119
129
|
if (failed.length > 0) {
|
|
120
|
-
console.log('
|
|
130
|
+
console.log('Failed connections:');
|
|
121
131
|
failed.forEach(f => {
|
|
122
132
|
console.log(` ✗ ${f.name}: ${f.error}`);
|
|
123
133
|
});
|
|
@@ -136,14 +146,14 @@ export class ConnectionPool {
|
|
|
136
146
|
if (includeTools) {
|
|
137
147
|
// Use cached tools count instead of calling listTools again
|
|
138
148
|
if (this.toolsCache.has(name)) {
|
|
139
|
-
toolsCount = this.toolsCache.get(name);
|
|
149
|
+
toolsCount = this.toolsCache.get(name).length;
|
|
140
150
|
}
|
|
141
151
|
else {
|
|
142
152
|
// Fallback: if not cached, fetch it now
|
|
143
153
|
try {
|
|
144
154
|
const result = await client.listTools();
|
|
145
155
|
toolsCount = result.tools.length;
|
|
146
|
-
this.toolsCache.set(name,
|
|
156
|
+
this.toolsCache.set(name, result.tools || []);
|
|
147
157
|
}
|
|
148
158
|
catch (e) {
|
|
149
159
|
status = 'error';
|