@maplezzk/mcps 1.0.30-beta.0 → 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/dist/core/client.js +125 -4
- package/dist/core/pool.js +1 -1
- package/package.json +1 -1
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/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);
|