@learnrudi/cli 1.10.5 → 1.10.7
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/packages-manifest.json +1 -1
- package/dist/router-mcp.js +152 -5
- package/package.json +14 -13
package/dist/router-mcp.js
CHANGED
|
@@ -32,6 +32,16 @@ const TOOL_INDEX_PATH = path.join(RUDI_HOME, 'cache', 'tool-index.json');
|
|
|
32
32
|
|
|
33
33
|
const REQUEST_TIMEOUT_MS = 30000;
|
|
34
34
|
const PROTOCOL_VERSION = '2024-11-05';
|
|
35
|
+
const DEFAULT_IDLE_TTL_MS = 10 * 60 * 1000;
|
|
36
|
+
const DEFAULT_MAX_SERVERS = 8;
|
|
37
|
+
const DEFAULT_CLEANUP_INTERVAL_MS = 30000;
|
|
38
|
+
const DEFAULT_FORCE_KILL_MS = 2000;
|
|
39
|
+
|
|
40
|
+
const IDLE_TTL_MS = readIntEnv('RUDI_ROUTER_IDLE_TTL_MS', DEFAULT_IDLE_TTL_MS);
|
|
41
|
+
const MAX_SERVERS = readIntEnv('RUDI_ROUTER_MAX_SERVERS', DEFAULT_MAX_SERVERS);
|
|
42
|
+
const CLEANUP_INTERVAL_MS = readIntEnv('RUDI_ROUTER_CLEANUP_INTERVAL_MS', DEFAULT_CLEANUP_INTERVAL_MS);
|
|
43
|
+
const FORCE_KILL_MS = readIntEnv('RUDI_ROUTER_FORCE_KILL_MS', DEFAULT_FORCE_KILL_MS);
|
|
44
|
+
const LIVE_TOOL_LIST = readBoolEnv('RUDI_ROUTER_LIVE_TOOL_LIST', false);
|
|
35
45
|
|
|
36
46
|
// =============================================================================
|
|
37
47
|
// STATE
|
|
@@ -45,6 +55,7 @@ let rudiConfig = null;
|
|
|
45
55
|
|
|
46
56
|
/** @type {Object | null} */
|
|
47
57
|
let toolIndex = null;
|
|
58
|
+
let cleanupTimer = null;
|
|
48
59
|
|
|
49
60
|
// =============================================================================
|
|
50
61
|
// TYPES (JSDoc)
|
|
@@ -57,6 +68,10 @@ let toolIndex = null;
|
|
|
57
68
|
* @property {Map<string|number, PendingRequest>} pending
|
|
58
69
|
* @property {string} buffer
|
|
59
70
|
* @property {boolean} initialized
|
|
71
|
+
* @property {string} stackId
|
|
72
|
+
* @property {number} spawnedAt
|
|
73
|
+
* @property {number} lastUsedAt
|
|
74
|
+
* @property {boolean} terminating
|
|
60
75
|
*/
|
|
61
76
|
|
|
62
77
|
/**
|
|
@@ -96,6 +111,102 @@ function debug(msg) {
|
|
|
96
111
|
}
|
|
97
112
|
}
|
|
98
113
|
|
|
114
|
+
// =============================================================================
|
|
115
|
+
// ENV & PROCESS HELPERS
|
|
116
|
+
// =============================================================================
|
|
117
|
+
|
|
118
|
+
function readIntEnv(name, fallback) {
|
|
119
|
+
const raw = process.env[name];
|
|
120
|
+
if (raw === undefined) return fallback;
|
|
121
|
+
const value = Number(raw);
|
|
122
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readBoolEnv(name, fallback) {
|
|
126
|
+
const raw = process.env[name];
|
|
127
|
+
if (raw === undefined) return fallback;
|
|
128
|
+
return ['1', 'true', 'yes', 'on'].includes(String(raw).toLowerCase());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function hasProcessExited(proc) {
|
|
132
|
+
return proc.exitCode !== null || proc.signalCode !== null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isProcessUsable(proc) {
|
|
136
|
+
return proc && !hasProcessExited(proc) && !proc.killed;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function markServerUsed(server) {
|
|
140
|
+
server.lastUsedAt = Date.now();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function rejectPending(server, reason) {
|
|
144
|
+
for (const [, pending] of server.pending) {
|
|
145
|
+
clearTimeout(pending.timeout);
|
|
146
|
+
pending.reject(new Error(reason));
|
|
147
|
+
}
|
|
148
|
+
server.pending.clear();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function terminateServer(stackId, server, reason) {
|
|
152
|
+
if (!server || server.terminating) return;
|
|
153
|
+
|
|
154
|
+
server.terminating = true;
|
|
155
|
+
serverPool.delete(stackId);
|
|
156
|
+
|
|
157
|
+
if (hasProcessExited(server.process)) {
|
|
158
|
+
rejectPending(server, `Stack ${stackId} exited`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
log(`Stopping stack ${stackId}: ${reason}`);
|
|
163
|
+
try {
|
|
164
|
+
server.process.kill('SIGTERM');
|
|
165
|
+
} catch {
|
|
166
|
+
// Ignore kill errors; process may already be gone.
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const killTimer = setTimeout(() => {
|
|
170
|
+
if (!hasProcessExited(server.process)) {
|
|
171
|
+
log(`Force killing stack ${stackId}`);
|
|
172
|
+
try {
|
|
173
|
+
server.process.kill('SIGKILL');
|
|
174
|
+
} catch {
|
|
175
|
+
// Ignore kill errors; process may already be gone.
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}, FORCE_KILL_MS);
|
|
179
|
+
if (killTimer.unref) killTimer.unref();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function cleanupServerPool() {
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
|
|
185
|
+
for (const [stackId, server] of serverPool) {
|
|
186
|
+
if (server.terminating) continue;
|
|
187
|
+
if (!isProcessUsable(server.process)) {
|
|
188
|
+
terminateServer(stackId, server, 'process-not-usable');
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (server.pending.size > 0) continue;
|
|
192
|
+
if (IDLE_TTL_MS > 0 && now - server.lastUsedAt > IDLE_TTL_MS) {
|
|
193
|
+
terminateServer(stackId, server, `idle ${Math.round((now - server.lastUsedAt) / 1000)}s`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (MAX_SERVERS > 0 && serverPool.size > MAX_SERVERS) {
|
|
198
|
+
const evictable = Array.from(serverPool.entries())
|
|
199
|
+
.filter(([, server]) => !server.terminating && server.pending.size === 0)
|
|
200
|
+
.sort((a, b) => a[1].lastUsedAt - b[1].lastUsedAt);
|
|
201
|
+
|
|
202
|
+
let index = 0;
|
|
203
|
+
while (serverPool.size > MAX_SERVERS && index < evictable.length) {
|
|
204
|
+
const [stackId, server] = evictable[index++];
|
|
205
|
+
terminateServer(stackId, server, 'pool-limit');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
99
210
|
// =============================================================================
|
|
100
211
|
// CONFIG & SECRETS
|
|
101
212
|
// =============================================================================
|
|
@@ -208,7 +319,11 @@ function spawnStackServer(stackId, stackConfig) {
|
|
|
208
319
|
rl,
|
|
209
320
|
pending: new Map(),
|
|
210
321
|
buffer: '',
|
|
211
|
-
initialized: false
|
|
322
|
+
initialized: false,
|
|
323
|
+
stackId,
|
|
324
|
+
spawnedAt: Date.now(),
|
|
325
|
+
lastUsedAt: Date.now(),
|
|
326
|
+
terminating: false
|
|
212
327
|
};
|
|
213
328
|
|
|
214
329
|
// Handle responses from stack
|
|
@@ -227,6 +342,7 @@ function spawnStackServer(stackId, stackConfig) {
|
|
|
227
342
|
if (pending) {
|
|
228
343
|
clearTimeout(pending.timeout);
|
|
229
344
|
server.pending.delete(response.id);
|
|
345
|
+
markServerUsed(server);
|
|
230
346
|
pending.resolve(response);
|
|
231
347
|
}
|
|
232
348
|
} catch (err) {
|
|
@@ -241,11 +357,13 @@ function spawnStackServer(stackId, stackConfig) {
|
|
|
241
357
|
|
|
242
358
|
childProcess.on('error', (err) => {
|
|
243
359
|
log(`Stack process error (${stackId}): ${err.message}`);
|
|
360
|
+
rejectPending(server, `Stack ${stackId} error: ${err.message}`);
|
|
244
361
|
serverPool.delete(stackId);
|
|
245
362
|
});
|
|
246
363
|
|
|
247
364
|
childProcess.on('exit', (code, signal) => {
|
|
248
365
|
debug(`Stack ${stackId} exited: code=${code}, signal=${signal}`);
|
|
366
|
+
rejectPending(server, `Stack ${stackId} exited (code=${code}, signal=${signal || 'none'})`);
|
|
249
367
|
rl.close();
|
|
250
368
|
serverPool.delete(stackId);
|
|
251
369
|
});
|
|
@@ -260,9 +378,13 @@ function spawnStackServer(stackId, stackConfig) {
|
|
|
260
378
|
*/
|
|
261
379
|
function getOrSpawnServer(stackId) {
|
|
262
380
|
const existing = serverPool.get(stackId);
|
|
263
|
-
if (existing &&
|
|
381
|
+
if (existing && isProcessUsable(existing.process) && !existing.terminating) {
|
|
382
|
+
markServerUsed(existing);
|
|
264
383
|
return existing;
|
|
265
384
|
}
|
|
385
|
+
if (existing) {
|
|
386
|
+
terminateServer(stackId, existing, 'stale');
|
|
387
|
+
}
|
|
266
388
|
|
|
267
389
|
const stackConfig = rudiConfig?.stacks?.[stackId];
|
|
268
390
|
if (!stackConfig) {
|
|
@@ -287,6 +409,11 @@ function getOrSpawnServer(stackId) {
|
|
|
287
409
|
*/
|
|
288
410
|
async function sendToStack(server, request, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
289
411
|
return new Promise((resolve, reject) => {
|
|
412
|
+
if (!isProcessUsable(server.process) || server.terminating) {
|
|
413
|
+
reject(new Error(`Stack ${server.stackId} is not available`));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
290
417
|
const timeout = setTimeout(() => {
|
|
291
418
|
server.pending.delete(request.id);
|
|
292
419
|
reject(new Error(`Request timeout: ${request.method}`));
|
|
@@ -296,6 +423,7 @@ async function sendToStack(server, request, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
296
423
|
|
|
297
424
|
const line = JSON.stringify(request) + '\n';
|
|
298
425
|
debug(`>> ${line.slice(0, 200)}`);
|
|
426
|
+
markServerUsed(server);
|
|
299
427
|
server.process.stdin?.write(line);
|
|
300
428
|
});
|
|
301
429
|
}
|
|
@@ -307,6 +435,7 @@ async function sendToStack(server, request, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
307
435
|
*/
|
|
308
436
|
async function initializeStack(server, stackId) {
|
|
309
437
|
if (server.initialized) return;
|
|
438
|
+
markServerUsed(server);
|
|
310
439
|
|
|
311
440
|
const initRequest = {
|
|
312
441
|
jsonrpc: '2.0',
|
|
@@ -350,6 +479,7 @@ async function initializeStack(server, stackId) {
|
|
|
350
479
|
*/
|
|
351
480
|
async function listTools() {
|
|
352
481
|
const tools = [];
|
|
482
|
+
const skippedStacks = [];
|
|
353
483
|
|
|
354
484
|
for (const [stackId, stackConfig] of Object.entries(rudiConfig?.stacks || {})) {
|
|
355
485
|
if (!stackConfig.installed) continue;
|
|
@@ -376,6 +506,10 @@ async function listTools() {
|
|
|
376
506
|
}
|
|
377
507
|
|
|
378
508
|
// 3. Fall back to querying the stack (slow, spawns server)
|
|
509
|
+
if (!LIVE_TOOL_LIST) {
|
|
510
|
+
skippedStacks.push(stackId);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
379
513
|
try {
|
|
380
514
|
const server = getOrSpawnServer(stackId);
|
|
381
515
|
await initializeStack(server, stackId);
|
|
@@ -399,6 +533,10 @@ async function listTools() {
|
|
|
399
533
|
}
|
|
400
534
|
}
|
|
401
535
|
|
|
536
|
+
if (skippedStacks.length > 0) {
|
|
537
|
+
log(`Skipped live tools/list for ${skippedStacks.length} stacks (enable RUDI_ROUTER_LIVE_TOOL_LIST=1 or run "rudi index")`);
|
|
538
|
+
}
|
|
539
|
+
|
|
402
540
|
return tools;
|
|
403
541
|
}
|
|
404
542
|
|
|
@@ -521,6 +659,8 @@ async function handleRequest(request) {
|
|
|
521
659
|
*/
|
|
522
660
|
async function main() {
|
|
523
661
|
log('Starting RUDI Router MCP Server');
|
|
662
|
+
log(`Pool config: max=${MAX_SERVERS <= 0 ? 'unlimited' : MAX_SERVERS}, idleTTL=${IDLE_TTL_MS}ms, cleanup=${CLEANUP_INTERVAL_MS}ms`);
|
|
663
|
+
log(`Live tools/list: ${LIVE_TOOL_LIST ? 'enabled' : 'disabled'}`);
|
|
524
664
|
|
|
525
665
|
// Load config
|
|
526
666
|
rudiConfig = loadRudiConfig();
|
|
@@ -574,8 +714,9 @@ async function main() {
|
|
|
574
714
|
// Clean up all spawned servers
|
|
575
715
|
for (const [stackId, server] of serverPool) {
|
|
576
716
|
debug(`Killing stack ${stackId}`);
|
|
577
|
-
server
|
|
717
|
+
terminateServer(stackId, server, 'stdin-closed');
|
|
578
718
|
}
|
|
719
|
+
if (cleanupTimer) clearInterval(cleanupTimer);
|
|
579
720
|
process.exit(0);
|
|
580
721
|
});
|
|
581
722
|
|
|
@@ -583,18 +724,24 @@ async function main() {
|
|
|
583
724
|
process.on('SIGTERM', () => {
|
|
584
725
|
log('SIGTERM received, shutting down');
|
|
585
726
|
for (const [stackId, server] of serverPool) {
|
|
586
|
-
server
|
|
727
|
+
terminateServer(stackId, server, 'sigterm');
|
|
587
728
|
}
|
|
729
|
+
if (cleanupTimer) clearInterval(cleanupTimer);
|
|
588
730
|
process.exit(0);
|
|
589
731
|
});
|
|
590
732
|
|
|
591
733
|
process.on('SIGINT', () => {
|
|
592
734
|
log('SIGINT received, shutting down');
|
|
593
735
|
for (const [stackId, server] of serverPool) {
|
|
594
|
-
server
|
|
736
|
+
terminateServer(stackId, server, 'sigint');
|
|
595
737
|
}
|
|
738
|
+
if (cleanupTimer) clearInterval(cleanupTimer);
|
|
596
739
|
process.exit(0);
|
|
597
740
|
});
|
|
741
|
+
|
|
742
|
+
// Periodic pool cleanup (idle eviction, LRU capping)
|
|
743
|
+
cleanupTimer = setInterval(cleanupServerPool, CLEANUP_INTERVAL_MS);
|
|
744
|
+
if (cleanupTimer.unref) cleanupTimer.unref();
|
|
598
745
|
}
|
|
599
746
|
|
|
600
747
|
// Run
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@learnrudi/cli",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.7",
|
|
4
4
|
"description": "RUDI CLI - Install and manage MCP stacks, runtimes, and AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -12,18 +12,26 @@
|
|
|
12
12
|
"scripts",
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node src/index.js",
|
|
17
|
+
"prebuild": "node scripts/generate-manifest.js",
|
|
18
|
+
"build": "esbuild src/index.js --bundle --platform=node --format=cjs --outfile=dist/index.cjs --external:better-sqlite3 && cp src/router-mcp.js dist/router-mcp.js && cp src/packages-manifest.json dist/packages-manifest.json",
|
|
19
|
+
"postinstall": "node scripts/postinstall.js",
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
21
|
+
"test": "node --test src/__tests__/"
|
|
22
|
+
},
|
|
15
23
|
"dependencies": {
|
|
16
|
-
"better-sqlite3": "^12.5.0",
|
|
17
24
|
"@learnrudi/core": "1.0.5",
|
|
18
|
-
"@learnrudi/embeddings": "0.1.0",
|
|
19
25
|
"@learnrudi/db": "1.0.2",
|
|
26
|
+
"@learnrudi/embeddings": "0.1.0",
|
|
20
27
|
"@learnrudi/env": "1.0.1",
|
|
21
28
|
"@learnrudi/manifest": "1.0.0",
|
|
22
29
|
"@learnrudi/mcp": "1.0.0",
|
|
23
30
|
"@learnrudi/registry-client": "1.0.5",
|
|
31
|
+
"@learnrudi/runner": "1.0.1",
|
|
24
32
|
"@learnrudi/secrets": "1.0.1",
|
|
25
33
|
"@learnrudi/utils": "1.0.0",
|
|
26
|
-
"
|
|
34
|
+
"better-sqlite3": "^12.5.0"
|
|
27
35
|
},
|
|
28
36
|
"devDependencies": {
|
|
29
37
|
"esbuild": "^0.27.2"
|
|
@@ -49,12 +57,5 @@
|
|
|
49
57
|
"publishConfig": {
|
|
50
58
|
"access": "public"
|
|
51
59
|
},
|
|
52
|
-
"license": "MIT"
|
|
53
|
-
|
|
54
|
-
"start": "node src/index.js",
|
|
55
|
-
"prebuild": "node scripts/generate-manifest.js",
|
|
56
|
-
"build": "esbuild src/index.js --bundle --platform=node --format=cjs --outfile=dist/index.cjs --external:better-sqlite3 && cp src/router-mcp.js dist/router-mcp.js && cp src/packages-manifest.json dist/packages-manifest.json",
|
|
57
|
-
"postinstall": "node scripts/postinstall.js",
|
|
58
|
-
"test": "node --test src/__tests__/"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
60
|
+
"license": "MIT"
|
|
61
|
+
}
|