@lanonasis/cli 1.5.1 → 2.0.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.
@@ -1,519 +1,154 @@
1
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';
2
+ /**
3
+ * CLI-Embedded MCP Server
4
+ * Uses the same configuration and authentication as @lanonasis/cli v1.5.2+
5
+ * Can run standalone or be invoked by CLI commands
6
+ */
7
+ import { fileURLToPath } from 'url';
8
+ import { dirname, join } from 'path';
9
+ import { spawn } from 'child_process';
5
10
  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;
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+ export class CLIMCPServer {
32
14
  config;
33
15
  constructor() {
34
16
  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
17
  }
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
- };
18
+ /**
19
+ * Start MCP server using CLI configuration
20
+ */
21
+ async start(options = {}) {
22
+ await this.config.init();
23
+ const { mode = 'stdio', port = 3001, verbose = false, useRemote = this.config.shouldUseRemoteMCP() } = options;
24
+ if (useRemote) {
25
+ await this.startRemoteMCP(options);
26
+ }
27
+ else {
28
+ await this.startLocalMCP(options);
29
+ }
30
+ }
31
+ /**
32
+ * Start local MCP server using CLI auth config
33
+ */
34
+ async startLocalMCP(options) {
35
+ const { mode, port, verbose } = options;
36
+ // Path to CLI-aligned MCP server in submodule
37
+ const mcpServerPath = join(__dirname, '../../../mcp-server/dist/cli-aligned-mcp-server.js');
38
+ const args = mode === 'http' ? ['--http'] : ['--stdio'];
39
+ if (verbose) {
40
+ console.error('🚀 Starting CLI-aligned MCP Server...');
41
+ console.error(`Mode: ${mode}`);
42
+ console.error(`Config: ~/.maas/config.json`);
43
+ console.error(`Auth: ${this.config.hasVendorKey() ? 'Vendor Key' : 'JWT Token'}`);
44
+ }
45
+ // Set environment variables from CLI config
46
+ const env = {
47
+ ...process.env,
48
+ PORT: port?.toString(),
49
+ MEMORY_API_URL: this.config.getApiUrl(),
50
+ LANONASIS_VENDOR_KEY: this.config.getVendorKey(),
51
+ LANONASIS_TOKEN: this.config.getToken(),
52
+ MCP_VERBOSE: verbose ? 'true' : 'false',
53
+ CLI_ALIGNED: 'true'
54
+ };
55
+ const child = spawn('node', [mcpServerPath, ...args], {
56
+ env,
57
+ stdio: mode === 'stdio' ? ['pipe', 'pipe', 'inherit'] : 'inherit'
257
58
  });
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://dashboard.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/v1/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/v1/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/v1/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/v1/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/v1/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/v1/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/v1/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/v1/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/v1/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
- };
59
+ if (mode === 'stdio') {
60
+ // For stdio mode, pipe stdin/stdout for MCP protocol
61
+ process.stdin.pipe(child.stdin);
62
+ child.stdout.pipe(process.stdout);
63
+ }
64
+ child.on('error', (error) => {
65
+ console.error('❌ MCP Server failed to start:', error.message);
66
+ process.exit(1);
67
+ });
68
+ child.on('exit', (code) => {
69
+ if (code !== 0) {
70
+ console.error(`❌ MCP Server exited with code ${code}`);
71
+ process.exit(code || 1);
504
72
  }
505
73
  });
74
+ // Handle graceful shutdown
75
+ process.on('SIGINT', () => {
76
+ child.kill('SIGINT');
77
+ });
78
+ process.on('SIGTERM', () => {
79
+ child.kill('SIGTERM');
80
+ });
81
+ }
82
+ /**
83
+ * Connect to remote MCP server
84
+ */
85
+ async startRemoteMCP(options) {
86
+ const { verbose } = options;
87
+ if (verbose) {
88
+ console.error('🌐 Connecting to remote MCP server...');
89
+ console.error(`URL: ${this.config.getMCPServerUrl()}`);
90
+ }
91
+ // For remote MCP, we'd need to implement a proxy or client
92
+ // For now, fall back to local mode
93
+ console.error('⚠️ Remote MCP not yet implemented, falling back to local mode');
94
+ await this.startLocalMCP({ ...options, useRemote: false });
95
+ }
96
+ /**
97
+ * Check if MCP server is available and configured
98
+ */
99
+ async checkStatus() {
100
+ await this.config.init();
101
+ return {
102
+ available: true, // CLI always has MCP server available
103
+ configured: this.config.hasVendorKey() || !!this.config.getToken(),
104
+ authMethod: this.config.hasVendorKey() ? 'vendor_key' :
105
+ this.config.getToken() ? 'jwt' : 'none',
106
+ mode: this.config.shouldUseRemoteMCP() ? 'remote' : 'local'
107
+ };
506
108
  }
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');
109
+ }
110
+ // Main execution when run as standalone script
111
+ async function main() {
112
+ const args = process.argv.slice(2);
113
+ const server = new CLIMCPServer();
114
+ // Parse command line arguments
115
+ const options = {
116
+ mode: args.includes('--http') ? 'http' : 'stdio',
117
+ port: parseInt(args.find(arg => arg.startsWith('--port='))?.split('=')[1] || '3001'),
118
+ verbose: args.includes('--verbose') || args.includes('-v'),
119
+ useRemote: args.includes('--remote')
120
+ };
121
+ if (args.includes('--status')) {
122
+ const status = await server.checkStatus();
123
+ console.log(JSON.stringify(status, null, 2));
124
+ return;
125
+ }
126
+ if (args.includes('--help')) {
127
+ console.log(`
128
+ CLI MCP Server - CLI-aligned Model Context Protocol server
129
+
130
+ Usage:
131
+ lanonasis-mcp-server [options]
132
+
133
+ Options:
134
+ --stdio Use stdio transport (default)
135
+ --http Use HTTP transport
136
+ --port=3001 HTTP port (default: 3001)
137
+ --remote Use remote MCP server
138
+ --verbose, -v Verbose logging
139
+ --status Check server status
140
+ --help Show this help
141
+
142
+ Examples:
143
+ lanonasis-mcp-server # Start stdio server
144
+ lanonasis-mcp-server --http --port=3002 # Start HTTP server
145
+ lanonasis-mcp-server --status # Check status
146
+ `);
147
+ return;
512
148
  }
149
+ await server.start(options);
150
+ }
151
+ if (import.meta.url === `file://${process.argv[1]}`) {
152
+ main().catch(console.error);
513
153
  }
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
- });
154
+ export default CLIMCPServer;