@maplezzk/mcps 1.0.29 → 1.0.30
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 +1 -0
- package/dist/core/client.js +125 -4
- package/dist/core/config.js +13 -8
- package/dist/core/pool.js +1 -1
- 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
package/dist/core/client.js
CHANGED
|
@@ -3,6 +3,30 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
|
3
3
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
4
4
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
5
5
|
import { EventSource } from 'eventsource';
|
|
6
|
+
import { exec } from 'child_process';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
const execAsync = promisify(exec);
|
|
11
|
+
// 日志文件路径
|
|
12
|
+
const logFile = path.join(process.env.HOME || '~', '.mcps', 'daemon.log');
|
|
13
|
+
// 日志函数(异步,避免阻塞)
|
|
14
|
+
const log = (message) => {
|
|
15
|
+
const timestamp = new Date().toISOString();
|
|
16
|
+
const logMessage = `[${timestamp}] ${message}\n`;
|
|
17
|
+
try {
|
|
18
|
+
fs.appendFileSync(logFile, logMessage);
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
// 忽略日志写入错误
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
console.log(message);
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
// 忽略 console 错误
|
|
28
|
+
}
|
|
29
|
+
};
|
|
6
30
|
// Required for SSEClientTransport in Node.js environment
|
|
7
31
|
// @ts-ignore
|
|
8
32
|
global.EventSource = EventSource;
|
|
@@ -26,9 +50,22 @@ const resolveEnvPlaceholders = (input) => {
|
|
|
26
50
|
export class McpClientService {
|
|
27
51
|
client = null;
|
|
28
52
|
transport = null;
|
|
29
|
-
|
|
53
|
+
serverName = '';
|
|
54
|
+
serverType = '';
|
|
55
|
+
serverCommand = '';
|
|
56
|
+
serverArgs = [];
|
|
57
|
+
daemonPid = process.pid;
|
|
58
|
+
childPids = new Set();
|
|
59
|
+
static globalPidsBeforeConnection = new Set();
|
|
60
|
+
async connect(config, serverName = '') {
|
|
61
|
+
this.serverName = serverName;
|
|
62
|
+
this.serverType = config.type;
|
|
63
|
+
this.daemonPid = process.pid;
|
|
30
64
|
try {
|
|
31
65
|
if (config.type === 'stdio') {
|
|
66
|
+
// 保存命令和参数用于后续清理进程
|
|
67
|
+
this.serverCommand = config.command;
|
|
68
|
+
this.serverArgs = config.args || [];
|
|
32
69
|
const resolvedConfigEnv = {};
|
|
33
70
|
if (config.env) {
|
|
34
71
|
for (const key in config.env) {
|
|
@@ -68,12 +105,81 @@ export class McpClientService {
|
|
|
68
105
|
capabilities: {},
|
|
69
106
|
});
|
|
70
107
|
await this.client.connect(this.transport);
|
|
108
|
+
// 连接成功后,立即查找并保存子进程 PID
|
|
109
|
+
if (config.type === 'stdio') {
|
|
110
|
+
await this.recordChildPids();
|
|
111
|
+
}
|
|
71
112
|
}
|
|
72
113
|
catch (error) {
|
|
73
114
|
// Error will be handled by the caller
|
|
74
115
|
throw error;
|
|
75
116
|
}
|
|
76
117
|
}
|
|
118
|
+
// 记录子进程 PID(使用快照差异法)
|
|
119
|
+
async recordChildPids() {
|
|
120
|
+
try {
|
|
121
|
+
// 获取连接前的所有子进程快照
|
|
122
|
+
const getSnapshot = async () => {
|
|
123
|
+
const pids = new Set();
|
|
124
|
+
const getAllDescendants = async (parentPid) => {
|
|
125
|
+
try {
|
|
126
|
+
const { stdout } = await execAsync(`pgrep -P ${parentPid}`);
|
|
127
|
+
const childPids = stdout.trim().split('\n').filter(pid => pid);
|
|
128
|
+
for (const pid of childPids) {
|
|
129
|
+
pids.add(parseInt(pid));
|
|
130
|
+
await getAllDescendants(parseInt(pid));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// 没有子进程
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
await getAllDescendants(this.daemonPid);
|
|
138
|
+
return pids;
|
|
139
|
+
};
|
|
140
|
+
// 记录连接前的快照(第一次连接时为空)
|
|
141
|
+
const beforeSnapshot = new Set(McpClientService.globalPidsBeforeConnection);
|
|
142
|
+
// 等待一小段时间,确保子进程完全启动
|
|
143
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
144
|
+
// 获取连接后的快照
|
|
145
|
+
const afterSnapshot = await getSnapshot();
|
|
146
|
+
// 找出差异(新启动的进程)
|
|
147
|
+
const newPids = new Set();
|
|
148
|
+
for (const pid of afterSnapshot) {
|
|
149
|
+
if (!beforeSnapshot.has(pid)) {
|
|
150
|
+
newPids.add(pid);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// 更新全局快照
|
|
154
|
+
McpClientService.globalPidsBeforeConnection = afterSnapshot;
|
|
155
|
+
// 过滤出匹配我们命令的进程
|
|
156
|
+
const commandBaseName = this.serverCommand.split('/').pop() || this.serverCommand;
|
|
157
|
+
for (const pid of newPids) {
|
|
158
|
+
try {
|
|
159
|
+
const { stdout: cmdOutput } = await execAsync(`ps -p ${pid} -o command=`);
|
|
160
|
+
const cmdLine = cmdOutput.trim();
|
|
161
|
+
// 检查命令行是否匹配
|
|
162
|
+
const isMatch = cmdLine.includes(commandBaseName) ||
|
|
163
|
+
cmdLine.includes('npm exec') || // npx 会转换成 npm exec
|
|
164
|
+
cmdLine.includes('npx') ||
|
|
165
|
+
(this.serverArgs.length > 0 && this.serverArgs.some(arg => {
|
|
166
|
+
return cmdLine.includes(arg) && arg.length > 10;
|
|
167
|
+
}));
|
|
168
|
+
if (isMatch) {
|
|
169
|
+
this.childPids.add(pid);
|
|
170
|
+
// 调试日志已移除
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// 进程可能已经不存在了
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
// 记录失败,不影响主流程
|
|
180
|
+
console.error(`[Daemon] Failed to record child PIDs: ${e}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
77
183
|
async listTools() {
|
|
78
184
|
if (!this.client)
|
|
79
185
|
throw new Error('Client not connected');
|
|
@@ -87,9 +193,24 @@ export class McpClientService {
|
|
|
87
193
|
arguments: args,
|
|
88
194
|
});
|
|
89
195
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
196
|
+
close() {
|
|
197
|
+
// 对于 stdio 类型的服务器,先杀掉子进程(在关闭 transport 之前)
|
|
198
|
+
if (this.serverType === 'stdio' && this.childPids.size > 0) {
|
|
199
|
+
log(`[Daemon] Closing ${this.serverName}, killing ${this.childPids.size} child process(es)...`);
|
|
200
|
+
// 直接使用 SIGKILL,确保进程被终止
|
|
201
|
+
for (const pid of this.childPids) {
|
|
202
|
+
try {
|
|
203
|
+
process.kill(pid, 'SIGKILL');
|
|
204
|
+
log(`[Daemon] SIGKILLED child process ${pid} (${this.serverName})`);
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
log(`[Daemon] Failed to kill ${pid}: ${e}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
this.childPids.clear();
|
|
211
|
+
log(`[Daemon] All child processes cleared for ${this.serverName}`);
|
|
93
212
|
}
|
|
213
|
+
// 暂时不关闭 transport,避免卡住
|
|
214
|
+
log(`[Daemon] ${this.serverName} close() completed`);
|
|
94
215
|
}
|
|
95
216
|
}
|
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;
|
package/dist/core/pool.js
CHANGED
|
@@ -55,7 +55,7 @@ export class ConnectionPool {
|
|
|
55
55
|
for (const [name, client] of this.clients) {
|
|
56
56
|
console.log(`[Daemon] Closing connection to ${name}...`);
|
|
57
57
|
try {
|
|
58
|
-
|
|
58
|
+
client.close(); // 同步调用,不使用 await
|
|
59
59
|
}
|
|
60
60
|
catch (e) {
|
|
61
61
|
console.error(`[Daemon] Error closing ${name}:`, e);
|
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",
|
|
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
|
-
};
|