@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 CHANGED
@@ -25,11 +25,7 @@ mcps 支持守护进程模式,可以保持与 MCP 服务的长连接,显著
25
25
 
26
26
  **启动守护进程:**
27
27
  ```bash
28
- mcps daemon
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 daemon restart
36
+ mcps restart
41
37
 
42
38
  # 仅重置特定服务的连接
43
- mcps daemon restart my-server
39
+ mcps restart my-server
44
40
  ```
45
41
 
46
42
  **停止守护进程:**
47
43
  ```bash
48
- mcps daemon stop
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 server list
58
+ mcps ls
56
59
  ```
57
60
 
58
61
  **添加 Stdio 服务:**
59
62
  ```bash
60
63
  # 添加本地 Node.js 服务
61
- mcps server add my-server --command node --args ./build/index.js
64
+ mcps add my-server --command node --args ./build/index.js
62
65
 
63
66
  # 使用 npx/uvx 添加服务
64
- mcps server add fetch --command uvx --args mcp-server-fetch
67
+ mcps add fetch --command uvx --args mcp-server-fetch
65
68
  ```
66
69
 
67
70
  **添加 SSE 服务:**
68
71
  ```bash
69
- mcps server add remote-server --type sse --url http://localhost:8000/sse
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 server add my-http-server --type http --url http://localhost:8000/mcp
77
+ mcps add my-http-server --type http --url http://localhost:8000/mcp
75
78
  ```
76
79
 
77
80
  **移除服务:**
78
81
  ```bash
79
- mcps server remove my-server
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
- ### 2. 工具交互 (Tool Interaction)
97
+ ### 3. 工具交互 (Tool Interaction)
83
98
 
84
99
  **查看服务下的可用工具:**
85
100
  ```bash
@@ -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
- export const registerDaemonCommand = (program) => {
9
- const daemonCmd = program.command('daemon')
10
- .description('Manage the mcps daemon');
11
- daemonCmd.command('start', { isDefault: true })
12
- .description('Start the daemon (default)')
13
- .option('-p, --port <number>', 'Port to listen on', String(DAEMON_PORT))
14
- .action(async (options) => {
15
- const port = parseInt(options.port);
16
- startDaemon(port);
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
- daemonCmd.command('stop')
19
- .description('Stop the running daemon')
20
- .action(async () => {
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:${DAEMON_PORT}/stop`, { method: 'POST' });
23
- console.log(chalk.green('Daemon stopped successfully.'));
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 (e) {
26
- console.error(chalk.red('Failed to stop daemon. Is it running?'));
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
- daemonCmd.command('status')
30
- .description('Check daemon status')
31
- .action(async () => {
32
- try {
33
- const res = await fetch(`http://localhost:${DAEMON_PORT}/status`);
34
- const data = await res.json();
35
- console.log(chalk.green(`Daemon is running (v${data.version})`));
36
- if (data.connections && data.connections.length > 0) {
37
- console.log(chalk.bold('\nActive Connections:'));
38
- data.connections.forEach((conn) => {
39
- const count = conn.toolsCount !== null ? `(${conn.toolsCount} tools)` : '(error listing tools)';
40
- const status = conn.status === 'error' ? chalk.red('[Error]') : '';
41
- console.log(chalk.cyan(`- ${conn.name} ${chalk.gray(count)} ${status}`));
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
- else {
45
- console.log(chalk.gray('No active connections.'));
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:${DAEMON_PORT}/restart`, {
57
- method: 'POST',
58
- body: JSON.stringify({ server: serverName })
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
- catch (e) {
64
- console.error(chalk.red('Failed to restart. Is the daemon running?'));
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
- // ... (middleware) ...
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 connections = await connectionPool.getActiveConnectionDetails();
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 tools = await client.listTools();
292
+ const toolsResult = await client.listTools();
161
293
  res.writeHead(200, { 'Content-Type': 'application/json' });
162
- res.end(JSON.stringify({ tools }));
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
- console.log(chalk.green(`
177
- 🚀 mcps Daemon started on port ${port}
178
- -----------------------------------
179
- - Keeps connections to MCP servers alive
180
- - Improves performance for frequent tool calls
181
- - Run 'mcps call ...' in another terminal to use it automatically
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
  };
@@ -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 serverCmd = program.command('server')
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
- serverCmd.command('add <name>')
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
- serverCmd.command('remove <name>')
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
- serverCmd.command('update <name>')
87
- .description('Update a server')
88
- .option('--command <command>', 'New command')
89
- .option('--url <url>', 'New URL')
90
- .action((name, options) => {
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
  };
@@ -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 rawEnv = config.env ? { ...process.env, ...config.env } : process.env;
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: config.args,
52
+ args,
26
53
  env: env,
27
54
  });
28
55
  }
29
56
  else if (config.type === 'http') {
30
- this.transport = new StreamableHTTPClientTransport(new URL(config.url));
57
+ const url = resolveEnvPlaceholders(config.url);
58
+ this.transport = new StreamableHTTPClientTransport(new URL(url));
31
59
  }
32
60
  else {
33
- this.transport = new SSEClientTransport(new URL(config.url));
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',
@@ -1,2 +1,3 @@
1
- export const DAEMON_PORT = 4100;
1
+ // Support custom port via environment variable
2
+ export const DAEMON_PORT = parseInt(process.env.MCPS_PORT || '4100');
2
3
  export const DAEMON_BASE_URL = `http://localhost:${DAEMON_PORT}`;
@@ -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
- return data.tools;
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
- async getClient(serverName) {
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
- await client.connect(serverConfig);
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 getActiveConnectionDetails() {
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
- try {
51
- const result = await client.listTools();
52
- toolsCount = result.tools.length;
53
- }
54
- catch (e) {
55
- status = 'error';
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maplezzk/mcps",
3
- "version": "1.0.8",
3
+ "version": "1.0.18",
4
4
  "description": "A CLI to manage and use MCP servers",
5
5
  "publishConfig": {
6
6
  "access": "public"