@lanonasis/cli 1.2.2 → 1.4.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/dist/commands/mcp.js +31 -12
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +519 -0
- package/dist/utils/mcp-client.d.ts +50 -1
- package/dist/utils/mcp-client.js +149 -31
- package/package.json +5 -2
package/dist/commands/mcp.js
CHANGED
|
@@ -54,41 +54,60 @@ export function mcpCommands(program) {
|
|
|
54
54
|
});
|
|
55
55
|
// Connect command
|
|
56
56
|
mcp.command('connect')
|
|
57
|
-
.description('Connect to MCP server (local or
|
|
57
|
+
.description('Connect to MCP server (local, remote, or WebSocket)')
|
|
58
58
|
.option('-l, --local', 'Connect to local MCP server')
|
|
59
59
|
.option('-r, --remote', 'Connect to remote MCP server (api.lanonasis.com)')
|
|
60
|
+
.option('-w, --websocket', 'Connect using WebSocket mode for enterprise users')
|
|
60
61
|
.option('-s, --server <path>', 'Local MCP server path')
|
|
61
|
-
.option('-u, --url <url>', 'Remote
|
|
62
|
+
.option('-u, --url <url>', 'Remote/WebSocket server URL')
|
|
62
63
|
.action(async (options) => {
|
|
63
64
|
const spinner = ora('Connecting to MCP server...').start();
|
|
64
65
|
const config = new CLIConfig();
|
|
65
66
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
67
|
+
let connectionMode;
|
|
68
|
+
// Determine connection mode - WebSocket takes precedence over remote and local
|
|
69
|
+
if (options.websocket) {
|
|
70
|
+
connectionMode = 'websocket';
|
|
71
|
+
}
|
|
72
|
+
else if (options.remote) {
|
|
73
|
+
connectionMode = 'remote';
|
|
74
|
+
}
|
|
75
|
+
else if (options.local) {
|
|
76
|
+
connectionMode = 'local';
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
69
79
|
// Default to remote if authenticated, otherwise local
|
|
70
|
-
|
|
80
|
+
connectionMode = !!config.get('token') ? 'remote' : 'local';
|
|
71
81
|
}
|
|
72
|
-
// Save
|
|
73
|
-
config.set('
|
|
82
|
+
// Save preferences
|
|
83
|
+
config.set('mcpConnectionMode', connectionMode);
|
|
74
84
|
if (options.server) {
|
|
75
85
|
config.set('mcpServerPath', options.server);
|
|
76
86
|
}
|
|
77
87
|
if (options.url) {
|
|
78
|
-
|
|
88
|
+
if (connectionMode === 'websocket') {
|
|
89
|
+
config.set('mcpWebSocketUrl', options.url);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
config.set('mcpServerUrl', options.url);
|
|
93
|
+
}
|
|
79
94
|
}
|
|
80
95
|
const client = getMCPClient();
|
|
81
96
|
const connected = await client.connect({
|
|
82
|
-
|
|
97
|
+
connectionMode,
|
|
83
98
|
serverPath: options.server,
|
|
84
99
|
serverUrl: options.url
|
|
85
100
|
});
|
|
86
101
|
if (connected) {
|
|
87
|
-
spinner.succeed(chalk.green(`Connected to
|
|
88
|
-
if (
|
|
102
|
+
spinner.succeed(chalk.green(`Connected to MCP server in ${connectionMode} mode`));
|
|
103
|
+
if (connectionMode === 'remote') {
|
|
89
104
|
console.log(chalk.cyan('ℹ️ Using remote MCP via api.lanonasis.com'));
|
|
90
105
|
console.log(chalk.cyan('📡 SSE endpoint active for real-time updates'));
|
|
91
106
|
}
|
|
107
|
+
else if (connectionMode === 'websocket') {
|
|
108
|
+
console.log(chalk.cyan('ℹ️ Using enterprise WebSocket MCP server'));
|
|
109
|
+
console.log(chalk.cyan('📡 WebSocket connection active with auto-reconnect'));
|
|
110
|
+
}
|
|
92
111
|
}
|
|
93
112
|
else {
|
|
94
113
|
spinner.fail('Failed to connect to MCP server');
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { CLIConfig } from './utils/config.js';
|
|
6
|
+
// MCP Protocol Compliance: Redirect all console output to stderr
|
|
7
|
+
// This prevents stdout pollution which breaks JSON-RPC communication
|
|
8
|
+
const originalConsoleError = console.error;
|
|
9
|
+
// Silent mode for MCP protocol compliance
|
|
10
|
+
const isSilentMode = process.env.LANONASIS_SILENT === 'true' || process.argv.includes('--silent');
|
|
11
|
+
if (isSilentMode) {
|
|
12
|
+
// Completely silence all output except JSON-RPC
|
|
13
|
+
console.log = () => { };
|
|
14
|
+
console.error = () => { };
|
|
15
|
+
console.warn = () => { };
|
|
16
|
+
console.info = () => { };
|
|
17
|
+
console.debug = () => { };
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
// Redirect to stderr for debugging
|
|
21
|
+
console.log = (...args) => originalConsoleError('[MCP-LOG]', ...args);
|
|
22
|
+
console.error = (...args) => originalConsoleError('[MCP-ERROR]', ...args);
|
|
23
|
+
console.warn = (...args) => originalConsoleError('[MCP-WARN]', ...args);
|
|
24
|
+
console.info = (...args) => originalConsoleError('[MCP-INFO]', ...args);
|
|
25
|
+
}
|
|
26
|
+
// Disable colors and verbose output for MCP protocol compliance
|
|
27
|
+
process.env.FORCE_COLOR = '0';
|
|
28
|
+
process.env.DEBUG = '';
|
|
29
|
+
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
|
|
30
|
+
class LanonasisMCPServer {
|
|
31
|
+
server;
|
|
32
|
+
config;
|
|
33
|
+
constructor() {
|
|
34
|
+
this.config = new CLIConfig();
|
|
35
|
+
this.server = new Server({
|
|
36
|
+
name: 'lanonasis-mcp-server',
|
|
37
|
+
version: '1.3.0',
|
|
38
|
+
}, {
|
|
39
|
+
capabilities: {
|
|
40
|
+
tools: {},
|
|
41
|
+
resources: {},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
this.setupHandlers();
|
|
45
|
+
}
|
|
46
|
+
setupHandlers() {
|
|
47
|
+
// List available tools - Comprehensive MCP toolset matching legacy CLI
|
|
48
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
49
|
+
return {
|
|
50
|
+
tools: [
|
|
51
|
+
// Memory Management Tools
|
|
52
|
+
{
|
|
53
|
+
name: 'create_memory',
|
|
54
|
+
description: 'Create a new memory entry with vector embedding',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
title: { type: 'string', description: 'Memory title' },
|
|
59
|
+
content: { type: 'string', description: 'Memory content' },
|
|
60
|
+
memory_type: { type: 'string', description: 'Type of memory', enum: ['context', 'project', 'knowledge', 'reference', 'personal', 'workflow'] },
|
|
61
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Memory tags' },
|
|
62
|
+
topic_id: { type: 'string', description: 'Topic ID for organization' }
|
|
63
|
+
},
|
|
64
|
+
required: ['title', 'content']
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'search_memories',
|
|
69
|
+
description: 'Search through memories with semantic vector search',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
query: { type: 'string', description: 'Search query' },
|
|
74
|
+
memory_type: { type: 'string', description: 'Filter by memory type' },
|
|
75
|
+
limit: { type: 'number', description: 'Maximum results to return', default: 10 },
|
|
76
|
+
threshold: { type: 'number', description: 'Similarity threshold (0.0-1.0)', default: 0.7 },
|
|
77
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags' }
|
|
78
|
+
},
|
|
79
|
+
required: ['query']
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'get_memory',
|
|
84
|
+
description: 'Get a specific memory by ID',
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
id: { type: 'string', description: 'Memory ID' }
|
|
89
|
+
},
|
|
90
|
+
required: ['id']
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'update_memory',
|
|
95
|
+
description: 'Update an existing memory',
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
id: { type: 'string', description: 'Memory ID' },
|
|
100
|
+
title: { type: 'string', description: 'Memory title' },
|
|
101
|
+
content: { type: 'string', description: 'Memory content' },
|
|
102
|
+
memory_type: { type: 'string', description: 'Type of memory' },
|
|
103
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Memory tags' }
|
|
104
|
+
},
|
|
105
|
+
required: ['id']
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'delete_memory',
|
|
110
|
+
description: 'Delete a memory by ID',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
id: { type: 'string', description: 'Memory ID' }
|
|
115
|
+
},
|
|
116
|
+
required: ['id']
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'list_memories',
|
|
121
|
+
description: 'List memories with pagination and filters',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
limit: { type: 'number', description: 'Number of memories to return', default: 20 },
|
|
126
|
+
offset: { type: 'number', description: 'Offset for pagination', default: 0 },
|
|
127
|
+
memory_type: { type: 'string', description: 'Filter by memory type' },
|
|
128
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags' }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
// API Key Management Tools
|
|
133
|
+
{
|
|
134
|
+
name: 'create_api_key',
|
|
135
|
+
description: 'Create a new API key',
|
|
136
|
+
inputSchema: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
name: { type: 'string', description: 'API key name' },
|
|
140
|
+
description: { type: 'string', description: 'API key description' },
|
|
141
|
+
project_id: { type: 'string', description: 'Project ID' },
|
|
142
|
+
access_level: { type: 'string', description: 'Access level', enum: ['public', 'authenticated', 'team', 'admin', 'enterprise'] },
|
|
143
|
+
expires_in_days: { type: 'number', description: 'Expiration in days', default: 365 }
|
|
144
|
+
},
|
|
145
|
+
required: ['name']
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'list_api_keys',
|
|
150
|
+
description: 'List API keys',
|
|
151
|
+
inputSchema: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
properties: {
|
|
154
|
+
project_id: { type: 'string', description: 'Filter by project ID' },
|
|
155
|
+
active_only: { type: 'boolean', description: 'Show only active keys', default: true }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'rotate_api_key',
|
|
161
|
+
description: 'Rotate an API key',
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
key_id: { type: 'string', description: 'API key ID to rotate' }
|
|
166
|
+
},
|
|
167
|
+
required: ['key_id']
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'delete_api_key',
|
|
172
|
+
description: 'Delete an API key',
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: 'object',
|
|
175
|
+
properties: {
|
|
176
|
+
key_id: { type: 'string', description: 'API key ID to delete' }
|
|
177
|
+
},
|
|
178
|
+
required: ['key_id']
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
// Project Management Tools
|
|
182
|
+
{
|
|
183
|
+
name: 'create_project',
|
|
184
|
+
description: 'Create a new project',
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: 'object',
|
|
187
|
+
properties: {
|
|
188
|
+
name: { type: 'string', description: 'Project name' },
|
|
189
|
+
description: { type: 'string', description: 'Project description' },
|
|
190
|
+
organization_id: { type: 'string', description: 'Organization ID' }
|
|
191
|
+
},
|
|
192
|
+
required: ['name']
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: 'list_projects',
|
|
197
|
+
description: 'List projects',
|
|
198
|
+
inputSchema: {
|
|
199
|
+
type: 'object',
|
|
200
|
+
properties: {
|
|
201
|
+
organization_id: { type: 'string', description: 'Filter by organization ID' }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
// Organization Management Tools
|
|
206
|
+
{
|
|
207
|
+
name: 'get_organization_info',
|
|
208
|
+
description: 'Get organization information',
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
// Authentication Tools
|
|
215
|
+
{
|
|
216
|
+
name: 'get_auth_status',
|
|
217
|
+
description: 'Get authentication status',
|
|
218
|
+
inputSchema: {
|
|
219
|
+
type: 'object',
|
|
220
|
+
properties: {}
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
// Configuration Tools
|
|
224
|
+
{
|
|
225
|
+
name: 'get_config',
|
|
226
|
+
description: 'Get configuration settings',
|
|
227
|
+
inputSchema: {
|
|
228
|
+
type: 'object',
|
|
229
|
+
properties: {
|
|
230
|
+
key: { type: 'string', description: 'Specific config key to retrieve' }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'set_config',
|
|
236
|
+
description: 'Set configuration setting',
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
key: { type: 'string', description: 'Configuration key' },
|
|
241
|
+
value: { type: 'string', description: 'Configuration value' }
|
|
242
|
+
},
|
|
243
|
+
required: ['key', 'value']
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
// Health and Status Tools
|
|
247
|
+
{
|
|
248
|
+
name: 'get_health_status',
|
|
249
|
+
description: 'Get system health status',
|
|
250
|
+
inputSchema: {
|
|
251
|
+
type: 'object',
|
|
252
|
+
properties: {}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
// Handle tool calls - Comprehensive implementation
|
|
259
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
260
|
+
const { name, arguments: args } = request.params;
|
|
261
|
+
try {
|
|
262
|
+
const apiKey = process.env.LANONASIS_API_KEY;
|
|
263
|
+
const apiUrl = process.env.LANONASIS_API_URL || 'https://api.lanonasis.com';
|
|
264
|
+
if (!apiKey) {
|
|
265
|
+
return {
|
|
266
|
+
content: [
|
|
267
|
+
{
|
|
268
|
+
type: 'text',
|
|
269
|
+
text: 'Error: LANONASIS_API_KEY environment variable is required'
|
|
270
|
+
}
|
|
271
|
+
]
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const headers = {
|
|
275
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
276
|
+
'Content-Type': 'application/json',
|
|
277
|
+
'User-Agent': 'lanonasis-mcp-server/1.3.0'
|
|
278
|
+
};
|
|
279
|
+
switch (name) {
|
|
280
|
+
// Memory Management Tools
|
|
281
|
+
case 'create_memory': {
|
|
282
|
+
const response = await fetch(`${apiUrl}/api/memory`, {
|
|
283
|
+
method: 'POST',
|
|
284
|
+
headers,
|
|
285
|
+
body: JSON.stringify(args)
|
|
286
|
+
});
|
|
287
|
+
if (!response.ok) {
|
|
288
|
+
const errorText = await response.text();
|
|
289
|
+
throw new Error(`Memory creation failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
290
|
+
}
|
|
291
|
+
const result = await response.json();
|
|
292
|
+
return {
|
|
293
|
+
content: [
|
|
294
|
+
{
|
|
295
|
+
type: 'text',
|
|
296
|
+
text: `✅ Memory created successfully:\n${JSON.stringify(result, null, 2)}`
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
case 'search_memories': {
|
|
302
|
+
const queryParams = new URLSearchParams();
|
|
303
|
+
if (args.query)
|
|
304
|
+
queryParams.append('query', String(args.query));
|
|
305
|
+
if (args.memory_type)
|
|
306
|
+
queryParams.append('memory_type', String(args.memory_type));
|
|
307
|
+
if (args.limit)
|
|
308
|
+
queryParams.append('limit', args.limit.toString());
|
|
309
|
+
if (args.threshold)
|
|
310
|
+
queryParams.append('threshold', args.threshold.toString());
|
|
311
|
+
if (args.tags && Array.isArray(args.tags)) {
|
|
312
|
+
args.tags.forEach((tag) => queryParams.append('tags', String(tag)));
|
|
313
|
+
}
|
|
314
|
+
const response = await fetch(`${apiUrl}/api/memory/search?${queryParams}`, {
|
|
315
|
+
method: 'GET',
|
|
316
|
+
headers
|
|
317
|
+
});
|
|
318
|
+
if (!response.ok) {
|
|
319
|
+
const errorText = await response.text();
|
|
320
|
+
throw new Error(`Memory search failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
321
|
+
}
|
|
322
|
+
const result = await response.json();
|
|
323
|
+
return {
|
|
324
|
+
content: [
|
|
325
|
+
{
|
|
326
|
+
type: 'text',
|
|
327
|
+
text: `🔍 Search results (${result.length || 0} found):\n${JSON.stringify(result, null, 2)}`
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
case 'get_memory': {
|
|
333
|
+
const response = await fetch(`${apiUrl}/api/memory/${args.id}`, {
|
|
334
|
+
method: 'GET',
|
|
335
|
+
headers
|
|
336
|
+
});
|
|
337
|
+
if (!response.ok) {
|
|
338
|
+
const errorText = await response.text();
|
|
339
|
+
throw new Error(`Memory retrieval failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
340
|
+
}
|
|
341
|
+
const result = await response.json();
|
|
342
|
+
return {
|
|
343
|
+
content: [
|
|
344
|
+
{
|
|
345
|
+
type: 'text',
|
|
346
|
+
text: `📄 Memory details:\n${JSON.stringify(result, null, 2)}`
|
|
347
|
+
}
|
|
348
|
+
]
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
case 'update_memory': {
|
|
352
|
+
const { id, ...updateData } = args;
|
|
353
|
+
const response = await fetch(`${apiUrl}/api/memory/${id}`, {
|
|
354
|
+
method: 'PUT',
|
|
355
|
+
headers,
|
|
356
|
+
body: JSON.stringify(updateData)
|
|
357
|
+
});
|
|
358
|
+
if (!response.ok) {
|
|
359
|
+
const errorText = await response.text();
|
|
360
|
+
throw new Error(`Memory update failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
361
|
+
}
|
|
362
|
+
const result = await response.json();
|
|
363
|
+
return {
|
|
364
|
+
content: [
|
|
365
|
+
{
|
|
366
|
+
type: 'text',
|
|
367
|
+
text: `✏️ Memory updated successfully:\n${JSON.stringify(result, null, 2)}`
|
|
368
|
+
}
|
|
369
|
+
]
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
case 'delete_memory': {
|
|
373
|
+
const response = await fetch(`${apiUrl}/api/memory/${args.id}`, {
|
|
374
|
+
method: 'DELETE',
|
|
375
|
+
headers
|
|
376
|
+
});
|
|
377
|
+
if (!response.ok) {
|
|
378
|
+
const errorText = await response.text();
|
|
379
|
+
throw new Error(`Memory deletion failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
content: [
|
|
383
|
+
{
|
|
384
|
+
type: 'text',
|
|
385
|
+
text: `🗑️ Memory deleted successfully (ID: ${args.id})`
|
|
386
|
+
}
|
|
387
|
+
]
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
case 'list_memories': {
|
|
391
|
+
const queryParams = new URLSearchParams();
|
|
392
|
+
if (args.limit)
|
|
393
|
+
queryParams.append('limit', args.limit.toString());
|
|
394
|
+
if (args.offset)
|
|
395
|
+
queryParams.append('offset', args.offset.toString());
|
|
396
|
+
if (args.memory_type)
|
|
397
|
+
queryParams.append('memory_type', String(args.memory_type));
|
|
398
|
+
if (args.tags && Array.isArray(args.tags)) {
|
|
399
|
+
args.tags.forEach((tag) => queryParams.append('tags', String(tag)));
|
|
400
|
+
}
|
|
401
|
+
const response = await fetch(`${apiUrl}/api/memory?${queryParams}`, {
|
|
402
|
+
method: 'GET',
|
|
403
|
+
headers
|
|
404
|
+
});
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
const errorText = await response.text();
|
|
407
|
+
throw new Error(`Memory listing failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
408
|
+
}
|
|
409
|
+
const result = await response.json();
|
|
410
|
+
return {
|
|
411
|
+
content: [
|
|
412
|
+
{
|
|
413
|
+
type: 'text',
|
|
414
|
+
text: `📋 Memory list (${result.length || 0} items):\n${JSON.stringify(result, null, 2)}`
|
|
415
|
+
}
|
|
416
|
+
]
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
// API Key Management Tools
|
|
420
|
+
case 'create_api_key': {
|
|
421
|
+
const response = await fetch(`${apiUrl}/api/api-keys`, {
|
|
422
|
+
method: 'POST',
|
|
423
|
+
headers,
|
|
424
|
+
body: JSON.stringify(args)
|
|
425
|
+
});
|
|
426
|
+
if (!response.ok) {
|
|
427
|
+
const errorText = await response.text();
|
|
428
|
+
throw new Error(`API key creation failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
429
|
+
}
|
|
430
|
+
const result = await response.json();
|
|
431
|
+
return {
|
|
432
|
+
content: [
|
|
433
|
+
{
|
|
434
|
+
type: 'text',
|
|
435
|
+
text: `🔑 API key created successfully:\n${JSON.stringify(result, null, 2)}`
|
|
436
|
+
}
|
|
437
|
+
]
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
case 'list_api_keys': {
|
|
441
|
+
const queryParams = new URLSearchParams();
|
|
442
|
+
if (args.project_id)
|
|
443
|
+
queryParams.append('project_id', String(args.project_id));
|
|
444
|
+
if (args.active_only !== undefined)
|
|
445
|
+
queryParams.append('active_only', args.active_only.toString());
|
|
446
|
+
const response = await fetch(`${apiUrl}/api/api-keys?${queryParams}`, {
|
|
447
|
+
method: 'GET',
|
|
448
|
+
headers
|
|
449
|
+
});
|
|
450
|
+
if (!response.ok) {
|
|
451
|
+
const errorText = await response.text();
|
|
452
|
+
throw new Error(`API key listing failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
453
|
+
}
|
|
454
|
+
const result = await response.json();
|
|
455
|
+
return {
|
|
456
|
+
content: [
|
|
457
|
+
{
|
|
458
|
+
type: 'text',
|
|
459
|
+
text: `🔑 API keys (${result.length || 0} found):\n${JSON.stringify(result, null, 2)}`
|
|
460
|
+
}
|
|
461
|
+
]
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
case 'get_health_status': {
|
|
465
|
+
const response = await fetch(`${apiUrl}/api/health`, {
|
|
466
|
+
method: 'GET',
|
|
467
|
+
headers
|
|
468
|
+
});
|
|
469
|
+
if (!response.ok) {
|
|
470
|
+
const errorText = await response.text();
|
|
471
|
+
throw new Error(`Health check failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
472
|
+
}
|
|
473
|
+
const result = await response.json();
|
|
474
|
+
return {
|
|
475
|
+
content: [
|
|
476
|
+
{
|
|
477
|
+
type: 'text',
|
|
478
|
+
text: `💚 System health status:\n${JSON.stringify(result, null, 2)}`
|
|
479
|
+
}
|
|
480
|
+
]
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
default:
|
|
484
|
+
return {
|
|
485
|
+
content: [
|
|
486
|
+
{
|
|
487
|
+
type: 'text',
|
|
488
|
+
text: `❌ Unknown tool: ${name}. Available tools: create_memory, search_memories, get_memory, update_memory, delete_memory, list_memories, create_api_key, list_api_keys, get_health_status`
|
|
489
|
+
}
|
|
490
|
+
]
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
496
|
+
return {
|
|
497
|
+
content: [
|
|
498
|
+
{
|
|
499
|
+
type: 'text',
|
|
500
|
+
text: `❌ Error: ${errorMessage}`
|
|
501
|
+
}
|
|
502
|
+
]
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
async run() {
|
|
508
|
+
const transport = new StdioServerTransport();
|
|
509
|
+
await this.server.connect(transport);
|
|
510
|
+
// Log to stderr that server is ready
|
|
511
|
+
console.error('[MCP-INFO] Lanonasis MCP Server started and ready');
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// Start the server
|
|
515
|
+
const server = new LanonasisMCPServer();
|
|
516
|
+
server.run().catch((error) => {
|
|
517
|
+
console.error('[MCP-ERROR] Failed to start server:', error);
|
|
518
|
+
process.exit(1);
|
|
519
|
+
});
|
|
@@ -2,12 +2,53 @@ interface MCPConnectionOptions {
|
|
|
2
2
|
serverPath?: string;
|
|
3
3
|
serverUrl?: string;
|
|
4
4
|
useRemote?: boolean;
|
|
5
|
+
useWebSocket?: boolean;
|
|
6
|
+
connectionMode?: 'local' | 'remote' | 'websocket';
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Interface for MCP tool arguments
|
|
10
|
+
*/
|
|
11
|
+
interface MCPToolArgs {
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Interface for MCP tool response
|
|
16
|
+
*/
|
|
17
|
+
export interface MCPToolResponse {
|
|
18
|
+
result?: unknown;
|
|
19
|
+
error?: {
|
|
20
|
+
code: number;
|
|
21
|
+
message: string;
|
|
22
|
+
};
|
|
23
|
+
id?: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
memory_type?: string;
|
|
26
|
+
length?: number;
|
|
27
|
+
forEach?: (callback: (item: any, index: number) => void) => void;
|
|
28
|
+
code?: number;
|
|
29
|
+
message?: string;
|
|
30
|
+
response?: any;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Interface for MCP WebSocket messages
|
|
34
|
+
*/
|
|
35
|
+
export interface MCPWebSocketMessage {
|
|
36
|
+
id: number;
|
|
37
|
+
method?: string;
|
|
38
|
+
params?: Record<string, unknown>;
|
|
39
|
+
result?: Record<string, unknown>;
|
|
40
|
+
error?: {
|
|
41
|
+
code: number;
|
|
42
|
+
message: string;
|
|
43
|
+
data?: unknown;
|
|
44
|
+
};
|
|
5
45
|
}
|
|
6
46
|
export declare class MCPClient {
|
|
7
47
|
private client;
|
|
8
48
|
private config;
|
|
9
49
|
private isConnected;
|
|
10
50
|
private sseConnection;
|
|
51
|
+
private wsConnection;
|
|
11
52
|
constructor();
|
|
12
53
|
/**
|
|
13
54
|
* Connect to MCP server (local or remote)
|
|
@@ -17,6 +58,14 @@ export declare class MCPClient {
|
|
|
17
58
|
* Initialize SSE connection for real-time updates
|
|
18
59
|
*/
|
|
19
60
|
private initializeSSE;
|
|
61
|
+
/**
|
|
62
|
+
* Initialize WebSocket connection for enterprise MCP server
|
|
63
|
+
*/
|
|
64
|
+
private initializeWebSocket;
|
|
65
|
+
/**
|
|
66
|
+
* Send a message over the WebSocket connection
|
|
67
|
+
*/
|
|
68
|
+
private sendWebSocketMessage;
|
|
20
69
|
/**
|
|
21
70
|
* Disconnect from MCP server
|
|
22
71
|
*/
|
|
@@ -24,7 +73,7 @@ export declare class MCPClient {
|
|
|
24
73
|
/**
|
|
25
74
|
* Call an MCP tool
|
|
26
75
|
*/
|
|
27
|
-
callTool(toolName: string, args:
|
|
76
|
+
callTool(toolName: string, args: MCPToolArgs): Promise<MCPToolResponse>;
|
|
28
77
|
/**
|
|
29
78
|
* Call remote tool via REST API with MCP interface
|
|
30
79
|
*/
|
package/dist/utils/mcp-client.js
CHANGED
|
@@ -5,6 +5,7 @@ import { CLIConfig } from './config.js';
|
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import { EventSource } from 'eventsource';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
|
+
import WebSocket from 'ws';
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
10
|
const __dirname = path.dirname(__filename);
|
|
10
11
|
export class MCPClient {
|
|
@@ -12,6 +13,7 @@ export class MCPClient {
|
|
|
12
13
|
config;
|
|
13
14
|
isConnected = false;
|
|
14
15
|
sseConnection = null;
|
|
16
|
+
wsConnection = null;
|
|
15
17
|
constructor() {
|
|
16
18
|
this.config = new CLIConfig();
|
|
17
19
|
}
|
|
@@ -20,34 +22,59 @@ export class MCPClient {
|
|
|
20
22
|
*/
|
|
21
23
|
async connect(options = {}) {
|
|
22
24
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
25
|
+
// Determine connection mode with priority to explicit mode option
|
|
26
|
+
const connectionMode = options.connectionMode ??
|
|
27
|
+
(options.useWebSocket ? 'websocket' :
|
|
28
|
+
options.useRemote ? 'remote' :
|
|
29
|
+
this.config.get('mcpConnectionMode') ??
|
|
30
|
+
this.config.get('mcpUseRemote') ? 'remote' : 'local');
|
|
31
|
+
let wsUrl;
|
|
32
|
+
let serverUrl;
|
|
33
|
+
let serverPath;
|
|
34
|
+
switch (connectionMode) {
|
|
35
|
+
case 'websocket':
|
|
36
|
+
// WebSocket connection mode for enterprise users
|
|
37
|
+
wsUrl = options.serverUrl ??
|
|
38
|
+
this.config.get('mcpWebSocketUrl') ??
|
|
39
|
+
'ws://localhost:8081/mcp/ws';
|
|
40
|
+
console.log(chalk.cyan(`Connecting to WebSocket MCP server at ${wsUrl}...`));
|
|
41
|
+
// Initialize WebSocket connection
|
|
42
|
+
await this.initializeWebSocket(wsUrl);
|
|
43
|
+
this.isConnected = true;
|
|
44
|
+
return true;
|
|
45
|
+
case 'remote':
|
|
46
|
+
// For remote MCP, we'll use the REST API with MCP-style interface
|
|
47
|
+
serverUrl = options.serverUrl ??
|
|
48
|
+
this.config.get('mcpServerUrl') ??
|
|
49
|
+
'https://api.lanonasis.com';
|
|
50
|
+
console.log(chalk.cyan(`Connecting to remote MCP server at ${serverUrl}...`));
|
|
51
|
+
// Initialize SSE connection for real-time updates
|
|
52
|
+
await this.initializeSSE(serverUrl);
|
|
53
|
+
this.isConnected = true;
|
|
54
|
+
return true;
|
|
55
|
+
case 'local':
|
|
56
|
+
default:
|
|
57
|
+
{
|
|
58
|
+
// Local MCP server connection
|
|
59
|
+
serverPath = options.serverPath ??
|
|
60
|
+
this.config.get('mcpServerPath') ??
|
|
61
|
+
path.join(__dirname, '../../../../onasis-gateway/mcp-server/server.js');
|
|
62
|
+
console.log(chalk.cyan(`Connecting to local MCP server at ${serverPath}...`));
|
|
63
|
+
const localTransport = new StdioClientTransport({
|
|
64
|
+
command: 'node',
|
|
65
|
+
args: [serverPath]
|
|
66
|
+
});
|
|
67
|
+
this.client = new Client({
|
|
68
|
+
name: '@lanonasis/cli',
|
|
69
|
+
version: '1.0.0'
|
|
70
|
+
}, {
|
|
71
|
+
capabilities: {}
|
|
72
|
+
});
|
|
73
|
+
await this.client.connect(localTransport);
|
|
74
|
+
}
|
|
75
|
+
this.isConnected = true;
|
|
76
|
+
console.log(chalk.green('✓ Connected to MCP server'));
|
|
77
|
+
return true;
|
|
51
78
|
}
|
|
52
79
|
}
|
|
53
80
|
catch (error) {
|
|
@@ -79,6 +106,87 @@ export class MCPClient {
|
|
|
79
106
|
};
|
|
80
107
|
}
|
|
81
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Initialize WebSocket connection for enterprise MCP server
|
|
111
|
+
*/
|
|
112
|
+
async initializeWebSocket(wsUrl) {
|
|
113
|
+
const token = this.config.get('token');
|
|
114
|
+
if (!token) {
|
|
115
|
+
throw new Error('API key required for WebSocket mode. Set LANONASIS_API_KEY or login first.');
|
|
116
|
+
}
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
try {
|
|
119
|
+
// Close existing connection if any
|
|
120
|
+
if (this.wsConnection) {
|
|
121
|
+
this.wsConnection.close();
|
|
122
|
+
this.wsConnection = null;
|
|
123
|
+
}
|
|
124
|
+
// Create new WebSocket connection with authentication
|
|
125
|
+
this.wsConnection = new WebSocket(wsUrl, {
|
|
126
|
+
headers: {
|
|
127
|
+
'Authorization': `Bearer ${token}`,
|
|
128
|
+
'X-API-Key': token
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
this.wsConnection.on('open', () => {
|
|
132
|
+
console.log(chalk.green('✅ Connected to MCP WebSocket server'));
|
|
133
|
+
// Send initialization message
|
|
134
|
+
this.sendWebSocketMessage({
|
|
135
|
+
id: 1,
|
|
136
|
+
method: 'initialize',
|
|
137
|
+
params: {
|
|
138
|
+
protocolVersion: '2024-11-05',
|
|
139
|
+
capabilities: {
|
|
140
|
+
tools: ['memory_management', 'workflow_orchestration']
|
|
141
|
+
},
|
|
142
|
+
clientInfo: {
|
|
143
|
+
name: '@lanonasis/cli',
|
|
144
|
+
version: '1.1.0'
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
resolve();
|
|
149
|
+
});
|
|
150
|
+
this.wsConnection.on('message', (data) => {
|
|
151
|
+
try {
|
|
152
|
+
const message = JSON.parse(data.toString());
|
|
153
|
+
console.log(chalk.blue('📡 MCP message:'), message.id, message.method || 'response');
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.error('Failed to parse WebSocket message:', error);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
this.wsConnection.on('error', (error) => {
|
|
160
|
+
console.error(chalk.red('WebSocket error:'), error);
|
|
161
|
+
reject(error);
|
|
162
|
+
});
|
|
163
|
+
this.wsConnection.on('close', (code, reason) => {
|
|
164
|
+
console.log(chalk.yellow(`WebSocket connection closed (${code}): ${reason}`));
|
|
165
|
+
// Auto-reconnect after delay
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
if (this.isConnected) {
|
|
168
|
+
console.log(chalk.blue('🔄 Attempting to reconnect to WebSocket...'));
|
|
169
|
+
this.initializeWebSocket(wsUrl).catch(err => {
|
|
170
|
+
console.error('Failed to reconnect:', err);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}, 5000);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
reject(error);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Send a message over the WebSocket connection
|
|
183
|
+
*/
|
|
184
|
+
sendWebSocketMessage(message) {
|
|
185
|
+
if (!this.wsConnection) {
|
|
186
|
+
throw new Error('WebSocket not connected');
|
|
187
|
+
}
|
|
188
|
+
this.wsConnection.send(JSON.stringify(message));
|
|
189
|
+
}
|
|
82
190
|
/**
|
|
83
191
|
* Disconnect from MCP server
|
|
84
192
|
*/
|
|
@@ -115,7 +223,12 @@ export class MCPClient {
|
|
|
115
223
|
name: toolName,
|
|
116
224
|
arguments: args
|
|
117
225
|
});
|
|
118
|
-
|
|
226
|
+
// Convert the SDK result to our expected MCPToolResponse format
|
|
227
|
+
return {
|
|
228
|
+
result: result,
|
|
229
|
+
code: 200,
|
|
230
|
+
message: 'Success'
|
|
231
|
+
};
|
|
119
232
|
}
|
|
120
233
|
catch (error) {
|
|
121
234
|
throw new Error(`MCP tool call failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
@@ -177,7 +290,8 @@ export class MCPClient {
|
|
|
177
290
|
// Handle dynamic endpoint for memory operations that need ID
|
|
178
291
|
let endpoint = mapping.endpoint;
|
|
179
292
|
if (endpoint.includes('{id}') && args.memory_id) {
|
|
180
|
-
|
|
293
|
+
// Ensure memory_id is treated as a string for replacement
|
|
294
|
+
endpoint = endpoint.replace('{id}', String(args.memory_id));
|
|
181
295
|
}
|
|
182
296
|
const response = await axios({
|
|
183
297
|
method: mapping.method,
|
|
@@ -192,7 +306,11 @@ export class MCPClient {
|
|
|
192
306
|
return response.data;
|
|
193
307
|
}
|
|
194
308
|
catch (error) {
|
|
195
|
-
|
|
309
|
+
// Safely handle errors with type checking
|
|
310
|
+
const errorObj = error;
|
|
311
|
+
const errorMsg = errorObj.response?.data?.error ||
|
|
312
|
+
(errorObj.message ? errorObj.message : 'Unknown error');
|
|
313
|
+
throw new Error(`Remote tool call failed: ${errorMsg}`);
|
|
196
314
|
}
|
|
197
315
|
}
|
|
198
316
|
/**
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lanonasis/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "LanOnasis Enterprise CLI - Memory as a Service, API Key Management, and Infrastructure Orchestration",
|
|
5
5
|
"main": "dist/index-simple.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"lanonasis": "dist/index-simple.js",
|
|
8
|
+
"lanonasis-mcp-server": "dist/mcp-server.js",
|
|
8
9
|
"memory": "dist/index-simple.js",
|
|
9
10
|
"maas": "dist/index-simple.js"
|
|
10
11
|
},
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
"dependencies": {
|
|
41
42
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
42
43
|
"@types/eventsource": "^1.1.15",
|
|
44
|
+
"@types/ws": "^8.18.1",
|
|
43
45
|
"axios": "^1.11.0",
|
|
44
46
|
"chalk": "^5.4.1",
|
|
45
47
|
"cli-table3": "^0.6.5",
|
|
@@ -52,7 +54,8 @@
|
|
|
52
54
|
"open": "^10.2.0",
|
|
53
55
|
"ora": "^8.2.0",
|
|
54
56
|
"table": "^6.9.0",
|
|
55
|
-
"word-wrap": "^1.2.5"
|
|
57
|
+
"word-wrap": "^1.2.5",
|
|
58
|
+
"ws": "^8.18.3"
|
|
56
59
|
},
|
|
57
60
|
"devDependencies": {
|
|
58
61
|
"@types/inquirer": "^9.0.7",
|