@lanonasis/cli 2.0.6 → 2.0.9
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/README.md +27 -4
- package/dist/commands/auth.js +13 -0
- package/dist/index-simple.js +6 -3
- package/dist/index.js +4 -1
- package/dist/mcp/client/enhanced-client.d.ts +116 -0
- package/dist/mcp/client/enhanced-client.js +379 -0
- package/dist/mcp/schemas/tool-schemas.d.ts +740 -0
- package/dist/mcp/schemas/tool-schemas.js +378 -0
- package/dist/mcp/server/lanonasis-server.d.ts +68 -0
- package/dist/mcp/server/lanonasis-server.js +696 -0
- package/dist/mcp/transports/transport-manager.d.ts +82 -0
- package/dist/mcp/transports/transport-manager.js +434 -0
- package/dist/utils/api.js +9 -4
- package/dist/utils/config.d.ts +3 -3
- package/dist/utils/config.js +16 -0
- package/dist/utils/mcp-client.d.ts +4 -0
- package/dist/utils/mcp-client.js +46 -35
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# @lanonasis/cli v2.0.
|
|
1
|
+
# @lanonasis/cli v2.0.9 - Enhanced MCP & Interactive CLI Experience
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@lanonasis/cli)
|
|
4
4
|
[](https://www.npmjs.com/package/@lanonasis/cli)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://api.lanonasis.com/.well-known/onasis.json)
|
|
7
7
|
|
|
8
|
-
🚀 **NEW IN v2.0**: Revolutionary interactive CLI experience with guided workflows, smart suggestions, achievement system, and power user mode. Professional command-line interface for LanOnasis Memory as a Service (MaaS) platform with **Golden Contract compliance**.
|
|
8
|
+
🚀 **NEW IN v2.0.9**: Advanced Model Context Protocol (MCP) support with multi-server connections, enhanced error handling, and enterprise-grade transport protocols. Revolutionary interactive CLI experience with guided workflows, smart suggestions, achievement system, and power user mode. Professional command-line interface for LanOnasis Memory as a Service (MaaS) platform with **Golden Contract compliance**.
|
|
9
9
|
|
|
10
10
|
## 🚀 Quick Start
|
|
11
11
|
|
|
@@ -152,13 +152,36 @@ onasis api-keys revoke <id> # Revoke API key
|
|
|
152
152
|
onasis api-keys rotate <id> # Rotate API key
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
### MCP Integration
|
|
155
|
+
### MCP Integration (Enhanced in v2.0.9)
|
|
156
156
|
```bash
|
|
157
|
-
|
|
157
|
+
# Connection Management
|
|
158
|
+
onasis mcp status # MCP server status with health info
|
|
158
159
|
onasis mcp connect --remote # Connect to remote MCP server
|
|
160
|
+
onasis mcp connect --local # Connect to local MCP server
|
|
159
161
|
onasis mcp disconnect # Disconnect from MCP
|
|
162
|
+
onasis mcp list-servers # List all connected servers
|
|
163
|
+
|
|
164
|
+
# Tool & Resource Management
|
|
160
165
|
onasis mcp tools # List available MCP tools
|
|
161
166
|
onasis mcp resources # List MCP resources
|
|
167
|
+
onasis mcp call <tool> --args # Execute MCP tool directly
|
|
168
|
+
|
|
169
|
+
# Advanced Features (v2.0.9)
|
|
170
|
+
onasis mcp health # Detailed health check with latency
|
|
171
|
+
onasis mcp server start # Start local MCP server
|
|
172
|
+
onasis mcp server stop # Stop local MCP server
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 🆕 MCP Server Mode (v2.0.9)
|
|
176
|
+
Run the CLI as a standalone MCP server:
|
|
177
|
+
```bash
|
|
178
|
+
# Start MCP server for IDE integrations
|
|
179
|
+
lanonasis-mcp-server --verbose
|
|
180
|
+
|
|
181
|
+
# Use with environment variables
|
|
182
|
+
LANONASIS_API_URL=https://api.lanonasis.com \
|
|
183
|
+
LANONASIS_TOKEN=your-token \
|
|
184
|
+
lanonasis-mcp-server
|
|
162
185
|
```
|
|
163
186
|
|
|
164
187
|
### Configuration Management
|
package/dist/commands/auth.js
CHANGED
|
@@ -21,11 +21,24 @@ export async function loginCommand(options) {
|
|
|
21
21
|
console.log(chalk.blue.bold('🔐 Onasis-Core Golden Contract Authentication'));
|
|
22
22
|
console.log(colors.info('━'.repeat(50)));
|
|
23
23
|
console.log();
|
|
24
|
+
// Debug: Check options
|
|
25
|
+
if (process.env.CLI_VERBOSE === 'true') {
|
|
26
|
+
console.log('Debug - Login options:', {
|
|
27
|
+
hasEmail: !!options.email,
|
|
28
|
+
hasPassword: !!options.password,
|
|
29
|
+
hasVendorKey: !!options.vendorKey
|
|
30
|
+
});
|
|
31
|
+
}
|
|
24
32
|
// Enhanced authentication flow - check for vendor key first
|
|
25
33
|
if (options.vendorKey) {
|
|
26
34
|
await handleVendorKeyAuth(options.vendorKey, config);
|
|
27
35
|
return;
|
|
28
36
|
}
|
|
37
|
+
// Check for email/password for direct credentials flow
|
|
38
|
+
if (options.email && options.password) {
|
|
39
|
+
await handleCredentialsFlow(options, config);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
29
42
|
// Show authentication options
|
|
30
43
|
const authChoice = await inquirer.prompt([
|
|
31
44
|
{
|
package/dist/index-simple.js
CHANGED
|
@@ -45,7 +45,7 @@ program
|
|
|
45
45
|
.alias(isOnasisInvocation ? 'lanonasis' : 'memory')
|
|
46
46
|
.alias(isOnasisInvocation ? 'memory' : 'maas')
|
|
47
47
|
.description(colors.info(`🧠 ${isOnasisInvocation ? 'Onasis-Core Golden Contract CLI' : 'LanOnasis Enterprise CLI'} - Memory as a Service, API Management & Infrastructure Orchestration`))
|
|
48
|
-
.version('2.0.
|
|
48
|
+
.version('2.0.6', '-v, --version', 'display version number')
|
|
49
49
|
.option('-V, --verbose', 'enable verbose logging')
|
|
50
50
|
.option('--api-url <url>', 'override API URL')
|
|
51
51
|
.option('--output <format>', 'output format (json, table, yaml)', 'table')
|
|
@@ -133,6 +133,8 @@ const healthCheck = async () => {
|
|
|
133
133
|
console.log(colors.primary('🏥 LanOnasis System Health Check'));
|
|
134
134
|
console.log(colors.info('═'.repeat(40)));
|
|
135
135
|
console.log();
|
|
136
|
+
// Initialize config
|
|
137
|
+
await cliConfig.init();
|
|
136
138
|
// Authentication status
|
|
137
139
|
process.stdout.write('Authentication status: ');
|
|
138
140
|
const isAuth = await cliConfig.isAuthenticated();
|
|
@@ -195,10 +197,11 @@ const healthCheck = async () => {
|
|
|
195
197
|
// Check if user is authenticated for protected commands
|
|
196
198
|
const requireAuth = (command) => {
|
|
197
199
|
command.hook('preAction', async () => {
|
|
200
|
+
await cliConfig.init();
|
|
198
201
|
const isAuthenticated = await cliConfig.isAuthenticated();
|
|
199
202
|
if (!isAuthenticated) {
|
|
200
203
|
console.error(chalk.red('✖ Authentication required'));
|
|
201
|
-
console.log(chalk.yellow('Please run:'), chalk.white('
|
|
204
|
+
console.log(chalk.yellow('Please run:'), chalk.white('lanonasis auth login'));
|
|
202
205
|
process.exit(1);
|
|
203
206
|
}
|
|
204
207
|
});
|
|
@@ -646,7 +649,7 @@ async function main() {
|
|
|
646
649
|
console.log(script);
|
|
647
650
|
return;
|
|
648
651
|
}
|
|
649
|
-
catch
|
|
652
|
+
catch {
|
|
650
653
|
console.error(colors.error('Failed to read completion script'));
|
|
651
654
|
process.exit(1);
|
|
652
655
|
}
|
package/dist/index.js
CHANGED
|
@@ -104,6 +104,8 @@ const healthCheck = async () => {
|
|
|
104
104
|
console.log(colors.primary('🏥 LanOnasis System Health Check'));
|
|
105
105
|
console.log(colors.info('═'.repeat(40)));
|
|
106
106
|
console.log();
|
|
107
|
+
// Initialize config
|
|
108
|
+
await cliConfig.init();
|
|
107
109
|
// Authentication status
|
|
108
110
|
process.stdout.write('Authentication status: ');
|
|
109
111
|
const isAuth = await cliConfig.isAuthenticated();
|
|
@@ -166,10 +168,11 @@ const healthCheck = async () => {
|
|
|
166
168
|
// Check if user is authenticated for protected commands
|
|
167
169
|
const requireAuth = (command) => {
|
|
168
170
|
command.hook('preAction', async () => {
|
|
171
|
+
await cliConfig.init();
|
|
169
172
|
const isAuthenticated = await cliConfig.isAuthenticated();
|
|
170
173
|
if (!isAuthenticated) {
|
|
171
174
|
console.error(chalk.red('✖ Authentication required'));
|
|
172
|
-
console.log(chalk.yellow('Please run:'), chalk.white('
|
|
175
|
+
console.log(chalk.yellow('Please run:'), chalk.white('lanonasis auth login'));
|
|
173
176
|
process.exit(1);
|
|
174
177
|
}
|
|
175
178
|
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced MCP Client with advanced features
|
|
3
|
+
* Provides multi-server support, connection pooling, and better error handling
|
|
4
|
+
*/
|
|
5
|
+
import { EventEmitter } from 'events';
|
|
6
|
+
export interface MCPServerConfig {
|
|
7
|
+
name: string;
|
|
8
|
+
url?: string;
|
|
9
|
+
command?: string;
|
|
10
|
+
args?: string[];
|
|
11
|
+
type: 'stdio' | 'http' | 'websocket';
|
|
12
|
+
timeout?: number;
|
|
13
|
+
maxRetries?: number;
|
|
14
|
+
priority?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ToolChain {
|
|
17
|
+
tools: Array<{
|
|
18
|
+
name: string;
|
|
19
|
+
args: Record<string, any>;
|
|
20
|
+
waitForCompletion?: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
mode: 'sequential' | 'parallel';
|
|
23
|
+
}
|
|
24
|
+
export interface ConnectionStatus {
|
|
25
|
+
server: string;
|
|
26
|
+
status: 'connected' | 'disconnected' | 'error' | 'connecting';
|
|
27
|
+
lastPing?: Date;
|
|
28
|
+
latency?: number;
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare class EnhancedMCPClient extends EventEmitter {
|
|
32
|
+
private clients;
|
|
33
|
+
private transports;
|
|
34
|
+
private connectionStatus;
|
|
35
|
+
private retryAttempts;
|
|
36
|
+
private healthCheckIntervals;
|
|
37
|
+
private spinner;
|
|
38
|
+
constructor();
|
|
39
|
+
/**
|
|
40
|
+
* Setup internal event handlers
|
|
41
|
+
*/
|
|
42
|
+
private setupEventHandlers;
|
|
43
|
+
/**
|
|
44
|
+
* Connect to multiple MCP servers
|
|
45
|
+
*/
|
|
46
|
+
connectMultiple(servers: MCPServerConfig[]): Promise<Map<string, boolean>>;
|
|
47
|
+
/**
|
|
48
|
+
* Connect to a single MCP server with retry logic
|
|
49
|
+
*/
|
|
50
|
+
connectSingle(config: MCPServerConfig): Promise<boolean>;
|
|
51
|
+
/**
|
|
52
|
+
* Create client with timeout
|
|
53
|
+
*/
|
|
54
|
+
private createClientWithTimeout;
|
|
55
|
+
/**
|
|
56
|
+
* Create MCP client based on config
|
|
57
|
+
*/
|
|
58
|
+
private createClient;
|
|
59
|
+
/**
|
|
60
|
+
* Create WebSocket transport
|
|
61
|
+
*/
|
|
62
|
+
private createWebSocketTransport;
|
|
63
|
+
/**
|
|
64
|
+
* Execute a chain of tools
|
|
65
|
+
*/
|
|
66
|
+
executeToolChain(chain: ToolChain): Promise<any[]>;
|
|
67
|
+
/**
|
|
68
|
+
* Execute a single tool with automatic server selection
|
|
69
|
+
*/
|
|
70
|
+
executeTool(toolName: string, args: Record<string, any>): Promise<any>;
|
|
71
|
+
/**
|
|
72
|
+
* Select the best server for a tool based on availability and latency
|
|
73
|
+
*/
|
|
74
|
+
private selectBestServer;
|
|
75
|
+
/**
|
|
76
|
+
* Select a failover server
|
|
77
|
+
*/
|
|
78
|
+
private selectFailoverServer;
|
|
79
|
+
/**
|
|
80
|
+
* Wait for tool completion (for async operations)
|
|
81
|
+
*/
|
|
82
|
+
private waitForToolCompletion;
|
|
83
|
+
/**
|
|
84
|
+
* Start health monitoring for a server
|
|
85
|
+
*/
|
|
86
|
+
private startHealthMonitoring;
|
|
87
|
+
/**
|
|
88
|
+
* Perform health check for a server
|
|
89
|
+
*/
|
|
90
|
+
private performHealthCheck;
|
|
91
|
+
/**
|
|
92
|
+
* Update connection status
|
|
93
|
+
*/
|
|
94
|
+
private updateConnectionStatus;
|
|
95
|
+
/**
|
|
96
|
+
* Get server configuration (placeholder - should be stored during connect)
|
|
97
|
+
*/
|
|
98
|
+
private getServerConfig;
|
|
99
|
+
/**
|
|
100
|
+
* Get all connection statuses
|
|
101
|
+
*/
|
|
102
|
+
getConnectionStatuses(): ConnectionStatus[];
|
|
103
|
+
/**
|
|
104
|
+
* Disconnect from all servers
|
|
105
|
+
*/
|
|
106
|
+
disconnectAll(): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Disconnect from a specific server
|
|
109
|
+
*/
|
|
110
|
+
disconnect(serverName: string): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Utility delay function
|
|
113
|
+
*/
|
|
114
|
+
private delay;
|
|
115
|
+
}
|
|
116
|
+
export declare const enhancedMCPClient: EnhancedMCPClient;
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced MCP Client with advanced features
|
|
3
|
+
* Provides multi-server support, connection pooling, and better error handling
|
|
4
|
+
*/
|
|
5
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
6
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
export class EnhancedMCPClient extends EventEmitter {
|
|
10
|
+
clients = new Map();
|
|
11
|
+
transports = new Map();
|
|
12
|
+
connectionStatus = new Map();
|
|
13
|
+
retryAttempts = new Map();
|
|
14
|
+
healthCheckIntervals = new Map();
|
|
15
|
+
spinner = null;
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
this.setupEventHandlers();
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Setup internal event handlers
|
|
22
|
+
*/
|
|
23
|
+
setupEventHandlers() {
|
|
24
|
+
this.on('connection:established', (server) => {
|
|
25
|
+
console.log(chalk.green(`✅ Connected to ${server}`));
|
|
26
|
+
});
|
|
27
|
+
this.on('connection:lost', (server) => {
|
|
28
|
+
console.log(chalk.yellow(`⚠️ Lost connection to ${server}`));
|
|
29
|
+
});
|
|
30
|
+
this.on('connection:error', (server, error) => {
|
|
31
|
+
console.log(chalk.red(`❌ Connection error for ${server}: ${error.message}`));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Connect to multiple MCP servers
|
|
36
|
+
*/
|
|
37
|
+
async connectMultiple(servers) {
|
|
38
|
+
const results = new Map();
|
|
39
|
+
// Sort servers by priority
|
|
40
|
+
const sortedServers = servers.sort((a, b) => (a.priority || 999) - (b.priority || 999));
|
|
41
|
+
// Connect in parallel with controlled concurrency
|
|
42
|
+
const connectionPromises = sortedServers.map(server => this.connectSingle(server).then(success => {
|
|
43
|
+
results.set(server.name, success);
|
|
44
|
+
return { server: server.name, success };
|
|
45
|
+
}));
|
|
46
|
+
await Promise.allSettled(connectionPromises);
|
|
47
|
+
// Start health monitoring for connected servers
|
|
48
|
+
for (const [name, success] of results) {
|
|
49
|
+
if (success) {
|
|
50
|
+
this.startHealthMonitoring(name);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return results;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Connect to a single MCP server with retry logic
|
|
57
|
+
*/
|
|
58
|
+
async connectSingle(config) {
|
|
59
|
+
const maxRetries = config.maxRetries || 3;
|
|
60
|
+
const timeout = config.timeout || 30000;
|
|
61
|
+
let attempts = 0;
|
|
62
|
+
while (attempts < maxRetries) {
|
|
63
|
+
try {
|
|
64
|
+
this.updateConnectionStatus(config.name, 'connecting');
|
|
65
|
+
const client = await this.createClientWithTimeout(config, timeout);
|
|
66
|
+
this.clients.set(config.name, client);
|
|
67
|
+
this.updateConnectionStatus(config.name, 'connected');
|
|
68
|
+
this.emit('connection:established', config.name);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
attempts++;
|
|
73
|
+
this.retryAttempts.set(config.name, attempts);
|
|
74
|
+
if (attempts >= maxRetries) {
|
|
75
|
+
this.updateConnectionStatus(config.name, 'error', error);
|
|
76
|
+
this.emit('connection:error', config.name, error);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
// Exponential backoff
|
|
80
|
+
const delay = Math.min(1000 * Math.pow(2, attempts), 10000);
|
|
81
|
+
console.log(chalk.yellow(`⏳ Retrying connection to ${config.name} in ${delay}ms...`));
|
|
82
|
+
await this.delay(delay);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Create client with timeout
|
|
89
|
+
*/
|
|
90
|
+
async createClientWithTimeout(config, timeout) {
|
|
91
|
+
return Promise.race([
|
|
92
|
+
this.createClient(config),
|
|
93
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Connection timeout after ${timeout}ms`)), timeout))
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Create MCP client based on config
|
|
98
|
+
*/
|
|
99
|
+
async createClient(config) {
|
|
100
|
+
let transport;
|
|
101
|
+
switch (config.type) {
|
|
102
|
+
case 'stdio':
|
|
103
|
+
if (!config.command) {
|
|
104
|
+
throw new Error('Command required for stdio transport');
|
|
105
|
+
}
|
|
106
|
+
transport = new StdioClientTransport({
|
|
107
|
+
command: config.command,
|
|
108
|
+
args: config.args || []
|
|
109
|
+
});
|
|
110
|
+
break;
|
|
111
|
+
case 'http':
|
|
112
|
+
if (!config.url) {
|
|
113
|
+
throw new Error('URL required for http transport');
|
|
114
|
+
}
|
|
115
|
+
// HTTP transport is not directly supported by MCP SDK
|
|
116
|
+
// Use stdio or websocket instead
|
|
117
|
+
throw new Error('HTTP transport not directly supported. Use websocket or stdio transport.');
|
|
118
|
+
break;
|
|
119
|
+
case 'websocket':
|
|
120
|
+
if (!config.url) {
|
|
121
|
+
throw new Error('URL required for websocket transport');
|
|
122
|
+
}
|
|
123
|
+
transport = await this.createWebSocketTransport(config.url);
|
|
124
|
+
break;
|
|
125
|
+
default:
|
|
126
|
+
throw new Error(`Unsupported transport type: ${config.type}`);
|
|
127
|
+
}
|
|
128
|
+
this.transports.set(config.name, transport);
|
|
129
|
+
const client = new Client({
|
|
130
|
+
name: `lanonasis-cli-${config.name}`,
|
|
131
|
+
version: '2.0.8'
|
|
132
|
+
}, {
|
|
133
|
+
capabilities: {
|
|
134
|
+
tools: {},
|
|
135
|
+
resources: {},
|
|
136
|
+
prompts: {}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
await client.connect(transport);
|
|
140
|
+
return client;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Create WebSocket transport
|
|
144
|
+
*/
|
|
145
|
+
async createWebSocketTransport(url) {
|
|
146
|
+
// Custom WebSocket transport implementation
|
|
147
|
+
const WebSocket = (await import('ws')).default;
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
const ws = new WebSocket(url);
|
|
150
|
+
ws.on('open', () => {
|
|
151
|
+
resolve({
|
|
152
|
+
send: (data) => ws.send(JSON.stringify(data)),
|
|
153
|
+
on: (event, handler) => ws.on(event, handler),
|
|
154
|
+
close: () => ws.close()
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
ws.on('error', reject);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Execute a chain of tools
|
|
162
|
+
*/
|
|
163
|
+
async executeToolChain(chain) {
|
|
164
|
+
const results = [];
|
|
165
|
+
if (chain.mode === 'sequential') {
|
|
166
|
+
for (const tool of chain.tools) {
|
|
167
|
+
const result = await this.executeTool(tool.name, tool.args);
|
|
168
|
+
results.push(result);
|
|
169
|
+
if (tool.waitForCompletion) {
|
|
170
|
+
await this.waitForToolCompletion(tool.name, result);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// Parallel execution
|
|
176
|
+
const promises = chain.tools.map(tool => this.executeTool(tool.name, tool.args));
|
|
177
|
+
const parallelResults = await Promise.allSettled(promises);
|
|
178
|
+
parallelResults.forEach((result, index) => {
|
|
179
|
+
if (result.status === 'fulfilled') {
|
|
180
|
+
results.push(result.value);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
results.push({ error: result.reason, tool: chain.tools[index].name });
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return results;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Execute a single tool with automatic server selection
|
|
191
|
+
*/
|
|
192
|
+
async executeTool(toolName, args) {
|
|
193
|
+
// Find the best available server for this tool
|
|
194
|
+
const server = await this.selectBestServer(toolName);
|
|
195
|
+
if (!server) {
|
|
196
|
+
throw new Error(`No available server for tool: ${toolName}`);
|
|
197
|
+
}
|
|
198
|
+
const client = this.clients.get(server);
|
|
199
|
+
if (!client) {
|
|
200
|
+
throw new Error(`Client not found for server: ${server}`);
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const result = await client.callTool({
|
|
204
|
+
name: toolName,
|
|
205
|
+
arguments: args
|
|
206
|
+
});
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
// Try failover to another server
|
|
211
|
+
const failoverServer = await this.selectFailoverServer(server, toolName);
|
|
212
|
+
if (failoverServer) {
|
|
213
|
+
console.log(chalk.yellow(`⚠️ Failing over to ${failoverServer}...`));
|
|
214
|
+
const failoverClient = this.clients.get(failoverServer);
|
|
215
|
+
if (failoverClient) {
|
|
216
|
+
return failoverClient.callTool({
|
|
217
|
+
name: toolName,
|
|
218
|
+
arguments: args
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Select the best server for a tool based on availability and latency
|
|
227
|
+
*/
|
|
228
|
+
async selectBestServer(toolName) {
|
|
229
|
+
const availableServers = Array.from(this.clients.keys()).filter(name => {
|
|
230
|
+
const status = this.connectionStatus.get(name);
|
|
231
|
+
return status?.status === 'connected';
|
|
232
|
+
});
|
|
233
|
+
if (availableServers.length === 0) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
// For now, return the first available server
|
|
237
|
+
// TODO: Implement smarter selection based on tool availability and latency
|
|
238
|
+
return availableServers[0];
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Select a failover server
|
|
242
|
+
*/
|
|
243
|
+
async selectFailoverServer(excludeServer, toolName) {
|
|
244
|
+
const availableServers = Array.from(this.clients.keys()).filter(name => {
|
|
245
|
+
const status = this.connectionStatus.get(name);
|
|
246
|
+
return name !== excludeServer && status?.status === 'connected';
|
|
247
|
+
});
|
|
248
|
+
return availableServers.length > 0 ? availableServers[0] : null;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Wait for tool completion (for async operations)
|
|
252
|
+
*/
|
|
253
|
+
async waitForToolCompletion(toolName, initialResult) {
|
|
254
|
+
// Implementation depends on the specific tool
|
|
255
|
+
// This is a placeholder for tools that return operation IDs
|
|
256
|
+
if (initialResult.operationId) {
|
|
257
|
+
let completed = false;
|
|
258
|
+
let attempts = 0;
|
|
259
|
+
const maxAttempts = 60; // 1 minute with 1 second intervals
|
|
260
|
+
while (!completed && attempts < maxAttempts) {
|
|
261
|
+
await this.delay(1000);
|
|
262
|
+
// Check operation status (implementation specific)
|
|
263
|
+
// completed = await this.checkOperationStatus(initialResult.operationId);
|
|
264
|
+
attempts++;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Start health monitoring for a server
|
|
270
|
+
*/
|
|
271
|
+
startHealthMonitoring(serverName) {
|
|
272
|
+
// Clear existing interval if any
|
|
273
|
+
const existingInterval = this.healthCheckIntervals.get(serverName);
|
|
274
|
+
if (existingInterval) {
|
|
275
|
+
clearInterval(existingInterval);
|
|
276
|
+
}
|
|
277
|
+
const interval = setInterval(async () => {
|
|
278
|
+
await this.performHealthCheck(serverName);
|
|
279
|
+
}, 30000); // Check every 30 seconds
|
|
280
|
+
this.healthCheckIntervals.set(serverName, interval);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Perform health check for a server
|
|
284
|
+
*/
|
|
285
|
+
async performHealthCheck(serverName) {
|
|
286
|
+
const client = this.clients.get(serverName);
|
|
287
|
+
if (!client)
|
|
288
|
+
return;
|
|
289
|
+
const startTime = Date.now();
|
|
290
|
+
try {
|
|
291
|
+
// Use a simple tool call as a health check
|
|
292
|
+
await client.listTools();
|
|
293
|
+
const latency = Date.now() - startTime;
|
|
294
|
+
this.updateConnectionStatus(serverName, 'connected', undefined, latency);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
this.updateConnectionStatus(serverName, 'error', error);
|
|
298
|
+
// Attempt reconnection
|
|
299
|
+
const config = this.getServerConfig(serverName);
|
|
300
|
+
if (config) {
|
|
301
|
+
console.log(chalk.yellow(`⚠️ Attempting to reconnect to ${serverName}...`));
|
|
302
|
+
await this.connectSingle(config);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Update connection status
|
|
308
|
+
*/
|
|
309
|
+
updateConnectionStatus(server, status, error, latency) {
|
|
310
|
+
this.connectionStatus.set(server, {
|
|
311
|
+
server,
|
|
312
|
+
status,
|
|
313
|
+
lastPing: new Date(),
|
|
314
|
+
latency,
|
|
315
|
+
error: error?.message
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Get server configuration (placeholder - should be stored during connect)
|
|
320
|
+
*/
|
|
321
|
+
getServerConfig(serverName) {
|
|
322
|
+
// TODO: Store and retrieve server configs
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Get all connection statuses
|
|
327
|
+
*/
|
|
328
|
+
getConnectionStatuses() {
|
|
329
|
+
return Array.from(this.connectionStatus.values());
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Disconnect from all servers
|
|
333
|
+
*/
|
|
334
|
+
async disconnectAll() {
|
|
335
|
+
// Stop all health monitoring
|
|
336
|
+
for (const interval of this.healthCheckIntervals.values()) {
|
|
337
|
+
clearInterval(interval);
|
|
338
|
+
}
|
|
339
|
+
this.healthCheckIntervals.clear();
|
|
340
|
+
// Disconnect all clients
|
|
341
|
+
for (const [name, client] of this.clients) {
|
|
342
|
+
try {
|
|
343
|
+
await client.close();
|
|
344
|
+
console.log(chalk.gray(`Disconnected from ${name}`));
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
console.log(chalk.yellow(`Warning: Error disconnecting from ${name}`));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
this.clients.clear();
|
|
351
|
+
this.transports.clear();
|
|
352
|
+
this.connectionStatus.clear();
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Disconnect from a specific server
|
|
356
|
+
*/
|
|
357
|
+
async disconnect(serverName) {
|
|
358
|
+
const interval = this.healthCheckIntervals.get(serverName);
|
|
359
|
+
if (interval) {
|
|
360
|
+
clearInterval(interval);
|
|
361
|
+
this.healthCheckIntervals.delete(serverName);
|
|
362
|
+
}
|
|
363
|
+
const client = this.clients.get(serverName);
|
|
364
|
+
if (client) {
|
|
365
|
+
await client.close();
|
|
366
|
+
this.clients.delete(serverName);
|
|
367
|
+
}
|
|
368
|
+
this.transports.delete(serverName);
|
|
369
|
+
this.connectionStatus.delete(serverName);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Utility delay function
|
|
373
|
+
*/
|
|
374
|
+
delay(ms) {
|
|
375
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Export singleton instance
|
|
379
|
+
export const enhancedMCPClient = new EnhancedMCPClient();
|