@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.
@@ -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
- const result = await this.handleToolCall(name, args);
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
- * Register MCP resources
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
- * Handle tool calls
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
- // Create and connect transport
662
- this.transport = new StdioServerTransport();
663
- await this.server.connect(this.transport);
664
- if (this.options.verbose) {
665
- console.log(chalk.green('āœ… Lanonasis MCP Server started'));
666
- console.log(chalk.gray('Waiting for client connections...'));
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]}`) {