@lanonasis/cli 3.0.13 ā 3.2.14
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 +445 -19
- package/dist/commands/config.js +519 -1
- package/dist/commands/mcp.js +299 -0
- package/dist/index-simple.js +30 -0
- package/dist/index.js +35 -1
- package/dist/mcp/schemas/tool-schemas.d.ts +4 -4
- 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 +57 -1
- package/dist/utils/config.js +530 -42
- package/dist/utils/mcp-client.d.ts +83 -2
- package/dist/utils/mcp-client.js +414 -15
- package/package.json +8 -4
|
@@ -13,8 +13,18 @@ export class LanonasisMCPServer {
|
|
|
13
13
|
apiClient;
|
|
14
14
|
transport = null;
|
|
15
15
|
options;
|
|
16
|
+
// Connection pool management
|
|
17
|
+
connectionPool = new Map();
|
|
18
|
+
maxConnections = 10;
|
|
19
|
+
connectionCleanupInterval = null;
|
|
20
|
+
// Transport protocol management
|
|
21
|
+
supportedTransports = ['stdio', 'websocket', 'http'];
|
|
22
|
+
transportFailures = new Map();
|
|
23
|
+
enableFallback = true;
|
|
16
24
|
constructor(options = {}) {
|
|
17
25
|
this.options = options;
|
|
26
|
+
// Initialize transport settings
|
|
27
|
+
this.enableFallback = options.enableTransportFallback !== false; // Default to true
|
|
18
28
|
// Initialize server with metadata
|
|
19
29
|
this.server = new Server({
|
|
20
30
|
name: options.name || "lanonasis-maas-server",
|
|
@@ -57,10 +67,13 @@ export class LanonasisMCPServer {
|
|
|
57
67
|
await this.registerTools();
|
|
58
68
|
await this.registerResources();
|
|
59
69
|
await this.registerPrompts();
|
|
70
|
+
// Start connection cleanup monitoring
|
|
71
|
+
this.startConnectionCleanup();
|
|
60
72
|
if (this.options.verbose) {
|
|
61
73
|
console.log(chalk.cyan('š Lanonasis MCP Server initialized'));
|
|
62
74
|
console.log(chalk.gray(`API URL: ${apiUrl}`));
|
|
63
75
|
console.log(chalk.gray(`Authenticated: ${token ? 'Yes' : 'No'}`));
|
|
76
|
+
console.log(chalk.gray(`Max connections: ${this.maxConnections}`));
|
|
64
77
|
}
|
|
65
78
|
}
|
|
66
79
|
/**
|
|
@@ -305,14 +318,101 @@ export class LanonasisMCPServer {
|
|
|
305
318
|
}
|
|
306
319
|
}
|
|
307
320
|
}
|
|
321
|
+
},
|
|
322
|
+
// Connection management tools
|
|
323
|
+
{
|
|
324
|
+
name: 'connection_stats',
|
|
325
|
+
description: 'Get connection pool statistics',
|
|
326
|
+
inputSchema: {
|
|
327
|
+
type: 'object',
|
|
328
|
+
properties: {}
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: 'connection_auth_status',
|
|
333
|
+
description: 'Get authentication status for all connections',
|
|
334
|
+
inputSchema: {
|
|
335
|
+
type: 'object',
|
|
336
|
+
properties: {}
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: 'connection_validate_auth',
|
|
341
|
+
description: 'Validate authentication for a specific connection',
|
|
342
|
+
inputSchema: {
|
|
343
|
+
type: 'object',
|
|
344
|
+
properties: {
|
|
345
|
+
clientId: {
|
|
346
|
+
type: 'string',
|
|
347
|
+
description: 'Client ID to validate'
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
required: ['clientId']
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
// Transport management tools
|
|
354
|
+
{
|
|
355
|
+
name: 'transport_status',
|
|
356
|
+
description: 'Get transport protocol status and statistics',
|
|
357
|
+
inputSchema: {
|
|
358
|
+
type: 'object',
|
|
359
|
+
properties: {}
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
name: 'transport_test',
|
|
364
|
+
description: 'Test availability of a specific transport',
|
|
365
|
+
inputSchema: {
|
|
366
|
+
type: 'object',
|
|
367
|
+
properties: {
|
|
368
|
+
transport: {
|
|
369
|
+
type: 'string',
|
|
370
|
+
enum: ['stdio', 'websocket', 'http'],
|
|
371
|
+
description: 'Transport to test'
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
required: ['transport']
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: 'transport_reset_failures',
|
|
379
|
+
description: 'Reset failure count for a transport',
|
|
380
|
+
inputSchema: {
|
|
381
|
+
type: 'object',
|
|
382
|
+
properties: {
|
|
383
|
+
transport: {
|
|
384
|
+
type: 'string',
|
|
385
|
+
enum: ['stdio', 'websocket', 'http'],
|
|
386
|
+
description: 'Transport to reset (optional, resets all if not specified)'
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
308
390
|
}
|
|
309
391
|
]
|
|
310
392
|
}));
|
|
311
393
|
// Tool call handler (CallToolRequestSchema already imported above)
|
|
312
394
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
313
395
|
const { name, arguments: args } = request.params;
|
|
396
|
+
// Generate or extract client ID for connection tracking
|
|
397
|
+
const clientId = this.extractClientId(request) || this.generateClientId();
|
|
398
|
+
// Authenticate the connection before processing the request
|
|
314
399
|
try {
|
|
315
|
-
|
|
400
|
+
await this.authenticateRequest(request, clientId);
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
return {
|
|
404
|
+
content: [
|
|
405
|
+
{
|
|
406
|
+
type: 'text',
|
|
407
|
+
text: `Authentication Error: ${error instanceof Error ? error.message : 'Authentication failed'}`
|
|
408
|
+
}
|
|
409
|
+
],
|
|
410
|
+
isError: true
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
this.updateConnectionActivity(clientId);
|
|
414
|
+
try {
|
|
415
|
+
const result = await this.handleToolCall(name, args, clientId);
|
|
316
416
|
return {
|
|
317
417
|
content: [
|
|
318
418
|
{
|
|
@@ -336,8 +436,8 @@ export class LanonasisMCPServer {
|
|
|
336
436
|
});
|
|
337
437
|
}
|
|
338
438
|
/**
|
|
339
|
-
|
|
340
|
-
|
|
439
|
+
* Register MCP resources
|
|
440
|
+
*/
|
|
341
441
|
async registerResources() {
|
|
342
442
|
const { ListResourcesRequestSchema, ReadResourceRequestSchema } = await import('@modelcontextprotocol/sdk/types.js');
|
|
343
443
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
@@ -365,6 +465,18 @@ export class LanonasisMCPServer {
|
|
|
365
465
|
name: 'Usage Statistics',
|
|
366
466
|
description: 'Memory usage and API statistics',
|
|
367
467
|
mimeType: 'application/json'
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
uri: 'connections://pool',
|
|
471
|
+
name: 'Connection Pool',
|
|
472
|
+
description: 'Current connection pool status and statistics',
|
|
473
|
+
mimeType: 'application/json'
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
uri: 'transport://status',
|
|
477
|
+
name: 'Transport Status',
|
|
478
|
+
description: 'Transport protocol status and failure statistics',
|
|
479
|
+
mimeType: 'application/json'
|
|
368
480
|
}
|
|
369
481
|
]
|
|
370
482
|
}));
|
|
@@ -486,11 +598,11 @@ Please choose an option (1-4):`
|
|
|
486
598
|
}
|
|
487
599
|
return prompt;
|
|
488
600
|
});
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
|
|
601
|
+
} /**
|
|
602
|
+
|
|
603
|
+
* Handle tool calls
|
|
492
604
|
*/
|
|
493
|
-
async handleToolCall(name, args) {
|
|
605
|
+
async handleToolCall(name, args, clientId) {
|
|
494
606
|
// Ensure we're initialized
|
|
495
607
|
if (!this.apiClient) {
|
|
496
608
|
await this.initialize();
|
|
@@ -533,6 +645,52 @@ Please choose an option (1-4):`
|
|
|
533
645
|
return await this.handleSystemHealth(args.verbose);
|
|
534
646
|
case 'system_config':
|
|
535
647
|
return await this.handleSystemConfig(args);
|
|
648
|
+
// Connection management operations
|
|
649
|
+
case 'connection_stats':
|
|
650
|
+
return this.getConnectionPoolStats();
|
|
651
|
+
case 'connection_auth_status':
|
|
652
|
+
return this.getAuthenticationStatus();
|
|
653
|
+
case 'connection_validate_auth':
|
|
654
|
+
if (!args.clientId) {
|
|
655
|
+
throw new Error('clientId is required');
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
clientId: args.clientId,
|
|
659
|
+
authenticated: this.validateConnectionAuth(args.clientId),
|
|
660
|
+
connection: this.getConnection(args.clientId) ? {
|
|
661
|
+
transport: this.getConnection(args.clientId).transport,
|
|
662
|
+
connectedAt: this.getConnection(args.clientId).connectedAt.toISOString(),
|
|
663
|
+
lastActivity: this.getConnection(args.clientId).lastActivity.toISOString()
|
|
664
|
+
} : null
|
|
665
|
+
};
|
|
666
|
+
// Transport management operations
|
|
667
|
+
case 'transport_status':
|
|
668
|
+
return this.getTransportStatus();
|
|
669
|
+
case 'transport_test':
|
|
670
|
+
if (!args.transport) {
|
|
671
|
+
throw new Error('transport is required');
|
|
672
|
+
}
|
|
673
|
+
const isAvailable = await this.checkTransportAvailability(args.transport);
|
|
674
|
+
return {
|
|
675
|
+
transport: args.transport,
|
|
676
|
+
available: isAvailable,
|
|
677
|
+
tested_at: new Date().toISOString()
|
|
678
|
+
};
|
|
679
|
+
case 'transport_reset_failures':
|
|
680
|
+
if (args.transport) {
|
|
681
|
+
this.transportFailures.delete(args.transport);
|
|
682
|
+
return {
|
|
683
|
+
success: true,
|
|
684
|
+
message: `Reset failures for ${args.transport} transport`
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
this.transportFailures.clear();
|
|
689
|
+
return {
|
|
690
|
+
success: true,
|
|
691
|
+
message: 'Reset failures for all transports'
|
|
692
|
+
};
|
|
693
|
+
}
|
|
536
694
|
default:
|
|
537
695
|
throw new Error(`Unknown tool: ${name}`);
|
|
538
696
|
}
|
|
@@ -575,6 +733,27 @@ Please choose an option (1-4):`
|
|
|
575
733
|
};
|
|
576
734
|
}
|
|
577
735
|
break;
|
|
736
|
+
case 'connections':
|
|
737
|
+
if (path === 'pool') {
|
|
738
|
+
return {
|
|
739
|
+
...this.getConnectionPoolStats(),
|
|
740
|
+
connections: Array.from(this.connectionPool.values()).map(conn => ({
|
|
741
|
+
clientId: conn.clientId,
|
|
742
|
+
connectedAt: conn.connectedAt.toISOString(),
|
|
743
|
+
lastActivity: conn.lastActivity.toISOString(),
|
|
744
|
+
transport: conn.transport,
|
|
745
|
+
authenticated: conn.authenticated,
|
|
746
|
+
uptime: Date.now() - conn.connectedAt.getTime(),
|
|
747
|
+
clientInfo: conn.clientInfo
|
|
748
|
+
}))
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
break;
|
|
752
|
+
case 'transport':
|
|
753
|
+
if (path === 'status') {
|
|
754
|
+
return this.getTransportStatus();
|
|
755
|
+
}
|
|
756
|
+
break;
|
|
578
757
|
}
|
|
579
758
|
throw new Error(`Unknown resource: ${uri}`);
|
|
580
759
|
}
|
|
@@ -586,7 +765,9 @@ Please choose an option (1-4):`
|
|
|
586
765
|
status: 'healthy',
|
|
587
766
|
server: this.options.name || 'lanonasis-maas-server',
|
|
588
767
|
version: this.options.version || '3.0.1',
|
|
589
|
-
timestamp: new Date().toISOString()
|
|
768
|
+
timestamp: new Date().toISOString(),
|
|
769
|
+
connections: this.getConnectionPoolStats(),
|
|
770
|
+
authentication: this.getAuthenticationStatus()
|
|
590
771
|
};
|
|
591
772
|
if (verbose) {
|
|
592
773
|
health.api = {
|
|
@@ -602,6 +783,16 @@ Please choose an option (1-4):`
|
|
|
602
783
|
health.api.status = 'error';
|
|
603
784
|
health.api.error = error instanceof Error ? error.message : 'Unknown error';
|
|
604
785
|
}
|
|
786
|
+
// Include detailed connection information
|
|
787
|
+
health.connectionDetails = Array.from(this.connectionPool.values()).map(conn => ({
|
|
788
|
+
clientId: conn.clientId,
|
|
789
|
+
transport: conn.transport,
|
|
790
|
+
authenticated: conn.authenticated,
|
|
791
|
+
connectedAt: conn.connectedAt.toISOString(),
|
|
792
|
+
lastActivity: conn.lastActivity.toISOString(),
|
|
793
|
+
uptime: Date.now() - conn.connectedAt.getTime(),
|
|
794
|
+
clientInfo: conn.clientInfo
|
|
795
|
+
}));
|
|
605
796
|
}
|
|
606
797
|
return health;
|
|
607
798
|
}
|
|
@@ -619,7 +810,9 @@ Please choose an option (1-4):`
|
|
|
619
810
|
return {
|
|
620
811
|
apiUrl: this.config.getApiUrl(),
|
|
621
812
|
mcpServerUrl: this.config.get('mcpServerUrl'),
|
|
622
|
-
mcpUseRemote: this.config.get('mcpUseRemote')
|
|
813
|
+
mcpUseRemote: this.config.get('mcpUseRemote'),
|
|
814
|
+
maxConnections: this.maxConnections,
|
|
815
|
+
transport: this.getTransportStatus()
|
|
623
816
|
};
|
|
624
817
|
}
|
|
625
818
|
}
|
|
@@ -627,6 +820,18 @@ Please choose an option (1-4):`
|
|
|
627
820
|
if (!args.key || !args.value) {
|
|
628
821
|
throw new Error('Key and value required for set action');
|
|
629
822
|
}
|
|
823
|
+
// Handle special configuration keys
|
|
824
|
+
if (args.key === 'maxConnections') {
|
|
825
|
+
const newMax = parseInt(args.value);
|
|
826
|
+
if (isNaN(newMax) || newMax < 1 || newMax > 100) {
|
|
827
|
+
throw new Error('maxConnections must be a number between 1 and 100');
|
|
828
|
+
}
|
|
829
|
+
this.maxConnections = newMax;
|
|
830
|
+
return {
|
|
831
|
+
success: true,
|
|
832
|
+
message: `Set ${args.key} to ${args.value}`
|
|
833
|
+
};
|
|
834
|
+
}
|
|
630
835
|
this.config.set(args.key, args.value);
|
|
631
836
|
await this.config.save();
|
|
632
837
|
return {
|
|
@@ -635,6 +840,557 @@ Please choose an option (1-4):`
|
|
|
635
840
|
};
|
|
636
841
|
}
|
|
637
842
|
throw new Error('Invalid action');
|
|
843
|
+
} /**
|
|
844
|
+
|
|
845
|
+
* Connection pool management methods
|
|
846
|
+
*/
|
|
847
|
+
/**
|
|
848
|
+
* Add a new connection to the pool
|
|
849
|
+
*/
|
|
850
|
+
addConnection(clientId, transport, clientInfo) {
|
|
851
|
+
// Check if we've reached the maximum number of connections
|
|
852
|
+
if (this.connectionPool.size >= this.maxConnections) {
|
|
853
|
+
if (this.options.verbose) {
|
|
854
|
+
console.log(chalk.yellow(`ā ļø Maximum connections (${this.maxConnections}) reached, rejecting new connection`));
|
|
855
|
+
}
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
const connection = {
|
|
859
|
+
clientId,
|
|
860
|
+
connectedAt: new Date(),
|
|
861
|
+
lastActivity: new Date(),
|
|
862
|
+
transport,
|
|
863
|
+
authenticated: false,
|
|
864
|
+
clientInfo
|
|
865
|
+
};
|
|
866
|
+
this.connectionPool.set(clientId, connection);
|
|
867
|
+
if (this.options.verbose) {
|
|
868
|
+
console.log(chalk.cyan(`ā
Added connection ${clientId} (${transport}) - Total: ${this.connectionPool.size}`));
|
|
869
|
+
}
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Remove a connection from the pool
|
|
874
|
+
*/
|
|
875
|
+
removeConnection(clientId) {
|
|
876
|
+
const connection = this.connectionPool.get(clientId);
|
|
877
|
+
if (connection) {
|
|
878
|
+
this.connectionPool.delete(clientId);
|
|
879
|
+
if (this.options.verbose) {
|
|
880
|
+
const uptime = Date.now() - connection.connectedAt.getTime();
|
|
881
|
+
console.log(chalk.gray(`š Removed connection ${clientId} (uptime: ${Math.round(uptime / 1000)}s) - Total: ${this.connectionPool.size}`));
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Update connection activity timestamp
|
|
887
|
+
*/
|
|
888
|
+
updateConnectionActivity(clientId) {
|
|
889
|
+
const connection = this.connectionPool.get(clientId);
|
|
890
|
+
if (connection) {
|
|
891
|
+
connection.lastActivity = new Date();
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Mark connection as authenticated
|
|
896
|
+
*/
|
|
897
|
+
authenticateConnection(clientId) {
|
|
898
|
+
const connection = this.connectionPool.get(clientId);
|
|
899
|
+
if (connection) {
|
|
900
|
+
connection.authenticated = true;
|
|
901
|
+
if (this.options.verbose) {
|
|
902
|
+
console.log(chalk.green(`š Connection ${clientId} authenticated`));
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Get connection pool statistics
|
|
908
|
+
*/
|
|
909
|
+
getConnectionPoolStats() {
|
|
910
|
+
const stats = {
|
|
911
|
+
totalConnections: this.connectionPool.size,
|
|
912
|
+
activeConnections: 0,
|
|
913
|
+
authenticatedConnections: 0,
|
|
914
|
+
connectionsByTransport: {}
|
|
915
|
+
};
|
|
916
|
+
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
|
|
917
|
+
for (const connection of this.connectionPool.values()) {
|
|
918
|
+
// Count as active if there was activity in the last 5 minutes
|
|
919
|
+
if (connection.lastActivity.getTime() > fiveMinutesAgo) {
|
|
920
|
+
stats.activeConnections++;
|
|
921
|
+
}
|
|
922
|
+
if (connection.authenticated) {
|
|
923
|
+
stats.authenticatedConnections++;
|
|
924
|
+
}
|
|
925
|
+
stats.connectionsByTransport[connection.transport] =
|
|
926
|
+
(stats.connectionsByTransport[connection.transport] || 0) + 1;
|
|
927
|
+
}
|
|
928
|
+
return stats;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Start connection cleanup monitoring
|
|
932
|
+
*/
|
|
933
|
+
startConnectionCleanup() {
|
|
934
|
+
// Clean up stale connections every 2 minutes
|
|
935
|
+
this.connectionCleanupInterval = setInterval(() => {
|
|
936
|
+
this.cleanupStaleConnections();
|
|
937
|
+
}, 2 * 60 * 1000);
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Stop connection cleanup monitoring
|
|
941
|
+
*/
|
|
942
|
+
stopConnectionCleanup() {
|
|
943
|
+
if (this.connectionCleanupInterval) {
|
|
944
|
+
clearInterval(this.connectionCleanupInterval);
|
|
945
|
+
this.connectionCleanupInterval = null;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Clean up stale connections (no activity for 10 minutes)
|
|
950
|
+
*/
|
|
951
|
+
cleanupStaleConnections() {
|
|
952
|
+
const tenMinutesAgo = Date.now() - (10 * 60 * 1000);
|
|
953
|
+
const staleConnections = [];
|
|
954
|
+
for (const [clientId, connection] of this.connectionPool.entries()) {
|
|
955
|
+
if (connection.lastActivity.getTime() < tenMinutesAgo) {
|
|
956
|
+
staleConnections.push(clientId);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
for (const clientId of staleConnections) {
|
|
960
|
+
this.removeConnection(clientId);
|
|
961
|
+
if (this.options.verbose) {
|
|
962
|
+
console.log(chalk.yellow(`š§¹ Cleaned up stale connection: ${clientId}`));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Generate unique client ID for new connections
|
|
968
|
+
*/
|
|
969
|
+
generateClientId() {
|
|
970
|
+
return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Extract client ID from request headers or metadata
|
|
974
|
+
*/
|
|
975
|
+
extractClientId(request) {
|
|
976
|
+
// Try to extract client ID from request metadata
|
|
977
|
+
// This would depend on the MCP transport implementation
|
|
978
|
+
return request.meta?.clientId || null;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Authenticate incoming MCP request
|
|
982
|
+
*/
|
|
983
|
+
async authenticateRequest(request, clientId) {
|
|
984
|
+
// Check if connection already exists and is authenticated
|
|
985
|
+
const existingConnection = this.getConnection(clientId);
|
|
986
|
+
if (existingConnection && existingConnection.authenticated) {
|
|
987
|
+
return; // Already authenticated
|
|
988
|
+
}
|
|
989
|
+
// Extract authentication information from request
|
|
990
|
+
const authInfo = this.extractAuthInfo(request);
|
|
991
|
+
if (!authInfo.token && !authInfo.vendorKey) {
|
|
992
|
+
// For stdio connections, use the CLI's stored credentials
|
|
993
|
+
if (this.isStdioConnection(request)) {
|
|
994
|
+
const isAuthenticated = await this.validateStoredCredentials();
|
|
995
|
+
if (isAuthenticated) {
|
|
996
|
+
this.ensureConnectionExists(clientId, 'stdio');
|
|
997
|
+
this.authenticateConnection(clientId);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
throw new Error('Authentication required. No valid credentials provided.');
|
|
1002
|
+
}
|
|
1003
|
+
// Validate provided credentials
|
|
1004
|
+
const isValid = await this.validateCredentials(authInfo.token, authInfo.vendorKey);
|
|
1005
|
+
if (!isValid) {
|
|
1006
|
+
throw new Error('Invalid credentials provided.');
|
|
1007
|
+
}
|
|
1008
|
+
// Add/update connection in pool and mark as authenticated
|
|
1009
|
+
const transport = this.determineTransport(request);
|
|
1010
|
+
this.ensureConnectionExists(clientId, transport, authInfo.clientInfo);
|
|
1011
|
+
this.authenticateConnection(clientId);
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Extract authentication information from request
|
|
1015
|
+
*/
|
|
1016
|
+
extractAuthInfo(request) {
|
|
1017
|
+
// Try to extract from various possible locations
|
|
1018
|
+
const headers = request.headers || {};
|
|
1019
|
+
const meta = request.meta || {};
|
|
1020
|
+
const params = request.params || {};
|
|
1021
|
+
return {
|
|
1022
|
+
token: headers.authorization?.replace('Bearer ', '') ||
|
|
1023
|
+
headers['x-auth-token'] ||
|
|
1024
|
+
meta.token ||
|
|
1025
|
+
params.token,
|
|
1026
|
+
vendorKey: headers['x-api-key'] ||
|
|
1027
|
+
headers['x-vendor-key'] ||
|
|
1028
|
+
meta.vendorKey ||
|
|
1029
|
+
params.vendorKey,
|
|
1030
|
+
clientInfo: {
|
|
1031
|
+
name: headers['x-client-name'] || meta.clientName || 'unknown',
|
|
1032
|
+
version: headers['x-client-version'] || meta.clientVersion || '1.0.0'
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Check if this is a stdio connection
|
|
1038
|
+
*/
|
|
1039
|
+
isStdioConnection(request) {
|
|
1040
|
+
// Stdio connections typically don't have HTTP-style headers
|
|
1041
|
+
return !request.headers || Object.keys(request.headers).length === 0;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Determine transport type from request
|
|
1045
|
+
*/
|
|
1046
|
+
determineTransport(request) {
|
|
1047
|
+
if (this.isStdioConnection(request)) {
|
|
1048
|
+
return 'stdio';
|
|
1049
|
+
}
|
|
1050
|
+
const headers = request.headers || {};
|
|
1051
|
+
if (headers.upgrade === 'websocket' || headers.connection?.includes('Upgrade')) {
|
|
1052
|
+
return 'websocket';
|
|
1053
|
+
}
|
|
1054
|
+
return 'http';
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Ensure connection exists in pool
|
|
1058
|
+
*/
|
|
1059
|
+
ensureConnectionExists(clientId, transport, clientInfo) {
|
|
1060
|
+
if (!this.connectionPool.has(clientId)) {
|
|
1061
|
+
const success = this.addConnection(clientId, transport, clientInfo);
|
|
1062
|
+
if (!success) {
|
|
1063
|
+
throw new Error('Maximum connections reached. Please try again later.');
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Validate stored CLI credentials
|
|
1069
|
+
*/
|
|
1070
|
+
async validateStoredCredentials() {
|
|
1071
|
+
try {
|
|
1072
|
+
return await this.config.validateStoredCredentials();
|
|
1073
|
+
}
|
|
1074
|
+
catch (error) {
|
|
1075
|
+
if (this.options.verbose) {
|
|
1076
|
+
console.log(chalk.yellow(`ā ļø Stored credentials validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
1077
|
+
}
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Validate provided credentials against the API
|
|
1083
|
+
*/
|
|
1084
|
+
async validateCredentials(token, vendorKey) {
|
|
1085
|
+
if (!token && !vendorKey) {
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
try {
|
|
1089
|
+
// Import axios dynamically to avoid circular dependency
|
|
1090
|
+
const axios = (await import('axios')).default;
|
|
1091
|
+
// Ensure service discovery is done
|
|
1092
|
+
await this.config.discoverServices();
|
|
1093
|
+
const authBase = this.config.getDiscoveredApiUrl();
|
|
1094
|
+
const headers = {
|
|
1095
|
+
'X-Project-Scope': 'lanonasis-maas'
|
|
1096
|
+
};
|
|
1097
|
+
if (vendorKey) {
|
|
1098
|
+
headers['X-API-Key'] = vendorKey;
|
|
1099
|
+
headers['X-Auth-Method'] = 'vendor_key';
|
|
1100
|
+
}
|
|
1101
|
+
else if (token) {
|
|
1102
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
1103
|
+
headers['X-Auth-Method'] = 'jwt';
|
|
1104
|
+
}
|
|
1105
|
+
// Validate against server with health endpoint
|
|
1106
|
+
await axios.get(`${authBase}/api/v1/health`, {
|
|
1107
|
+
headers,
|
|
1108
|
+
timeout: 10000
|
|
1109
|
+
});
|
|
1110
|
+
return true;
|
|
1111
|
+
}
|
|
1112
|
+
catch (error) {
|
|
1113
|
+
if (this.options.verbose) {
|
|
1114
|
+
console.log(chalk.yellow(`ā ļø Credential validation failed: ${error.response?.status || error.message}`));
|
|
1115
|
+
}
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Validate connection authentication status
|
|
1121
|
+
*/
|
|
1122
|
+
validateConnectionAuth(clientId) {
|
|
1123
|
+
const connection = this.getConnection(clientId);
|
|
1124
|
+
return connection ? connection.authenticated : false;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Get authentication status for all connections
|
|
1128
|
+
*/
|
|
1129
|
+
getAuthenticationStatus() {
|
|
1130
|
+
const stats = this.getConnectionPoolStats();
|
|
1131
|
+
return {
|
|
1132
|
+
totalConnections: stats.totalConnections,
|
|
1133
|
+
authenticatedConnections: stats.authenticatedConnections,
|
|
1134
|
+
unauthenticatedConnections: stats.totalConnections - stats.authenticatedConnections
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Transport protocol management methods
|
|
1139
|
+
*/
|
|
1140
|
+
/**
|
|
1141
|
+
* Check if a transport is available and working
|
|
1142
|
+
*/
|
|
1143
|
+
async checkTransportAvailability(transport) {
|
|
1144
|
+
try {
|
|
1145
|
+
switch (transport) {
|
|
1146
|
+
case 'stdio':
|
|
1147
|
+
// Stdio is always available if we can start the process
|
|
1148
|
+
return true;
|
|
1149
|
+
case 'websocket':
|
|
1150
|
+
// Check if WebSocket server can be started
|
|
1151
|
+
return await this.testWebSocketAvailability();
|
|
1152
|
+
case 'http':
|
|
1153
|
+
// Check if HTTP server can be started
|
|
1154
|
+
return await this.testHttpAvailability();
|
|
1155
|
+
default:
|
|
1156
|
+
return false;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
catch (error) {
|
|
1160
|
+
this.recordTransportFailure(transport, error);
|
|
1161
|
+
return false;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Test WebSocket transport availability
|
|
1166
|
+
*/
|
|
1167
|
+
async testWebSocketAvailability() {
|
|
1168
|
+
try {
|
|
1169
|
+
// This would typically involve checking if we can bind to a WebSocket port
|
|
1170
|
+
// For now, we'll assume it's available unless we have recorded failures
|
|
1171
|
+
const failures = this.transportFailures.get('websocket');
|
|
1172
|
+
if (failures && failures.count > 3) {
|
|
1173
|
+
const timeSinceLastFailure = Date.now() - failures.lastFailure.getTime();
|
|
1174
|
+
// Don't retry for 5 minutes after multiple failures
|
|
1175
|
+
if (timeSinceLastFailure < 5 * 60 * 1000) {
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return true;
|
|
1180
|
+
}
|
|
1181
|
+
catch (error) {
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Test HTTP transport availability
|
|
1187
|
+
*/
|
|
1188
|
+
async testHttpAvailability() {
|
|
1189
|
+
try {
|
|
1190
|
+
// This would typically involve checking if we can bind to an HTTP port
|
|
1191
|
+
// For now, we'll assume it's available unless we have recorded failures
|
|
1192
|
+
const failures = this.transportFailures.get('http');
|
|
1193
|
+
if (failures && failures.count > 3) {
|
|
1194
|
+
const timeSinceLastFailure = Date.now() - failures.lastFailure.getTime();
|
|
1195
|
+
// Don't retry for 5 minutes after multiple failures
|
|
1196
|
+
if (timeSinceLastFailure < 5 * 60 * 1000) {
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
catch (error) {
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Record a transport failure
|
|
1208
|
+
*/
|
|
1209
|
+
recordTransportFailure(transport, error) {
|
|
1210
|
+
const existing = this.transportFailures.get(transport);
|
|
1211
|
+
const failure = {
|
|
1212
|
+
count: existing ? existing.count + 1 : 1,
|
|
1213
|
+
lastFailure: new Date()
|
|
1214
|
+
};
|
|
1215
|
+
this.transportFailures.set(transport, failure);
|
|
1216
|
+
if (this.options.verbose) {
|
|
1217
|
+
console.log(chalk.yellow(`ā ļø Transport ${transport} failure #${failure.count}: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Get the best available transport
|
|
1222
|
+
*/
|
|
1223
|
+
async getBestAvailableTransport() {
|
|
1224
|
+
const preferred = this.options.preferredTransport || 'stdio';
|
|
1225
|
+
// Try preferred transport first
|
|
1226
|
+
if (await this.checkTransportAvailability(preferred)) {
|
|
1227
|
+
return preferred;
|
|
1228
|
+
}
|
|
1229
|
+
if (!this.enableFallback) {
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
// Try other transports in order of preference
|
|
1233
|
+
const fallbackOrder = this.supportedTransports.filter(t => t !== preferred);
|
|
1234
|
+
for (const transport of fallbackOrder) {
|
|
1235
|
+
if (await this.checkTransportAvailability(transport)) {
|
|
1236
|
+
if (this.options.verbose) {
|
|
1237
|
+
console.log(chalk.cyan(`š Falling back to ${transport} transport`));
|
|
1238
|
+
}
|
|
1239
|
+
return transport;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Handle transport-specific errors with clear messages
|
|
1246
|
+
*/
|
|
1247
|
+
handleTransportError(transport, error) {
|
|
1248
|
+
this.recordTransportFailure(transport, error);
|
|
1249
|
+
const baseMessage = `${transport.toUpperCase()} transport failed`;
|
|
1250
|
+
let specificMessage = '';
|
|
1251
|
+
let troubleshooting = '';
|
|
1252
|
+
switch (transport) {
|
|
1253
|
+
case 'stdio':
|
|
1254
|
+
if (error.code === 'ENOENT') {
|
|
1255
|
+
specificMessage = 'Server executable not found';
|
|
1256
|
+
troubleshooting = 'Ensure the MCP server is installed and accessible';
|
|
1257
|
+
}
|
|
1258
|
+
else if (error.code === 'EACCES') {
|
|
1259
|
+
specificMessage = 'Permission denied';
|
|
1260
|
+
troubleshooting = 'Check file permissions for the MCP server executable';
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
specificMessage = 'Process communication failed';
|
|
1264
|
+
troubleshooting = 'Check if the server process can be started';
|
|
1265
|
+
}
|
|
1266
|
+
break;
|
|
1267
|
+
case 'websocket':
|
|
1268
|
+
if (error.code === 'EADDRINUSE') {
|
|
1269
|
+
specificMessage = 'WebSocket port already in use';
|
|
1270
|
+
troubleshooting = 'Try a different port or stop the conflicting service';
|
|
1271
|
+
}
|
|
1272
|
+
else if (error.code === 'ECONNREFUSED') {
|
|
1273
|
+
specificMessage = 'WebSocket connection refused';
|
|
1274
|
+
troubleshooting = 'Check if the WebSocket server is running and accessible';
|
|
1275
|
+
}
|
|
1276
|
+
else if (error.code === 'ENOTFOUND') {
|
|
1277
|
+
specificMessage = 'WebSocket server not found';
|
|
1278
|
+
troubleshooting = 'Verify the WebSocket server URL is correct';
|
|
1279
|
+
}
|
|
1280
|
+
else {
|
|
1281
|
+
specificMessage = 'WebSocket connection failed';
|
|
1282
|
+
troubleshooting = 'Check network connectivity and firewall settings';
|
|
1283
|
+
}
|
|
1284
|
+
break;
|
|
1285
|
+
case 'http':
|
|
1286
|
+
if (error.code === 'EADDRINUSE') {
|
|
1287
|
+
specificMessage = 'HTTP port already in use';
|
|
1288
|
+
troubleshooting = 'Try a different port or stop the conflicting service';
|
|
1289
|
+
}
|
|
1290
|
+
else if (error.code === 'ECONNREFUSED') {
|
|
1291
|
+
specificMessage = 'HTTP connection refused';
|
|
1292
|
+
troubleshooting = 'Check if the HTTP server is running and accessible';
|
|
1293
|
+
}
|
|
1294
|
+
else if (error.response?.status) {
|
|
1295
|
+
specificMessage = `HTTP ${error.response.status} error`;
|
|
1296
|
+
troubleshooting = 'Check server status and authentication';
|
|
1297
|
+
}
|
|
1298
|
+
else {
|
|
1299
|
+
specificMessage = 'HTTP connection failed';
|
|
1300
|
+
troubleshooting = 'Check network connectivity and server availability';
|
|
1301
|
+
}
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
const fullMessage = `${baseMessage}: ${specificMessage}. ${troubleshooting}`;
|
|
1305
|
+
return new Error(fullMessage);
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Attempt to start server with transport fallback
|
|
1309
|
+
*/
|
|
1310
|
+
async startWithTransportFallback() {
|
|
1311
|
+
const availableTransport = await this.getBestAvailableTransport();
|
|
1312
|
+
if (!availableTransport) {
|
|
1313
|
+
const failureMessages = Array.from(this.transportFailures.entries())
|
|
1314
|
+
.map(([transport, failure]) => `${transport}: ${failure.count} failures`)
|
|
1315
|
+
.join(', ');
|
|
1316
|
+
throw new Error(`No available transports. All transports have failed: ${failureMessages}. ` +
|
|
1317
|
+
'Please check your configuration and network connectivity.');
|
|
1318
|
+
}
|
|
1319
|
+
try {
|
|
1320
|
+
await this.startTransport(availableTransport);
|
|
1321
|
+
return availableTransport;
|
|
1322
|
+
}
|
|
1323
|
+
catch (error) {
|
|
1324
|
+
const transportError = this.handleTransportError(availableTransport, error);
|
|
1325
|
+
if (this.enableFallback && this.supportedTransports.length > 1) {
|
|
1326
|
+
// Try next available transport
|
|
1327
|
+
const nextTransport = await this.getBestAvailableTransport();
|
|
1328
|
+
if (nextTransport && nextTransport !== availableTransport) {
|
|
1329
|
+
console.log(chalk.yellow(`ā ļø ${transportError.message}`));
|
|
1330
|
+
console.log(chalk.cyan(`š Attempting fallback to ${nextTransport} transport...`));
|
|
1331
|
+
try {
|
|
1332
|
+
await this.startTransport(nextTransport);
|
|
1333
|
+
return nextTransport;
|
|
1334
|
+
}
|
|
1335
|
+
catch (fallbackError) {
|
|
1336
|
+
const fallbackTransportError = this.handleTransportError(nextTransport, fallbackError);
|
|
1337
|
+
throw new Error(`Primary transport failed: ${transportError.message}. Fallback also failed: ${fallbackTransportError.message}`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
throw transportError;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Start a specific transport
|
|
1346
|
+
*/
|
|
1347
|
+
async startTransport(transport) {
|
|
1348
|
+
switch (transport) {
|
|
1349
|
+
case 'stdio':
|
|
1350
|
+
this.transport = new StdioServerTransport();
|
|
1351
|
+
await this.server.connect(this.transport);
|
|
1352
|
+
break;
|
|
1353
|
+
case 'websocket':
|
|
1354
|
+
// WebSocket transport would be implemented here
|
|
1355
|
+
// For now, we'll simulate it
|
|
1356
|
+
throw new Error('WebSocket transport not yet implemented');
|
|
1357
|
+
case 'http':
|
|
1358
|
+
// HTTP transport would be implemented here
|
|
1359
|
+
// For now, we'll simulate it
|
|
1360
|
+
throw new Error('HTTP transport not yet implemented');
|
|
1361
|
+
default:
|
|
1362
|
+
throw new Error(`Unsupported transport: ${transport}`);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Get transport status and statistics
|
|
1367
|
+
*/
|
|
1368
|
+
getTransportStatus() {
|
|
1369
|
+
const failures = {};
|
|
1370
|
+
for (const [transport, failure] of this.transportFailures.entries()) {
|
|
1371
|
+
failures[transport] = {
|
|
1372
|
+
count: failure.count,
|
|
1373
|
+
lastFailure: failure.lastFailure.toISOString()
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
return {
|
|
1377
|
+
supportedTransports: this.supportedTransports,
|
|
1378
|
+
preferredTransport: this.options.preferredTransport || 'stdio',
|
|
1379
|
+
enableFallback: this.enableFallback,
|
|
1380
|
+
transportFailures: failures
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Check if connection limit allows new connections
|
|
1385
|
+
*/
|
|
1386
|
+
canAcceptNewConnection() {
|
|
1387
|
+
return this.connectionPool.size < this.maxConnections;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Get connection by client ID
|
|
1391
|
+
*/
|
|
1392
|
+
getConnection(clientId) {
|
|
1393
|
+
return this.connectionPool.get(clientId);
|
|
638
1394
|
}
|
|
639
1395
|
/**
|
|
640
1396
|
* Setup error handling
|
|
@@ -658,20 +1414,54 @@ Please choose an option (1-4):`
|
|
|
658
1414
|
*/
|
|
659
1415
|
async start() {
|
|
660
1416
|
await this.initialize();
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1417
|
+
try {
|
|
1418
|
+
// Start server with transport fallback
|
|
1419
|
+
const activeTransport = await this.startWithTransportFallback();
|
|
1420
|
+
// Add the initial connection to the pool
|
|
1421
|
+
const initialClientId = this.generateClientId();
|
|
1422
|
+
this.addConnection(initialClientId, activeTransport, {
|
|
1423
|
+
name: `${activeTransport}-client`,
|
|
1424
|
+
version: '1.0.0'
|
|
1425
|
+
});
|
|
1426
|
+
if (this.options.verbose) {
|
|
1427
|
+
console.log(chalk.green('ā
Lanonasis MCP Server started'));
|
|
1428
|
+
console.log(chalk.gray(`Active transport: ${activeTransport}`));
|
|
1429
|
+
console.log(chalk.gray('Waiting for client connections...'));
|
|
1430
|
+
if (this.enableFallback) {
|
|
1431
|
+
console.log(chalk.gray('Transport fallback: enabled'));
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
// Keep the process alive
|
|
1435
|
+
process.stdin.resume();
|
|
1436
|
+
}
|
|
1437
|
+
catch (error) {
|
|
1438
|
+
console.error(chalk.red('ā Failed to start MCP Server:'));
|
|
1439
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
1440
|
+
if (this.options.verbose) {
|
|
1441
|
+
console.log(chalk.yellow('\nš§ Troubleshooting tips:'));
|
|
1442
|
+
console.log(chalk.cyan('⢠Check if all required dependencies are installed'));
|
|
1443
|
+
console.log(chalk.cyan('⢠Verify network connectivity and firewall settings'));
|
|
1444
|
+
console.log(chalk.cyan('⢠Try enabling transport fallback: --enable-fallback'));
|
|
1445
|
+
console.log(chalk.cyan('⢠Use --verbose for detailed error information'));
|
|
1446
|
+
const transportStatus = this.getTransportStatus();
|
|
1447
|
+
if (Object.keys(transportStatus.transportFailures).length > 0) {
|
|
1448
|
+
console.log(chalk.yellow('\nš Transport failure history:'));
|
|
1449
|
+
for (const [transport, failure] of Object.entries(transportStatus.transportFailures)) {
|
|
1450
|
+
console.log(chalk.gray(` ${transport}: ${failure.count} failures (last: ${failure.lastFailure})`));
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
throw error;
|
|
667
1455
|
}
|
|
668
|
-
// Keep the process alive
|
|
669
|
-
process.stdin.resume();
|
|
670
1456
|
}
|
|
671
1457
|
/**
|
|
672
1458
|
* Stop the server
|
|
673
1459
|
*/
|
|
674
1460
|
async stop() {
|
|
1461
|
+
// Stop connection cleanup
|
|
1462
|
+
this.stopConnectionCleanup();
|
|
1463
|
+
// Clear all connections
|
|
1464
|
+
this.connectionPool.clear();
|
|
675
1465
|
if (this.transport) {
|
|
676
1466
|
await this.server.close();
|
|
677
1467
|
this.transport = null;
|
|
@@ -686,6 +1476,12 @@ Please choose an option (1-4):`
|
|
|
686
1476
|
getServer() {
|
|
687
1477
|
return this.server;
|
|
688
1478
|
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Get connection pool for testing/monitoring
|
|
1481
|
+
*/
|
|
1482
|
+
getConnectionPool() {
|
|
1483
|
+
return new Map(this.connectionPool);
|
|
1484
|
+
}
|
|
689
1485
|
}
|
|
690
1486
|
// CLI entry point
|
|
691
1487
|
if (import.meta.url === `file://${process.argv[1]}`) {
|