@lanonasis/cli 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +357 -80
- package/dist/commands/auth.js +151 -1
- package/dist/commands/mcp.js +37 -30
- package/dist/commands/memory.js +78 -53
- package/dist/index-simple.js +189 -522
- package/dist/index.js +221 -327
- package/dist/utils/api.d.ts +2 -12
- package/dist/utils/api.js +0 -17
- package/dist/utils/completions.d.ts +28 -0
- package/dist/utils/completions.js +276 -0
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.js +15 -2
- package/dist/utils/formatting.d.ts +0 -2
- package/dist/utils/formatting.js +0 -13
- package/dist/utils/mcp-client.d.ts +6 -49
- package/dist/utils/mcp-client.js +82 -161
- package/dist/utils/mcp-client.test.d.ts +1 -0
- package/dist/utils/mcp-client.test.js +125 -0
- package/dist/utils/output.d.ts +23 -0
- package/dist/utils/output.js +97 -0
- package/dist/utils/websocket-mcp-client.d.ts +60 -0
- package/dist/utils/websocket-mcp-client.js +182 -0
- package/dist/utils/websocket-mcp-client.test.d.ts +1 -0
- package/dist/utils/websocket-mcp-client.test.js +126 -0
- package/package.json +10 -17
- package/dist/commands/api-keys.d.ts +0 -3
- package/dist/commands/api-keys.js +0 -812
- package/dist/mcp-server.d.ts +0 -2
- package/dist/mcp-server.js +0 -519
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket MCP Client for Enterprise Connections
|
|
3
|
+
* Connects to mcp.lanonasis.com WebSocket server for real-time MCP operations
|
|
4
|
+
*/
|
|
5
|
+
import WebSocket from 'ws';
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
export class WebSocketMCPClient extends EventEmitter {
|
|
9
|
+
ws = null;
|
|
10
|
+
url;
|
|
11
|
+
apiKey;
|
|
12
|
+
reconnectInterval;
|
|
13
|
+
maxReconnectAttempts;
|
|
14
|
+
timeout;
|
|
15
|
+
reconnectAttempts = 0;
|
|
16
|
+
isConnected = false;
|
|
17
|
+
messageId = 0;
|
|
18
|
+
pendingRequests = new Map();
|
|
19
|
+
constructor(options) {
|
|
20
|
+
super();
|
|
21
|
+
this.url = options.url || 'wss://mcp.lanonasis.com/mcp';
|
|
22
|
+
this.apiKey = options.apiKey;
|
|
23
|
+
this.reconnectInterval = options.reconnectInterval || 5000;
|
|
24
|
+
this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
|
|
25
|
+
this.timeout = options.timeout || 30000;
|
|
26
|
+
}
|
|
27
|
+
async connect() {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
try {
|
|
30
|
+
this.ws = new WebSocket(this.url, {
|
|
31
|
+
headers: {
|
|
32
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
33
|
+
'X-API-Key': this.apiKey
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
this.ws.on('open', () => {
|
|
37
|
+
this.isConnected = true;
|
|
38
|
+
this.reconnectAttempts = 0;
|
|
39
|
+
this.emit('connected');
|
|
40
|
+
// Send initialize message
|
|
41
|
+
this.sendRequest('initialize', {
|
|
42
|
+
protocolVersion: '2024-11-05',
|
|
43
|
+
capabilities: {
|
|
44
|
+
roots: { listChanged: true },
|
|
45
|
+
sampling: {}
|
|
46
|
+
},
|
|
47
|
+
clientInfo: {
|
|
48
|
+
name: '@lanonasis/cli',
|
|
49
|
+
version: '1.2.0'
|
|
50
|
+
}
|
|
51
|
+
}).then(() => {
|
|
52
|
+
resolve();
|
|
53
|
+
}).catch(reject);
|
|
54
|
+
});
|
|
55
|
+
this.ws.on('message', (data) => {
|
|
56
|
+
try {
|
|
57
|
+
const message = JSON.parse(data.toString());
|
|
58
|
+
this.handleMessage(message);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.error(chalk.red('Failed to parse WebSocket message:'), error);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
this.ws.on('close', (code, reason) => {
|
|
65
|
+
this.isConnected = false;
|
|
66
|
+
this.emit('disconnected', { code, reason: reason.toString() });
|
|
67
|
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
68
|
+
this.reconnectAttempts++;
|
|
69
|
+
console.log(chalk.yellow(`WebSocket disconnected. Reconnecting... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`));
|
|
70
|
+
setTimeout(() => this.connect(), this.reconnectInterval);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.error(chalk.red('Max reconnection attempts reached. Connection failed.'));
|
|
74
|
+
this.emit('error', new Error('Max reconnection attempts reached'));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
this.ws.on('error', (error) => {
|
|
78
|
+
console.error('Failed to parse message:', error instanceof Error ? error.message : 'Unknown error');
|
|
79
|
+
this.emit('error', error);
|
|
80
|
+
reject(error);
|
|
81
|
+
});
|
|
82
|
+
// Connection timeout
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
if (!this.isConnected) {
|
|
85
|
+
reject(new Error('WebSocket connection timeout'));
|
|
86
|
+
}
|
|
87
|
+
}, this.timeout);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
reject(error);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
handleMessage(message) {
|
|
95
|
+
if (message.id && this.pendingRequests.has(message.id)) {
|
|
96
|
+
const pending = this.pendingRequests.get(message.id);
|
|
97
|
+
if (pending) {
|
|
98
|
+
clearTimeout(pending.timeout);
|
|
99
|
+
this.pendingRequests.delete(message.id);
|
|
100
|
+
if (message.type === 'error' || message.error) {
|
|
101
|
+
pending.reject(new Error(message.error?.message || 'Unknown error'));
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
pending.resolve(message.result);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (message.type === 'notification') {
|
|
109
|
+
this.emit('notification', message);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async sendRequest(method, params) {
|
|
113
|
+
if (!this.isConnected) {
|
|
114
|
+
throw new Error('WebSocket not connected');
|
|
115
|
+
}
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const id = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
118
|
+
const request = {
|
|
119
|
+
id,
|
|
120
|
+
type: 'request',
|
|
121
|
+
method,
|
|
122
|
+
params: params || {}
|
|
123
|
+
};
|
|
124
|
+
const timeout = setTimeout(() => {
|
|
125
|
+
this.pendingRequests.delete(id);
|
|
126
|
+
reject(new Error(`Request timeout: ${method}`));
|
|
127
|
+
}, this.timeout);
|
|
128
|
+
this.pendingRequests.set(id, { resolve, reject, timeout });
|
|
129
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
130
|
+
this.ws.send(JSON.stringify(request));
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
this.pendingRequests.delete(id);
|
|
134
|
+
clearTimeout(timeout);
|
|
135
|
+
reject(new Error('WebSocket not connected'));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async listTools() {
|
|
140
|
+
const result = await this.sendRequest('tools/list');
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
async callTool(name, arguments_ = {}) {
|
|
144
|
+
const result = await this.sendRequest('tools/call', { name, arguments: arguments_ });
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
async deleteMemory(id) {
|
|
148
|
+
return this.callTool('delete_memory', { id });
|
|
149
|
+
}
|
|
150
|
+
async searchMemories(args) {
|
|
151
|
+
return this.callTool('search_memories', args);
|
|
152
|
+
}
|
|
153
|
+
async listResources() {
|
|
154
|
+
const result = await this.sendRequest('resources/list');
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
async getMemories(query) {
|
|
158
|
+
const result = await this.sendRequest('resources/read', { uri: query });
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
disconnect() {
|
|
162
|
+
if (this.ws) {
|
|
163
|
+
this.isConnected = false;
|
|
164
|
+
this.ws.close();
|
|
165
|
+
this.ws = null;
|
|
166
|
+
}
|
|
167
|
+
// Clear all pending requests
|
|
168
|
+
for (const [, pending] of this.pendingRequests) {
|
|
169
|
+
clearTimeout(pending.timeout);
|
|
170
|
+
pending.reject(new Error('Connection closed'));
|
|
171
|
+
}
|
|
172
|
+
this.pendingRequests.clear();
|
|
173
|
+
}
|
|
174
|
+
getConnectionStatus() {
|
|
175
|
+
return {
|
|
176
|
+
connected: this.isConnected,
|
|
177
|
+
url: this.url,
|
|
178
|
+
reconnectAttempts: this.reconnectAttempts,
|
|
179
|
+
pendingRequests: this.pendingRequests.size
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { WebSocketMCPClient } from './websocket-mcp-client';
|
|
3
|
+
import WebSocket from 'ws';
|
|
4
|
+
// Mock WebSocket for testing
|
|
5
|
+
class MockWebSocket extends WebSocket {
|
|
6
|
+
constructor(url) {
|
|
7
|
+
super(url);
|
|
8
|
+
// Override for testing
|
|
9
|
+
}
|
|
10
|
+
send(data) {
|
|
11
|
+
// Mock send implementation
|
|
12
|
+
this.emit('message', JSON.stringify({
|
|
13
|
+
id: '1',
|
|
14
|
+
type: 'response',
|
|
15
|
+
result: { success: true }
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
describe('WebSocketMCPClient', () => {
|
|
20
|
+
let client;
|
|
21
|
+
const mockOptions = {
|
|
22
|
+
url: 'ws://localhost:8081',
|
|
23
|
+
apiKey: 'test-api-key'
|
|
24
|
+
};
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
client = new WebSocketMCPClient(mockOptions);
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
if (client) {
|
|
30
|
+
client.disconnect();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
describe('constructor', () => {
|
|
34
|
+
it('should create client with correct options', () => {
|
|
35
|
+
expect(client).toBeDefined();
|
|
36
|
+
expect(client.isConnected()).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
it('should use default URL when not provided', () => {
|
|
39
|
+
const clientWithDefaults = new WebSocketMCPClient({ apiKey: 'test' });
|
|
40
|
+
expect(clientWithDefaults).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('connection management', () => {
|
|
44
|
+
it('should report disconnected state initially', () => {
|
|
45
|
+
expect(client.isConnected()).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it('should handle connection attempts', async () => {
|
|
48
|
+
// Mock successful connection
|
|
49
|
+
const connectPromise = client.connect();
|
|
50
|
+
// Simulate connection success
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
client.emit('connected');
|
|
53
|
+
}, 10);
|
|
54
|
+
await expect(connectPromise).resolves.toBe(true);
|
|
55
|
+
});
|
|
56
|
+
it('should handle disconnection', () => {
|
|
57
|
+
client.disconnect();
|
|
58
|
+
expect(client.isConnected()).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('message handling', () => {
|
|
62
|
+
it('should handle MCP request/response cycle', async () => {
|
|
63
|
+
// This would require more sophisticated mocking
|
|
64
|
+
// For now, test basic message structure
|
|
65
|
+
const message = {
|
|
66
|
+
id: '1',
|
|
67
|
+
type: 'request',
|
|
68
|
+
method: 'test_method',
|
|
69
|
+
params: { test: 'data' }
|
|
70
|
+
};
|
|
71
|
+
expect(message.type).toBe('request');
|
|
72
|
+
expect(message.method).toBe('test_method');
|
|
73
|
+
});
|
|
74
|
+
it('should generate unique message IDs', () => {
|
|
75
|
+
// Test ID generation logic
|
|
76
|
+
const client1 = new WebSocketMCPClient(mockOptions);
|
|
77
|
+
const client2 = new WebSocketMCPClient(mockOptions);
|
|
78
|
+
// Both should start with different internal state
|
|
79
|
+
expect(client1).not.toBe(client2);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('error handling', () => {
|
|
83
|
+
it('should handle connection errors gracefully', () => {
|
|
84
|
+
const errorHandler = jest.fn();
|
|
85
|
+
client.on('error', errorHandler);
|
|
86
|
+
// Simulate error
|
|
87
|
+
client.emit('error', new Error('Test error'));
|
|
88
|
+
expect(errorHandler).toHaveBeenCalledWith(new Error('Test error'));
|
|
89
|
+
});
|
|
90
|
+
it('should handle malformed messages', () => {
|
|
91
|
+
// Test error handling for invalid JSON
|
|
92
|
+
expect(() => {
|
|
93
|
+
JSON.parse('invalid json');
|
|
94
|
+
}).toThrow();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('MCP protocol compliance', () => {
|
|
98
|
+
it('should create valid MCP message structure', () => {
|
|
99
|
+
const message = {
|
|
100
|
+
id: '123',
|
|
101
|
+
type: 'request',
|
|
102
|
+
method: 'memory/create',
|
|
103
|
+
params: {
|
|
104
|
+
title: 'Test Memory',
|
|
105
|
+
content: 'Test content'
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
expect(message).toHaveProperty('id');
|
|
109
|
+
expect(message).toHaveProperty('type');
|
|
110
|
+
expect(message).toHaveProperty('method');
|
|
111
|
+
expect(message).toHaveProperty('params');
|
|
112
|
+
});
|
|
113
|
+
it('should handle MCP response format', () => {
|
|
114
|
+
const response = {
|
|
115
|
+
id: '123',
|
|
116
|
+
type: 'response',
|
|
117
|
+
result: {
|
|
118
|
+
memory_id: 'mem_123',
|
|
119
|
+
success: true
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
expect(response.type).toBe('response');
|
|
123
|
+
expect(response.result).toHaveProperty('success');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lanonasis/cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "dist/index
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "Lanonasis Unified CLI - Enterprise AI Infrastructure & Memory as a Service",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"lanonasis": "dist/index
|
|
8
|
-
"
|
|
9
|
-
"memory": "dist/index
|
|
10
|
-
"maas": "dist/index
|
|
7
|
+
"lanonasis": "dist/index.js",
|
|
8
|
+
"onasis": "dist/index.js",
|
|
9
|
+
"memory": "dist/index.js",
|
|
10
|
+
"maas": "dist/index.js"
|
|
11
11
|
},
|
|
12
12
|
"type": "module",
|
|
13
13
|
"scripts": {
|
|
14
14
|
"dev": "tsx src/index.ts",
|
|
15
|
-
"build": "tsc && chmod +x dist/index
|
|
15
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
16
16
|
"start": "node dist/index.js",
|
|
17
17
|
"test": "jest",
|
|
18
18
|
"lint": "eslint src/**/*.ts",
|
|
@@ -26,11 +26,7 @@
|
|
|
26
26
|
"maas",
|
|
27
27
|
"enterprise",
|
|
28
28
|
"infrastructure",
|
|
29
|
-
"orchestrator"
|
|
30
|
-
"api-keys",
|
|
31
|
-
"secrets",
|
|
32
|
-
"mcp",
|
|
33
|
-
"security"
|
|
29
|
+
"orchestrator"
|
|
34
30
|
],
|
|
35
31
|
"author": "Lanonasis (Seye Derick)",
|
|
36
32
|
"license": "MIT",
|
|
@@ -41,10 +37,8 @@
|
|
|
41
37
|
"dependencies": {
|
|
42
38
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
43
39
|
"@types/eventsource": "^1.1.15",
|
|
44
|
-
"@types/ws": "^8.18.1",
|
|
45
40
|
"axios": "^1.11.0",
|
|
46
41
|
"chalk": "^5.4.1",
|
|
47
|
-
"cli-table3": "^0.6.5",
|
|
48
42
|
"commander": "^12.1.0",
|
|
49
43
|
"date-fns": "^4.1.0",
|
|
50
44
|
"dotenv": "^17.2.1",
|
|
@@ -54,8 +48,7 @@
|
|
|
54
48
|
"open": "^10.2.0",
|
|
55
49
|
"ora": "^8.2.0",
|
|
56
50
|
"table": "^6.9.0",
|
|
57
|
-
"word-wrap": "^1.2.5"
|
|
58
|
-
"ws": "^8.18.3"
|
|
51
|
+
"word-wrap": "^1.2.5"
|
|
59
52
|
},
|
|
60
53
|
"devDependencies": {
|
|
61
54
|
"@types/inquirer": "^9.0.7",
|