@maplezzk/mcps 1.0.18 → 1.0.29

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
@@ -6,10 +6,13 @@
6
6
 
7
7
  ## 功能特性
8
8
 
9
- - 🔌 **服务管理**:轻松添加、移除、查看和更新 MCP 服务(支持 Stdio 和 SSE 模式)。
10
- - 🛠️ **工具发现**:查看已配置服务中所有可用的工具。
11
- - 🚀 **工具执行**:直接在命令行调用工具,支持参数自动解析。
12
- - 🔄 **持久化存储**:配置自动保存至 `~/.mcps/mcp.json`。
9
+ - 🔌 **服务管理**:轻松添加、移除、查看和更新 MCP 服务(支持 Stdio、SSEHTTP 模式)
10
+ - 🛠️ **工具发现**:查看已配置服务中所有可用的工具
11
+ - 🚀 **工具执行**:直接在命令行调用工具,支持参数自动解析
12
+ - 🔄 **守护进程**:保持与 MCP 服务的长连接,显著提高性能
13
+ - 📊 **表格输出**:清晰的服务器状态和工具列表展示
14
+ - 🔍 **工具筛选**:按关键词筛选工具,支持简洁模式
15
+ - 🚨 **详细日志**:可选的详细日志模式,方便调试
13
16
 
14
17
  ## 安装
15
18
 
@@ -17,6 +20,25 @@
17
20
  npm install -g @maplezzk/mcps
18
21
  ```
19
22
 
23
+ ## 快速开始
24
+
25
+ ```bash
26
+ # 1. 添加一个服务
27
+ mcps add fetch --command uvx --args mcp-server-fetch
28
+
29
+ # 2. 启动守护进程
30
+ mcps start
31
+
32
+ # 3. 查看服务状态
33
+ mcps status
34
+
35
+ # 4. 查看可用工具
36
+ mcps tools fetch
37
+
38
+ # 5. 调用工具
39
+ mcps call fetch fetch url="https://example.com"
40
+ ```
41
+
20
42
  ## 使用指南
21
43
 
22
44
  ### 1. 守护进程 (Daemon Mode)
@@ -25,12 +47,25 @@ mcps 支持守护进程模式,可以保持与 MCP 服务的长连接,显著
25
47
 
26
48
  **启动守护进程:**
27
49
  ```bash
50
+ # 普通模式
28
51
  mcps start
52
+
53
+ # 详细模式(显示每个服务器的连接过程和禁用的服务器)
54
+ mcps start --verbose
29
55
  ```
30
56
 
31
- **重启连接:**
32
- 如果修改了配置文件,或者服务出现异常,可以使用重启命令刷新连接:
57
+ 输出示例:
58
+ ```
59
+ Starting daemon in background...
60
+ [Daemon] Connecting to 7 server(s)...
61
+ [Daemon] - chrome-devtools... Connected ✓
62
+ [Daemon] - fetch... Connected ✓
63
+ [Daemon] - gitlab-mr-creator... Connected ✓
64
+ [Daemon] Connected: 7/7
65
+ Daemon started successfully on port 4100.
66
+ ```
33
67
 
68
+ **重启连接:**
34
69
  ```bash
35
70
  # 重置所有连接
36
71
  mcps restart
@@ -49,15 +84,36 @@ mcps stop
49
84
  mcps status
50
85
  ```
51
86
 
52
- > **注意**:旧的三词命令(如 `mcps daemon start`)仍然可用,保持向后兼容。
87
+ 输出示例:
88
+ ```
89
+ Daemon is running (v1.0.29)
90
+
91
+ Active Connections:
92
+ NAME STATUS TOOLS
93
+ ───────────────── ────────── ──────
94
+ chrome-devtools Connected 26
95
+ fetch Connected 1
96
+ gitlab-mr-creator Connected 30
97
+ Total: 3 connection(s)
98
+ ```
53
99
 
54
100
  ### 2. 服务管理 (Server Management)
55
101
 
56
- **查看所有服务:**
102
+ **查看所有服务(配置信息):**
57
103
  ```bash
58
104
  mcps ls
