@maplezzk/mcps 1.0.14 → 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,128 +1,196 @@
1
1
  import { spawn } from 'child_process';
2
2
  import http from 'http';
3
+ import net from 'net';
3
4
  import chalk from 'chalk';
4
5
  import { connectionPool } from '../core/pool.js';
5
6
  import { createRequire } from 'module';
6
7
  import { DAEMON_PORT } from '../core/constants.js';
7
8
  const require = createRequire(import.meta.url);
8
9
  const pkg = require('../../package.json');
9
- export const registerDaemonCommand = (program) => {
10
- const daemonCmd = program.command('daemon')
11
- .description('Manage the mcps daemon')
12
- .usage('start|stop|restart|status'); // Simplify usage display
13
- daemonCmd.command('start', { isDefault: true, hidden: true }) // Hide default start from help but keep functionality
14
- .description('Start the daemon (default)')
15
- .option('-p, --port <number>', 'Port to listen on', String(DAEMON_PORT))
16
- .action(async (options) => {
17
- const port = parseInt(options.port);
18
- // Check if already running
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');
24
+ });
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
19
33
  try {
20
34
  const res = await fetch(`http://localhost:${port}/status`);
21
35
  if (res.ok) {
22
36
  console.log(chalk.yellow(`Daemon is already running on port ${port}.`));
37
+ process.exit(0);
23
38
  return;
24
39
  }
25
40
  }
26
41
  catch {
27
- // Not running, safe to start
28
- // If we are already detached (indicated by env var), run the server
29
- if (process.env.MCPS_DAEMON_DETACHED === 'true') {
30
- startDaemon(port);
31
- return;
32
- }
33
- // Otherwise, spawn a detached process
34
- console.log(chalk.gray('Starting daemon in background...'));
35
- const subprocess = spawn(process.execPath, [process.argv[1], 'daemon', 'start'], {
36
- detached: true,
37
- // Pipe stdout/stderr so we can see initialization logs
38
- stdio: ['ignore', 'pipe', 'pipe'],
39
- env: {
40
- ...process.env,
41
- MCPS_DAEMON_DETACHED: 'true'
42
- }
43
- });
44
- // Stream logs to current console while waiting for ready
45
- if (subprocess.stdout) {
46
- subprocess.stdout.on('data', (data) => {
47
- process.stdout.write(chalk.gray(`[Daemon] ${data}`));
48
- });
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'
63
+ }
64
+ });
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;
49
78
  }
50
- if (subprocess.stderr) {
51
- subprocess.stderr.on('data', (data) => {
52
- process.stderr.write(chalk.red(`[Daemon] ${data}`));
53
- });
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;
54
95
  }
55
- subprocess.unref();
56
- // Wait briefly to ensure it started (optional but good UX)
57
- // We can poll status for a second
58
- const start = Date.now();
59
- // Increased timeout to allow for connection initialization
60
- while (Date.now() - start < 10000) {
61
- try {
62
- const res = await fetch(`http://localhost:${port}/status`);
63
- if (res.ok) {
64
- const data = await res.json();
65
- if (data.initialized) {
66
- console.log(chalk.green(`Daemon started successfully on port ${port}.`));
67
- process.exit(0);
68
- }
69
- }
96
+ }
97
+ try {
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);
70
104
  }
71
- catch { }
72
- await new Promise(r => setTimeout(r, 200));
73
105
  }
74
- console.log(chalk.yellow('Daemon started (async check timeout, but likely running).'));
75
106
  }
76
- });
77
- daemonCmd.command('stop')
78
- .description('Stop the running daemon')
79
- .action(async () => {
80
- try {
81
- await fetch(`http://localhost:${DAEMON_PORT}/stop`, { method: 'POST' });
82
- console.log(chalk.green('Daemon stopped successfully.'));
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}`));
135
+ });
83
136
  }
84
- catch (e) {
85
- console.error(chalk.red('Failed to stop daemon. Is it running?'));
137
+ else {
138
+ console.log(chalk.gray('No active connections.'));
86
139
  }
87
- });
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));
88
188
  daemonCmd.command('status')
89
189
  .description('Check daemon status')
90
- .action(async () => {
91
- try {
92
- const res = await fetch(`http://localhost:${DAEMON_PORT}/status`);
93
- const data = await res.json();
94
- console.log(chalk.green(`Daemon is running (v${data.version})`));
95
- if (data.connections && data.connections.length > 0) {
96
- console.log(chalk.bold('\nActive Connections:'));
97
- data.connections.forEach((conn) => {
98
- const count = conn.toolsCount !== null ? `(${conn.toolsCount} tools)` : (data.initializing ? '(initializing)' : '(error listing tools)');
99
- const status = conn.status === 'error' ? chalk.red('[Error]') : '';
100
- console.log(chalk.cyan(`- ${conn.name} ${chalk.gray(count)} ${status}`));
101
- });
102
- }
103
- else {
104
- console.log(chalk.gray('No active connections.'));
105
- }
106
- }
107
- catch (e) {
108
- console.error(chalk.red('Daemon is not running.'));
109
- }
110
- });
190
+ .action(() => statusAction(daemonCmd));
111
191
  daemonCmd.command('restart [server]')
