@maplezzk/mcps 1.0.8 → 1.0.18
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 +30 -15
- package/dist/commands/daemon.js +207 -60
- package/dist/commands/server.js +92 -27
- package/dist/core/client.js +33 -4
- package/dist/core/constants.js +2 -1
- package/dist/core/daemon-client.js +2 -1
- package/dist/core/pool.js +58 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,11 +25,7 @@ mcps 支持守护进程模式,可以保持与 MCP 服务的长连接,显著
|
|
|
25
25
|
|
|
26
26
|
**启动守护进程:**
|
|
27
27
|
```bash
|
|
28
|
-
mcps
|
|
29
|
-
```
|
|
30
|
-
或者
|
|
31
|
-
```bash
|
|
32
|
-
mcps daemon start
|
|
28
|
+
mcps start
|
|
33
29
|
```
|
|
34
30
|
|
|
35
31
|
**重启连接:**
|
|
@@ -37,49 +33,68 @@ mcps daemon start
|
|
|
37
33
|
|
|
38
34
|
```bash
|
|
39
35
|
# 重置所有连接
|
|
40
|
-
mcps
|
|
36
|
+
mcps restart
|
|
41
37
|
|
|
42
38
|
# 仅重置特定服务的连接
|
|
43
|
-
mcps
|
|
39
|
+
mcps restart my-server
|
|
44
40
|
```
|
|
45
41
|
|
|
46
42
|
**停止守护进程:**
|
|
47
43
|
```bash
|
|
48
|
-
mcps
|
|
44
|
+
mcps stop
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**查看守护进程状态:**
|
|
48
|
+
```bash
|
|
49
|
+
mcps status
|
|
49
50
|
```
|
|
50
51
|
|
|
52
|
+
> **注意**:旧的三词命令(如 `mcps daemon start`)仍然可用,保持向后兼容。
|
|
53
|
+
|
|
51
54
|
### 2. 服务管理 (Server Management)
|
|
52
55
|
|
|
53
56
|
**查看所有服务:**
|
|
54
57
|
```bash
|
|
55
|
-
mcps
|
|
58
|
+
mcps ls
|
|
56
59
|
```
|
|
57
60
|
|
|
58
61
|
**添加 Stdio 服务:**
|
|
59
62
|
```bash
|
|
60
63
|
# 添加本地 Node.js 服务
|
|
61
|
-
mcps
|
|
64
|
+
mcps add my-server --command node --args ./build/index.js
|
|
62
65
|
|
|
63
66
|
# 使用 npx/uvx 添加服务
|
|
64
|
-
mcps
|
|
67
|
+
mcps add fetch --command uvx --args mcp-server-fetch
|
|
65
68
|
```
|
|
66
69
|
|
|
67
70
|
**添加 SSE 服务:**
|
|
68
71
|
```bash
|
|
69
|
-
mcps
|
|
72
|
+
mcps add remote-server --type sse --url http://localhost:8000/sse
|
|
70
73
|
```
|
|
71
74
|
|
|
72
75
|
**添加 Streamable HTTP 服务:**
|
|
73
76
|
```bash
|
|
74
|
-
mcps
|
|
77
|
+
mcps add my-http-server --type http --url http://localhost:8000/mcp
|
|
75
78
|
```
|
|
76
79
|
|
|
77
80
|
**移除服务:**
|
|
78
81
|
```bash
|
|
79
|
-
mcps
|
|
82
|
+
mcps rm my-server
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**更新服务:**
|
|
86
|
+
```bash
|
|
87
|
+
# 刷新所有服务连接
|
|
88
|
+
mcps update
|
|
89
|
+
|
|
90
|
+
# 更新特定服务的命令
|
|
91
|
+
mcps update my-server --command new-command
|
|
92
|
+
|
|
93
|
+
# 更新特定服务的参数
|
|
94
|
+
mcps update my-server --args arg1 arg2
|
|
80
95
|
```
|
|
81
96
|
|
|
82
|
-
###
|
|
97
|
+
### 3. 工具交互 (Tool Interaction)
|
|
83
98
|
|
|
84
99
|
**查看服务下的可用工具:**
|
|
85
100
|
```bash
|
package/dist/commands/daemon.js
CHANGED
|
@@ -1,73 +1,203 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
1
2
|
import http from 'http';
|
|
3
|
+
import net from 'net';
|
|
2
4
|
import chalk from 'chalk';
|
|
3
5
|
import { connectionPool } from '../core/pool.js';
|
|
4
6
|
import { createRequire } from 'module';
|
|
5
7
|
import { DAEMON_PORT } from '../core/constants.js';
|
|
6
8
|
const require = createRequire(import.meta.url);
|
|
7
9
|
const pkg = require('../../package.json');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
// Check if a port is in use
|
|
11
|
+
function isPortInUse(port) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const server = net.createServer();
|
|
14
|
+
server.once('error', () => {
|
|
15
|
+
resolve(true); // Port is in use
|
|
16
|
+
});
|
|
17
|
+
server.once('listening', () => {
|
|
18
|
+
server.once('close', () => {
|
|
19
|
+
resolve(false); // Port is available
|
|
20
|
+
});
|
|
21
|
+
server.close();
|
|
22
|
+
});
|
|
23
|
+
server.listen(port, '127.0.0.1');
|
|
17
24
|
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
}
|
|
26
|
+
// Action functions for daemon commands
|
|
27
|
+
const startAction = async (options, parentCmd) => {
|
|
28
|
+
const port = parseInt(options.port || DAEMON_PORT);
|
|
29
|
+
// Check if port is in use (more reliable than HTTP check)
|
|
30
|
+
const portInUse = await isPortInUse(port);
|
|
31
|
+
if (portInUse) {
|
|
32
|
+
// Try to check if it's our daemon via HTTP
|
|
21
33
|
try {
|
|
22
|
-
await fetch(`http://localhost:${
|
|
23
|
-
|
|
34
|
+
const res = await fetch(`http://localhost:${port}/status`);
|
|
35
|
+
if (res.ok) {
|
|
36
|
+
console.log(chalk.yellow(`Daemon is already running on port ${port}.`));
|
|
37
|
+
process.exit(0);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
24
40
|
}
|
|
25
|
-
catch
|
|
26
|
-
|
|
41
|
+
catch {
|
|
42
|
+
// Port is in use but not our daemon
|
|
43
|
+
console.error(chalk.red(`Port ${port} is already in use by another process.`));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// If we are already detached (indicated by env var), run the server
|
|
49
|
+
if (process.env.MCPS_DAEMON_DETACHED === 'true') {
|
|
50
|
+
startDaemon(port);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Otherwise, spawn a detached process
|
|
54
|
+
console.log(chalk.gray('Starting daemon in background...'));
|
|
55
|
+
let childFailed = false;
|
|
56
|
+
const subprocess = spawn(process.execPath, [process.argv[1], 'daemon', 'start'], {
|
|
57
|
+
detached: true,
|
|
58
|
+
// Pipe stdout/stderr so we can see initialization logs
|
|
59
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
60
|
+
env: {
|
|
61
|
+
...process.env,
|
|
62
|
+
MCPS_DAEMON_DETACHED: 'true'
|
|
27
63
|
}
|
|
28
64
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
65
|
+
// Stream logs to current console while waiting for ready
|
|
66
|
+
if (subprocess.stdout) {
|
|
67
|
+
subprocess.stdout.on('data', (data) => {
|
|
68
|
+
process.stdout.write(chalk.gray(`[Daemon] ${data}`));
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (subprocess.stderr) {
|
|
72
|
+
subprocess.stderr.on('data', (data) => {
|
|
73
|
+
const msg = data.toString();
|
|
74
|
+
process.stderr.write(chalk.red(`[Daemon] ${msg}`));
|
|
75
|
+
// Detect port conflict in child process
|
|
76
|
+
if (msg.includes('Port') && msg.includes('is already in use')) {
|
|
77
|
+
childFailed = true;
|
|
43
78
|
}
|
|
44
|
-
|
|
45
|
-
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
subprocess.unref();
|
|
82
|
+
// Wait briefly to ensure it started (optional but good UX)
|
|
83
|
+
// We can poll status for a second
|
|
84
|
+
const start = Date.now();
|
|
85
|
+
// Increased timeout to allow for connection initialization
|
|
86
|
+
while (Date.now() - start < 10000) {
|
|
87
|
+
// If child reported port conflict, check if daemon is actually running
|
|
88
|
+
if (childFailed) {
|
|
89
|
+
const stillRunning = await isPortInUse(port);
|
|
90
|
+
if (stillRunning) {
|
|
91
|
+
// Another daemon is running
|
|
92
|
+
console.log(chalk.yellow(`\nDaemon is already running on port ${port}.`));
|
|
93
|
+
process.exit(0);
|
|
94
|
+
return;
|
|
46
95
|
}
|
|
47
96
|
}
|
|
48
|
-
catch (e) {
|
|
49
|
-
console.error(chalk.red('Daemon is not running.'));
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
daemonCmd.command('restart [server]')
|
|
53
|
-
.description('Restart the daemon or a specific server connection')
|
|
54
|
-
.action(async (serverName) => {
|
|
55
97
|
try {
|
|
56
|
-
const res = await fetch(`http://localhost:${
|
|
57
|
-
|
|
58
|
-
|
|
98
|
+
const res = await fetch(`http://localhost:${port}/status`);
|
|
99
|
+
if (res.ok) {
|
|
100
|
+
const data = await res.json();
|
|
101
|
+
if (data.initialized) {
|
|
102
|
+
console.log(chalk.green(`Daemon started successfully on port ${port}.`));
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch { }
|
|
108
|
+
await new Promise(r => setTimeout(r, 200));
|
|
109
|
+
}
|
|
110
|
+
console.log(chalk.yellow('Daemon started (async check timeout, but likely running).'));
|
|
111
|
+
process.exit(0);
|
|
112
|
+
};
|
|
113
|
+
const stopAction = async (parentCmd) => {
|
|
114
|
+
try {
|
|
115
|
+
const port = parseInt(parentCmd.opts().port || DAEMON_PORT);
|
|
116
|
+
await fetch(`http://localhost:${port}/stop`, { method: 'POST' });
|
|
117
|
+
console.log(chalk.green('Daemon stopped successfully.'));
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
console.error(chalk.red('Failed to stop daemon. Is it running?'));
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const statusAction = async (parentCmd) => {
|
|
124
|
+
try {
|
|
125
|
+
const port = parseInt(parentCmd.opts().port || DAEMON_PORT);
|
|
126
|
+
const res = await fetch(`http://localhost:${port}/status`);
|
|
127
|
+
const data = await res.json();
|
|
128
|
+
console.log(chalk.green(`Daemon is running (v${data.version})`));
|
|
129
|
+
if (data.connections && data.connections.length > 0) {
|
|
130
|
+
console.log(chalk.bold('\nActive Connections:'));
|
|
131
|
+
data.connections.forEach((conn) => {
|
|
132
|
+
const count = conn.toolsCount !== null ? `(${conn.toolsCount} tools)` : (data.initializing ? '(initializing)' : '(error listing tools)');
|
|
133
|
+
const status = conn.status === 'error' ? chalk.red('[Error]') : '';
|
|
134
|
+
console.log(chalk.cyan(`- ${conn.name} ${chalk.gray(count)} ${status}`));
|
|
59
135
|
});
|
|
60
|
-
const data = await res.json();
|
|
61
|
-
console.log(chalk.green(data.message));
|
|
62
136
|
}
|
|
63
|
-
|
|
64
|
-
console.
|
|
137
|
+
else {
|
|
138
|
+
console.log(chalk.gray('No active connections.'));
|
|
65
139
|
}
|
|
66
|
-
}
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
console.error(chalk.red('Daemon is not running.'));
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const restartAction = async (serverName, parentCmd) => {
|
|
146
|
+
try {
|
|
147
|
+
const port = parseInt(parentCmd.opts().port || DAEMON_PORT);
|
|
148
|
+
const res = await fetch(`http://localhost:${port}/restart`, {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
body: JSON.stringify({ server: serverName })
|
|
151
|
+
});
|
|
152
|
+
const data = await res.json();
|
|
153
|
+
console.log(chalk.green(data.message));
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
console.error(chalk.red('Failed to restart. Is the daemon running?'));
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
export const registerDaemonCommand = (program) => {
|
|
160
|
+
// ===== Top-level commands (new, simplified) =====
|
|
161
|
+
program.command('start')
|
|
162
|
+
.description('Start the daemon')
|
|
163
|
+
.option('-p, --port <number>', 'Daemon port', String(DAEMON_PORT))
|
|
164
|
+
.action((options) => startAction(options, program));
|
|
165
|
+
program.command('stop')
|
|
166
|
+
.description('Stop the daemon')
|
|
167
|
+
.option('-p, --port <number>', 'Daemon port', String(DAEMON_PORT))
|
|
168
|
+
.action(() => stopAction(program));
|
|
169
|
+
program.command('status')
|
|
170
|
+
.description('Check daemon status')
|
|
171
|
+
.option('-p, --port <number>', 'Daemon port', String(DAEMON_PORT))
|
|
172
|
+
.action(() => statusAction(program));
|
|
173
|
+
program.command('restart [server]')
|
|
174
|
+
.description('Restart the daemon or a specific server connection')
|
|
175
|
+
.option('-p, --port <number>', 'Daemon port', String(DAEMON_PORT))
|
|
176
|
+
.action((serverName) => restartAction(serverName, program));
|
|
177
|
+
// ===== Legacy daemon subcommands (for backward compatibility) =====
|
|
178
|
+
const daemonCmd = program.command('daemon')
|
|
179
|
+
.description('Manage the mcps daemon (legacy, use top-level commands)')
|
|
180
|
+
.usage('start|stop|restart|status')
|
|
181
|
+
.option('-p, --port <number>', 'Daemon port', String(DAEMON_PORT));
|
|
182
|
+
daemonCmd.command('start', { isDefault: true, hidden: true })
|
|
183
|
+
.description('Start the daemon (default)')
|
|
184
|
+
.action((options) => startAction(options, daemonCmd));
|
|
185
|
+
daemonCmd.command('stop')
|
|
186
|
+
.description('Stop the running daemon')
|
|
187
|
+
.action(() => stopAction(daemonCmd));
|
|
188
|
+
daemonCmd.command('status')
|
|
189
|
+
.description('Check daemon status')
|
|
190
|
+
.action(() => statusAction(daemonCmd));
|
|
191
|
+
daemonCmd.command('restart [server]')
|
|
192
|
+
.description('Restart the daemon or a specific server connection')
|
|
193
|
+
.action((serverName) => restartAction(serverName, daemonCmd));
|
|
67
194
|
};
|
|
68
195
|
const startDaemon = (port) => {
|
|
69
196
|
const server = http.createServer(async (req, res) => {
|
|
70
|
-
//
|
|
197
|
+
// Basic Error Handling
|
|
198
|
+
req.on('error', (err) => {
|
|
199
|
+
console.error('[Daemon] Request error:', err);
|
|
200
|
+
});
|
|
71
201
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
72
202
|
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
73
203
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
@@ -77,12 +207,14 @@ const startDaemon = (port) => {
|
|
|
77
207
|
return;
|
|
78
208
|
}
|
|
79
209
|
if (req.method === 'GET' && req.url === '/status') {
|
|
80
|
-
const
|
|
210
|
+
const initStatus = connectionPool.getInitStatus();
|
|
211
|
+
const connections = await connectionPool.getActiveConnectionDetails(!initStatus.initializing);
|
|
81
212
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
82
213
|
res.end(JSON.stringify({
|
|
83
214
|
status: 'running',
|
|
84
215
|
version: pkg.version,
|
|
85
|
-
connections
|
|
216
|
+
connections,
|
|
217
|
+
...initStatus
|
|
86
218
|
}));
|
|
87
219
|
return;
|
|
88
220
|
}
|
|
@@ -157,9 +289,9 @@ const startDaemon = (port) => {
|
|
|
157
289
|
return;
|
|
158
290
|
}
|
|
159
291
|
const client = await connectionPool.getClient(serverName);
|
|
160
|
-
const
|
|
292
|
+
const toolsResult = await client.listTools();
|
|
161
293
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
162
|
-
res.end(JSON.stringify(
|
|
294
|
+
res.end(JSON.stringify(toolsResult));
|
|
163
295
|
}
|
|
164
296
|
catch (error) {
|
|
165
297
|
console.error(`[Daemon] Error listing tools:`, error);
|
|
@@ -172,14 +304,25 @@ const startDaemon = (port) => {
|
|
|
172
304
|
res.writeHead(404);
|
|
173
305
|
res.end();
|
|
174
306
|
});
|
|
175
|
-
server.listen(port, () => {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
307
|
+
server.listen(port, async () => {
|
|
308
|
+
// Initialize all connections eagerly
|
|
309
|
+
try {
|
|
310
|
+
await connectionPool.initializeAll();
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
console.error('[Daemon] Error during initialization:', error.message);
|
|
314
|
+
// Don't exit, continue running with partial connections
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
server.on('error', (e) => {
|
|
318
|
+
if (e.code === 'EADDRINUSE') {
|
|
319
|
+
console.error(chalk.red(`Port ${port} is already in use by another daemon.`));
|
|
320
|
+
process.exit(1); // Exit with error if port is in use
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
console.error('[Daemon] Server error:', e);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
183
326
|
});
|
|
184
327
|
const shutdown = async () => {
|
|
185
328
|
console.log('\n[Daemon] Shutting down...');
|
|
@@ -189,4 +332,8 @@ const startDaemon = (port) => {
|
|
|
189
332
|
};
|
|
190
333
|
process.on('SIGINT', shutdown);
|
|
191
334
|
process.on('SIGTERM', shutdown);
|
|
335
|
+
// Prevent unhandled rejections from crashing the daemon
|
|
336
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
337
|
+
console.error('[Daemon] Unhandled Rejection at:', promise, 'reason:', reason);
|
|
338
|
+
});
|
|
192
339
|
};
|
package/dist/commands/server.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { configManager } from '../core/config.js';
|
|
3
|
+
import { DaemonClient } from '../core/daemon-client.js';
|
|
3
4
|
export const registerServerCommands = (program) => {
|
|
4
|
-
const
|
|
5
|
-
.description('Manage MCP servers');
|
|
6
|
-
serverCmd.command('list')
|
|
7
|
-
.alias('ls')
|
|
8
|
-
.description('List all configured servers')
|
|
9
|
-
.action(() => {
|
|
5
|
+
const listServersAction = () => {
|
|
10
6
|
const servers = configManager.listServers();
|
|
11
7
|
if (servers.length === 0) {
|
|
12
8
|
console.log(chalk.yellow('No servers configured.'));
|
|
@@ -25,15 +21,8 @@ export const registerServerCommands = (program) => {
|
|
|
25
21
|
}
|
|
26
22
|
console.log('');
|
|
27
23
|
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
.description('Add a new MCP server')
|
|
31
|
-
.option('--type <type>', 'Server type (stdio, sse, or http)', 'stdio')
|
|
32
|
-
.option('--command <command>', 'Command to execute (for stdio)')
|
|
33
|
-
.option('--args [args...]', 'Arguments for the command', [])
|
|
34
|
-
.option('--url <url>', 'URL for SSE/HTTP connection')
|
|
35
|
-
.option('--env <env...>', 'Environment variables (KEY=VALUE)', [])
|
|
36
|
-
.action((name, options) => {
|
|
24
|
+
};
|
|
25
|
+
const addServerAction = (name, options) => {
|
|
37
26
|
try {
|
|
38
27
|
if (options.type === 'sse' || options.type === 'http') {
|
|
39
28
|
if (!options.url)
|
|
@@ -70,11 +59,8 @@ export const registerServerCommands = (program) => {
|
|
|
70
59
|
catch (error) {
|
|
71
60
|
console.error(chalk.red(`Error adding server: ${error.message}`));
|
|
72
61
|
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
.alias('rm')
|
|
76
|
-
.description('Remove a server')
|
|
77
|
-
.action((name) => {
|
|
62
|
+
};
|
|
63
|
+
const removeServerAction = (name) => {
|
|
78
64
|
try {
|
|
79
65
|
configManager.removeServer(name);
|
|
80
66
|
console.log(chalk.green(`Server "${name}" removed.`));
|
|
@@ -82,27 +68,106 @@ export const registerServerCommands = (program) => {
|
|
|
82
68
|
catch (error) {
|
|
83
69
|
console.error(chalk.red(error.message));
|
|
84
70
|
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
71
|
+
};
|
|
72
|
+
const updateServerAction = async (name, options) => {
|
|
73
|
+
// If no server name provided, refresh all connections
|
|
74
|
+
if (!name) {
|
|
75
|
+
try {
|
|
76
|
+
await DaemonClient.ensureDaemon();
|
|
77
|
+
// Call daemon restart API to refresh all connections
|
|
78
|
+
const port = parseInt(process.env.MCPS_PORT || '4100');
|
|
79
|
+
const res = await fetch(`http://localhost:${port}/restart`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'Content-Type': 'application/json' },
|
|
82
|
+
body: JSON.stringify({})
|
|
83
|
+
});
|
|
84
|
+
if (res.ok) {
|
|
85
|
+
const data = await res.json();
|
|
86
|
+
console.log(chalk.green(data.message));
|
|
87
|
+
console.log(chalk.gray('All servers will be reconnected on next use.'));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
throw new Error('Failed to refresh connections');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.error(chalk.red(`Failed to refresh all servers: ${error.message}`));
|
|
95
|
+
console.error(chalk.yellow('Make sure the daemon is running (use: mcps start)'));
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Update specific server configuration
|
|
91
100
|
try {
|
|
92
101
|
const updates = {};
|
|
93
102
|
if (options.command)
|
|
94
103
|
updates.command = options.command;
|
|
104
|
+
if (options.args)
|
|
105
|
+
updates.args = options.args;
|
|
95
106
|
if (options.url)
|
|
96
107
|
updates.url = options.url;
|
|
97
108
|
if (Object.keys(updates).length === 0) {
|
|
98
109
|
console.log(chalk.yellow('No updates provided.'));
|
|
110
|
+
console.log(chalk.gray('Use: mcps update <server> --command <cmd> --args <args>'));
|
|
99
111
|
return;
|
|
100
112
|
}
|
|
101
113
|
configManager.updateServer(name, updates);
|
|
102
114
|
console.log(chalk.green(`Server "${name}" updated.`));
|
|
115
|
+
console.log(chalk.gray('Note: Restart the daemon to apply changes: mcps restart'));
|
|
103
116
|
}
|
|
104
117
|
catch (error) {
|
|
105
118
|
console.error(chalk.red(`Error updating server: ${error.message}`));
|
|
106
119
|
}
|
|
107
|
-
}
|
|
120
|
+
};
|
|
121
|
+
// ===== Top-level commands (new, simplified) =====
|
|
122
|
+
// List command (already exists, keeping as-is)
|
|
123
|
+
program.command('list')
|
|
124
|
+
.alias('ls')
|
|
125
|
+
.description('List all configured servers')
|
|
126
|
+
.action(listServersAction);
|
|
127
|
+
// Add server command
|
|
128
|
+
program.command('add <name>')
|
|
129
|
+
.description('Add a new MCP server')
|
|
130
|
+
.option('--type <type>', 'Server type (stdio, sse, or http)', 'stdio')
|
|
131
|
+
.option('--command <command>', 'Command to execute (for stdio)')
|
|
132
|
+
.option('--args [args...]', 'Arguments for the command', [])
|
|
133
|
+
.option('--url <url>', 'URL for SSE/HTTP connection')
|
|
134
|
+
.option('--env <env...>', 'Environment variables (KEY=VALUE)', [])
|
|
135
|
+
.action(addServerAction);
|
|
136
|
+
// Remove server command
|
|
137
|
+
program.command('remove <name>')
|
|
138
|
+
.alias('rm')
|
|
139
|
+
.description('Remove a server')
|
|
140
|
+
.action(removeServerAction);
|
|
141
|
+
// Update server command
|
|
142
|
+
program.command('update [name]')
|
|
143
|
+
.description('Update a server configuration or refresh all servers')
|
|
144
|
+
.option('--command <command>', 'New command')
|
|
145
|
+
.option('--args [args...]', 'New arguments for the command')
|
|
146
|
+
.option('--url <url>', 'New URL')
|
|
147
|
+
.action(updateServerAction);
|
|
148
|
+
// ===== Legacy server subcommands (for backward compatibility) =====
|
|
149
|
+
const serverCmd = program.command('server')
|
|
150
|
+
.description('Manage MCP servers (legacy, use top-level commands)');
|
|
151
|
+
serverCmd.command('list')
|
|
152
|
+
.alias('ls')
|
|
153
|
+
.description('List all configured servers')
|
|
154
|
+
.action(listServersAction);
|
|
155
|
+
serverCmd.command('add <name>')
|
|
156
|
+
.description('Add a new MCP server')
|
|
157
|
+
.option('--type <type>', 'Server type (stdio, sse, or http)', 'stdio')
|
|
158
|
+
.option('--command <command>', 'Command to execute (for stdio)')
|
|
159
|
+
.option('--args [args...]', 'Arguments for the command', [])
|
|
160
|
+
.option('--url <url>', 'URL for SSE/HTTP connection')
|
|
161
|
+
.option('--env <env...>', 'Environment variables (KEY=VALUE)', [])
|
|
162
|
+
.action(addServerAction);
|
|
163
|
+
serverCmd.command('remove <name>')
|
|
164
|
+
.alias('rm')
|
|
165
|
+
.description('Remove a server')
|
|
166
|
+
.action(removeServerAction);
|
|
167
|
+
serverCmd.command('update [name]')
|
|
168
|
+
.description('Update a server configuration or refresh all servers')
|
|
169
|
+
.option('--command <command>', 'New command')
|
|
170
|
+
.option('--args [args...]', 'New arguments for the command')
|
|
171
|
+
.option('--url <url>', 'New URL')
|
|
172
|
+
.action(updateServerAction);
|
|
108
173
|
};
|
package/dist/core/client.js
CHANGED
|
@@ -6,13 +6,39 @@ import { EventSource } from 'eventsource';
|
|
|
6
6
|
// Required for SSEClientTransport in Node.js environment
|
|
7
7
|
// @ts-ignore
|
|
8
8
|
global.EventSource = EventSource;
|
|
9
|
+
const resolveEnvPlaceholders = (input) => {
|
|
10
|
+
const missing = new Set();
|
|
11
|
+
const resolved = input.replace(/\$\{([A-Za-z0-9_]+)\}|\$([A-Za-z0-9_]+)/g, (match, braced, bare) => {
|
|
12
|
+
const key = braced || bare;
|
|
13
|
+
const val = process.env[key];
|
|
14
|
+
if (val === undefined) {
|
|
15
|
+
missing.add(key);
|
|
16
|
+
return match;
|
|
17
|
+
}
|
|
18
|
+
return val;
|
|
19
|
+
});
|
|
20
|
+
if (missing.size > 0) {
|
|
21
|
+
const list = Array.from(missing).join(', ');
|
|
22
|
+
throw new Error(`Missing environment variables: ${list}`);
|
|
23
|
+
}
|
|
24
|
+
return resolved;
|
|
25
|
+
};
|
|
9
26
|
export class McpClientService {
|
|
10
27
|
client = null;
|
|
11
28
|
transport = null;
|
|
12
29
|
async connect(config) {
|
|
13
30
|
try {
|
|
14
31
|
if (config.type === 'stdio') {
|
|
15
|
-
const
|
|
32
|
+
const resolvedConfigEnv = {};
|
|
33
|
+
if (config.env) {
|
|
34
|
+
for (const key in config.env) {
|
|
35
|
+
const val = config.env[key];
|
|
36
|
+
if (typeof val === 'string') {
|
|
37
|
+
resolvedConfigEnv[key] = resolveEnvPlaceholders(val);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const rawEnv = config.env ? { ...process.env, ...resolvedConfigEnv } : process.env;
|
|
16
42
|
const env = {};
|
|
17
43
|
for (const key in rawEnv) {
|
|
18
44
|
const val = rawEnv[key];
|
|
@@ -20,17 +46,20 @@ export class McpClientService {
|
|
|
20
46
|
env[key] = val;
|
|
21
47
|
}
|
|
22
48
|
}
|
|
49
|
+
const args = config.args ? config.args.map(arg => resolveEnvPlaceholders(arg)) : [];
|
|
23
50
|
this.transport = new StdioClientTransport({
|
|
24
51
|
command: config.command,
|
|
25
|
-
args
|
|
52
|
+
args,
|
|
26
53
|
env: env,
|
|
27
54
|
});
|
|
28
55
|
}
|
|
29
56
|
else if (config.type === 'http') {
|
|
30
|
-
|
|
57
|
+
const url = resolveEnvPlaceholders(config.url);
|
|
58
|
+
this.transport = new StreamableHTTPClientTransport(new URL(url));
|
|
31
59
|
}
|
|
32
60
|
else {
|
|
33
|
-
|
|
61
|
+
const url = resolveEnvPlaceholders(config.url);
|
|
62
|
+
this.transport = new SSEClientTransport(new URL(url));
|
|
34
63
|
}
|
|
35
64
|
this.client = new Client({
|
|
36
65
|
name: 'mcp-cli',
|
package/dist/core/constants.js
CHANGED
|
@@ -61,6 +61,7 @@ export class DaemonClient {
|
|
|
61
61
|
throw new Error(err.error || 'Daemon error');
|
|
62
62
|
}
|
|
63
63
|
const data = await response.json();
|
|
64
|
-
|
|
64
|
+
// The daemon returns { tools: { tools: [...] } } because of ListToolsResult wrapping
|
|
65
|
+
return data.tools.tools || [];
|
|
65
66
|
}
|
|
66
67
|
}
|
package/dist/core/pool.js
CHANGED
|
@@ -2,7 +2,9 @@ import { McpClientService } from './client.js';
|
|
|
2
2
|
import { configManager } from './config.js';
|
|
3
3
|
export class ConnectionPool {
|
|
4
4
|
clients = new Map();
|
|
5
|
-
|
|
5
|
+
initializing = false;
|
|
6
|
+
initialized = false;
|
|
7
|
+
async getClient(serverName, options) {
|
|
6
8
|
if (this.clients.has(serverName)) {
|
|
7
9
|
return this.clients.get(serverName);
|
|
8
10
|
}
|
|
@@ -10,9 +12,17 @@ export class ConnectionPool {
|
|
|
10
12
|
if (!serverConfig) {
|
|
11
13
|
throw new Error(`Server "${serverName}" not found in config.`);
|
|
12
14
|
}
|
|
13
|
-
console.log(`[Daemon] Connecting to server: ${serverName}...`);
|
|
14
15
|
const client = new McpClientService();
|
|
15
|
-
|
|
16
|
+
const connectPromise = client.connect(serverConfig);
|
|
17
|
+
if (options?.timeoutMs) {
|
|
18
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
19
|
+
setTimeout(() => reject(new Error(`Connection timeout after ${options.timeoutMs}ms`)), options.timeoutMs);
|
|
20
|
+
});
|
|
21
|
+
await Promise.race([connectPromise, timeoutPromise]);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
await connectPromise;
|
|
25
|
+
}
|
|
16
26
|
this.clients.set(serverName, client);
|
|
17
27
|
return client;
|
|
18
28
|
}
|
|
@@ -42,17 +52,55 @@ export class ConnectionPool {
|
|
|
42
52
|
}
|
|
43
53
|
this.clients.clear();
|
|
44
54
|
}
|
|
45
|
-
async
|
|
55
|
+
async initializeAll() {
|
|
56
|
+
const servers = configManager.listServers();
|
|
57
|
+
this.initializing = true;
|
|
58
|
+
this.initialized = false;
|
|
59
|
+
// 过滤掉 disabled 的服务器
|
|
60
|
+
const enabledServers = servers.filter(server => {
|
|
61
|
+
const disabled = server.disabled === true;
|
|
62
|
+
if (disabled) {
|
|
63
|
+
console.log(`[Daemon] Skipping disabled server: ${server.name}`);
|
|
64
|
+
}
|
|
65
|
+
return !disabled;
|
|
66
|
+
});
|
|
67
|
+
if (enabledServers.length === 0) {
|
|
68
|
+
console.log('[Daemon] No enabled servers to initialize.');
|
|
69
|
+
this.initializing = false;
|
|
70
|
+
this.initialized = true;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
console.log(`[Daemon] Initializing ${enabledServers.length} connection(s)...`);
|
|
74
|
+
for (const server of enabledServers) {
|
|
75
|
+
try {
|
|
76
|
+
console.log(`[Daemon] Connecting to server: ${server.name}...`);
|
|
77
|
+
await this.getClient(server.name, { timeoutMs: 8000 });
|
|
78
|
+
console.log(`[Daemon] ✓ Connected to ${server.name}`);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.error(`[Daemon] ✗ Failed to connect to ${server.name}:`, error.message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.initializing = false;
|
|
85
|
+
this.initialized = true;
|
|
86
|
+
console.log('[Daemon] Initialization complete.');
|
|
87
|
+
}
|
|
88
|
+
getInitStatus() {
|
|
89
|
+
return { initializing: this.initializing, initialized: this.initialized };
|
|
90
|
+
}
|
|
91
|
+
async getActiveConnectionDetails(includeTools = true) {
|
|
46
92
|
const details = [];
|
|
47
93
|
for (const [name, client] of this.clients) {
|
|
48
94
|
let toolsCount = null;
|
|
49
95
|
let status = 'connected';
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
96
|
+
if (includeTools) {
|
|
97
|
+
try {
|
|
98
|
+
const result = await client.listTools();
|
|
99
|
+
toolsCount = result.tools.length;
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
status = 'error';
|
|
103
|
+
}
|
|
56
104
|
}
|
|
57
105
|
details.push({ name, toolsCount, status });
|
|
58
106
|
}
|