59
105
  ```
60
106
 
107
+ 输出示例:
108
+ ```
109
+ NAME TYPE ENABLED COMMAND/URL
110
+ ───────────────── ────── ─────── ─────────────
111
+ chrome-devtools stdio ✓ npx -y chrome-devtools-mcp ...
112
+ fetch stdio ✓ uvx mcp-server-fetch
113
+ my-server stdio ✗ npx my-server
114
+ Total: 3 server(s)
115
+ ```
116
+
61
117
  **添加 Stdio 服务:**
62
118
  ```bash
63
119
  # 添加本地 Node.js 服务
@@ -65,6 +121,9 @@ mcps add my-server --command node --args ./build/index.js
65
121
 
66
122
  # 使用 npx/uvx 添加服务
67
123
  mcps add fetch --command uvx --args mcp-server-fetch
124
+
125
+ # 添加带环境变量的服务
126
+ mcps add my-db --command npx --args @modelcontextprotocol/server-postgres --env POSTGRES_CONNECTION_STRING="${DATABASE_URL}"
68
127
  ```
69
128
 
70
129
  **添加 SSE 服务:**
@@ -92,13 +151,61 @@ mcps update my-server --command new-command
92
151
 
93
152
  # 更新特定服务的参数
94
153
  mcps update my-server --args arg1 arg2
154
+
155
+ # 同时更新命令和参数
156
+ mcps update my-server --command node --args ./new-build/index.js
95
157
  ```
96
158
 
97
159
  ### 3. 工具交互 (Tool Interaction)
98
160
 
99
161
  **查看服务下的可用工具:**
100
162
  ```bash
101
- mcps tools fetch
163
+ # 详细模式(显示所有信息)
164
+ mcps tools chrome-devtools
165
+
166
+ # 简洁模式(只显示工具名称)
167
+ mcps tools chrome-devtools --simple
168
+
169
+ # 筛选工具(按关键词)
170
+ mcps tools chrome-devtools --tool screenshot
171
+
172
+ # 多个关键词 + 简洁模式
173
+ mcps tools gitlab-mr-creator --tool file --tool wiki --simple
174
+ ```
175
+
176
+ 详细模式输出示例:
177
+ ```
178
+ Available Tools for chrome-devtools:
179
+
180
+ - take_screenshot
181
+ Take a screenshot of the page or element.
182
+ Arguments:
183
+ format*: string (Type of format to save the screenshot as...)
184
+ quality: number (Compression quality from 0-100)
185
+ uid: string (The uid of an element to screenshot...)
186
+ ...
187
+
188
+ - click
189
+ Clicks on the provided element
190
+ Arguments:
191
+ uid*: string (The uid of an element...)
192
+ ...
193
+ ```
194
+
195
+ 简洁模式输出示例:
196
+ ```
197
+ $ mcps tools chrome-devtools -s
198
+ click
199
+ close_page
200
+ drag
201
+ emulate
202
+ evaluate_script
203
+ fill
204
+ ...
205
+ take_screenshot
206
+ take_snapshot
207
+
208
+ Total: 26 tool(s)
102
209
  ```
103
210
 
104
211
  **调用工具:**
@@ -117,11 +224,17 @@ mcps call <server_name> <tool_name> [arguments...]
117
224
  # 简单的字符串参数
118
225
  mcps call fetch fetch url="https://example.com"
119
226
 
227
+ # 带多个参数
228
+ mcps call fetch fetch url="https://example.com" max_length=5000
229
+
120
230
  # JSON 对象参数
121
231
  mcps call my-server createUser user='{"name": "Alice", "age": 30}'
122
232
 
123
233
  # 布尔值/数字参数
