@yeaft/webchat-agent 1.0.80 → 1.0.82
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/package.json +1 -1
- package/yeaft/mcp.js +98 -11
package/package.json
CHANGED
package/yeaft/mcp.js
CHANGED
|
@@ -34,6 +34,71 @@ const DEFAULT_TIMEOUT_MS = 30000;
|
|
|
34
34
|
/** Server startup timeout (10 seconds). */
|
|
35
35
|
const STARTUP_TIMEOUT_MS = 10000;
|
|
36
36
|
|
|
37
|
+
// ─── Process-level connection pool ────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** @type {Map<string, { connection: MCPServerConnection, refs: number, startPromise: Promise<void> }>} */
|
|
40
|
+
const mcpConnectionPool = new Map();
|
|
41
|
+
|
|
42
|
+
function stableEnv(env = {}) {
|
|
43
|
+
return Object.fromEntries(
|
|
44
|
+
Object.entries(env || {})
|
|
45
|
+
.filter(([, value]) => value !== undefined)
|
|
46
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mcpConnectionKey(config) {
|
|
51
|
+
return JSON.stringify({
|
|
52
|
+
name: config.name,
|
|
53
|
+
command: config.command,
|
|
54
|
+
args: Array.isArray(config.args) ? config.args : [],
|
|
55
|
+
env: stableEnv(config.env),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function acquireMcpConnection(serverConfig) {
|
|
60
|
+
const key = mcpConnectionKey(serverConfig);
|
|
61
|
+
let entry = mcpConnectionPool.get(key);
|
|
62
|
+
if (!entry) {
|
|
63
|
+
const connection = new MCPServerConnection(serverConfig.name, serverConfig);
|
|
64
|
+
entry = {
|
|
65
|
+
connection,
|
|
66
|
+
refs: 0,
|
|
67
|
+
startPromise: connection.start(),
|
|
68
|
+
};
|
|
69
|
+
connection.once('close', () => {
|
|
70
|
+
if (mcpConnectionPool.get(key) === entry) {
|
|
71
|
+
mcpConnectionPool.delete(key);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
mcpConnectionPool.set(key, entry);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
entry.refs += 1;
|
|
78
|
+
try {
|
|
79
|
+
await entry.startPromise;
|
|
80
|
+
return { key, connection: entry.connection };
|
|
81
|
+
} catch (err) {
|
|
82
|
+
entry.refs -= 1;
|
|
83
|
+
if (entry.refs <= 0 && mcpConnectionPool.get(key) === entry) {
|
|
84
|
+
mcpConnectionPool.delete(key);
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function releaseMcpConnection(key, connection) {
|
|
91
|
+
const entry = mcpConnectionPool.get(key);
|
|
92
|
+
if (!entry || entry.connection !== connection) {
|
|
93
|
+
await connection.stop();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
entry.refs -= 1;
|
|
97
|
+
if (entry.refs > 0) return;
|
|
98
|
+
mcpConnectionPool.delete(key);
|
|
99
|
+
await connection.stop();
|
|
100
|
+
}
|
|
101
|
+
|
|
37
102
|
// ─── MCP Server Connection ─────────────────────────────────
|
|
38
103
|
|
|
39
104
|
/**
|
|
@@ -46,6 +111,7 @@ class MCPServerConnection extends EventEmitter {
|
|
|
46
111
|
#pendingRequests;
|
|
47
112
|
#tools;
|
|
48
113
|
#ready;
|
|
114
|
+
#closed;
|
|
49
115
|
#buffer;
|
|
50
116
|
|
|
51
117
|
/**
|
|
@@ -59,6 +125,7 @@ class MCPServerConnection extends EventEmitter {
|
|
|
59
125
|
this.#pendingRequests = new Map();
|
|
60
126
|
this.#tools = [];
|
|
61
127
|
this.#ready = false;
|
|
128
|
+
this.#closed = false;
|
|
62
129
|
this.#buffer = '';
|
|
63
130
|
this.config = config;
|
|
64
131
|
}
|
|
@@ -102,6 +169,7 @@ class MCPServerConnection extends EventEmitter {
|
|
|
102
169
|
|
|
103
170
|
this.#process.on('close', (code) => {
|
|
104
171
|
this.#ready = false;
|
|
172
|
+
this.#closed = true;
|
|
105
173
|
this.emit('close', code);
|
|
106
174
|
// Reject all pending requests
|
|
107
175
|
for (const [, { reject: rej }] of this.#pendingRequests) {
|
|
@@ -252,9 +320,16 @@ class MCPServerConnection extends EventEmitter {
|
|
|
252
320
|
if (this.#process) {
|
|
253
321
|
this.#ready = false;
|
|
254
322
|
this.#process.kill('SIGTERM');
|
|
255
|
-
// Wait a bit then force kill
|
|
256
|
-
|
|
257
|
-
|
|
323
|
+
// Wait a bit then force kill. Test fakes do not emit close, so cap the
|
|
324
|
+
// wait at 2s and do not require a close event for cleanup to finish.
|
|
325
|
+
await Promise.race([
|
|
326
|
+
new Promise(resolve => {
|
|
327
|
+
if (this.#closed) return resolve();
|
|
328
|
+
this.#process.once('close', resolve);
|
|
329
|
+
}),
|
|
330
|
+
new Promise(resolve => setTimeout(resolve, 2000)),
|
|
331
|
+
]);
|
|
332
|
+
if (this.#process && !this.#process.killed && !this.#closed) {
|
|
258
333
|
this.#process.kill('SIGKILL');
|
|
259
334
|
}
|
|
260
335
|
this.#process = null;
|
|
@@ -268,7 +343,7 @@ class MCPServerConnection extends EventEmitter {
|
|
|
268
343
|
* MCPManager — manages multiple MCP server connections.
|
|
269
344
|
*/
|
|
270
345
|
export class MCPManager {
|
|
271
|
-
/** @type {Map<string, MCPServerConnection>} */
|
|
346
|
+
/** @type {Map<string, { key: string, connection: MCPServerConnection }>} */
|
|
272
347
|
#servers = new Map();
|
|
273
348
|
|
|
274
349
|
/** @type {Map<string, object>} */
|
|
@@ -288,10 +363,10 @@ export class MCPManager {
|
|
|
288
363
|
await this.disconnect(name);
|
|
289
364
|
}
|
|
290
365
|
|
|
291
|
-
const
|
|
292
|
-
|
|
366
|
+
const pooled = await acquireMcpConnection(serverConfig);
|
|
367
|
+
const connection = pooled.connection;
|
|
293
368
|
|
|
294
|
-
this.#servers.set(name,
|
|
369
|
+
this.#servers.set(name, pooled);
|
|
295
370
|
|
|
296
371
|
// Index tools
|
|
297
372
|
for (const tool of connection.tools) {
|
|
@@ -329,14 +404,15 @@ export class MCPManager {
|
|
|
329
404
|
* @param {string} name — server name
|
|
330
405
|
*/
|
|
331
406
|
async disconnect(name) {
|
|
332
|
-
const
|
|
333
|
-
if (
|
|
407
|
+
const entry = this.#servers.get(name);
|
|
408
|
+
if (entry) {
|
|
409
|
+
const connection = entry.connection;
|
|
334
410
|
// Remove tools from index
|
|
335
411
|
for (const tool of connection.tools) {
|
|
336
412
|
this.#toolIndex.delete(tool.name);
|
|
337
413
|
}
|
|
338
|
-
await connection.stop();
|
|
339
414
|
this.#servers.delete(name);
|
|
415
|
+
await releaseMcpConnection(entry.key, connection);
|
|
340
416
|
}
|
|
341
417
|
}
|
|
342
418
|
|
|
@@ -400,7 +476,7 @@ export class MCPManager {
|
|
|
400
476
|
* @returns {{ name: string, ready: boolean, toolCount: number }[]}
|
|
401
477
|
*/
|
|
402
478
|
status() {
|
|
403
|
-
return [...this.#servers.entries()].map(([name, conn]) => ({
|
|
479
|
+
return [...this.#servers.entries()].map(([name, { connection: conn }]) => ({
|
|
404
480
|
name,
|
|
405
481
|
ready: conn.ready,
|
|
406
482
|
toolCount: conn.tools.length,
|
|
@@ -439,3 +515,14 @@ export async function createMCPManager(config) {
|
|
|
439
515
|
|
|
440
516
|
return manager;
|
|
441
517
|
}
|
|
518
|
+
|
|
519
|
+
export async function __resetMcpConnectionPoolForTests({ stop = true } = {}) {
|
|
520
|
+
const entries = [...mcpConnectionPool.values()];
|
|
521
|
+
mcpConnectionPool.clear();
|
|
522
|
+
if (!stop) return;
|
|
523
|
+
await Promise.all(entries.map(entry => entry.connection.stop().catch(() => {})));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function __mcpConnectionPoolSizeForTests() {
|
|
527
|
+
return mcpConnectionPool.size;
|
|
528
|
+
}
|