genesis-ai-cli 14.9.0 → 14.11.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/dist/src/index.js +71 -0
- package/dist/src/lifecycle/shutdown.d.ts +31 -0
- package/dist/src/lifecycle/shutdown.js +129 -0
- package/dist/src/mcp/index.d.ts +8 -0
- package/dist/src/mcp/index.js +246 -18
- package/package.json +1 -1
package/dist/src/index.js
CHANGED
|
@@ -1438,6 +1438,65 @@ async function cmdAutonomous(subcommand, options) {
|
|
|
1438
1438
|
console.log(c(`Unknown autonomous subcommand: ${subcommand}`, 'red'));
|
|
1439
1439
|
console.log('Use: status, init, balance, approvals, approve, stop, agent, run');
|
|
1440
1440
|
}
|
|
1441
|
+
/**
|
|
1442
|
+
* v14.10: Observability stats command
|
|
1443
|
+
*/
|
|
1444
|
+
async function cmdStats(subcommand, options) {
|
|
1445
|
+
const { getMCPMetrics } = await import('./mcp/index.js');
|
|
1446
|
+
const { getMetricsRegistry } = await import('./observability/metrics.js');
|
|
1447
|
+
console.log(c('\n=== GENESIS OBSERVABILITY STATS (v14.11) ===\n', 'bold'));
|
|
1448
|
+
if (!subcommand || subcommand === 'mcp') {
|
|
1449
|
+
const { mcpCallsTotal, mcpLatency, mcpConnectionsActive, mcpErrorsTotal, registry } = getMCPMetrics();
|
|
1450
|
+
console.log(c('MCP Metrics:', 'cyan'));
|
|
1451
|
+
console.log(` Active connections: ${mcpConnectionsActive.get()}`);
|
|
1452
|
+
console.log();
|
|
1453
|
+
// Display Prometheus format
|
|
1454
|
+
console.log(c('Prometheus Export:', 'dim'));
|
|
1455
|
+
console.log(registry.toPrometheus());
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
if (subcommand === 'prometheus') {
|
|
1459
|
+
const registry = getMetricsRegistry('genesis');
|
|
1460
|
+
console.log(registry.toPrometheus());
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
if (subcommand === 'json') {
|
|
1464
|
+
const { mcpCallsTotal, mcpLatency, mcpConnectionsActive, mcpErrorsTotal } = getMCPMetrics();
|
|
1465
|
+
console.log(JSON.stringify({
|
|
1466
|
+
mcp: {
|
|
1467
|
+
activeConnections: mcpConnectionsActive.get(),
|
|
1468
|
+
},
|
|
1469
|
+
timestamp: new Date().toISOString(),
|
|
1470
|
+
}, null, 2));
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
if (subcommand === 'alert') {
|
|
1474
|
+
// Test Slack alerts
|
|
1475
|
+
const { getAlerter } = await import('./observability/alerting.js');
|
|
1476
|
+
const alerter = getAlerter();
|
|
1477
|
+
const slackUrl = process.env.SLACK_WEBHOOK_URL;
|
|
1478
|
+
if (!slackUrl) {
|
|
1479
|
+
console.log(c('Slack not configured.', 'yellow'));
|
|
1480
|
+
console.log('Set SLACK_WEBHOOK_URL in your .env file to enable alerts.');
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
console.log(c('Sending test alert to Slack...', 'cyan'));
|
|
1484
|
+
const sent = await alerter.info('Genesis Test Alert', 'This is a test alert from Genesis observability system.', { labels: { test: 'true', version: '14.10' } });
|
|
1485
|
+
if (sent) {
|
|
1486
|
+
console.log(c('Test alert sent successfully!', 'green'));
|
|
1487
|
+
}
|
|
1488
|
+
else {
|
|
1489
|
+
console.log(c('Failed to send test alert.', 'red'));
|
|
1490
|
+
}
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
console.log(c('Usage:', 'yellow'));
|
|
1494
|
+
console.log(' genesis stats - Show MCP stats');
|
|
1495
|
+
console.log(' genesis stats mcp - Show MCP stats');
|
|
1496
|
+
console.log(' genesis stats prometheus - Prometheus format');
|
|
1497
|
+
console.log(' genesis stats json - JSON format');
|
|
1498
|
+
console.log(' genesis stats alert - Test Slack alert');
|
|
1499
|
+
}
|
|
1441
1500
|
async function cmdHardware() {
|
|
1442
1501
|
console.log(c('\n=== HARDWARE PROFILE ===\n', 'bold'));
|
|
1443
1502
|
const hw = (0, index_js_1.detectHardware)();
|
|
@@ -1580,6 +1639,10 @@ ${c('Commands:', 'bold')}
|
|
|
1580
1639
|
${c('install', 'green')} Install/update Genesis globally (npm)
|
|
1581
1640
|
${c('status', 'green')} Show MCP servers status
|
|
1582
1641
|
${c('hardware', 'green')} Show hardware profile & router config
|
|
1642
|
+
${c('stats', 'green')} [subcommand] v14.10: Observability metrics
|
|
1643
|
+
mcp MCP call stats (default)
|
|
1644
|
+
prometheus Prometheus format
|
|
1645
|
+
json JSON format
|
|
1583
1646
|
${c('help', 'green')} Show this help
|
|
1584
1647
|
|
|
1585
1648
|
${c('MCP Servers (13):', 'bold')}
|
|
@@ -2046,6 +2109,10 @@ async function cmdInstall(options) {
|
|
|
2046
2109
|
// Main
|
|
2047
2110
|
// ============================================================================
|
|
2048
2111
|
async function main() {
|
|
2112
|
+
// v14.11: Install graceful shutdown handlers
|
|
2113
|
+
const { installShutdownHandlers, registerDefaultHandlers } = await import('./lifecycle/shutdown.js');
|
|
2114
|
+
installShutdownHandlers();
|
|
2115
|
+
registerDefaultHandlers();
|
|
2049
2116
|
const args = process.argv.slice(2);
|
|
2050
2117
|
const command = args[0];
|
|
2051
2118
|
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
@@ -2203,6 +2270,10 @@ async function main() {
|
|
|
2203
2270
|
// v14.7: Autonomous bounty hunting
|
|
2204
2271
|
await cmdBounty(positional, options);
|
|
2205
2272
|
break;
|
|
2273
|
+
case 'stats':
|
|
2274
|
+
// v14.10: Observability stats
|
|
2275
|
+
await cmdStats(positional, options);
|
|
2276
|
+
break;
|
|
2206
2277
|
default:
|
|
2207
2278
|
console.error(c(`Unknown command: ${command}`, 'red'));
|
|
2208
2279
|
console.log('Use "genesis help" for usage information');
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graceful Shutdown Handler (v14.11)
|
|
3
|
+
*
|
|
4
|
+
* Ensures clean shutdown on SIGTERM/SIGINT:
|
|
5
|
+
* - Completes in-flight requests
|
|
6
|
+
* - Closes MCP connections
|
|
7
|
+
* - Flushes logs and metrics
|
|
8
|
+
* - Stops rate limiters
|
|
9
|
+
*/
|
|
10
|
+
type ShutdownHandler = () => Promise<void> | void;
|
|
11
|
+
/**
|
|
12
|
+
* Register a shutdown handler
|
|
13
|
+
*/
|
|
14
|
+
export declare function onShutdown(handler: ShutdownHandler): void;
|
|
15
|
+
/**
|
|
16
|
+
* Set shutdown timeout
|
|
17
|
+
*/
|
|
18
|
+
export declare function setShutdownTimeout(ms: number): void;
|
|
19
|
+
/**
|
|
20
|
+
* Check if shutdown is in progress
|
|
21
|
+
*/
|
|
22
|
+
export declare function isShutdownInProgress(): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Install signal handlers (call once at startup)
|
|
25
|
+
*/
|
|
26
|
+
export declare function installShutdownHandlers(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Register default Genesis shutdown handlers
|
|
29
|
+
*/
|
|
30
|
+
export declare function registerDefaultHandlers(): void;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Graceful Shutdown Handler (v14.11)
|
|
4
|
+
*
|
|
5
|
+
* Ensures clean shutdown on SIGTERM/SIGINT:
|
|
6
|
+
* - Completes in-flight requests
|
|
7
|
+
* - Closes MCP connections
|
|
8
|
+
* - Flushes logs and metrics
|
|
9
|
+
* - Stops rate limiters
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.onShutdown = onShutdown;
|
|
13
|
+
exports.setShutdownTimeout = setShutdownTimeout;
|
|
14
|
+
exports.isShutdownInProgress = isShutdownInProgress;
|
|
15
|
+
exports.installShutdownHandlers = installShutdownHandlers;
|
|
16
|
+
exports.registerDefaultHandlers = registerDefaultHandlers;
|
|
17
|
+
const shutdownHandlers = [];
|
|
18
|
+
let isShuttingDown = false;
|
|
19
|
+
let shutdownTimeout = 30000; // 30s max shutdown time
|
|
20
|
+
/**
|
|
21
|
+
* Register a shutdown handler
|
|
22
|
+
*/
|
|
23
|
+
function onShutdown(handler) {
|
|
24
|
+
shutdownHandlers.push(handler);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Set shutdown timeout
|
|
28
|
+
*/
|
|
29
|
+
function setShutdownTimeout(ms) {
|
|
30
|
+
shutdownTimeout = ms;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Check if shutdown is in progress
|
|
34
|
+
*/
|
|
35
|
+
function isShutdownInProgress() {
|
|
36
|
+
return isShuttingDown;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Execute graceful shutdown
|
|
40
|
+
*/
|
|
41
|
+
async function executeShutdown(signal) {
|
|
42
|
+
if (isShuttingDown) {
|
|
43
|
+
console.log(`[Shutdown] Already shutting down, ignoring ${signal}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
isShuttingDown = true;
|
|
47
|
+
console.log(`\n[Shutdown] Received ${signal}, starting graceful shutdown...`);
|
|
48
|
+
// Set a hard timeout
|
|
49
|
+
const forceExitTimer = setTimeout(() => {
|
|
50
|
+
console.error('[Shutdown] Timeout exceeded, forcing exit');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}, shutdownTimeout);
|
|
53
|
+
try {
|
|
54
|
+
// Run all handlers in reverse order (LIFO)
|
|
55
|
+
for (let i = shutdownHandlers.length - 1; i >= 0; i--) {
|
|
56
|
+
try {
|
|
57
|
+
await Promise.resolve(shutdownHandlers[i]());
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
console.error(`[Shutdown] Handler ${i} failed:`, error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
console.log('[Shutdown] Graceful shutdown complete');
|
|
64
|
+
clearTimeout(forceExitTimer);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error('[Shutdown] Error during shutdown:', error);
|
|
69
|
+
clearTimeout(forceExitTimer);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Install signal handlers (call once at startup)
|
|
75
|
+
*/
|
|
76
|
+
function installShutdownHandlers() {
|
|
77
|
+
process.on('SIGTERM', () => executeShutdown('SIGTERM'));
|
|
78
|
+
process.on('SIGINT', () => executeShutdown('SIGINT'));
|
|
79
|
+
// Handle uncaught errors gracefully
|
|
80
|
+
process.on('uncaughtException', (error) => {
|
|
81
|
+
console.error('[Fatal] Uncaught exception:', error);
|
|
82
|
+
executeShutdown('uncaughtException').catch(() => process.exit(1));
|
|
83
|
+
});
|
|
84
|
+
process.on('unhandledRejection', (reason) => {
|
|
85
|
+
console.error('[Fatal] Unhandled rejection:', reason);
|
|
86
|
+
// Don't exit on unhandled rejection, just log it
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Register default Genesis shutdown handlers
|
|
91
|
+
*/
|
|
92
|
+
function registerDefaultHandlers() {
|
|
93
|
+
// Close MCP connections
|
|
94
|
+
onShutdown(async () => {
|
|
95
|
+
try {
|
|
96
|
+
const { getMCPClient } = await import('../mcp/index.js');
|
|
97
|
+
const client = getMCPClient();
|
|
98
|
+
await client.close();
|
|
99
|
+
console.log('[Shutdown] MCP connections closed');
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// MCP module may not be loaded
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// Stop rate limiters
|
|
106
|
+
onShutdown(async () => {
|
|
107
|
+
try {
|
|
108
|
+
const { resetRateLimiter } = await import('./rate-limiter.js');
|
|
109
|
+
resetRateLimiter();
|
|
110
|
+
console.log('[Shutdown] Rate limiters stopped');
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Rate limiter may not be loaded
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
// Flush alerter
|
|
117
|
+
onShutdown(async () => {
|
|
118
|
+
try {
|
|
119
|
+
const { getAlerter } = await import('../observability/alerting.js');
|
|
120
|
+
const alerter = getAlerter();
|
|
121
|
+
await alerter.flush();
|
|
122
|
+
alerter.stop();
|
|
123
|
+
console.log('[Shutdown] Alerts flushed');
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Alerter may not be loaded
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
package/dist/src/mcp/index.d.ts
CHANGED
|
@@ -30,6 +30,14 @@ export * from './parallel-executor.js';
|
|
|
30
30
|
export * from './transformers.js';
|
|
31
31
|
export * from './client-manager.js';
|
|
32
32
|
import { MCPServerName } from '../types.js';
|
|
33
|
+
import { Counter, Histogram } from '../observability/metrics.js';
|
|
34
|
+
export declare function getMCPMetrics(): {
|
|
35
|
+
mcpCallsTotal: Counter;
|
|
36
|
+
mcpLatency: Histogram;
|
|
37
|
+
mcpConnectionsActive: import("../observability/metrics.js").Gauge;
|
|
38
|
+
mcpErrorsTotal: Counter;
|
|
39
|
+
registry: import("../observability/metrics.js").MetricsRegistry;
|
|
40
|
+
};
|
|
33
41
|
export type MCPMode = 'real' | 'simulated' | 'hybrid';
|
|
34
42
|
export interface MCPCallOptions {
|
|
35
43
|
timeout?: number;
|
package/dist/src/mcp/index.js
CHANGED
|
@@ -38,6 +38,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
38
38
|
};
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
40
|
exports.MCP_SERVER_REGISTRY = exports.mcpClient = void 0;
|
|
41
|
+
exports.getMCPMetrics = getMCPMetrics;
|
|
41
42
|
exports.getMCPClient = getMCPClient;
|
|
42
43
|
exports.resetMCPClient = resetMCPClient;
|
|
43
44
|
exports.isSimulatedMode = isSimulatedMode;
|
|
@@ -57,6 +58,113 @@ __exportStar(require("./client-manager.js"), exports);
|
|
|
57
58
|
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
58
59
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
59
60
|
const crypto_1 = require("crypto");
|
|
61
|
+
const metrics_js_1 = require("../observability/metrics.js");
|
|
62
|
+
const alerting_js_1 = require("../observability/alerting.js");
|
|
63
|
+
const rate_limiter_js_1 = require("../lifecycle/rate-limiter.js");
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// MCP Rate Limiters (v14.11 - Hardening)
|
|
66
|
+
// Per-provider rate limits to respect API quotas
|
|
67
|
+
// ============================================================================
|
|
68
|
+
const PROVIDER_RATE_LIMITS = {
|
|
69
|
+
// OpenAI: 10,000 RPM (tier 2) = 166.67/sec
|
|
70
|
+
'openai': { maxTokens: 500, refillRate: 166 },
|
|
71
|
+
// Anthropic: 4,000 RPM = 66.67/sec
|
|
72
|
+
'anthropic': { maxTokens: 200, refillRate: 66 },
|
|
73
|
+
// GitHub: 5,000/hour = 1.39/sec
|
|
74
|
+
'github': { maxTokens: 50, refillRate: 1.4 },
|
|
75
|
+
// arXiv: 3/second
|
|
76
|
+
'arxiv': { maxTokens: 10, refillRate: 3 },
|
|
77
|
+
// Semantic Scholar: 100/second (generous)
|
|
78
|
+
'semantic-scholar': { maxTokens: 100, refillRate: 100 },
|
|
79
|
+
// Brave Search: 15/second
|
|
80
|
+
'brave-search': { maxTokens: 30, refillRate: 15 },
|
|
81
|
+
// Default for others: 10/second
|
|
82
|
+
'default': { maxTokens: 50, refillRate: 10 },
|
|
83
|
+
};
|
|
84
|
+
const providerRateLimiters = new Map();
|
|
85
|
+
function getProviderRateLimiter(server) {
|
|
86
|
+
if (!providerRateLimiters.has(server)) {
|
|
87
|
+
const config = PROVIDER_RATE_LIMITS[server] || PROVIDER_RATE_LIMITS['default'];
|
|
88
|
+
providerRateLimiters.set(server, new rate_limiter_js_1.RateLimiter(config));
|
|
89
|
+
}
|
|
90
|
+
return providerRateLimiters.get(server);
|
|
91
|
+
}
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// MCP Metrics (v14.10 - Observability)
|
|
94
|
+
// ============================================================================
|
|
95
|
+
const metricsRegistry = (0, metrics_js_1.getMetricsRegistry)('genesis');
|
|
96
|
+
const mcpCallsTotal = metricsRegistry.counter({
|
|
97
|
+
name: 'mcp_calls_total',
|
|
98
|
+
help: 'Total MCP tool calls',
|
|
99
|
+
labels: ['server', 'tool', 'status'],
|
|
100
|
+
});
|
|
101
|
+
const mcpLatency = metricsRegistry.histogram({
|
|
102
|
+
name: 'mcp_latency_seconds',
|
|
103
|
+
help: 'MCP call latency in seconds',
|
|
104
|
+
labels: ['server', 'tool'],
|
|
105
|
+
buckets: [0.1, 0.5, 1, 2, 5, 10, 30, 60],
|
|
106
|
+
});
|
|
107
|
+
const mcpConnectionsActive = metricsRegistry.gauge({
|
|
108
|
+
name: 'mcp_connections_active',
|
|
109
|
+
help: 'Active MCP server connections',
|
|
110
|
+
});
|
|
111
|
+
const mcpErrorsTotal = metricsRegistry.counter({
|
|
112
|
+
name: 'mcp_errors_total',
|
|
113
|
+
help: 'Total MCP errors by type',
|
|
114
|
+
labels: ['server', 'error_type'],
|
|
115
|
+
});
|
|
116
|
+
// Export for CLI stats
|
|
117
|
+
function getMCPMetrics() {
|
|
118
|
+
return { mcpCallsTotal, mcpLatency, mcpConnectionsActive, mcpErrorsTotal, registry: metricsRegistry };
|
|
119
|
+
}
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// Security: Secret Sanitization (v14.11)
|
|
122
|
+
// ============================================================================
|
|
123
|
+
const SECRET_PATTERNS = [
|
|
124
|
+
/sk-[a-zA-Z0-9-_]{20,}/g, // OpenAI keys
|
|
125
|
+
/sk-ant-[a-zA-Z0-9-_]{20,}/g, // Anthropic keys
|
|
126
|
+
/ghp_[a-zA-Z0-9]{36}/g, // GitHub PAT
|
|
127
|
+
/gho_[a-zA-Z0-9]{36}/g, // GitHub OAuth
|
|
128
|
+
/[a-zA-Z0-9]{32,}/g, // Generic API keys (if looks like key)
|
|
129
|
+
/Bearer\s+[a-zA-Z0-9-_.]+/gi, // Bearer tokens
|
|
130
|
+
/password[=:]\s*["']?[^"'\s]+/gi, // Passwords in strings
|
|
131
|
+
/api[_-]?key[=:]\s*["']?[^"'\s]+/gi, // API keys in strings
|
|
132
|
+
];
|
|
133
|
+
function sanitizeSecrets(input) {
|
|
134
|
+
let sanitized = input;
|
|
135
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
136
|
+
sanitized = sanitized.replace(pattern, (match) => {
|
|
137
|
+
// Keep first 4 chars for identification, redact rest
|
|
138
|
+
if (match.length > 8) {
|
|
139
|
+
return match.slice(0, 4) + '***REDACTED***';
|
|
140
|
+
}
|
|
141
|
+
return '***REDACTED***';
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return sanitized;
|
|
145
|
+
}
|
|
146
|
+
function sanitizeObject(obj) {
|
|
147
|
+
const sanitized = {};
|
|
148
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
149
|
+
const lowerKey = key.toLowerCase();
|
|
150
|
+
// Redact known sensitive keys
|
|
151
|
+
if (lowerKey.includes('password') || lowerKey.includes('secret') ||
|
|
152
|
+
lowerKey.includes('token') || lowerKey.includes('key') ||
|
|
153
|
+
lowerKey.includes('auth') || lowerKey.includes('credential')) {
|
|
154
|
+
sanitized[key] = '***REDACTED***';
|
|
155
|
+
}
|
|
156
|
+
else if (typeof value === 'string') {
|
|
157
|
+
sanitized[key] = sanitizeSecrets(value);
|
|
158
|
+
}
|
|
159
|
+
else if (typeof value === 'object' && value !== null) {
|
|
160
|
+
sanitized[key] = sanitizeObject(value);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
sanitized[key] = value;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return sanitized;
|
|
167
|
+
}
|
|
60
168
|
/**
|
|
61
169
|
* Registry of MCP servers and how to spawn them.
|
|
62
170
|
* These are the 18 MCP servers Genesis uses.
|
|
@@ -470,6 +578,8 @@ class MCPConnectionManager {
|
|
|
470
578
|
if (this.logCalls) {
|
|
471
579
|
console.log(`[MCP] Connected to ${server}`);
|
|
472
580
|
}
|
|
581
|
+
// v14.10: Track active connections
|
|
582
|
+
mcpConnectionsActive.inc();
|
|
473
583
|
return {
|
|
474
584
|
client,
|
|
475
585
|
transport,
|
|
@@ -480,37 +590,153 @@ class MCPConnectionManager {
|
|
|
480
590
|
/**
|
|
481
591
|
* Call a tool on an MCP server
|
|
482
592
|
* v7.18: Added timeout wrapper for faster failure
|
|
593
|
+
* v14.10: Added structured logging with latency tracking
|
|
483
594
|
*/
|
|
484
595
|
async callTool(server, tool, args) {
|
|
596
|
+
const startTime = performance.now();
|
|
597
|
+
const callId = `${server}.${tool}.${Date.now().toString(36)}`;
|
|
598
|
+
// v14.11: Rate limiting per provider
|
|
599
|
+
const rateLimiter = getProviderRateLimiter(server);
|
|
600
|
+
const rateCheck = rateLimiter.check(server);
|
|
601
|
+
if (!rateCheck.allowed) {
|
|
602
|
+
const waitMs = rateCheck.retryAfterMs || 1000;
|
|
603
|
+
if (this.logCalls) {
|
|
604
|
+
console.log(JSON.stringify({
|
|
605
|
+
level: 'warn',
|
|
606
|
+
time: Date.now(),
|
|
607
|
+
msg: 'Rate limited, waiting',
|
|
608
|
+
server,
|
|
609
|
+
tool,
|
|
610
|
+
waitMs,
|
|
611
|
+
}));
|
|
612
|
+
}
|
|
613
|
+
// Wait and retry
|
|
614
|
+
await new Promise(resolve => setTimeout(resolve, waitMs));
|
|
615
|
+
}
|
|
485
616
|
const connection = await this.getConnection(server);
|
|
486
617
|
if (this.logCalls) {
|
|
487
|
-
|
|
618
|
+
// v14.11: Sanitize args to prevent secret leakage
|
|
619
|
+
const safeArgs = sanitizeObject(args);
|
|
620
|
+
console.log(JSON.stringify({
|
|
621
|
+
level: 'debug',
|
|
622
|
+
time: Date.now(),
|
|
623
|
+
msg: 'MCP call started',
|
|
624
|
+
callId,
|
|
625
|
+
server,
|
|
626
|
+
tool,
|
|
627
|
+
argsPreview: sanitizeSecrets(JSON.stringify(safeArgs).slice(0, 100)),
|
|
628
|
+
}));
|
|
488
629
|
}
|
|
489
630
|
// v7.18: Wrap call in timeout for faster failure (15s default, 30s/120s for heavy ops)
|
|
490
631
|
const isHeavyOp = ['firecrawl_crawl', 'parse_paper_content', 'web_search'].includes(tool);
|
|
491
632
|
const isImageGen = server === 'huggingface' || server === 'stability-ai' || tool.includes('generate') || tool.includes('infer');
|
|
492
633
|
const callTimeout = isImageGen ? 120000 : isHeavyOp ? 30000 : 15000; // 120s for image gen (HF cold start)
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
634
|
+
// v14.11: Retry with exponential backoff (3 attempts)
|
|
635
|
+
const maxRetries = 3;
|
|
636
|
+
let lastError = null;
|
|
637
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
638
|
+
try {
|
|
639
|
+
const result = await Promise.race([
|
|
640
|
+
connection.client.callTool({
|
|
641
|
+
name: tool,
|
|
642
|
+
arguments: args,
|
|
643
|
+
}),
|
|
644
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`MCP call to ${server}.${tool} timed out after ${callTimeout}ms`)), callTimeout)),
|
|
645
|
+
]);
|
|
646
|
+
const latencyMs = Math.round(performance.now() - startTime);
|
|
647
|
+
// v14.10: Track metrics
|
|
648
|
+
mcpCallsTotal.inc({ server, tool, status: 'success' });
|
|
649
|
+
mcpLatency.observe(latencyMs / 1000, { server, tool });
|
|
650
|
+
if (this.logCalls) {
|
|
651
|
+
console.log(JSON.stringify({
|
|
652
|
+
level: 'info',
|
|
653
|
+
time: Date.now(),
|
|
654
|
+
msg: 'MCP call completed',
|
|
655
|
+
callId,
|
|
656
|
+
server,
|
|
657
|
+
tool,
|
|
658
|
+
latencyMs,
|
|
659
|
+
success: true,
|
|
660
|
+
attempt: attempt + 1,
|
|
661
|
+
}));
|
|
507
662
|
}
|
|
508
|
-
|
|
509
|
-
|
|
663
|
+
// Parse result content
|
|
664
|
+
const content = result.content;
|
|
665
|
+
if (content && content.length > 0) {
|
|
666
|
+
const first = content[0];
|
|
667
|
+
if (first.type === 'text' && typeof first.text === 'string') {
|
|
668
|
+
try {
|
|
669
|
+
return JSON.parse(first.text);
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
return first.text;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
510
675
|
}
|
|
676
|
+
return result;
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
lastError = error;
|
|
680
|
+
const errorMsg = lastError.message || '';
|
|
681
|
+
// v14.11: Only retry on transient errors (timeout, rate limit, server error)
|
|
682
|
+
const isRetryable = errorMsg.includes('timed out') ||
|
|
683
|
+
errorMsg.includes('rate limit') ||
|
|
684
|
+
errorMsg.includes('429') ||
|
|
685
|
+
errorMsg.includes('503') ||
|
|
686
|
+
errorMsg.includes('ECONNRESET');
|
|
687
|
+
if (!isRetryable || attempt === maxRetries - 1) {
|
|
688
|
+
break; // Don't retry on permanent errors or last attempt
|
|
689
|
+
}
|
|
690
|
+
// Exponential backoff with jitter: 1s, 2s, 4s
|
|
691
|
+
const baseDelay = Math.pow(2, attempt) * 1000;
|
|
692
|
+
const jitter = Math.random() * baseDelay * 0.3;
|
|
693
|
+
const delay = baseDelay + jitter;
|
|
694
|
+
if (this.logCalls) {
|
|
695
|
+
console.log(JSON.stringify({
|
|
696
|
+
level: 'warn',
|
|
697
|
+
time: Date.now(),
|
|
698
|
+
msg: 'MCP call failed, retrying',
|
|
699
|
+
callId,
|
|
700
|
+
server,
|
|
701
|
+
tool,
|
|
702
|
+
attempt: attempt + 1,
|
|
703
|
+
nextRetryMs: Math.round(delay),
|
|
704
|
+
error: errorMsg,
|
|
705
|
+
}));
|
|
706
|
+
}
|
|
707
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
511
708
|
}
|
|
512
709
|
}
|
|
513
|
-
|
|
710
|
+
// All retries failed
|
|
711
|
+
const latencyMs = Math.round(performance.now() - startTime);
|
|
712
|
+
const errorType = lastError?.message?.includes('timed out') ? 'timeout' : 'error';
|
|
713
|
+
// v14.10: Track error metrics
|
|
714
|
+
mcpCallsTotal.inc({ server, tool, status: 'error' });
|
|
715
|
+
mcpLatency.observe(latencyMs / 1000, { server, tool });
|
|
716
|
+
mcpErrorsTotal.inc({ server, error_type: errorType });
|
|
717
|
+
// v14.10: Send alert on MCP failures (if Slack configured)
|
|
718
|
+
try {
|
|
719
|
+
const alerter = (0, alerting_js_1.getAlerter)();
|
|
720
|
+
await alerter.warning(`MCP call failed: ${server}.${tool}`, lastError?.message || 'Unknown error', { labels: { server, tool, errorType, latencyMs: String(latencyMs), retries: String(maxRetries) } });
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
// Don't fail main operation if alerting fails
|
|
724
|
+
}
|
|
725
|
+
if (this.logCalls) {
|
|
726
|
+
console.log(JSON.stringify({
|
|
727
|
+
level: 'error',
|
|
728
|
+
time: Date.now(),
|
|
729
|
+
msg: 'MCP call failed after retries',
|
|
730
|
+
callId,
|
|
731
|
+
server,
|
|
732
|
+
tool,
|
|
733
|
+
latencyMs,
|
|
734
|
+
success: false,
|
|
735
|
+
retries: maxRetries,
|
|
736
|
+
error: lastError?.message,
|
|
737
|
+
}));
|
|
738
|
+
}
|
|
739
|
+
throw lastError || new Error(`MCP call to ${server}.${tool} failed`);
|
|
514
740
|
}
|
|
515
741
|
/**
|
|
516
742
|
* List available tools on an MCP server (names only)
|
|
@@ -558,6 +784,8 @@ class MCPConnectionManager {
|
|
|
558
784
|
}
|
|
559
785
|
connection.connected = false;
|
|
560
786
|
this.connections.delete(server);
|
|
787
|
+
// v14.10: Track active connections
|
|
788
|
+
mcpConnectionsActive.dec();
|
|
561
789
|
}
|
|
562
790
|
}
|
|
563
791
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genesis-ai-cli",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.11.0",
|
|
4
4
|
"description": "Fully Autonomous AI System with RSI (Recursive Self-Improvement) - Self-funding, Self-deploying, Production Memory, A2A Protocol & Governance",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|