@lanonasis/cli 3.1.13 → 3.3.15
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/__tests__/auth-persistence.test.d.ts +1 -0
- package/dist/__tests__/auth-persistence.test.js +243 -0
- package/dist/__tests__/cross-device-integration.test.d.ts +1 -0
- package/dist/__tests__/cross-device-integration.test.js +305 -0
- package/dist/__tests__/mcp-connection-reliability.test.d.ts +1 -0
- package/dist/__tests__/mcp-connection-reliability.test.js +489 -0
- package/dist/__tests__/setup.d.ts +1 -0
- package/dist/__tests__/setup.js +26 -0
- package/dist/commands/api-keys.js +12 -6
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +420 -50
- package/dist/commands/config.js +519 -1
- package/dist/commands/mcp.js +299 -0
- package/dist/index.js +5 -1
- package/dist/mcp/server/lanonasis-server.d.ts +161 -6
- package/dist/mcp/server/lanonasis-server.js +813 -17
- package/dist/mcp/server/mcp/server/lanonasis-server.js +911 -0
- package/dist/mcp/server/utils/api.js +431 -0
- package/dist/mcp/server/utils/config.js +855 -0
- package/dist/utils/config.d.ts +40 -1
- package/dist/utils/config.js +273 -36
- package/dist/utils/mcp-client.d.ts +83 -2
- package/dist/utils/mcp-client.js +414 -15
- package/package.json +8 -4
package/dist/utils/mcp-client.js
CHANGED
|
@@ -5,16 +5,18 @@ import { CLIConfig } from './config.js';
|
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import { EventSource } from 'eventsource';
|
|
8
|
-
import { fileURLToPath } from 'url';
|
|
9
8
|
import WebSocket from 'ws';
|
|
10
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
-
const __dirname = path.dirname(__filename);
|
|
12
9
|
export class MCPClient {
|
|
13
10
|
client = null;
|
|
14
11
|
config;
|
|
15
12
|
isConnected = false;
|
|
16
13
|
sseConnection = null;
|
|
17
14
|
wsConnection = null;
|
|
15
|
+
retryAttempts = 0;
|
|
16
|
+
maxRetries = 3;
|
|
17
|
+
healthCheckInterval = null;
|
|
18
|
+
connectionStartTime = 0;
|
|
19
|
+
lastHealthCheck = null;
|
|
18
20
|
constructor() {
|
|
19
21
|
this.config = new CLIConfig();
|
|
20
22
|
}
|
|
@@ -25,12 +27,22 @@ export class MCPClient {
|
|
|
25
27
|
await this.config.init();
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
|
-
* Connect to MCP server
|
|
30
|
+
* Connect to MCP server with retry logic
|
|
29
31
|
*/
|
|
30
32
|
async connect(options = {}) {
|
|
33
|
+
this.retryAttempts = 0;
|
|
34
|
+
return this.connectWithRetry(options);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Connect to MCP server with retry logic and exponential backoff
|
|
38
|
+
*/
|
|
39
|
+
async connectWithRetry(options = {}) {
|
|
31
40
|
try {
|
|
41
|
+
this.connectionStartTime = Date.now();
|
|
32
42
|
// Initialize config if not already done
|
|
33
43
|
await this.init();
|
|
44
|
+
// Validate authentication before attempting connection
|
|
45
|
+
await this.validateAuthBeforeConnect();
|
|
34
46
|
// Determine connection mode with priority to explicit mode option
|
|
35
47
|
// Default to 'remote' for better user experience
|
|
36
48
|
const connectionMode = options.connectionMode ??
|
|
@@ -49,10 +61,17 @@ export class MCPClient {
|
|
|
49
61
|
this.config.getMCPServerUrl() ??
|
|
50
62
|
'wss://mcp.lanonasis.com/ws';
|
|
51
63
|
wsUrl = wsUrlValue;
|
|
52
|
-
|
|
64
|
+
if (this.retryAttempts === 0) {
|
|
65
|
+
console.log(chalk.cyan(`Connecting to WebSocket MCP server at ${wsUrl}...`));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.log(chalk.yellow(`Retry ${this.retryAttempts}/${this.maxRetries}: Connecting to WebSocket MCP server...`));
|
|
69
|
+
}
|
|
53
70
|
// Initialize WebSocket connection
|
|
54
71
|
await this.initializeWebSocket(wsUrl);
|
|
55
72
|
this.isConnected = true;
|
|
73
|
+
this.retryAttempts = 0;
|
|
74
|
+
this.startHealthMonitoring();
|
|
56
75
|
return true;
|
|
57
76
|
}
|
|
58
77
|
case 'remote': {
|
|
@@ -62,17 +81,24 @@ export class MCPClient {
|
|
|
62
81
|
this.config.getMCPRestUrl() ??
|
|
63
82
|
'https://mcp.lanonasis.com/api/v1';
|
|
64
83
|
serverUrl = serverUrlValue;
|
|
65
|
-
|
|
84
|
+
if (this.retryAttempts === 0) {
|
|
85
|
+
console.log(chalk.cyan(`Connecting to remote MCP server at ${serverUrl}...`));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.log(chalk.yellow(`Retry ${this.retryAttempts}/${this.maxRetries}: Connecting to remote MCP server...`));
|
|
89
|
+
}
|
|
66
90
|
// Initialize SSE connection for real-time updates
|
|
67
91
|
await this.initializeSSE(serverUrl);
|
|
68
92
|
this.isConnected = true;
|
|
93
|
+
this.retryAttempts = 0;
|
|
94
|
+
this.startHealthMonitoring();
|
|
69
95
|
return true;
|
|
70
96
|
}
|
|
71
97
|
default: {
|
|
72
98
|
// Local MCP server connection (default)
|
|
73
99
|
const serverPathValue = options.serverPath ??
|
|
74
100
|
this.config.get('mcpServerPath') ??
|
|
75
|
-
path.join(
|
|
101
|
+
path.join(path.resolve(), '../../../../onasis-gateway/mcp-server/server.js');
|
|
76
102
|
serverPath = serverPathValue;
|
|
77
103
|
// Check if the server file exists
|
|
78
104
|
if (!fs.existsSync(serverPath)) {
|
|
@@ -81,7 +107,12 @@ export class MCPClient {
|
|
|
81
107
|
console.log(chalk.cyan('💡 Or install local server: npm install -g @lanonasis/mcp-server'));
|
|
82
108
|
throw new Error(`MCP server not found at ${serverPath}`);
|
|
83
109
|
}
|
|
84
|
-
|
|
110
|
+
if (this.retryAttempts === 0) {
|
|
111
|
+
console.log(chalk.cyan(`Connecting to local MCP server at ${serverPath}...`));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
console.log(chalk.yellow(`Retry ${this.retryAttempts}/${this.maxRetries}: Connecting to local MCP server...`));
|
|
115
|
+
}
|
|
85
116
|
const localTransport = new StdioClientTransport({
|
|
86
117
|
command: 'node',
|
|
87
118
|
args: [serverPath]
|
|
@@ -92,16 +123,231 @@ export class MCPClient {
|
|
|
92
123
|
});
|
|
93
124
|
await this.client.connect(localTransport);
|
|
94
125
|
this.isConnected = true;
|
|
126
|
+
this.retryAttempts = 0;
|
|
95
127
|
console.log(chalk.green('✓ Connected to MCP server'));
|
|
128
|
+
this.startHealthMonitoring();
|
|
96
129
|
return true;
|
|
97
130
|
}
|
|
98
131
|
}
|
|
99
132
|
}
|
|
100
133
|
catch (error) {
|
|
101
|
-
|
|
134
|
+
return this.handleConnectionFailure(error, options);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Handle connection failures with retry logic and specific error messages
|
|
139
|
+
*/
|
|
140
|
+
async handleConnectionFailure(error, options) {
|
|
141
|
+
// Check if this is an authentication error (don't retry these)
|
|
142
|
+
if (this.isAuthenticationError(error)) {
|
|
143
|
+
console.error(chalk.red('Authentication failed:'), error.message);
|
|
144
|
+
this.provideAuthenticationGuidance(error);
|
|
102
145
|
this.isConnected = false;
|
|
103
146
|
return false;
|
|
104
147
|
}
|
|
148
|
+
this.retryAttempts++;
|
|
149
|
+
if (this.retryAttempts >= this.maxRetries) {
|
|
150
|
+
console.error(chalk.red(`Failed to connect after ${this.maxRetries} attempts`));
|
|
151
|
+
this.provideNetworkTroubleshootingGuidance(error);
|
|
152
|
+
this.isConnected = false;
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
// For network errors, retry with exponential backoff
|
|
156
|
+
const delay = await this.exponentialBackoff(this.retryAttempts);
|
|
157
|
+
console.log(chalk.yellow(`Network error, retrying in ${delay}ms... (${this.retryAttempts}/${this.maxRetries})`));
|
|
158
|
+
console.log(chalk.gray(`Error: ${error.message}`));
|
|
159
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
160
|
+
return this.connectWithRetry(options);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Check if error is authentication-related
|
|
164
|
+
*/
|
|
165
|
+
isAuthenticationError(error) {
|
|
166
|
+
const errorMessage = error.message?.toLowerCase() || '';
|
|
167
|
+
return errorMessage.includes('authentication_required') ||
|
|
168
|
+
errorMessage.includes('authentication_invalid') ||
|
|
169
|
+
errorMessage.includes('unauthorized') ||
|
|
170
|
+
errorMessage.includes('invalid token') ||
|
|
171
|
+
errorMessage.includes('token is invalid') ||
|
|
172
|
+
errorMessage.includes('401') ||
|
|
173
|
+
errorMessage.includes('403') ||
|
|
174
|
+
(error.response?.status >= 401 && error.response?.status <= 403);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Provide authentication-specific guidance
|
|
178
|
+
*/
|
|
179
|
+
provideAuthenticationGuidance(error) {
|
|
180
|
+
console.log(chalk.yellow('\n🔐 Authentication Issue Detected:'));
|
|
181
|
+
if (error.message?.includes('AUTHENTICATION_REQUIRED')) {
|
|
182
|
+
console.log(chalk.cyan('• No credentials found. Run: lanonasis auth login'));
|
|
183
|
+
console.log(chalk.cyan('• Or set vendor key: lanonasis auth login --vendor-key pk_xxx.sk_xxx'));
|
|
184
|
+
}
|
|
185
|
+
else if (error.message?.includes('AUTHENTICATION_INVALID')) {
|
|
186
|
+
console.log(chalk.cyan('• Invalid credentials. Check your vendor key format'));
|
|
187
|
+
console.log(chalk.cyan('• Expected format: pk_xxx.sk_xxx'));
|
|
188
|
+
console.log(chalk.cyan('• Try: lanonasis auth logout && lanonasis auth login'));
|
|
189
|
+
}
|
|
190
|
+
else if (error.message?.includes('expired')) {
|
|
191
|
+
console.log(chalk.cyan('• Token expired. Re-authenticate: lanonasis auth login'));
|
|
192
|
+
console.log(chalk.cyan('• Or refresh: lanonasis auth refresh (if available)'));
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
console.log(chalk.cyan('• Check authentication status: lanonasis auth status'));
|
|
196
|
+
console.log(chalk.cyan('• Re-authenticate: lanonasis auth login'));
|
|
197
|
+
console.log(chalk.cyan('• Verify vendor key: lanonasis auth login --vendor-key pk_xxx.sk_xxx'));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Provide network troubleshooting guidance
|
|
202
|
+
*/
|
|
203
|
+
provideNetworkTroubleshootingGuidance(error) {
|
|
204
|
+
console.log(chalk.yellow('\n🌐 Network Issue Detected:'));
|
|
205
|
+
if (error.message?.includes('ECONNREFUSED') || error.message?.includes('connect ECONNREFUSED')) {
|
|
206
|
+
console.log(chalk.cyan('• Connection refused. Service may be down:'));
|
|
207
|
+
console.log(chalk.cyan(' - For remote: Check https://mcp.lanonasis.com/health'));
|
|
208
|
+
console.log(chalk.cyan(' - For WebSocket: Check wss://mcp.lanonasis.com/ws'));
|
|
209
|
+
console.log(chalk.cyan(' - For local: Install local MCP server'));
|
|
210
|
+
}
|
|
211
|
+
else if (error.message?.includes('timeout') || error.message?.includes('ETIMEDOUT')) {
|
|
212
|
+
console.log(chalk.cyan('• Connection timeout. Check network:'));
|
|
213
|
+
console.log(chalk.cyan(' - Verify internet connectivity'));
|
|
214
|
+
console.log(chalk.cyan(' - Check firewall settings'));
|
|
215
|
+
console.log(chalk.cyan(' - Try different connection mode: --mode remote'));
|
|
216
|
+
}
|
|
217
|
+
else if (error.message?.includes('ENOTFOUND') || error.message?.includes('getaddrinfo')) {
|
|
218
|
+
console.log(chalk.cyan('• DNS resolution failed:'));
|
|
219
|
+
console.log(chalk.cyan(' - Check DNS settings'));
|
|
220
|
+
console.log(chalk.cyan(' - Verify server URL is correct'));
|
|
221
|
+
console.log(chalk.cyan(' - Try using IP address instead of hostname'));
|
|
222
|
+
}
|
|
223
|
+
else if (error.message?.includes('certificate') || error.message?.includes('SSL') || error.message?.includes('TLS')) {
|
|
224
|
+
console.log(chalk.cyan('• SSL/TLS certificate issue:'));
|
|
225
|
+
console.log(chalk.cyan(' - Check system time and date'));
|
|
226
|
+
console.log(chalk.cyan(' - Update CA certificates'));
|
|
227
|
+
console.log(chalk.cyan(' - Try different connection mode'));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(chalk.cyan('• General network error:'));
|
|
231
|
+
console.log(chalk.cyan(' - Check server status'));
|
|
232
|
+
console.log(chalk.cyan(' - Verify network connectivity'));
|
|
233
|
+
console.log(chalk.cyan(' - Try: lanonasis mcp diagnose (when available)'));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Calculate exponential backoff delay with jitter
|
|
238
|
+
*/
|
|
239
|
+
async exponentialBackoff(attempt) {
|
|
240
|
+
// Base delay of 1 second, exponentially increasing
|
|
241
|
+
const baseDelay = 1000;
|
|
242
|
+
const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
|
|
243
|
+
// Cap at 10 seconds maximum
|
|
244
|
+
const cappedDelay = Math.min(exponentialDelay, 10000);
|
|
245
|
+
// Add jitter (±25% randomization) to avoid thundering herd
|
|
246
|
+
const jitter = cappedDelay * 0.25 * (Math.random() - 0.5);
|
|
247
|
+
return Math.round(cappedDelay + jitter);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Validate authentication credentials before attempting MCP connection
|
|
251
|
+
*/
|
|
252
|
+
async validateAuthBeforeConnect() {
|
|
253
|
+
const token = this.config.get('token');
|
|
254
|
+
const vendorKey = this.config.get('vendorKey');
|
|
255
|
+
// Check if we have any authentication credentials
|
|
256
|
+
if (!token && !vendorKey) {
|
|
257
|
+
throw new Error('AUTHENTICATION_REQUIRED: No authentication credentials found. Run "lanonasis auth login" first.');
|
|
258
|
+
}
|
|
259
|
+
// If we have a token, check if it's expired or needs refresh
|
|
260
|
+
if (token) {
|
|
261
|
+
try {
|
|
262
|
+
await this.validateAndRefreshToken(token);
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
throw new Error(`AUTHENTICATION_INVALID: ${error instanceof Error ? error.message : 'Token validation failed'}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// If we have a vendor key, validate its format
|
|
269
|
+
if (vendorKey && !token) {
|
|
270
|
+
if (!this.validateVendorKeyFormat(vendorKey)) {
|
|
271
|
+
throw new Error('AUTHENTICATION_INVALID: Invalid vendor key format. Expected format: pk_xxx.sk_xxx');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Validate vendor key format
|
|
277
|
+
*/
|
|
278
|
+
validateVendorKeyFormat(vendorKey) {
|
|
279
|
+
// Vendor key should be in format: pk_xxx.sk_xxx
|
|
280
|
+
const vendorKeyPattern = /^pk_[a-zA-Z0-9]+\.sk_[a-zA-Z0-9]+$/;
|
|
281
|
+
return vendorKeyPattern.test(vendorKey);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Validate and refresh token if needed
|
|
285
|
+
*/
|
|
286
|
+
async validateAndRefreshToken(token) {
|
|
287
|
+
try {
|
|
288
|
+
// Try to decode the JWT token to check expiration
|
|
289
|
+
const tokenParts = token.split('.');
|
|
290
|
+
if (tokenParts.length === 3) {
|
|
291
|
+
const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString());
|
|
292
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
293
|
+
// Check if token is expired or expires within 5 minutes
|
|
294
|
+
if (payload.exp && payload.exp < currentTime + 300) {
|
|
295
|
+
console.log(chalk.yellow('Token is expired or expiring soon, attempting refresh...'));
|
|
296
|
+
await this.refreshTokenIfNeeded();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
// If we can't decode the token, try to validate it with the server
|
|
302
|
+
await this.validateTokenWithServer(token);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Refresh token if needed
|
|
307
|
+
*/
|
|
308
|
+
async refreshTokenIfNeeded() {
|
|
309
|
+
const refreshToken = this.config.get('refreshToken');
|
|
310
|
+
if (!refreshToken) {
|
|
311
|
+
throw new Error('No refresh token available. Please re-authenticate.');
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const axios = (await import('axios')).default;
|
|
315
|
+
const authUrl = this.config.get('authUrl') ?? 'https://api.lanonasis.com';
|
|
316
|
+
const response = await axios.post(`${authUrl}/auth/refresh`, {
|
|
317
|
+
refresh_token: refreshToken
|
|
318
|
+
}, {
|
|
319
|
+
timeout: 10000
|
|
320
|
+
});
|
|
321
|
+
if (response.data.access_token) {
|
|
322
|
+
await this.config.setAndSave('token', response.data.access_token);
|
|
323
|
+
console.log(chalk.green('✓ Token refreshed successfully'));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
throw new Error('Failed to refresh token. Please re-authenticate.');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Validate token with server
|
|
332
|
+
*/
|
|
333
|
+
async validateTokenWithServer(token) {
|
|
334
|
+
try {
|
|
335
|
+
const axios = (await import('axios')).default;
|
|
336
|
+
const authUrl = this.config.get('authUrl') ?? 'https://api.lanonasis.com';
|
|
337
|
+
await axios.get(`${authUrl}/auth/validate`, {
|
|
338
|
+
headers: {
|
|
339
|
+
'Authorization': `Bearer ${token}`,
|
|
340
|
+
'x-api-key': String(token)
|
|
341
|
+
},
|
|
342
|
+
timeout: 10000
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
if (error.response?.status === 401 || error.response?.status === 403) {
|
|
347
|
+
throw new Error('Token is invalid or expired. Please re-authenticate.');
|
|
348
|
+
}
|
|
349
|
+
throw new Error(`Token validation failed: ${error.message}`);
|
|
350
|
+
}
|
|
105
351
|
}
|
|
106
352
|
/**
|
|
107
353
|
* Initialize SSE connection for real-time updates
|
|
@@ -208,10 +454,142 @@ export class MCPClient {
|
|
|
208
454
|
}
|
|
209
455
|
this.wsConnection.send(JSON.stringify(message));
|
|
210
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Start health monitoring for the connection
|
|
459
|
+
*/
|
|
460
|
+
startHealthMonitoring() {
|
|
461
|
+
// Clear any existing health check interval
|
|
462
|
+
this.stopHealthMonitoring();
|
|
463
|
+
// Start health monitoring every 30 seconds
|
|
464
|
+
this.healthCheckInterval = setInterval(async () => {
|
|
465
|
+
await this.performHealthCheck();
|
|
466
|
+
}, 30000);
|
|
467
|
+
// Perform initial health check
|
|
468
|
+
setTimeout(() => this.performHealthCheck(), 5000);
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Stop health monitoring
|
|
472
|
+
*/
|
|
473
|
+
stopHealthMonitoring() {
|
|
474
|
+
if (this.healthCheckInterval) {
|
|
475
|
+
clearInterval(this.healthCheckInterval);
|
|
476
|
+
this.healthCheckInterval = null;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Perform a health check on the current connection
|
|
481
|
+
*/
|
|
482
|
+
async performHealthCheck() {
|
|
483
|
+
if (!this.isConnected) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
this.lastHealthCheck = new Date();
|
|
488
|
+
const connectionMode = this.config.get('mcpConnectionMode') ?? 'remote';
|
|
489
|
+
switch (connectionMode) {
|
|
490
|
+
case 'websocket':
|
|
491
|
+
await this.checkWebSocketHealth();
|
|
492
|
+
break;
|
|
493
|
+
case 'remote':
|
|
494
|
+
await this.checkRemoteHealth();
|
|
495
|
+
break;
|
|
496
|
+
default:
|
|
497
|
+
await this.checkLocalHealth();
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
console.log(chalk.yellow('⚠️ Health check failed, attempting reconnection...'));
|
|
503
|
+
await this.handleHealthCheckFailure();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Check WebSocket connection health
|
|
508
|
+
*/
|
|
509
|
+
async checkWebSocketHealth() {
|
|
510
|
+
if (!this.wsConnection || this.wsConnection.readyState !== WebSocket.OPEN) {
|
|
511
|
+
throw new Error('WebSocket connection not open');
|
|
512
|
+
}
|
|
513
|
+
// Send a ping message to check connectivity
|
|
514
|
+
this.sendWebSocketMessage({
|
|
515
|
+
id: Date.now(),
|
|
516
|
+
method: 'ping',
|
|
517
|
+
params: {}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Check remote connection health
|
|
522
|
+
*/
|
|
523
|
+
async checkRemoteHealth() {
|
|
524
|
+
const apiUrl = this.config.getMCPRestUrl() ?? 'https://mcp.lanonasis.com/api/v1';
|
|
525
|
+
const token = this.config.get('token');
|
|
526
|
+
if (!token) {
|
|
527
|
+
throw new Error('No authentication token available');
|
|
528
|
+
}
|
|
529
|
+
try {
|
|
530
|
+
const axios = (await import('axios')).default;
|
|
531
|
+
await axios.get(`${apiUrl}/health`, {
|
|
532
|
+
headers: {
|
|
533
|
+
'Authorization': `Bearer ${token}`,
|
|
534
|
+
'x-api-key': String(token)
|
|
535
|
+
},
|
|
536
|
+
timeout: 5000
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
throw new Error(`Remote health check failed: ${error}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Check local connection health
|
|
545
|
+
*/
|
|
546
|
+
async checkLocalHealth() {
|
|
547
|
+
if (!this.client) {
|
|
548
|
+
throw new Error('Local MCP client not initialized');
|
|
549
|
+
}
|
|
550
|
+
// Try to list tools as a health check
|
|
551
|
+
try {
|
|
552
|
+
await this.client.listTools();
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
throw new Error(`Local health check failed: ${error}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Handle health check failure by attempting reconnection
|
|
560
|
+
*/
|
|
561
|
+
async handleHealthCheckFailure() {
|
|
562
|
+
this.isConnected = false;
|
|
563
|
+
this.stopHealthMonitoring();
|
|
564
|
+
// Attempt to reconnect with current configuration
|
|
565
|
+
const connectionMode = this.config.get('mcpConnectionMode') ?? 'remote';
|
|
566
|
+
const options = {
|
|
567
|
+
connectionMode: connectionMode
|
|
568
|
+
};
|
|
569
|
+
// Add specific URLs if available
|
|
570
|
+
if (connectionMode === 'websocket') {
|
|
571
|
+
options.serverUrl = this.config.get('mcpWebSocketUrl');
|
|
572
|
+
}
|
|
573
|
+
else if (connectionMode === 'remote') {
|
|
574
|
+
options.serverUrl = this.config.get('mcpServerUrl');
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
options.serverPath = this.config.get('mcpServerPath');
|
|
578
|
+
}
|
|
579
|
+
// Attempt reconnection
|
|
580
|
+
const reconnected = await this.connect(options);
|
|
581
|
+
if (reconnected) {
|
|
582
|
+
console.log(chalk.green('✓ Reconnected to MCP server'));
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
console.log(chalk.red('✗ Failed to reconnect to MCP server'));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
211
588
|
/**
|
|
212
589
|
* Disconnect from MCP server
|
|
213
590
|
*/
|
|
214
591
|
async disconnect() {
|
|
592
|
+
this.stopHealthMonitoring();
|
|
215
593
|
if (this.client) {
|
|
216
594
|
await this.client.close();
|
|
217
595
|
this.client = null;
|
|
@@ -220,6 +598,10 @@ export class MCPClient {
|
|
|
220
598
|
this.sseConnection.close();
|
|
221
599
|
this.sseConnection = null;
|
|
222
600
|
}
|
|
601
|
+
if (this.wsConnection) {
|
|
602
|
+
this.wsConnection.close();
|
|
603
|
+
this.wsConnection = null;
|
|
604
|
+
}
|
|
223
605
|
this.isConnected = false;
|
|
224
606
|
}
|
|
225
607
|
/**
|
|
@@ -372,16 +754,33 @@ export class MCPClient {
|
|
|
372
754
|
return this.isConnected;
|
|
373
755
|
}
|
|
374
756
|
/**
|
|
375
|
-
* Get connection status details
|
|
757
|
+
* Get connection status details with health information
|
|
376
758
|
*/
|
|
377
759
|
getConnectionStatus() {
|
|
378
|
-
const
|
|
760
|
+
const connectionMode = this.config.get('mcpConnectionMode') ??
|
|
761
|
+
(this.config.get('mcpUseRemote') ? 'remote' : 'local');
|
|
762
|
+
let server;
|
|
763
|
+
switch (connectionMode) {
|
|
764
|
+
case 'websocket':
|
|
765
|
+
server = this.config.get('mcpWebSocketUrl') ?? 'wss://mcp.lanonasis.com/ws';
|
|
766
|
+
break;
|
|
767
|
+
case 'remote':
|
|
768
|
+
server = this.config.get('mcpServerUrl') ?? 'https://mcp.lanonasis.com/api/v1';
|
|
769
|
+
break;
|
|
770
|
+
default:
|
|
771
|
+
server = this.config.get('mcpServerPath') ?? 'local MCP server';
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
const connectionUptime = this.connectionStartTime > 0
|
|
775
|
+
? Date.now() - this.connectionStartTime
|
|
776
|
+
: undefined;
|
|
379
777
|
return {
|
|
380
778
|
connected: this.isConnected,
|
|
381
|
-
mode:
|
|
382
|
-
server
|
|
383
|
-
|
|
384
|
-
|
|
779
|
+
mode: connectionMode,
|
|
780
|
+
server,
|
|
781
|
+
lastHealthCheck: this.lastHealthCheck ?? undefined,
|
|
782
|
+
connectionUptime,
|
|
783
|
+
failureCount: this.retryAttempts
|
|
385
784
|
};
|
|
386
785
|
}
|
|
387
786
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lanonasis/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.15",
|
|
4
4
|
"description": "LanOnasis Enterprise CLI - Memory as a Service, API Key Management, and Infrastructure Orchestration",
|
|
5
5
|
"main": "dist/index-simple.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"build:mcp-client": "tsc src/mcp/client/enhanced-client.ts --outDir dist/mcp/client",
|
|
22
22
|
"start": "node dist/index.js",
|
|
23
23
|
"start:mcp-server": "node dist/mcp/server/lanonasis-server.js",
|
|
24
|
-
"test": "jest",
|
|
25
|
-
"test:mcp": "jest --testPathPattern=mcp",
|
|
24
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
25
|
+
"test:mcp": "NODE_OPTIONS=--experimental-vm-modules jest --testPathPattern=mcp",
|
|
26
26
|
"test:integration": "npm run build:mcp && npm run test:mcp",
|
|
27
27
|
"lint": "eslint src/**/*.ts",
|
|
28
28
|
"type-check": "tsc --noEmit",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"repository": {
|
|
57
57
|
"type": "git",
|
|
58
|
-
"url": "https://github.com/lanonasis/lanonasis-maas.git",
|
|
58
|
+
"url": "git+https://github.com/lanonasis/lanonasis-maas.git",
|
|
59
59
|
"directory": "cli"
|
|
60
60
|
},
|
|
61
61
|
"bugs": {
|
|
@@ -84,11 +84,15 @@
|
|
|
84
84
|
"ws": "^8.18.3"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
|
+
"@jest/globals": "^29.7.0",
|
|
87
88
|
"@types/inquirer": "^9.0.7",
|
|
89
|
+
"@types/jest": "^29.5.14",
|
|
88
90
|
"@types/node": "^22.10.2",
|
|
89
91
|
"@typescript-eslint/eslint-plugin": "^8.18.1",
|
|
90
92
|
"@typescript-eslint/parser": "^8.18.1",
|
|
91
93
|
"eslint": "^9.17.0",
|
|
94
|
+
"jest": "^29.7.0",
|
|
95
|
+
"ts-jest": "^29.2.5",
|
|
92
96
|
"tsx": "^4.19.2",
|
|
93
97
|
"typescript": "^5.7.2"
|
|
94
98
|
},
|