@maplezzk/mcps 1.1.3 → 1.1.5
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/commands/daemon.js +273 -46
- package/dist/commands/server.js +43 -12
- package/dist/core/client.js +37 -7
- package/dist/core/pool.js +17 -10
- package/package.json +1 -1
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,27 +143,98 @@ 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
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
185
|
+
const logPath = '/tmp/mcps-daemon/stdout.log';
|
|
186
|
+
const fs = await import('fs');
|
|
187
|
+
// 发送 stop 请求并显示日志
|
|
188
|
+
const requestPromise = daemonRequest('POST', '/stop');
|
|
189
|
+
// 显示关闭日志
|
|
190
|
+
const showLogs = async () => {
|
|
191
|
+
try {
|
|
192
|
+
if (!fs.existsSync(logPath)) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
let lastSize = fs.statSync(logPath).size;
|
|
196
|
+
const startTime = Date.now();
|
|
197
|
+
const timeout = 5000; // 5秒超时
|
|
198
|
+
while (Date.now() - startTime < timeout) {
|
|
199
|
+
await new Promise(r => setTimeout(r, 200));
|
|
200
|
+
try {
|
|
201
|
+
const { size: currentSize } = fs.statSync(logPath);
|
|
202
|
+
if (currentSize > lastSize) {
|
|
203
|
+
const buffer = Buffer.alloc(currentSize - lastSize);
|
|
204
|
+
const fd = fs.openSync(logPath, 'r');
|
|
205
|
+
fs.readSync(fd, buffer, 0, currentSize - lastSize, lastSize);
|
|
206
|
+
fs.closeSync(fd);
|
|
207
|
+
const newLogs = buffer.toString('utf-8');
|
|
208
|
+
const relevantLogs = newLogs.split('\n').filter(line => {
|
|
209
|
+
return line.includes('Closing connection to') ||
|
|
210
|
+
line.includes('Shutting down');
|
|
211
|
+
});
|
|
212
|
+
if (relevantLogs.length > 0) {
|
|
213
|
+
relevantLogs.forEach(log => {
|
|
214
|
+
if (log.trim()) {
|
|
215
|
+
console.log(chalk.yellow(log));
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
lastSize = currentSize;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
// 忽略读取错误
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
// 忽略日志显示错误
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const [{ ok }] = await Promise.all([
|
|
232
|
+
requestPromise,
|
|
233
|
+
showLogs()
|
|
234
|
+
]);
|
|
235
|
+
if (ok) {
|
|
236
|
+
console.log(chalk.green('Daemon stopped successfully.'));
|
|
237
|
+
}
|
|
130
238
|
}
|
|
131
239
|
catch (e) {
|
|
132
240
|
console.error(chalk.red('Failed to stop daemon. Is it running?'));
|
|
@@ -134,9 +242,7 @@ const stopAction = async (options) => {
|
|
|
134
242
|
};
|
|
135
243
|
const statusAction = async (options) => {
|
|
136
244
|
try {
|
|
137
|
-
const
|
|
138
|
-
const res = await fetch(`http://localhost:${port}/status`);
|
|
139
|
-
const data = await res.json();
|
|
245
|
+
const { data } = await daemonRequest('GET', '/status');
|
|
140
246
|
console.log('');
|
|
141
247
|
console.log(chalk.green(`Daemon is running (v${data.version})`));
|
|
142
248
|
if (data.connections && data.connections.length > 0) {
|
|
@@ -195,16 +301,98 @@ const statusAction = async (options) => {
|
|
|
195
301
|
};
|
|
196
302
|
const restartAction = async (serverName, options) => {
|
|
197
303
|
try {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
304
|
+
const body = serverName ? JSON.stringify({ server: serverName }) : JSON.stringify({});
|
|
305
|
+
// 启动日志显示(在后台读取守护进程日志)
|
|
306
|
+
const logPath = '/tmp/mcps-daemon/stdout.log';
|
|
307
|
+
const fs = await import('fs');
|
|
308
|
+
// 发送 restart 请求
|
|
309
|
+
const requestPromise = daemonRequest('POST', '/restart', body);
|
|
310
|
+
// 在后台显示日志
|
|
311
|
+
const showLogs = async () => {
|
|
312
|
+
try {
|
|
313
|
+
if (!fs.existsSync(logPath)) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// 获取当前日志文件大小
|
|
317
|
+
let lastSize = fs.statSync(logPath).size;
|
|
318
|
+
// 等待请求完成,同时显示新日志
|
|
319
|
+
const startTime = Date.now();
|
|
320
|
+
const timeout = 30000; // 30秒超时
|
|
321
|
+
while (Date.now() - startTime < timeout) {
|
|
322
|
+
await new Promise(r => setTimeout(r, 200)); // 每200ms检查一次
|
|
323
|
+
try {
|
|
324
|
+
const { size: currentSize } = fs.statSync(logPath);
|
|
325
|
+
if (currentSize > lastSize) {
|
|
326
|
+
// 读取新增的日志内容
|
|
327
|
+
const buffer = Buffer.alloc(currentSize - lastSize);
|
|
328
|
+
const fd = fs.openSync(logPath, 'r');
|
|
329
|
+
fs.readSync(fd, buffer, 0, currentSize - lastSize, lastSize);
|
|
330
|
+
fs.closeSync(fd);
|
|
331
|
+
const newLogs = buffer.toString('utf-8');
|
|
332
|
+
// 只显示关闭和重启相关的日志
|
|
333
|
+
const relevantLogs = newLogs.split('\n').filter(line => {
|
|
334
|
+
return line.includes('Closing connection to') ||
|
|
335
|
+
line.includes('Connected ✓') ||
|
|
336
|
+
line.includes('Connecting to') ||
|
|
337
|
+
line.includes('Connected: ') ||
|
|
338
|
+
line.trim().startsWith('- '); // 包含服务名行
|
|
339
|
+
});
|
|
340
|
+
if (relevantLogs.length > 0) {
|
|
341
|
+
// 检查是否已经完成
|
|
342
|
+
const hasCompletion = newLogs.includes('All servers reinitialized successfully');
|
|
343
|
+
relevantLogs.forEach(log => {
|
|
344
|
+
if (log.trim()) {
|
|
345
|
+
if (log.includes('Closing')) {
|
|
346
|
+
console.log(chalk.yellow(log));
|
|
347
|
+
}
|
|
348
|
+
else if (log.includes('Connected ✓')) {
|
|
349
|
+
console.log(chalk.green(log));
|
|
350
|
+
}
|
|
351
|
+
else if (log.includes('Connected:')) {
|
|
352
|
+
// 最终连接统计
|
|
353
|
+
console.log(chalk.green(log));
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
console.log(log);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
if (hasCompletion) {
|
|
361
|
+
break; // 完成后退出
|
|
362
|
+
}
|
|
363
|
+
// 更新起始位置
|
|
364
|
+
lastSize = currentSize;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
// 忽略读取错误
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
catch (e) {
|
|
374
|
+
// 忽略日志显示错误
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
// 同时执行请求和日志显示
|
|
378
|
+
const [{ status, ok, data }] = await Promise.all([
|
|
379
|
+
requestPromise,
|
|
380
|
+
showLogs()
|
|
381
|
+
]);
|
|
382
|
+
if (ok) {
|
|
383
|
+
console.log(chalk.green(data.message));
|
|
384
|
+
}
|
|
385
|
+
else if (status === 404) {
|
|
386
|
+
console.error(chalk.yellow(data.error || 'Server not found'));
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
console.error(chalk.red(data.error || 'Failed to restart'));
|
|
390
|
+
}
|
|
205
391
|
}
|
|
206
392
|
catch (e) {
|
|
207
|
-
console.error(chalk.red('Failed to restart.
|
|
393
|
+
console.error(chalk.red('Failed to restart.'));
|
|
394
|
+
console.error(chalk.red(`Error: ${e.message}`));
|
|
395
|
+
console.error(chalk.gray(`Stack: ${e.stack}`));
|
|
208
396
|
}
|
|
209
397
|
};
|
|
210
398
|
export const registerDaemonCommand = (program) => {
|
|
@@ -279,18 +467,57 @@ const startDaemon = (port) => {
|
|
|
279
467
|
req.on('end', async () => {
|
|
280
468
|
try {
|
|
281
469
|
const { server: serverName } = JSON.parse(body || '{}');
|
|
470
|
+
const verbose = process.env.MCPS_VERBOSE === 'true';
|
|
471
|
+
if (verbose) {
|
|
472
|
+
console.log(`[Daemon] Received restart request for: ${serverName || 'all servers'}`);
|
|
473
|
+
}
|
|
282
474
|
if (serverName) {
|
|
475
|
+
// Restart specific server connection
|
|
476
|
+
if (verbose) {
|
|
477
|
+
console.log(`[Daemon] Closing server: ${serverName}`);
|
|
478
|
+
}
|
|
283
479
|
const closed = await connectionPool.closeClient(serverName);
|
|
284
|
-
|
|
285
|
-
|
|
480
|
+
if (closed) {
|
|
481
|
+
// Reconnect to the specific server
|
|
482
|
+
try {
|
|
483
|
+
if (verbose) {
|
|
484
|
+
console.log(`[Daemon] Reconnecting to: ${serverName}`);
|
|
485
|
+
}
|
|
486
|
+
await connectionPool.getClient(serverName);
|
|
487
|
+
if (verbose) {
|
|
488
|
+
console.log(`[Daemon] Successfully restarted: ${serverName}`);
|
|
489
|
+
}
|
|
490
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
491
|
+
res.end(JSON.stringify({ message: `Server "${serverName}" restarted successfully.` }));
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
console.error(`[Daemon] Failed to reconnect to ${serverName}:`, error);
|
|
495
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
496
|
+
res.end(JSON.stringify({ error: `Failed to reconnect to "${serverName}": ${error.message}` }));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
if (verbose) {
|
|
501
|
+
console.log(`[Daemon] Server not found: ${serverName}`);
|
|
502
|
+
}
|
|
503
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
504
|
+
res.end(JSON.stringify({ error: `Server "${serverName}" not found or not connected.` }));
|
|
505
|
+
}
|
|
286
506
|
}
|
|
287
507
|
else {
|
|
508
|
+
// Restart all connections
|
|
509
|
+
console.log('Closing all connections...');
|
|
288
510
|
await connectionPool.closeAll();
|
|
511
|
+
console.log('All connections closed. Reinitializing...');
|
|
512
|
+
// Reinitialize all servers
|
|
513
|
+
await connectionPool.initializeAll();
|
|
514
|
+
console.log('All servers reinitialized successfully');
|
|
289
515
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
290
|
-
res.end(JSON.stringify({ message: 'All
|
|
516
|
+
res.end(JSON.stringify({ message: 'All servers restarted successfully.' }));
|
|
291
517
|
}
|
|
292
518
|
}
|
|
293
519
|
catch (error) {
|
|
520
|
+
console.error('[Daemon] Error during restart:', error);
|
|
294
521
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
295
522
|
res.end(JSON.stringify({ error: error.message }));
|
|
296
523
|
}
|
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
|
@@ -55,17 +55,24 @@ export class ConnectionPool {
|
|
|
55
55
|
return false;
|
|
56
56
|
}
|
|
57
57
|
async closeAll() {
|
|
58
|
+
const verbose = process.env.MCPS_VERBOSE === 'true';
|
|
59
|
+
if (verbose) {
|
|
60
|
+
console.log('closeAll() called');
|
|
61
|
+
}
|
|
58
62
|
for (const [name, client] of this.clients) {
|
|
59
|
-
console.log(`
|
|
63
|
+
console.log(`Closing connection to ${name}...`);
|
|
60
64
|
try {
|
|
61
|
-
client.close();
|
|
65
|
+
client.close();
|
|
62
66
|
}
|
|
63
67
|
catch (e) {
|
|
64
|
-
console.error(`
|
|
68
|
+
console.error(`Error closing ${name}:`, e);
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
this.clients.clear();
|
|
68
72
|
this.toolsCache.clear();
|
|
73
|
+
if (verbose) {
|
|
74
|
+
console.log('Connection pools cleared');
|
|
75
|
+
}
|
|
69
76
|
}
|
|
70
77
|
async initializeAll() {
|
|
71
78
|
const servers = configManager.listServers();
|
|
@@ -76,20 +83,20 @@ export class ConnectionPool {
|
|
|
76
83
|
const enabledServers = servers.filter(server => {
|
|
77
84
|
const disabled = server.disabled === true;
|
|
78
85
|
if (verbose && disabled) {
|
|
79
|
-
console.log(`
|
|
86
|
+
console.log(`Skipping disabled server: ${server.name}`);
|
|
80
87
|
}
|
|
81
88
|
return !disabled;
|
|
82
89
|
});
|
|
83
90
|
if (enabledServers.length === 0) {
|
|
84
|
-
console.log('
|
|
91
|
+
console.log('No enabled servers to initialize.');
|
|
85
92
|
this.initializing = false;
|
|
86
93
|
this.initialized = true;
|
|
87
94
|
return;
|
|
88
95
|
}
|
|
89
|
-
console.log(`
|
|
96
|
+
console.log(`Connecting to ${enabledServers.length} server(s)...`);
|
|
90
97
|
const results = [];
|
|
91
98
|
for (const server of enabledServers) {
|
|
92
|
-
process.stdout.write(
|
|
99
|
+
process.stdout.write(`- ${server.name}... `);
|
|
93
100
|
try {
|
|
94
101
|
await this.getClient(server.name, { timeoutMs: 8000 });
|
|
95
102
|
results.push({ name: server.name, success: true });
|
|
@@ -111,16 +118,16 @@ export class ConnectionPool {
|
|
|
111
118
|
results.push({ name: server.name, success: false, error: errorMsg });
|
|
112
119
|
console.log('Failed ✗');
|
|
113
120
|
if (verbose) {
|
|
114
|
-
console.error(`
|
|
121
|
+
console.error(`Error: ${errorMsg}`);
|
|
115
122
|
}
|
|
116
123
|
}
|
|
117
124
|
}
|
|
118
125
|
// Print summary
|
|
119
126
|
const successCount = results.filter(r => r.success).length;
|
|
120
127
|
const failed = results.filter(r => !r.success);
|
|
121
|
-
console.log(`
|
|
128
|
+
console.log(`Connected: ${successCount}/${enabledServers.length}`);
|
|
122
129
|
if (failed.length > 0) {
|
|
123
|
-
console.log('
|
|
130
|
+
console.log('Failed connections:');
|
|
124
131
|
failed.forEach(f => {
|
|
125
132
|
console.log(` ✗ ${f.name}: ${f.error}`);
|
|
126
133
|
});
|