@maplezzk/mcps 1.0.24 → 1.0.30-beta.0
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 +217 -11
- package/dist/commands/tools.js +22 -3
- package/dist/core/client.js +1 -1
- package/dist/core/config.js +13 -8
- package/dist/core/daemon-client.js +2 -2
- package/dist/core/pool.js +45 -16
- package/dist/index.js +0 -0
- package/dist/tests/helpers.js +52 -0
- package/dist/tests/unit/config.test.js +218 -0
- package/package.json +14 -4
- package/dist/commands/config.js +0 -83
package/README.md
CHANGED
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
## 功能特性
|
|
8
8
|
|
|
9
|
-
- 🔌 **服务管理**:轻松添加、移除、查看和更新 MCP 服务(支持 Stdio 和
|
|
10
|
-
- 🛠️
|
|
11
|
-
- 🚀
|
|
12
|
-
- 🔄
|
|
9
|
+
- 🔌 **服务管理**:轻松添加、移除、查看和更新 MCP 服务(支持 Stdio、SSE 和 HTTP 模式)
|
|
10
|
+
- 🛠️ **工具发现**:查看已配置服务中所有可用的工具
|
|
11
|
+
- 🚀 **工具执行**:直接在命令行调用工具,支持参数自动解析
|
|
12
|
+
- 🔄 **守护进程**:保持与 MCP 服务的长连接,显著提高性能
|
|
13
|
+
- 📊 **表格输出**:清晰的服务器状态和工具列表展示
|
|
14
|
+
- 🔍 **工具筛选**:按关键词筛选工具,支持简洁模式
|
|
15
|
+
- 🚨 **详细日志**:可选的详细日志模式,方便调试
|
|
16
|
+
- ✅ **自动化测试**:完整的测试套件,确保代码质量
|
|
13
17
|
|
|
14
18
|
## 安装
|
|
15
19
|
|
|
@@ -17,6 +21,25 @@
|
|
|
17
21
|
npm install -g @maplezzk/mcps
|
|
18
22
|
```
|
|
19
23
|
|
|
24
|
+
## 快速开始
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# 1. 添加一个服务
|
|
28
|
+
mcps add fetch --command uvx --args mcp-server-fetch
|
|
29
|
+
|
|
30
|
+
# 2. 启动守护进程
|
|
31
|
+
mcps start
|
|
32
|
+
|
|
33
|
+
# 3. 查看服务状态
|
|
34
|
+
mcps status
|
|
35
|
+
|
|
36
|
+
# 4. 查看可用工具
|
|
37
|
+
mcps tools fetch
|
|
38
|
+
|
|
39
|
+
# 5. 调用工具
|
|
40
|
+
mcps call fetch fetch url="https://example.com"
|
|
41
|
+
```
|
|
42
|
+
|
|
20
43
|
## 使用指南
|
|
21
44
|
|
|
22
45
|
### 1. 守护进程 (Daemon Mode)
|
|
@@ -25,12 +48,25 @@ mcps 支持守护进程模式,可以保持与 MCP 服务的长连接,显著
|
|
|
25
48
|
|
|
26
49
|
**启动守护进程:**
|
|
27
50
|
```bash
|
|
51
|
+
# 普通模式
|
|
28
52
|
mcps start
|
|
53
|
+
|
|
54
|
+
# 详细模式(显示每个服务器的连接过程和禁用的服务器)
|
|
55
|
+
mcps start --verbose
|
|
29
56
|
```
|
|
30
57
|
|
|
31
|
-
|
|
32
|
-
|
|
58
|
+
输出示例:
|
|
59
|
+
```
|
|
60
|
+
Starting daemon in background...
|
|
61
|
+
[Daemon] Connecting to 7 server(s)...
|
|
62
|
+
[Daemon] - chrome-devtools... Connected ✓
|
|
63
|
+
[Daemon] - fetch... Connected ✓
|
|
64
|
+
[Daemon] - gitlab-mr-creator... Connected ✓
|
|
65
|
+
[Daemon] Connected: 7/7
|
|
66
|
+
Daemon started successfully on port 4100.
|
|
67
|
+
```
|
|
33
68
|
|
|
69
|
+
**重启连接:**
|
|
34
70
|
```bash
|
|
35
71
|
# 重置所有连接
|
|
36
72
|
mcps restart
|
|
@@ -49,15 +85,36 @@ mcps stop
|
|
|
49
85
|
mcps status
|
|
50
86
|
```
|
|
51
87
|
|
|
52
|
-
|
|
88
|
+
输出示例:
|
|
89
|
+
```
|
|
90
|
+
Daemon is running (v1.0.29)
|
|
91
|
+
|
|
92
|
+
Active Connections:
|
|
93
|
+
NAME STATUS TOOLS
|
|
94
|
+
───────────────── ────────── ──────
|
|
95
|
+
chrome-devtools Connected 26
|
|
96
|
+
fetch Connected 1
|
|
97
|
+
gitlab-mr-creator Connected 30
|
|
98
|
+
Total: 3 connection(s)
|
|
99
|
+
```
|
|
53
100
|
|
|
54
101
|
### 2. 服务管理 (Server Management)
|
|
55
102
|
|
|
56
|
-
|
|
103
|
+
**查看所有服务(配置信息):**
|
|
57
104
|
```bash
|
|
58
105
|
mcps ls
|
|
59
106
|
```
|
|
60
107
|
|
|
108
|
+
输出示例:
|
|
109
|
+
```
|
|
110
|
+
NAME TYPE ENABLED COMMAND/URL
|
|
111
|
+
───────────────── ────── ─────── ─────────────
|
|
112
|
+
chrome-devtools stdio ✓ npx -y chrome-devtools-mcp ...
|
|
113
|
+
fetch stdio ✓ uvx mcp-server-fetch
|
|
114
|
+
my-server stdio ✗ npx my-server
|
|
115
|
+
Total: 3 server(s)
|
|
116
|
+
```
|
|
117
|
+
|
|
61
118
|
**添加 Stdio 服务:**
|
|
62
119
|
```bash
|
|
63
120
|
# 添加本地 Node.js 服务
|
|
@@ -65,6 +122,9 @@ mcps add my-server --command node --args ./build/index.js
|
|
|
65
122
|
|
|
66
123
|
# 使用 npx/uvx 添加服务
|
|
67
124
|
mcps add fetch --command uvx --args mcp-server-fetch
|
|
125
|
+
|
|
126
|
+
# 添加带环境变量的服务
|
|
127
|
+
mcps add my-db --command npx --args @modelcontextprotocol/server-postgres --env POSTGRES_CONNECTION_STRING="${DATABASE_URL}"
|
|
68
128
|
```
|
|
69
129
|
|
|
70
130
|
**添加 SSE 服务:**
|
|
@@ -92,13 +152,61 @@ mcps update my-server --command new-command
|
|
|
92
152
|
|
|
93
153
|
# 更新特定服务的参数
|
|
94
154
|
mcps update my-server --args arg1 arg2
|
|
155
|
+
|
|
156
|
+
# 同时更新命令和参数
|
|
157
|
+
mcps update my-server --command node --args ./new-build/index.js
|
|
95
158
|
```
|
|
96
159
|
|
|
97
160
|
### 3. 工具交互 (Tool Interaction)
|
|
98
161
|
|
|
99
162
|
**查看服务下的可用工具:**
|
|
100
163
|
```bash
|
|
101
|
-
|
|
164
|
+
# 详细模式(显示所有信息)
|
|
165
|
+
mcps tools chrome-devtools
|
|
166
|
+
|
|
167
|
+
# 简洁模式(只显示工具名称)
|
|
168
|
+
mcps tools chrome-devtools --simple
|
|
169
|
+
|
|
170
|
+
# 筛选工具(按关键词)
|
|
171
|
+
mcps tools chrome-devtools --tool screenshot
|
|
172
|
+
|
|
173
|
+
# 多个关键词 + 简洁模式
|
|
174
|
+
mcps tools gitlab-mr-creator --tool file --tool wiki --simple
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
详细模式输出示例:
|
|
178
|
+
```
|
|
179
|
+
Available Tools for chrome-devtools:
|
|
180
|
+
|
|
181
|
+
- take_screenshot
|
|
182
|
+
Take a screenshot of the page or element.
|
|
183
|
+
Arguments:
|
|
184
|
+
format*: string (Type of format to save the screenshot as...)
|
|
185
|
+
quality: number (Compression quality from 0-100)
|
|
186
|
+
uid: string (The uid of an element to screenshot...)
|
|
187
|
+
...
|
|
188
|
+
|
|
189
|
+
- click
|
|
190
|
+
Clicks on the provided element
|
|
191
|
+
Arguments:
|
|
192
|
+
uid*: string (The uid of an element...)
|
|
193
|
+
...
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
简洁模式输出示例:
|
|
197
|
+
```
|
|
198
|
+
$ mcps tools chrome-devtools -s
|
|
199
|
+
click
|
|
200
|
+
close_page
|
|
201
|
+
drag
|
|
202
|
+
emulate
|
|
203
|
+
evaluate_script
|
|
204
|
+
fill
|
|
205
|
+
...
|
|
206
|
+
take_screenshot
|
|
207
|
+
take_snapshot
|
|
208
|
+
|
|
209
|
+
Total: 26 tool(s)
|
|
102
210
|
```
|
|
103
211
|
|
|
104
212
|
**调用工具:**
|
|
@@ -117,11 +225,17 @@ mcps call <server_name> <tool_name> [arguments...]
|
|
|
117
225
|
# 简单的字符串参数
|
|
118
226
|
mcps call fetch fetch url="https://example.com"
|
|
119
227
|
|
|
228
|
+
# 带多个参数
|
|
229
|
+
mcps call fetch fetch url="https://example.com" max_length=5000
|
|
230
|
+
|
|
120
231
|
# JSON 对象参数
|
|
121
232
|
mcps call my-server createUser user='{"name": "Alice", "age": 30}'
|
|
122
233
|
|
|
123
234
|
# 布尔值/数字参数
|
|
124
|
-
mcps call
|
|
235
|
+
mcps call chrome-devtools take_screenshot fullPage=true quality=90
|
|
236
|
+
|
|
237
|
+
# 混合参数
|
|
238
|
+
mcps call my-server config debug=true timeout=5000 options='{"retries": 3}'
|
|
125
239
|
```
|
|
126
240
|
|
|
127
241
|
## 配置文件
|
|
@@ -129,7 +243,99 @@ mcps call my-server config debug=true timeout=5000
|
|
|
129
243
|
默认情况下,配置文件存储在:
|
|
130
244
|
`~/.mcps/mcp.json`
|
|
131
245
|
|
|
132
|
-
您可以通过设置 `
|
|
246
|
+
您可以通过设置 `MCPS_CONFIG_DIR` 环境变量来更改存储位置。
|
|
247
|
+
|
|
248
|
+
配置文件示例:
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"servers": [
|
|
252
|
+
{
|
|
253
|
+
"name": "fetch",
|
|
254
|
+
"type": "stdio",
|
|
255
|
+
"command": "uvx",
|
|
256
|
+
"args": ["mcp-server-fetch"]
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"name": "my-server",
|
|
260
|
+
"type": "stdio",
|
|
261
|
+
"command": "node",
|
|
262
|
+
"args": ["./build/index.js"],
|
|
263
|
+
"env": {
|
|
264
|
+
"API_KEY": "${API_KEY}"
|
|
265
|
+
},
|
|
266
|
+
"disabled": false
|
|
267
|
+
}
|
|
268
|
+
]
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## 环境变量
|
|
273
|
+
|
|
274
|
+
- `MCPS_CONFIG_DIR`: 配置文件目录(默认:`~/.mcps`)
|
|
275
|
+
- `MCPS_PORT`: Daemon 端口(默认:`4100`)
|
|
276
|
+
- `MCPS_VERBOSE`: 详细日志模式(默认:`false`)
|
|
277
|
+
|
|
278
|
+
## 命令参考
|
|
279
|
+
|
|
280
|
+
### 服务管理
|
|
281
|
+
- `mcps ls` - 列出所有服务
|
|
282
|
+
- `mcps add <name>` - 添加新服务
|
|
283
|
+
- `mcps rm <name>` - 移除服务
|
|
284
|
+
- `mcps update [name]` - 更新服务配置
|
|
285
|
+
|
|
286
|
+
### 守护进程
|
|
287
|
+
- `mcps start [-v]` - 启动守护进程(`-v` 显示详细日志)
|
|
288
|
+
- `mcps stop` - 停止守护进程
|
|
289
|
+
- `mcps status` - 查看守护进程状态
|
|
290
|
+
- `mcps restart [server]` - 重启守护进程或特定服务
|
|
291
|
+
|
|
292
|
+
### 工具交互
|
|
293
|
+
- `mcps tools <server> [-s] [-t <name>...]` - 查看可用工具
|
|
294
|
+
- `-s, --simple`: 只显示工具名称
|
|
295
|
+
- `-t, --tool`: 按名称筛选工具(可重复使用)
|
|
296
|
+
- `mcps call <server> <tool> [args...]` - 调用工具
|
|
297
|
+
|
|
298
|
+
## 性能优化
|
|
299
|
+
|
|
300
|
+
mcps 通过以下方式优化性能:
|
|
301
|
+
|
|
302
|
+
1. **守护进程模式**:保持长连接,避免重复启动开销
|
|
303
|
+
2. **工具缓存**:连接时缓存工具数量,避免重复查询
|
|
304
|
+
3. **异步连接**:并行初始化多个服务器连接
|
|
305
|
+
|
|
306
|
+
典型性能:
|
|
307
|
+
- 启动守护进程:10-15 秒(首次,取决于服务数量)
|
|
308
|
+
- 查看状态:~200ms
|
|
309
|
+
- 调用工具:~50-100ms
|
|
310
|
+
|
|
311
|
+
## 常见问题
|
|
312
|
+
|
|
313
|
+
**Q: 如何查看所有服务器的运行状态?**
|
|
314
|
+
```bash
|
|
315
|
+
mcps status # 查看活跃连接
|
|
316
|
+
mcps ls # 查看所有配置(包括禁用的)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Q: 某个服务连接失败了怎么办?**
|
|
320
|
+
```bash
|
|
321
|
+
# 查看详细日志
|
|
322
|
+
mcps start --verbose
|
|
323
|
+
|
|
324
|
+
# 重启该服务
|
|
325
|
+
mcps restart my-server
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Q: 如何临时禁用某个服务?**
|
|
329
|
+
在配置文件中设置 `"disabled": true`,或使用 `mcps update` 修改配置。
|
|
330
|
+
|
|
331
|
+
**Q: 工具太多怎么快速找到?**
|
|
332
|
+
```bash
|
|
333
|
+
# 筛选工具名称
|
|
334
|
+
mcps tools my-server --tool keyword
|
|
335
|
+
|
|
336
|
+
# 只显示名称
|
|
337
|
+
mcps tools my-server --simple
|
|
338
|
+
```
|
|
133
339
|
|
|
134
340
|
## 许可证
|
|
135
341
|
|
package/dist/commands/tools.js
CHANGED
|
@@ -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
|
-
.
|
|
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
|
-
|
|
44
|
-
|
|
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}`));
|
package/dist/core/client.js
CHANGED
package/dist/core/config.js
CHANGED
|
@@ -2,24 +2,29 @@ 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
|
|
6
|
-
const CONFIG_FILE = path.join(CONFIG_DIR, 'mcp.json');
|
|
5
|
+
const getDefaultConfigDir = () => process.env.MCPS_CONFIG_DIR || path.join(os.homedir(), '.mcps');
|
|
7
6
|
export class ConfigManager {
|
|
7
|
+
configDir;
|
|
8
|
+
configFile;
|
|
9
|
+
constructor(configDir) {
|
|
10
|
+
this.configDir = configDir || getDefaultConfigDir();
|
|
11
|
+
this.configFile = path.join(this.configDir, 'mcp.json');
|
|
12
|
+
}
|
|
8
13
|
ensureConfigDir() {
|
|
9
|
-
if (!fs.existsSync(
|
|
10
|
-
fs.mkdirSync(
|
|
14
|
+
if (!fs.existsSync(this.configDir)) {
|
|
15
|
+
fs.mkdirSync(this.configDir, { recursive: true });
|
|
11
16
|
}
|
|
12
17
|
}
|
|
13
18
|
loadConfig() {
|
|
14
19
|
this.ensureConfigDir();
|
|
15
|
-
if (!fs.existsSync(
|
|
20
|
+
if (!fs.existsSync(this.configFile)) {
|
|
16
21
|
return { servers: [] };
|
|
17
22
|
}
|
|
18
23
|
try {
|
|
19
|
-
const content = fs.readFileSync(
|
|
24
|
+
const content = fs.readFileSync(this.configFile, 'utf-8');
|
|
20
25
|
const json = JSON.parse(content);
|
|
21
26
|
// Log for debugging (can be removed later or controlled by verbose flag)
|
|
22
|
-
// console.log('Loading config from:',
|
|
27
|
+
// console.log('Loading config from:', this.configFile);
|
|
23
28
|
if (!json || typeof json !== 'object') {
|
|
24
29
|
console.warn('Invalid config file structure. Expected JSON object.');
|
|
25
30
|
return { servers: [] };
|
|
@@ -59,7 +64,7 @@ export class ConfigManager {
|
|
|
59
64
|
}
|
|
60
65
|
saveConfig(config) {
|
|
61
66
|
this.ensureConfigDir();
|
|
62
|
-
fs.writeFileSync(
|
|
67
|
+
fs.writeFileSync(this.configFile, JSON.stringify(config, null, 2), 'utf-8');
|
|
63
68
|
}
|
|
64
69
|
listServers() {
|
|
65
70
|
return this.loadConfig().servers;
|
|
@@ -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:
|
|
65
|
-
return data.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,6 +62,7 @@ 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();
|
|
@@ -74,21 +86,29 @@ export class ConnectionPool {
|
|
|
74
86
|
console.log(`[Daemon] Connecting to ${enabledServers.length} server(s)...`);
|
|
75
87
|
const results = [];
|
|
76
88
|
for (const server of enabledServers) {
|
|
77
|
-
|
|
78
|
-
process.stdout.write(`[Daemon] - ${server.name}... `);
|
|
79
|
-
}
|
|
89
|
+
process.stdout.write(`[Daemon] - ${server.name}... `);
|
|
80
90
|
try {
|
|
81
91
|
await this.getClient(server.name, { timeoutMs: 8000 });
|
|
82
92
|
results.push({ name: server.name, success: true });
|
|
83
|
-
|
|
84
|
-
console.log('✓');
|
|
85
|
-
}
|
|
93
|
+
console.log('Connected ✓');
|
|
86
94
|
}
|
|
87
95
|
catch (error) {
|
|
88
|
-
|
|
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 ✗');
|
|
89
110
|
if (verbose) {
|
|
90
|
-
console.
|
|
91
|
-
console.error(`[Daemon] Error: ${error.message}`);
|
|
111
|
+
console.error(`[Daemon] Error: ${errorMsg}`);
|
|
92
112
|
}
|
|
93
113
|
}
|
|
94
114
|
}
|
|
@@ -96,9 +116,10 @@ export class ConnectionPool {
|
|
|
96
116
|
const successCount = results.filter(r => r.success).length;
|
|
97
117
|
const failed = results.filter(r => !r.success);
|
|
98
118
|
console.log(`[Daemon] Connected: ${successCount}/${enabledServers.length}`);
|
|
99
|
-
if (
|
|
119
|
+
if (failed.length > 0) {
|
|
120
|
+
console.log('[Daemon] Failed connections:');
|
|
100
121
|
failed.forEach(f => {
|
|
101
|
-
console.
|
|
122
|
+
console.log(` ✗ ${f.name}: ${f.error}`);
|
|
102
123
|
});
|
|
103
124
|
}
|
|
104
125
|
this.initializing = false;
|
|
@@ -113,12 +134,20 @@ export class ConnectionPool {
|
|
|
113
134
|
let toolsCount = null;
|
|
114
135
|
let status = 'connected';
|
|
115
136
|
if (includeTools) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
toolsCount =
|
|
137
|
+
// Use cached tools count instead of calling listTools again
|
|
138
|
+
if (this.toolsCache.has(name)) {
|
|
139
|
+
toolsCount = this.toolsCache.get(name);
|
|
119
140
|
}
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
}
|
|
122
151
|
}
|
|
123
152
|
}
|
|
124
153
|
details.push({ name, toolsCount, status });
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { vi } from 'vitest';
|
|
5
|
+
export const TEST_CONFIG_DIR = path.join(os.tmpdir(), `mcps-test-${Date.now()}`);
|
|
6
|
+
export function setupTestConfig() {
|
|
7
|
+
// 确保测试配置目录存在
|
|
8
|
+
if (!fs.existsSync(TEST_CONFIG_DIR)) {
|
|
9
|
+
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
// 设置测试环境变量
|
|
12
|
+
process.env.MCPS_CONFIG_DIR = TEST_CONFIG_DIR;
|
|
13
|
+
return TEST_CONFIG_DIR;
|
|
14
|
+
}
|
|
15
|
+
export function cleanupTestConfig() {
|
|
16
|
+
if (fs.existsSync(TEST_CONFIG_DIR)) {
|
|
17
|
+
fs.rmSync(TEST_CONFIG_DIR, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function getTestConfigPath() {
|
|
21
|
+
return path.join(TEST_CONFIG_DIR, 'mcp.json');
|
|
22
|
+
}
|
|
23
|
+
export function createTestServer(overrides = {}) {
|
|
24
|
+
return {
|
|
25
|
+
name: 'test-server',
|
|
26
|
+
type: 'stdio',
|
|
27
|
+
command: 'node',
|
|
28
|
+
args: ['--version'],
|
|
29
|
+
...overrides
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function createMockClient(toolsCount = 5) {
|
|
33
|
+
const tools = [];
|
|
34
|
+
for (let i = 0; i < toolsCount; i++) {
|
|
35
|
+
tools.push({
|
|
36
|
+
name: `test_tool_${i}`,
|
|
37
|
+
description: `Test tool ${i}`,
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
param: { type: 'string', description: `Parameter ${i}` }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
listTools: vi.fn().mockResolvedValue({ tools }),
|
|
48
|
+
callTool: vi.fn().mockResolvedValue({ result: 'success' }),
|
|
49
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
50
|
+
close: vi.fn().mockResolvedValue(undefined)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { ConfigManager } from '../../core/config.js';
|
|
6
|
+
describe('ConfigManager', () => {
|
|
7
|
+
let testConfigDir;
|
|
8
|
+
let manager;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
// 创建临时配置目录
|
|
11
|
+
testConfigDir = path.join(os.tmpdir(), `mcps-config-test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
|
|
12
|
+
fs.mkdirSync(testConfigDir, { recursive: true });
|
|
13
|
+
// 创建新的 ConfigManager 实例,传入测试目录
|
|
14
|
+
manager = new ConfigManager(testConfigDir);
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
// 清理临时目录
|
|
18
|
+
if (fs.existsSync(testConfigDir)) {
|
|
19
|
+
try {
|
|
20
|
+
fs.rmSync(testConfigDir, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
// 忽略清理错误
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
describe('addServer', () => {
|
|
28
|
+
it('should add a stdio server', () => {
|
|
29
|
+
const server = {
|
|
30
|
+
name: 'test-stdio',
|
|
31
|
+
type: 'stdio',
|
|
32
|
+
command: 'node',
|
|
33
|
+
args: ['--version']
|
|
34
|
+
};
|
|
35
|
+
manager.addServer(server);
|
|
36
|
+
const servers = manager.listServers();
|
|
37
|
+
expect(servers).toHaveLength(1);
|
|
38
|
+
expect(servers[0]).toMatchObject(server);
|
|
39
|
+
});
|
|
40
|
+
it('should add an sse server', () => {
|
|
41
|
+
const server = {
|
|
42
|
+
name: 'test-sse',
|
|
43
|
+
type: 'sse',
|
|
44
|
+
url: 'http://localhost:3000/sse'
|
|
45
|
+
};
|
|
46
|
+
manager.addServer(server);
|
|
47
|
+
const retrieved = manager.getServer('test-sse');
|
|
48
|
+
expect(retrieved).toMatchObject(server);
|
|
49
|
+
});
|
|
50
|
+
it('should add an http server', () => {
|
|
51
|
+
const server = {
|
|
52
|
+
name: 'test-http',
|
|
53
|
+
type: 'http',
|
|
54
|
+
url: 'http://localhost:3000/mcp'
|
|
55
|
+
};
|
|
56
|
+
manager.addServer(server);
|
|
57
|
+
const retrieved = manager.getServer('test-http');
|
|
58
|
+
expect(retrieved).toMatchObject(server);
|
|
59
|
+
});
|
|
60
|
+
it('should add server with disabled flag', () => {
|
|
61
|
+
const server = {
|
|
62
|
+
name: 'disabled-server',
|
|
63
|
+
type: 'stdio',
|
|
64
|
+
command: 'node',
|
|
65
|
+
args: [],
|
|
66
|
+
disabled: true
|
|
67
|
+
};
|
|
68
|
+
manager.addServer(server);
|
|
69
|
+
const retrieved = manager.getServer('disabled-server');
|
|
70
|
+
expect(retrieved).toMatchObject({
|
|
71
|
+
name: 'disabled-server',
|
|
72
|
+
disabled: true
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
it('should throw error when adding duplicate server', () => {
|
|
76
|
+
const server = {
|
|
77
|
+
name: 'duplicate',
|
|
78
|
+
type: 'stdio',
|
|
79
|
+
command: 'node',
|
|
80
|
+
args: []
|
|
81
|
+
};
|
|
82
|
+
manager.addServer(server);
|
|
83
|
+
expect(() => manager.addServer(server)).toThrow('already exists');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('getServer', () => {
|
|
87
|
+
it('should retrieve existing server', () => {
|
|
88
|
+
const server = {
|
|
89
|
+
name: 'to-retrieve',
|
|
90
|
+
type: 'stdio',
|
|
91
|
+
command: 'node',
|
|
92
|
+
args: []
|
|
93
|
+
};
|
|
94
|
+
manager.addServer(server);
|
|
95
|
+
const retrieved = manager.getServer('to-retrieve');
|
|
96
|
+
expect(retrieved).toMatchObject(server);
|
|
97
|
+
});
|
|
98
|
+
it('should return undefined for non-existing server', () => {
|
|
99
|
+
const retrieved = manager.getServer('non-existing');
|
|
100
|
+
expect(retrieved).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('listServers', () => {
|
|
104
|
+
it('should return empty array when no servers', () => {
|
|
105
|
+
const servers = manager.listServers();
|
|
106
|
+
expect(servers).toEqual([]);
|
|
107
|
+
});
|
|
108
|
+
it('should return all servers including disabled ones', () => {
|
|
109
|
+
const server1 = {
|
|
110
|
+
name: 'server-1',
|
|
111
|
+
type: 'stdio',
|
|
112
|
+
command: 'node',
|
|
113
|
+
args: []
|
|
114
|
+
};
|
|
115
|
+
const server2 = {
|
|
116
|
+
name: 'server-2',
|
|
117
|
+
type: 'stdio',
|
|
118
|
+
command: 'npm',
|
|
119
|
+
args: ['start'],
|
|
120
|
+
disabled: true
|
|
121
|
+
};
|
|
122
|
+
manager.addServer(server1);
|
|
123
|
+
manager.addServer(server2);
|
|
124
|
+
const servers = manager.listServers();
|
|
125
|
+
expect(servers).toHaveLength(2);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe('removeServer', () => {
|
|
129
|
+
it('should remove existing server', () => {
|
|
130
|
+
const server = {
|
|
131
|
+
name: 'to-remove',
|
|
132
|
+
type: 'stdio',
|
|
133
|
+
command: 'node',
|
|
134
|
+
args: []
|
|
135
|
+
};
|
|
136
|
+
manager.addServer(server);
|
|
137
|
+
expect(manager.listServers()).toHaveLength(1);
|
|
138
|
+
manager.removeServer('to-remove');
|
|
139
|
+
expect(manager.listServers()).toHaveLength(0);
|
|
140
|
+
});
|
|
141
|
+
it('should throw error when removing non-existing server', () => {
|
|
142
|
+
expect(() => manager.removeServer('non-existing')).toThrow('not found');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe('updateServer', () => {
|
|
146
|
+
it('should update server configuration', () => {
|
|
147
|
+
const original = {
|
|
148
|
+
name: 'to-update',
|
|
149
|
+
type: 'stdio',
|
|
150
|
+
command: 'node',
|
|
151
|
+
args: ['--version']
|
|
152
|
+
};
|
|
153
|
+
manager.addServer(original);
|
|
154
|
+
const updates = {
|
|
155
|
+
command: 'npm',
|
|
156
|
+
args: ['start']
|
|
157
|
+
};
|
|
158
|
+
manager.updateServer('to-update', updates);
|
|
159
|
+
const updated = manager.getServer('to-update');
|
|
160
|
+
expect(updated).toMatchObject({
|
|
161
|
+
name: 'to-update',
|
|
162
|
+
command: 'npm',
|
|
163
|
+
args: ['start']
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
it('should preserve disabled status during update', () => {
|
|
167
|
+
const original = {
|
|
168
|
+
name: 'update-disabled',
|
|
169
|
+
type: 'stdio',
|
|
170
|
+
command: 'node',
|
|
171
|
+
args: [],
|
|
172
|
+
disabled: true
|
|
173
|
+
};
|
|
174
|
+
manager.addServer(original);
|
|
175
|
+
manager.updateServer('update-disabled', { command: 'npm' });
|
|
176
|
+
const updated = manager.getServer('update-disabled');
|
|
177
|
+
expect(updated?.disabled).toBe(true);
|
|
178
|
+
expect(updated?.command).toBe('npm');
|
|
179
|
+
});
|
|
180
|
+
it('should throw error when updating non-existing server', () => {
|
|
181
|
+
expect(() => manager.updateServer('non-existing', {})).toThrow('not found');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe('persistence', () => {
|
|
185
|
+
it('should persist configuration to file', () => {
|
|
186
|
+
const server = {
|
|
187
|
+
name: 'persistent',
|
|
188
|
+
type: 'stdio',
|
|
189
|
+
command: 'node',
|
|
190
|
+
args: []
|
|
191
|
+
};
|
|
192
|
+
manager.addServer(server);
|
|
193
|
+
const configFile = path.join(testConfigDir, 'mcp.json');
|
|
194
|
+
expect(fs.existsSync(configFile)).toBe(true);
|
|
195
|
+
const content = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
|
|
196
|
+
expect(content.servers).toHaveLength(1);
|
|
197
|
+
expect(content.servers[0].name).toBe('persistent');
|
|
198
|
+
});
|
|
199
|
+
it('should load existing configuration on instantiation', () => {
|
|
200
|
+
const configFile = path.join(testConfigDir, 'mcp.json');
|
|
201
|
+
const existingConfig = {
|
|
202
|
+
servers: [
|
|
203
|
+
{
|
|
204
|
+
name: 'existing',
|
|
205
|
+
type: 'stdio',
|
|
206
|
+
command: 'node',
|
|
207
|
+
args: []
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
};
|
|
211
|
+
fs.writeFileSync(configFile, JSON.stringify(existingConfig, null, 2));
|
|
212
|
+
const newManager = new ConfigManager(testConfigDir);
|
|
213
|
+
const servers = newManager.listServers();
|
|
214
|
+
expect(servers).toHaveLength(1);
|
|
215
|
+
expect(servers[0].name).toBe('existing');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maplezzk/mcps",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.30-beta.0",
|
|
4
4
|
"description": "A CLI to manage and use MCP servers",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
],
|
|
15
15
|
"author": "",
|
|
16
16
|
"license": "ISC",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/a13835614623/mcps"
|
|
20
|
+
},
|
|
17
21
|
"type": "module",
|
|
18
22
|
"bin": {
|
|
19
23
|
"mcps": "./dist/index.js"
|
|
@@ -23,10 +27,13 @@
|
|
|
23
27
|
],
|
|
24
28
|
"scripts": {
|
|
25
29
|
"build": "tsc",
|
|
26
|
-
"prepublishOnly": "npm run build",
|
|
30
|
+
"prepublishOnly": "npm run build && npm run test",
|
|
27
31
|
"start": "node dist/index.js",
|
|
28
32
|
"dev": "ts-node src/index.ts",
|
|
29
|
-
"test": "
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"test:watch": "vitest",
|
|
35
|
+
"test:ui": "vitest --ui",
|
|
36
|
+
"test:coverage": "vitest --coverage"
|
|
30
37
|
},
|
|
31
38
|
"dependencies": {
|
|
32
39
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
@@ -39,7 +46,10 @@
|
|
|
39
46
|
"devDependencies": {
|
|
40
47
|
"@types/eventsource": "^1.1.15",
|
|
41
48
|
"@types/node": "^22.10.7",
|
|
49
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
50
|
+
"@vitest/ui": "^4.0.18",
|
|
42
51
|
"ts-node": "^10.9.2",
|
|
43
|
-
"typescript": "^5.7.3"
|
|
52
|
+
"typescript": "^5.7.3",
|
|
53
|
+
"vitest": "^4.0.18"
|
|
44
54
|
}
|
|
45
55
|
}
|
package/dist/commands/config.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { configManager } from '../core/config.js';
|
|
5
|
-
export const registerConfigCommand = (program) => {
|
|
6
|
-
const configCmd = program.command('config')
|
|
7
|
-
.description('Manage configuration');
|
|
8
|
-
configCmd.command('import <file>')
|
|
9
|
-
.description('Import servers from a JSON configuration file (e.g., mcporter.json)')
|
|
10
|
-
.option('-f, --force', 'Overwrite existing servers with the same name', false)
|
|
11
|
-
.action((file, options) => {
|
|
12
|
-
try {
|
|
13
|
-
const absolutePath = path.resolve(file);
|
|
14
|
-
if (!fs.existsSync(absolutePath)) {
|
|
15
|
-
console.error(chalk.red(`File not found: ${absolutePath}`));
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
19
|
-
let json;
|
|
20
|
-
try {
|
|
21
|
-
json = JSON.parse(content);
|
|
22
|
-
}
|
|
23
|
-
catch (e) {
|
|
24
|
-
console.error(chalk.red('Invalid JSON file.'));
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
let importedCount = 0;
|
|
28
|
-
let skippedCount = 0;
|
|
29
|
-
// Support standard MCP config format (mcpServers object)
|
|
30
|
-
const serversMap = json.mcpServers || {};
|
|
31
|
-
Object.entries(serversMap).forEach(([name, config]) => {
|
|
32
|
-
// Skip disabled servers
|
|
33
|
-
if (config.disabled === true) {
|
|
34
|
-
skippedCount++;
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const serverName = name;
|
|
38
|
-
let newServer;
|
|
39
|
-
if (config.command) {
|
|
40
|
-
newServer = {
|
|
41
|
-
name: serverName,
|
|
42
|
-
type: 'stdio',
|
|
43
|
-
command: config.command,
|
|
44
|
-
args: config.args || [],
|
|
45
|
-
env: config.env,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
else if (config.url) {
|
|
49
|
-
newServer = {
|
|
50
|
-
name: serverName,
|
|
51
|
-
type: 'sse',
|
|
52
|
-
url: config.url
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
console.warn(chalk.yellow(`Skipping invalid server config for "${serverName}": missing command or url`));
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
const existing = configManager.getServer(serverName);
|
|
60
|
-
if (existing) {
|
|
61
|
-
if (options.force) {
|
|
62
|
-
configManager.updateServer(serverName, newServer);
|
|
63
|
-
console.log(chalk.gray(`Updated existing server: ${serverName}`));
|
|
64
|
-
importedCount++;
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
console.log(chalk.yellow(`Skipping existing server: ${serverName} (use --force to overwrite)`));
|
|
68
|
-
skippedCount++;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
configManager.addServer(newServer);
|
|
73
|
-
console.log(chalk.green(`Imported server: ${serverName}`));
|
|
74
|
-
importedCount++;
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
console.log(chalk.bold(`\nImport complete. Imported: ${importedCount}, Skipped: ${skippedCount}`));
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
console.error(chalk.red(`Import failed: ${error.message}`));
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
};
|