112
192
  .description('Restart the daemon or a specific server connection')
113
- .action(async (serverName) => {
114
- try {
115
- const res = await fetch(`http://localhost:${DAEMON_PORT}/restart`, {
116
- method: 'POST',
117
- body: JSON.stringify({ server: serverName })
118
- });
119
- const data = await res.json();
120
- console.log(chalk.green(data.message));
121
- }
122
- catch (e) {
123
- console.error(chalk.red('Failed to restart. Is the daemon running?'));
124
- }
125
- });
193
+ .action((serverName) => restartAction(serverName, daemonCmd));
126
194
  };
127
195
  const startDaemon = (port) => {
128
196
  const server = http.createServer(async (req, res) => {
@@ -238,12 +306,18 @@ const startDaemon = (port) => {
238
306
  });
239
307
  server.listen(port, async () => {
240
308
  // Initialize all connections eagerly
241
- await connectionPool.initializeAll();
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
+ }
242
316
  });
243
317
  server.on('error', (e) => {
244
318
  if (e.code === 'EADDRINUSE') {
245
- console.log(chalk.yellow(`Port ${port} is already in use.`));
246
- process.exit(0); // Exit gracefully if already running
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
247
321
  }
248
322
  else {
249
323
  console.error('[Daemon] Server error:', e);
@@ -258,4 +332,8 @@ const startDaemon = (port) => {
258
332
  };
259
333
  process.on('SIGINT', shutdown);
260
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
+ });
261
339
  };
@@ -1,5 +1,6 @@
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
5
  const listServersAction = () => {
5
6
  const servers = configManager.listServers();
@@ -21,25 +22,7 @@ export const registerServerCommands = (program) => {
21
22
  console.log('');
22
23
  });
23
24
  };
24
- // Register top-level list command
25
- program.command('list')
26
- .alias('ls')
27
- .description('List all configured servers')
28
- .action(listServersAction);
29
- const serverCmd = program.command('server')
30
- .description('Manage MCP servers');
31
- serverCmd.command('list')
32
- .alias('ls')
33
- .description('List all configured servers')
34
- .action(listServersAction);
35
- serverCmd.command('add <name>')
36
- .description('Add a new MCP server')
37
- .option('--type <type>', 'Server type (stdio, sse, or http)', 'stdio')
38
- .option('--command <command>', 'Command to execute (for stdio)')
39
- .option('--args [args...]', 'Arguments for the command', [])
40
- .option('--url <url>', 'URL for SSE/HTTP connection')
41
- .option('--env <env...>', 'Environment variables (KEY=VALUE)', [])
42
- .action((name, options) => {
25
+ const addServerAction = (name, options) => {
43
26
  try {
44
27
  if (options.type === 'sse' || options.type === 'http') {
45
28
  if (!options.url)
@@ -76,11 +59,8 @@ export const registerServerCommands = (program) => {
76
59
  catch (error) {
77
60
  console.error(chalk.red(`Error adding server: ${error.message}`));
78
61
  }
79
- });
80
- serverCmd.command('remove <name>')
81
- .alias('rm')
82
- .description('Remove a server')
83
- .action((name) => {
62
+ };
63
+ const removeServerAction = (name) => {
84
64
  try {
85
65
  configManager.removeServer(name);
86
66
  console.log(chalk.green(`Server "${name}" removed.`));
@@ -88,13 +68,35 @@ export const registerServerCommands = (program) => {
88
68
  catch (error) {
89
69
  console.error(chalk.red(error.message));
90
70
  }
91
- });
92
- serverCmd.command('update <name>')
93
- .description('Update a server')
94
- .option('--command <command>', 'New command')
95
- .option('--args [args...]', 'New arguments for the command')
96
- .option('--url <url>', 'New URL')
97
- .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
98
100
  try {
99
101
  const updates = {};
100
102
  if (options.command)
@@ -105,13 +107,67 @@ export const registerServerCommands = (program) => {
105
107
  updates.url = options.url;
106
108
  if (Object.keys(updates).length === 0) {
107
109
  console.log(chalk.yellow('No updates provided.'));
110
+ console.log(chalk.gray('Use: mcps update <server> --command <cmd> --args <args>'));
108
111
  return;
109
112
  }
110
113
  configManager.updateServer(name, updates);
111
114
  console.log(chalk.green(`Server "${name}" updated.`));
115
+ console.log(chalk.gray('Note: Restart the daemon to apply changes: mcps restart'));
112
116
  }
113
117
  catch (error) {
114
118
  console.error(chalk.red(`Error updating server: ${error.message}`));
115
119
  }
116
- });
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);
117
173
  };
@@ -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}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maplezzk/mcps",
3
- "version": "1.0.14",
3
+ "version": "1.0.18",
4
4
  "description": "A CLI to manage and use MCP servers",
5
5
  "publishConfig": {
6
6
  "access": "public"