124
- mcps call my-server config debug=true timeout=5000
234
+ mcps call chrome-devtools take_screenshot fullPage=true quality=90
235
+
236
+ # 混合参数
237
+ mcps call my-server config debug=true timeout=5000 options='{"retries": 3}'
125
238
  ```
126
239
 
127
240
  ## 配置文件
@@ -129,7 +242,99 @@ mcps call my-server config debug=true timeout=5000
129
242
  默认情况下,配置文件存储在:
130
243
  `~/.mcps/mcp.json`
131
244
 
132
- 您可以通过设置 `MCP_CONFIG_DIR` 环境变量来更改存储位置。
245
+ 您可以通过设置 `MCPS_CONFIG_DIR` 环境变量来更改存储位置。
246
+
247
+ 配置文件示例:
248
+ ```json
249
+ {
250
+ "servers": [
251
+ {
252
+ "name": "fetch",
253
+ "type": "stdio",
254
+ "command": "uvx",
255
+ "args": ["mcp-server-fetch"]
256
+ },
257
+ {
258
+ "name": "my-server",
259
+ "type": "stdio",
260
+ "command": "node",
261
+ "args": ["./build/index.js"],
262
+ "env": {
263
+ "API_KEY": "${API_KEY}"
264
+ },
265
+ "disabled": false
266
+ }
267
+ ]
268
+ }
269
+ ```
270
+
271
+ ## 环境变量
272
+
273
+ - `MCPS_CONFIG_DIR`: 配置文件目录(默认:`~/.mcps`)
274
+ - `MCPS_PORT`: Daemon 端口(默认:`4100`)
275
+ - `MCPS_VERBOSE`: 详细日志模式(默认:`false`)
276
+
277
+ ## 命令参考
278
+
279
+ ### 服务管理
280
+ - `mcps ls` - 列出所有服务
281
+ - `mcps add <name>` - 添加新服务
282
+ - `mcps rm <name>` - 移除服务
283
+ - `mcps update [name]` - 更新服务配置
284
+
285
+ ### 守护进程
286
+ - `mcps start [-v]` - 启动守护进程(`-v` 显示详细日志)
287
+ - `mcps stop` - 停止守护进程
288
+ - `mcps status` - 查看守护进程状态
289
+ - `mcps restart [server]` - 重启守护进程或特定服务
290
+
291
+ ### 工具交互
292
+ - `mcps tools <server> [-s] [-t <name>...]` - 查看可用工具
293
+ - `-s, --simple`: 只显示工具名称
294
+ - `-t, --tool`: 按名称筛选工具(可重复使用)
295
+ - `mcps call <server> <tool> [args...]` - 调用工具
296
+
297
+ ## 性能优化
298
+
299
+ mcps 通过以下方式优化性能:
300
+
301
+ 1. **守护进程模式**:保持长连接,避免重复启动开销
302
+ 2. **工具缓存**:连接时缓存工具数量,避免重复查询
303
+ 3. **异步连接**:并行初始化多个服务器连接
304
+
305
+ 典型性能:
306
+ - 启动守护进程:10-15 秒(首次,取决于服务数量)
307
+ - 查看状态:~200ms
308
+ - 调用工具:~50-100ms
309
+
310
+ ## 常见问题
311
+
312
+ **Q: 如何查看所有服务器的运行状态?**
313
+ ```bash
314
+ mcps status # 查看活跃连接
315
+ mcps ls # 查看所有配置(包括禁用的)
316
+ ```
317
+
318
+ **Q: 某个服务连接失败了怎么办?**
319
+ ```bash
320
+ # 查看详细日志
321
+ mcps start --verbose
322
+
323
+ # 重启该服务
324
+ mcps restart my-server
325
+ ```
326
+
327
+ **Q: 如何临时禁用某个服务?**
328
+ 在配置文件中设置 `"disabled": true`,或使用 `mcps update` 修改配置。
329
+
330
+ **Q: 工具太多怎么快速找到?**
331
+ ```bash
332
+ # 筛选工具名称
333
+ mcps tools my-server --tool keyword
334
+
335
+ # 只显示名称
336
+ mcps tools my-server --simple
337
+ ```
133
338
 
134
339
  ## 许可证
135
340
 
@@ -24,8 +24,8 @@ function isPortInUse(port) {
24
24
  });
25
25
  }
26
26
  // Action functions for daemon commands
27
- const startAction = async (options, parentCmd) => {
28
- const port = parseInt(options.port || DAEMON_PORT);
27
+ const startAction = async (options) => {
28
+ const port = parseInt(options.port || process.env.MCPS_PORT || DAEMON_PORT);
29
29
  // Check if port is in use (more reliable than HTTP check)
30
30
  const portInUse = await isPortInUse(port);
31
31
  if (portInUse) {
@@ -51,7 +51,7 @@ const startAction = async (options, parentCmd) => {
51
51
  return;
52
52
  }
53
53
  // Otherwise, spawn a detached process
54
- console.log(chalk.gray('Starting daemon in background...'));
54
+ console.log(chalk.cyan('Starting daemon in background...'));
55
55
  let childFailed = false;
56
56
  const subprocess = spawn(process.execPath, [process.argv[1], 'daemon', 'start'], {
57
57
  detached: true,
@@ -59,23 +59,27 @@ const startAction = async (options, parentCmd) => {
59
59
  stdio: ['ignore', 'pipe', 'pipe'],
60
60
  env: {
61
61
  ...process.env,
62
- MCPS_DAEMON_DETACHED: 'true'
62
+ MCPS_DAEMON_DETACHED: 'true',
63
+ MCPS_VERBOSE: options.verbose ? 'true' : 'false'
63
64
  }
64
65
  });
65
66
  // Stream logs to current console while waiting for ready
66
67
  if (subprocess.stdout) {
67
68
  subprocess.stdout.on('data', (data) => {
68
- process.stdout.write(chalk.gray(`[Daemon] ${data}`));
69
+ process.stdout.write(`${data}`);
69
70
  });
70
71
  }
71
72
  if (subprocess.stderr) {
72
73
  subprocess.stderr.on('data', (data) => {
73
74
  const msg = data.toString();
74
- process.stderr.write(chalk.red(`[Daemon] ${msg}`));
75
75
  // Detect port conflict in child process
76
76
  if (msg.includes('Port') && msg.includes('is already in use')) {
77
77
  childFailed = true;
78
78
  }
79
+ // Only show error output if it contains critical errors
80
+ if (msg.includes('Error') || msg.includes('EADDRINUSE')) {
81
+ process.stderr.write(chalk.red(`[Daemon] ${msg}`));
82
+ }
79
83
  });
80
84
  }
81
85
  subprocess.unref();
@@ -83,7 +87,7 @@ const startAction = async (options, parentCmd) => {
83
87
  // We can poll status for a second
84
88
  const start = Date.now();
85
89
  // Increased timeout to allow for connection initialization
86
- while (Date.now() - start < 10000) {
90
+ while (Date.now() - start < 30000) {
87
91
  // If child reported port conflict, check if daemon is actually running
88
92
  if (childFailed) {
89
93
  const stillRunning = await isPortInUse(port);
@@ -110,9 +114,9 @@ const startAction = async (options, parentCmd) => {
110
114
  console.log(chalk.yellow('Daemon started (async check timeout, but likely running).'));
111
115
  process.exit(0);
112
116
  };
113
- const stopAction = async (parentCmd) => {
117
+ const stopAction = async (options) => {
114
118
  try {
115
- const port = parseInt(parentCmd.opts().port || DAEMON_PORT);
119
+ const port = parseInt(options?.port || process.env.MCPS_PORT || DAEMON_PORT);
116
120
  await fetch(`http://localhost:${port}/stop`, { method: 'POST' });
117
121
  console.log(chalk.green('Daemon stopped successfully.'));
118
122
  }
@@ -120,31 +124,70 @@ const stopAction = async (parentCmd) => {
120
124
  console.error(chalk.red('Failed to stop daemon. Is it running?'));
121
125
  }
122
126
  };
123
- const statusAction = async (parentCmd) => {
127
+ const statusAction = async (options) => {
124
128
  try {
125
- const port = parseInt(parentCmd.opts().port || DAEMON_PORT);
129
+ const port = parseInt(options?.port || process.env.MCPS_PORT || DAEMON_PORT);
126
130
  const res = await fetch(`http://localhost:${port}/status`);
127
131
  const data = await res.json();
132
+ console.log('');
128
133
  console.log(chalk.green(`Daemon is running (v${data.version})`));
129
134
  if (data.connections && data.connections.length > 0) {
135
+ // Helper function to calculate display width (Chinese chars count as 2)
136
+ const getDisplayWidth = (str) => {
137
+ let width = 0;
138
+ for (const char of str) {
139
+ if (char.charCodeAt(0) > 127) {
140
+ width += 2;
141
+ }
142
+ else {
143
+ width += 1;
144
+ }
145
+ }
146
+ return width;
147
+ };
148
+ // Helper function to pad string considering Chinese characters
149
+ const padEndWidth = (str, targetWidth) => {
150
+ const displayWidth = getDisplayWidth(str);
151
+ const padding = Math.max(0, targetWidth - displayWidth);
152
+ return str + ' '.repeat(padding);
153
+ };
154
+ // Build table rows
155
+ const rows = data.connections.map((conn) => {
156
+ const statusText = conn.status === 'error' ? 'Error' : 'Connected';
157
+ const statusColor = conn.status === 'error' ? chalk.red : chalk.green;
158
+ const toolsCount = conn.toolsCount !== null ? conn.toolsCount : '-';
159
+ return {
160
+ name: conn.name,
161
+ status: statusColor(statusText),
162
+ tools: toolsCount
163
+ };
164
+ });
165
+ // Calculate column widths
166
+ const nameWidth = Math.max(4, ...rows.map((r) => getDisplayWidth(r.name)));
167
+ const statusWidth = 10;
168
+ const toolsWidth = 6;
169
+ // Print table header
130
170
  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}`));
171
+ console.log(chalk.bold(`${'NAME'.padEnd(nameWidth)} ${'STATUS'.padEnd(statusWidth)} ${'TOOLS'}`));
172
+ console.log(chalk.cyan('─'.repeat(nameWidth) + ' ' + '─'.repeat(statusWidth) + ' ' + '─'.repeat(toolsWidth)));
173
+ // Print table rows
174
+ rows.forEach((row) => {
175
+ console.log(`${padEndWidth(row.name, nameWidth)} ${String(row.status).padEnd(statusWidth)} ${String(row.tools)}`);
135
176
  });
177
+ console.log(chalk.cyan(`Total: ${data.connections.length} connection(s)`));
136
178
  }
137
179
  else {
138
- console.log(chalk.gray('No active connections.'));
180
+ console.log(chalk.yellow('\nNo active connections.'));
139
181
  }
182
+ console.log('');
140
183
  }
141
184
  catch (e) {
142
185
  console.error(chalk.red('Daemon is not running.'));
143
186
  }
144
187
  };
145
- const restartAction = async (serverName, parentCmd) => {
188
+ const restartAction = async (serverName, options) => {
146
189
  try {
147
- const port = parseInt(parentCmd.opts().port || DAEMON_PORT);
190
+ const port = parseInt(options?.port || process.env.MCPS_PORT || DAEMON_PORT);
148
191
  const res = await fetch(`http://localhost:${port}/restart`, {
149
192
  method: 'POST',
150
193
  body: JSON.stringify({ server: serverName })
@@ -161,19 +204,20 @@ export const registerDaemonCommand = (program) => {
161
204
  program.command('start')
162
205
  .description('Start the daemon')
163
206
  .option('-p, --port <number>', 'Daemon port', String(DAEMON_PORT))
164
- .action((options) => startAction(options, program));
207
+ .option('-v, --verbose', 'Show detailed logs')
208
+ .action((options) => startAction(options));
165
209
  program.command('stop')
166
210
  .description('Stop the daemon')
167
211
  .option('-p, --port <number>', 'Daemon port', String(DAEMON_PORT))
168
- .action(() => stopAction(program));
212
+ .action((options) => stopAction(options));
169
213
  program.command('status')
170
214
  .description('Check daemon status')
171
215
  .option('-p, --port <number>', 'Daemon port', String(DAEMON_PORT))
172
- .action(() => statusAction(program));
216
+ .action((options) => statusAction(options));
173
217
  program.command('restart [server]')
174
218
  .description('Restart the daemon or a specific server connection')
175
219
  .option('-p, --port <number>', 'Daemon port', String(DAEMON_PORT))
176
- .action((serverName) => restartAction(serverName, program));
220
+ .action((serverName, options) => restartAction(serverName, options));
177
221
  // ===== Legacy daemon subcommands (for backward compatibility) =====
178
222
  const daemonCmd = program.command('daemon')
179
223
  .description('Manage the mcps daemon (legacy, use top-level commands)')
@@ -181,16 +225,16 @@ export const registerDaemonCommand = (program) => {
181
225
  .option('-p, --port <number>', 'Daemon port', String(DAEMON_PORT));
182
226
  daemonCmd.command('start', { isDefault: true, hidden: true })
183
227
  .description('Start the daemon (default)')
184
- .action((options) => startAction(options, daemonCmd));
228
+ .action((options) => startAction(options));
185
229
  daemonCmd.command('stop')
186
230
  .description('Stop the running daemon')
187
- .action(() => stopAction(daemonCmd));
231
+ .action((options) => stopAction(options));
188
232
  daemonCmd.command('status')
189
233
  .description('Check daemon status')
190
- .action(() => statusAction(daemonCmd));
234
+ .action((options) => statusAction(options));
191
235
  daemonCmd.command('restart [server]')
192
236
  .description('Restart the daemon or a specific server connection')
193
- .action((serverName) => restartAction(serverName, daemonCmd));
237
+ .action((serverName, options) => restartAction(serverName, options));
194
238
  };
195
239
  const startDaemon = (port) => {
196
240
  const server = http.createServer(async (req, res) => {
@@ -8,19 +8,62 @@ export const registerServerCommands = (program) => {
8
8
  console.log(chalk.yellow('No servers configured.'));
9
9
  return;
10
10
  }
11
- console.log(chalk.bold('\nConfigured Servers:'));
12
- servers.forEach(s => {
13
- console.log(`- ${chalk.cyan(s.name)} [${chalk.magenta(s.type)}]`);
14
- if (s.type === 'stdio') {
15
- console.log(` Command: ${s.command} ${s.args.join(' ')}`);
16
- if (s.env)
17
- console.log(` Env: ${Object.keys(s.env).join(', ')}`);
11
+ // Helper function to calculate display width (Chinese chars count as 2)
12
+ const getDisplayWidth = (str) => {
13
+ let width = 0;
14
+ for (const char of str) {
15
+ if (char.charCodeAt(0) > 127) {
16
+ width += 2;
17
+ }
18
+ else {
19
+ width += 1;
20
+ }
21
+ }
22
+ return width;
23
+ };
24
+ // Helper function to pad string considering Chinese characters
25
+ const padEndWidth = (str, targetWidth) => {
26
+ const displayWidth = getDisplayWidth(str);
27
+ const padding = Math.max(0, targetWidth - displayWidth);
28
+ return str + ' '.repeat(padding);
29
+ };
30
+ // Build table rows
31
+ const rows = servers.map(server => {
32
+ const disabled = server.disabled === true;
33
+ const typeColor = server.type === 'stdio' ? chalk.cyan : chalk.yellow;
34
+ const enabledMark = disabled ? chalk.red('✗') : chalk.green('✓');
35
+ // Build command/URL string
36
+ let command = '';
37
+ if (server.type === 'stdio') {
38
+ command = `${server.command} ${server.args.join(' ')}`;
18
39
  }
19
40
  else {
20
- console.log(` URL: ${s.url}`);
41
+ command = server.url || '';
21
42
  }
22
- console.log('');
43
+ return {
44
+ name: server.name,
45
+ type: typeColor(server.type),
46
+ enabled: enabledMark,
47
+ command: command,
48
+ disabled
49
+ };
50
+ });
51
+ // Calculate column widths
52
+ const nameWidth = Math.max(4, ...rows.map(r => getDisplayWidth(r.name)));
53
+ const typeWidth = 6;
54
+ const enabledWidth = 7;
55
+ const commandWidth = Math.max(7, ...rows.map(r => getDisplayWidth(r.command)));
56
+ // Print table header
57
+ console.log('');
58
+ console.log(chalk.bold(`${'NAME'.padEnd(nameWidth)} ${'TYPE'.padEnd(typeWidth)} ${'ENABLED'.padEnd(enabledWidth)} ${'COMMAND/URL'}`));
59
+ console.log(chalk.gray('─'.repeat(nameWidth) + ' ' + '─'.repeat(typeWidth) + ' ' + '─'.repeat(enabledWidth) + ' ' + '─'.repeat(commandWidth)));
60
+ // Print table rows
61
+ rows.forEach(row => {
62
+ console.log(`${padEndWidth(row.name, nameWidth)} ${String(row.type).padEnd(typeWidth)} ${String(row.enabled).padEnd(enabledWidth)} ${row.command}`);
23
63
  });
64
+ console.log('');
65
+ console.log(chalk.cyan(`Total: ${servers.length} server(s)`));
66
+ console.log('');
24
67
  };
25
68
  const addServerAction = (name, options) => {
26
69
  try {
@@ -29,7 +29,9 @@ function printTools(serverName, tools) {
29
29
  export const registerToolsCommand = (program) => {
30
30
  program.command('tools <server>')
31
31
  .description('List available tools on a server')
32
- .action(async (serverName) => {
32
+ .option('-s, --simple', 'Show only tool names')
33
+ .option('-t, --tool <name...>', 'Filter tools by name(s)')
34
+ .action(async (serverName, options) => {
33
35
  // Check if server exists in config first
34
36
  const serverConfig = configManager.getServer(serverName);
35
37
  if (!serverConfig) {
@@ -40,8 +42,25 @@ export const registerToolsCommand = (program) => {
40
42
  // Auto-start daemon if needed
41
43
  await DaemonClient.ensureDaemon();
42
44
  // List via daemon
43
- const tools = await DaemonClient.listTools(serverName);
44
- printTools(serverName, tools);
45
+ let tools = await DaemonClient.listTools(serverName);
46
+ // Filter by tool names if specified
47
+ if (options.tool && options.tool.length > 0) {
48
+ const filters = Array.isArray(options.tool) ? options.tool : [options.tool];
49
+ tools = tools.filter((tool) => filters.some((filter) => tool.name.toLowerCase().includes(filter.toLowerCase())));
50
+ }
51
+ if (!tools || tools.length === 0) {
52
+ console.log(chalk.yellow('No tools found.'));
53
+ return;
54
+ }
55
+ if (options.simple) {
56
+ // Simple mode: only show tool names
57
+ tools.forEach((tool) => console.log(tool.name));
58
+ console.log(chalk.gray(`\nTotal: ${tools.length} tool(s)`));
59
+ }
60
+ else {
61
+ // Detailed mode: show full tool information
62
+ printTools(serverName, tools);
63
+ }
45
64
  }
46
65
  catch (error) {
47
66
  console.error(chalk.red(`Failed to list tools: ${error.message}`));
@@ -70,7 +70,7 @@ export class McpClientService {
70
70
  await this.client.connect(this.transport);
71
71
  }
72
72
  catch (error) {
73
- console.error(`Failed to connect to server ${config.name}:`, error);
73
+ // Error will be handled by the caller
74
74
  throw error;
75
75
  }
76
76
  }
@@ -2,7 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
  import { ServerConfigSchema } from '../types/config.js';
5
- const CONFIG_DIR = process.env.MCP_CONFIG_DIR || path.join(os.homedir(), '.mcps');
5
+ const CONFIG_DIR = process.env.MCPS_CONFIG_DIR || path.join(os.homedir(), '.mcps');
6
6
  const CONFIG_FILE = path.join(CONFIG_DIR, 'mcp.json');
7
7
  export class ConfigManager {
8
8
  ensureConfigDir() {
@@ -61,7 +61,7 @@ export class DaemonClient {
61
61
  throw new Error(err.error || 'Daemon error');
62
62
  }
63
63
  const data = await response.json();
64
- // The daemon returns { tools: { tools: [...] } } because of ListToolsResult wrapping
65
- return data.tools.tools || [];
64
+ // The daemon returns { tools: [...] }
65
+ return data.tools || [];
66
66
  }
67
67
  }
package/dist/core/pool.js CHANGED
@@ -2,6 +2,7 @@ import { McpClientService } from './client.js';
2
2
  import { configManager } from './config.js';
3
3
  export class ConnectionPool {
4
4
  clients = new Map();
5
+ toolsCache = new Map();
5
6
  initializing = false;
6
7
  initialized = false;
7
8
  async getClient(serverName, options) {
@@ -24,6 +25,15 @@ export class ConnectionPool {
24
25
  await connectPromise;
25
26
  }
26
27
  this.clients.set(serverName, client);
28
+ // Cache tools count after connection
29
+ try {
30
+ const result = await client.listTools();
31
+ this.toolsCache.set(serverName, result.tools.length);
32
+ }
33
+ catch (e) {
34
+ // Connection succeeded but listTools failed, cache as 0
35
+ this.toolsCache.set(serverName, 0);
36
+ }
27
37
  return client;
28
38
  }
29
39
  async closeClient(serverName) {
@@ -36,6 +46,7 @@ export class ConnectionPool {
36
46
  console.error(`[Daemon] Error closing ${serverName}:`, e);
37
47
  }
38
48
  this.clients.delete(serverName);
49
+ this.toolsCache.delete(serverName);
39
50
  return true;
40
51
  }
41
52
  return false;
@@ -51,15 +62,17 @@ export class ConnectionPool {
51
62
  }
52
63
  }
53
64
  this.clients.clear();
65
+ this.toolsCache.clear();
54
66
  }
55
67
  async initializeAll() {
56
68
  const servers = configManager.listServers();
57
69
  this.initializing = true;
58
70
  this.initialized = false;
71
+ const verbose = process.env.MCPS_VERBOSE === 'true';
59
72
  // 过滤掉 disabled 的服务器
60
73
  const enabledServers = servers.filter(server => {
61
74
  const disabled = server.disabled === true;
62
- if (disabled) {
75
+ if (verbose && disabled) {
63
76
  console.log(`[Daemon] Skipping disabled server: ${server.name}`);
64
77
  }
65
78
  return !disabled;
@@ -70,20 +83,47 @@ export class ConnectionPool {
70
83
  this.initialized = true;
71
84
  return;
72
85
  }
73
- console.log(`[Daemon] Initializing ${enabledServers.length} connection(s)...`);
86
+ console.log(`[Daemon] Connecting to ${enabledServers.length} server(s)...`);
87
+ const results = [];
74
88
  for (const server of enabledServers) {
89
+ process.stdout.write(`[Daemon] - ${server.name}... `);
75
90
  try {
76
- console.log(`[Daemon] Connecting to server: ${server.name}...`);
77
91
  await this.getClient(server.name, { timeoutMs: 8000 });
78
- console.log(`[Daemon] Connected to ${server.name}`);
92
+ results.push({ name: server.name, success: true });
93
+ console.log('Connected ✓');
79
94
  }
80
95
  catch (error) {
81
- console.error(`[Daemon] Failed to connect to ${server.name}:`, error.message);
96
+ // Extract clean error message
97
+ let errorMsg = 'Unknown error';
98
+ if (error?.message) {
99
+ // For spawn errors, the message usually contains the essential info
100
+ errorMsg = error.message;
101
+ }
102
+ else if (typeof error === 'string') {
103
+ errorMsg = error;
104
+ }
105
+ else if (error) {
106
+ errorMsg = String(error);
107
+ }
108
+ results.push({ name: server.name, success: false, error: errorMsg });
109
+ console.log('Failed ✗');
110
+ if (verbose) {
111
+ console.error(`[Daemon] Error: ${errorMsg}`);
112
+ }
82
113
  }
83
114
  }
115
+ // Print summary
116
+ const successCount = results.filter(r => r.success).length;
117
+ const failed = results.filter(r => !r.success);
118
+ console.log(`[Daemon] Connected: ${successCount}/${enabledServers.length}`);
119
+ if (failed.length > 0) {
120
+ console.log('[Daemon] Failed connections:');
121
+ failed.forEach(f => {
122
+ console.log(` ✗ ${f.name}: ${f.error}`);
123
+ });
124
+ }
84
125
  this.initializing = false;
85
126
  this.initialized = true;
86
- console.log('[Daemon] Initialization complete.');
87
127
  }
88
128
  getInitStatus() {
89
129
  return { initializing: this.initializing, initialized: this.initialized };
@@ -94,12 +134,20 @@ export class ConnectionPool {
94
134
  let toolsCount = null;
95
135
  let status = 'connected';
96
136
  if (includeTools) {
97
- try {
98
- const result = await client.listTools();
99
- toolsCount = result.tools.length;
137
+ // Use cached tools count instead of calling listTools again
138
+ if (this.toolsCache.has(name)) {
139
+ toolsCount = this.toolsCache.get(name);
100
140
  }
101
- catch (e) {
102
- status = 'error';
141
+ else {
142
+ // Fallback: if not cached, fetch it now
143
+ try {
144
+ const result = await client.listTools();
145
+ toolsCount = result.tools.length;
146
+ this.toolsCache.set(name, toolsCount);
147
+ }
148
+ catch (e) {
149
+ status = 'error';
150
+ }
103
151
  }
104
152
  }
105
153
  details.push({ name, toolsCount, status });
package/dist/index.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maplezzk/mcps",
3
- "version": "1.0.18",
3
+ "version": "1.0.29",
4
4
  "description": "A CLI to manage and use MCP servers",
5
5
  "publishConfig": {
6
6
  "access": "public"