@falkordb/mcpserver 1.0.1

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.
@@ -0,0 +1,489 @@
1
+ import { falkorDBService } from './falkordb.service';
2
+ import { AppError, CommonErrors } from '../errors/AppError.js';
3
+ // Mock the logger service
4
+ jest.mock('./logger.service.js', () => ({
5
+ logger: {
6
+ info: jest.fn().mockResolvedValue(undefined),
7
+ warn: jest.fn().mockResolvedValue(undefined),
8
+ error: jest.fn().mockResolvedValue(undefined),
9
+ debug: jest.fn().mockResolvedValue(undefined),
10
+ }
11
+ }));
12
+ // Mock the config
13
+ jest.mock('../config/index.js', () => ({
14
+ config: {
15
+ falkorDB: {
16
+ host: 'localhost',
17
+ port: 6379,
18
+ username: 'testuser',
19
+ password: 'testpass',
20
+ defaultReadOnly: false
21
+ }
22
+ }
23
+ }));
24
+ // Mock the FalkorDB library
25
+ jest.mock('falkordb', () => {
26
+ const mockSelectGraph = jest.fn();
27
+ const mockQuery = jest.fn();
28
+ const mockRoQuery = jest.fn();
29
+ const mockList = jest.fn();
30
+ const mockClose = jest.fn();
31
+ const mockPing = jest.fn();
32
+ const mockDelete = jest.fn();
33
+ return {
34
+ FalkorDB: {
35
+ connect: jest.fn().mockResolvedValue({
36
+ connection: Promise.resolve({
37
+ ping: mockPing
38
+ }),
39
+ selectGraph: mockSelectGraph.mockReturnValue({
40
+ query: mockQuery,
41
+ roQuery: mockRoQuery,
42
+ delete: mockDelete
43
+ }),
44
+ list: mockList,
45
+ close: mockClose
46
+ })
47
+ },
48
+ mockSelectGraph,
49
+ mockQuery,
50
+ mockRoQuery,
51
+ mockList,
52
+ mockClose,
53
+ mockPing,
54
+ mockDelete
55
+ };
56
+ });
57
+ describe('FalkorDB Service', () => {
58
+ let mockFalkorDB;
59
+ beforeAll(async () => {
60
+ // Access the mocks
61
+ mockFalkorDB = await import('falkordb');
62
+ });
63
+ beforeEach(() => {
64
+ jest.clearAllMocks();
65
+ // Reset service state
66
+ falkorDBService.client = null;
67
+ falkorDBService.retryCount = 0;
68
+ falkorDBService.initializingPromise = null;
69
+ });
70
+ describe('initialize', () => {
71
+ it('should successfully initialize and connect to FalkorDB', async () => {
72
+ // Arrange
73
+ mockFalkorDB.FalkorDB.connect.mockResolvedValue({
74
+ connection: Promise.resolve({
75
+ ping: mockFalkorDB.mockPing.mockResolvedValue('PONG')
76
+ }),
77
+ selectGraph: mockFalkorDB.mockSelectGraph,
78
+ list: mockFalkorDB.mockList,
79
+ close: mockFalkorDB.mockClose
80
+ });
81
+ // Act
82
+ await falkorDBService.initialize();
83
+ // Assert
84
+ expect(mockFalkorDB.FalkorDB.connect).toHaveBeenCalledWith({
85
+ socket: {
86
+ host: 'localhost',
87
+ port: 6379,
88
+ },
89
+ password: 'testpass',
90
+ username: 'testuser',
91
+ });
92
+ expect(mockFalkorDB.mockPing).toHaveBeenCalled();
93
+ expect(falkorDBService.client).not.toBeNull();
94
+ expect(falkorDBService.retryCount).toBe(0);
95
+ expect(falkorDBService.initializingPromise).toBeNull();
96
+ });
97
+ it('should await ongoing initialization if already initializing', async () => {
98
+ // Arrange
99
+ mockFalkorDB.FalkorDB.connect.mockResolvedValue({
100
+ connection: Promise.resolve({
101
+ ping: mockFalkorDB.mockPing.mockResolvedValue('PONG')
102
+ }),
103
+ selectGraph: mockFalkorDB.mockSelectGraph,
104
+ list: mockFalkorDB.mockList,
105
+ close: mockFalkorDB.mockClose
106
+ });
107
+ // Act - start two initializations concurrently
108
+ const init1 = falkorDBService.initialize();
109
+ const init2 = falkorDBService.initialize();
110
+ await Promise.all([init1, init2]);
111
+ // Assert - connect should only be called once
112
+ expect(mockFalkorDB.FalkorDB.connect).toHaveBeenCalledTimes(1);
113
+ expect(falkorDBService.client).not.toBeNull();
114
+ });
115
+ it('should retry connection on failure and eventually succeed', async () => {
116
+ // Arrange
117
+ const connectError = new Error('Connection failed');
118
+ mockFalkorDB.FalkorDB.connect
119
+ .mockRejectedValueOnce(connectError)
120
+ .mockRejectedValueOnce(connectError)
121
+ .mockResolvedValueOnce({
122
+ connection: Promise.resolve({
123
+ ping: mockFalkorDB.mockPing.mockResolvedValue('PONG')
124
+ }),
125
+ selectGraph: mockFalkorDB.mockSelectGraph,
126
+ list: mockFalkorDB.mockList,
127
+ close: mockFalkorDB.mockClose
128
+ });
129
+ // Mock setTimeout to avoid actual delays
130
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation((callback) => {
131
+ setImmediate(callback);
132
+ return {};
133
+ });
134
+ // Act
135
+ await falkorDBService.initialize();
136
+ // Assert
137
+ expect(mockFalkorDB.FalkorDB.connect).toHaveBeenCalledTimes(3);
138
+ expect(falkorDBService.client).not.toBeNull();
139
+ expect(falkorDBService.retryCount).toBe(0);
140
+ // Cleanup
141
+ setTimeoutSpy.mockRestore();
142
+ });
143
+ it('should throw AppError after max retries exceeded', async () => {
144
+ // Arrange
145
+ const connectError = new Error('Connection failed');
146
+ mockFalkorDB.FalkorDB.connect.mockRejectedValue(connectError);
147
+ // Mock setTimeout to avoid actual delays
148
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation((callback) => {
149
+ setImmediate(callback);
150
+ return {};
151
+ });
152
+ // Act & Assert
153
+ try {
154
+ await falkorDBService.initialize();
155
+ fail('Expected initialize to throw AppError');
156
+ }
157
+ catch (error) {
158
+ expect(error).toBeInstanceOf(AppError);
159
+ expect(error.name).toBe(CommonErrors.CONNECTION_FAILED);
160
+ }
161
+ expect(mockFalkorDB.FalkorDB.connect).toHaveBeenCalledTimes(6); // 1 initial + 5 retries
162
+ // Cleanup
163
+ setTimeoutSpy.mockRestore();
164
+ });
165
+ it('should handle ping failure during connection test', async () => {
166
+ // Arrange
167
+ const pingError = new Error('Ping failed');
168
+ mockFalkorDB.FalkorDB.connect.mockResolvedValue({
169
+ connection: Promise.resolve({
170
+ ping: mockFalkorDB.mockPing.mockRejectedValue(pingError)
171
+ }),
172
+ selectGraph: mockFalkorDB.mockSelectGraph,
173
+ list: mockFalkorDB.mockList,
174
+ close: mockFalkorDB.mockClose
175
+ });
176
+ // Mock setTimeout to avoid actual delays
177
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation((callback) => {
178
+ setImmediate(callback);
179
+ return {};
180
+ });
181
+ // Act & Assert
182
+ await expect(falkorDBService.initialize()).rejects.toThrow(AppError);
183
+ // Cleanup
184
+ setTimeoutSpy.mockRestore();
185
+ });
186
+ });
187
+ describe('executeQuery', () => {
188
+ it('should execute a query on the specified graph', async () => {
189
+ // Arrange
190
+ const graphName = 'testGraph';
191
+ const query = 'MATCH (n) RETURN n';
192
+ const params = { param1: 'value1' };
193
+ const expectedResult = { records: [{ id: 1 }] };
194
+ mockFalkorDB.mockQuery.mockResolvedValue(expectedResult);
195
+ // Force client to be available
196
+ falkorDBService.client = {
197
+ selectGraph: mockFalkorDB.mockSelectGraph
198
+ };
199
+ // Act
200
+ const result = await falkorDBService.executeQuery(graphName, query, params);
201
+ // Assert
202
+ expect(mockFalkorDB.mockSelectGraph).toHaveBeenCalledWith(graphName);
203
+ expect(mockFalkorDB.mockQuery).toHaveBeenCalledWith(query, params);
204
+ expect(mockFalkorDB.mockRoQuery).not.toHaveBeenCalled();
205
+ expect(result).toEqual(expectedResult);
206
+ });
207
+ it('should execute a query without params', async () => {
208
+ // Arrange
209
+ const graphName = 'testGraph';
210
+ const query = 'MATCH (n) RETURN n';
211
+ const expectedResult = { records: [{ id: 1 }] };
212
+ mockFalkorDB.mockQuery.mockResolvedValue(expectedResult);
213
+ // Force client to be available
214
+ falkorDBService.client = {
215
+ selectGraph: mockFalkorDB.mockSelectGraph
216
+ };
217
+ // Act
218
+ const result = await falkorDBService.executeQuery(graphName, query);
219
+ // Assert
220
+ expect(mockFalkorDB.mockSelectGraph).toHaveBeenCalledWith(graphName);
221
+ expect(mockFalkorDB.mockQuery).toHaveBeenCalledWith(query, undefined);
222
+ expect(result).toEqual(expectedResult);
223
+ });
224
+ it('should execute a read-only query when readOnly flag is true', async () => {
225
+ // Arrange
226
+ const graphName = 'testGraph';
227
+ const query = 'MATCH (n) RETURN n';
228
+ const params = { param1: 'value1' };
229
+ const expectedResult = { records: [{ id: 1 }] };
230
+ mockFalkorDB.mockRoQuery.mockResolvedValue(expectedResult);
231
+ // Force client to be available
232
+ falkorDBService.client = {
233
+ selectGraph: mockFalkorDB.mockSelectGraph
234
+ };
235
+ // Act
236
+ const result = await falkorDBService.executeQuery(graphName, query, params, true);
237
+ // Assert
238
+ expect(mockFalkorDB.mockSelectGraph).toHaveBeenCalledWith(graphName);
239
+ expect(mockFalkorDB.mockRoQuery).toHaveBeenCalledWith(query, params);
240
+ expect(mockFalkorDB.mockQuery).not.toHaveBeenCalled();
241
+ expect(result).toEqual(expectedResult);
242
+ });
243
+ it('should execute a read-only query without params', async () => {
244
+ // Arrange
245
+ const graphName = 'testGraph';
246
+ const query = 'MATCH (n) RETURN n';
247
+ const expectedResult = { records: [{ id: 1 }] };
248
+ mockFalkorDB.mockRoQuery.mockResolvedValue(expectedResult);
249
+ // Force client to be available
250
+ falkorDBService.client = {
251
+ selectGraph: mockFalkorDB.mockSelectGraph
252
+ };
253
+ // Act
254
+ const result = await falkorDBService.executeQuery(graphName, query, undefined, true);
255
+ // Assert
256
+ expect(mockFalkorDB.mockSelectGraph).toHaveBeenCalledWith(graphName);
257
+ expect(mockFalkorDB.mockRoQuery).toHaveBeenCalledWith(query, undefined);
258
+ expect(mockFalkorDB.mockQuery).not.toHaveBeenCalled();
259
+ expect(result).toEqual(expectedResult);
260
+ });
261
+ it('should throw AppError if client is not initialized', async () => {
262
+ // Arrange
263
+ falkorDBService.client = null;
264
+ // Act & Assert
265
+ await expect(falkorDBService.executeQuery('graph', 'query'))
266
+ .rejects
267
+ .toThrow(AppError);
268
+ await expect(falkorDBService.executeQuery('graph', 'query'))
269
+ .rejects
270
+ .toThrow('FalkorDB client not initialized');
271
+ });
272
+ it('should throw AppError when query execution fails', async () => {
273
+ // Arrange
274
+ const graphName = 'testGraph';
275
+ const query = 'INVALID QUERY';
276
+ const queryError = new Error('Query syntax error');
277
+ mockFalkorDB.mockQuery.mockRejectedValue(queryError);
278
+ // Force client to be available
279
+ falkorDBService.client = {
280
+ selectGraph: mockFalkorDB.mockSelectGraph
281
+ };
282
+ // Act & Assert
283
+ try {
284
+ await falkorDBService.executeQuery(graphName, query);
285
+ fail('Expected executeQuery to throw AppError');
286
+ }
287
+ catch (error) {
288
+ expect(error).toBeInstanceOf(AppError);
289
+ expect(error.name).toBe(CommonErrors.OPERATION_FAILED);
290
+ }
291
+ });
292
+ it('should throw AppError when read-only query execution fails', async () => {
293
+ // Arrange
294
+ const graphName = 'testGraph';
295
+ const query = 'CREATE (n) RETURN n';
296
+ const queryError = new Error('Write operations not allowed in read-only mode');
297
+ mockFalkorDB.mockRoQuery.mockRejectedValue(queryError);
298
+ // Force client to be available
299
+ falkorDBService.client = {
300
+ selectGraph: mockFalkorDB.mockSelectGraph
301
+ };
302
+ // Act & Assert
303
+ try {
304
+ await falkorDBService.executeQuery(graphName, query, undefined, true);
305
+ fail('Expected executeQuery to throw AppError');
306
+ }
307
+ catch (error) {
308
+ expect(error).toBeInstanceOf(AppError);
309
+ expect(error.name).toBe(CommonErrors.OPERATION_FAILED);
310
+ expect(error.message).toContain('read-only');
311
+ }
312
+ });
313
+ });
314
+ describe('executeReadOnlyQuery', () => {
315
+ it('should execute a read-only query using ro_query', async () => {
316
+ // Arrange
317
+ const graphName = 'testGraph';
318
+ const query = 'MATCH (n) RETURN n';
319
+ const params = { param1: 'value1' };
320
+ const expectedResult = { records: [{ id: 1 }] };
321
+ mockFalkorDB.mockRoQuery.mockResolvedValue(expectedResult);
322
+ // Force client to be available
323
+ falkorDBService.client = {
324
+ selectGraph: mockFalkorDB.mockSelectGraph
325
+ };
326
+ // Act
327
+ const result = await falkorDBService.executeReadOnlyQuery(graphName, query, params);
328
+ // Assert
329
+ expect(mockFalkorDB.mockSelectGraph).toHaveBeenCalledWith(graphName);
330
+ expect(mockFalkorDB.mockRoQuery).toHaveBeenCalledWith(query, params);
331
+ expect(mockFalkorDB.mockQuery).not.toHaveBeenCalled();
332
+ expect(result).toEqual(expectedResult);
333
+ });
334
+ it('should throw AppError if client is not initialized', async () => {
335
+ // Arrange
336
+ falkorDBService.client = null;
337
+ // Act & Assert
338
+ await expect(falkorDBService.executeReadOnlyQuery('graph', 'query'))
339
+ .rejects
340
+ .toThrow(AppError);
341
+ await expect(falkorDBService.executeReadOnlyQuery('graph', 'query'))
342
+ .rejects
343
+ .toThrow('FalkorDB client not initialized');
344
+ });
345
+ });
346
+ describe('listGraphs', () => {
347
+ it('should return a list of graphs', async () => {
348
+ // Arrange
349
+ const expectedGraphs = ['graph1', 'graph2'];
350
+ mockFalkorDB.mockList.mockResolvedValue(expectedGraphs);
351
+ // Force client to be available
352
+ falkorDBService.client = {
353
+ list: mockFalkorDB.mockList
354
+ };
355
+ // Act
356
+ const result = await falkorDBService.listGraphs();
357
+ // Assert
358
+ expect(mockFalkorDB.mockList).toHaveBeenCalled();
359
+ expect(result).toEqual(expectedGraphs);
360
+ });
361
+ it('should return empty array when no graphs exist', async () => {
362
+ // Arrange
363
+ const expectedGraphs = [];
364
+ mockFalkorDB.mockList.mockResolvedValue(expectedGraphs);
365
+ // Force client to be available
366
+ falkorDBService.client = {
367
+ list: mockFalkorDB.mockList
368
+ };
369
+ // Act
370
+ const result = await falkorDBService.listGraphs();
371
+ // Assert
372
+ expect(mockFalkorDB.mockList).toHaveBeenCalled();
373
+ expect(result).toEqual(expectedGraphs);
374
+ });
375
+ it('should throw AppError if client is not initialized', async () => {
376
+ // Arrange
377
+ falkorDBService.client = null;
378
+ // Act & Assert
379
+ await expect(falkorDBService.listGraphs())
380
+ .rejects
381
+ .toThrow(AppError);
382
+ await expect(falkorDBService.listGraphs())
383
+ .rejects
384
+ .toThrow('FalkorDB client not initialized');
385
+ });
386
+ it('should throw AppError when listing graphs fails', async () => {
387
+ // Arrange
388
+ const listError = new Error('Database connection lost');
389
+ mockFalkorDB.mockList.mockRejectedValue(listError);
390
+ // Force client to be available
391
+ falkorDBService.client = {
392
+ list: mockFalkorDB.mockList
393
+ };
394
+ // Act & Assert
395
+ try {
396
+ await falkorDBService.listGraphs();
397
+ fail('Expected listGraphs to throw AppError');
398
+ }
399
+ catch (error) {
400
+ expect(error).toBeInstanceOf(AppError);
401
+ expect(error.name).toBe(CommonErrors.OPERATION_FAILED);
402
+ }
403
+ });
404
+ });
405
+ describe('deleteGraph', () => {
406
+ it('should delete a graph successfully', async () => {
407
+ // Arrange
408
+ const graphName = 'testGraph';
409
+ mockFalkorDB.mockDelete.mockResolvedValue(undefined);
410
+ // Force client to be available
411
+ falkorDBService.client = {
412
+ selectGraph: mockFalkorDB.mockSelectGraph
413
+ };
414
+ // Act
415
+ await falkorDBService.deleteGraph(graphName);
416
+ // Assert
417
+ expect(mockFalkorDB.mockSelectGraph).toHaveBeenCalledWith(graphName);
418
+ expect(mockFalkorDB.mockDelete).toHaveBeenCalled();
419
+ });
420
+ it('should throw AppError if client is not initialized', async () => {
421
+ // Arrange
422
+ falkorDBService.client = null;
423
+ // Act & Assert
424
+ await expect(falkorDBService.deleteGraph('testGraph'))
425
+ .rejects
426
+ .toThrow(AppError);
427
+ await expect(falkorDBService.deleteGraph('testGraph'))
428
+ .rejects
429
+ .toThrow('FalkorDB client not initialized');
430
+ });
431
+ it('should throw AppError when delete operation fails', async () => {
432
+ // Arrange
433
+ const graphName = 'testGraph';
434
+ const deleteError = new Error('Graph not found');
435
+ mockFalkorDB.mockDelete.mockRejectedValue(deleteError);
436
+ // Force client to be available
437
+ falkorDBService.client = {
438
+ selectGraph: mockFalkorDB.mockSelectGraph
439
+ };
440
+ // Act & Assert
441
+ try {
442
+ await falkorDBService.deleteGraph(graphName);
443
+ fail('Expected deleteGraph to throw AppError');
444
+ }
445
+ catch (error) {
446
+ expect(error).toBeInstanceOf(AppError);
447
+ expect(error.name).toBe(CommonErrors.OPERATION_FAILED);
448
+ }
449
+ });
450
+ });
451
+ describe('close', () => {
452
+ it('should close the client connection successfully', async () => {
453
+ // Arrange
454
+ mockFalkorDB.mockClose.mockResolvedValue(undefined);
455
+ falkorDBService.client = {
456
+ close: mockFalkorDB.mockClose
457
+ };
458
+ falkorDBService.retryCount = 3;
459
+ // Act
460
+ await falkorDBService.close();
461
+ // Assert
462
+ expect(mockFalkorDB.mockClose).toHaveBeenCalled();
463
+ expect(falkorDBService.client).toBeNull();
464
+ expect(falkorDBService.retryCount).toBe(0);
465
+ });
466
+ it('should handle close error gracefully', async () => {
467
+ // Arrange
468
+ const closeError = new Error('Close failed');
469
+ mockFalkorDB.mockClose.mockRejectedValue(closeError);
470
+ falkorDBService.client = {
471
+ close: mockFalkorDB.mockClose
472
+ };
473
+ falkorDBService.retryCount = 2;
474
+ // Act
475
+ await falkorDBService.close();
476
+ // Assert
477
+ expect(mockFalkorDB.mockClose).toHaveBeenCalled();
478
+ expect(falkorDBService.client).toBeNull();
479
+ expect(falkorDBService.retryCount).toBe(0);
480
+ });
481
+ it('should not throw if client is already null', async () => {
482
+ // Arrange
483
+ falkorDBService.client = null;
484
+ // Act & Assert
485
+ await expect(falkorDBService.close()).resolves.not.toThrow();
486
+ expect(mockFalkorDB.mockClose).not.toHaveBeenCalled();
487
+ });
488
+ });
489
+ });
@@ -0,0 +1,151 @@
1
+ import { appendFileSync, existsSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { userLogDir } from 'platformdirs';
4
+ /**
5
+ * Enhanced logging service for MCP servers with both MCP client notifications and file fallback
6
+ * Logs are sent to MCP clients when connected, and always written to files for persistence
7
+ */
8
+ export class Logger {
9
+ logDir;
10
+ logFile;
11
+ mcpServer;
12
+ constructor() {
13
+ // For MCP servers, we primarily use MCP notifications for logging
14
+ // File logging is optional and only enabled if environment allows it
15
+ this.logDir = userLogDir('falkordb-mcp', 'mulliken-llc', '0.1.0', false, true);
16
+ this.logFile = join(this.logDir, 'falkordb-mcp.log');
17
+ // Only create log directory if we're in development or explicitly enabled
18
+ if (process.env.ENABLE_FILE_LOGGING === 'true' || process.env.NODE_ENV === 'development') {
19
+ try {
20
+ if (!existsSync(this.logDir)) {
21
+ mkdirSync(this.logDir, { recursive: true });
22
+ }
23
+ }
24
+ catch (error) {
25
+ // If we can't create logs directory, disable file logging
26
+ console.warn('Failed to create log directory:', error instanceof Error ? error.message : String(error));
27
+ this.logFile = '';
28
+ }
29
+ }
30
+ else {
31
+ // Disable file logging by default for MCP servers
32
+ this.logFile = '';
33
+ }
34
+ }
35
+ /**
36
+ * Set the MCP server instance to enable client notifications
37
+ * This should be called after the server is created but before starting
38
+ */
39
+ setMcpServer(server) {
40
+ this.mcpServer = server;
41
+ }
42
+ formatLog(level, message, context) {
43
+ const logEntry = {
44
+ timestamp: new Date().toISOString(),
45
+ level,
46
+ message,
47
+ ...(context && { context }),
48
+ pid: process.pid
49
+ };
50
+ return JSON.stringify(logEntry) + '\n';
51
+ }
52
+ writeLog(level, message, context) {
53
+ // Guard against file logging when disabled
54
+ if (!this.logFile) {
55
+ return;
56
+ }
57
+ try {
58
+ // Always write to file for persistence
59
+ const logLine = this.formatLog(level, message, context);
60
+ appendFileSync(this.logFile, logLine);
61
+ }
62
+ catch {
63
+ // If we can't log to file, we can't do much about it without breaking MCP
64
+ // In production, consider using a more robust logging solution
65
+ }
66
+ }
67
+ async sendMcpLog(level, message, context) {
68
+ if (!this.mcpServer) {
69
+ return;
70
+ }
71
+ try {
72
+ // Format log data for MCP client
73
+ const logData = context ? `${message} | ${JSON.stringify(context)}` : message;
74
+ // Map WARN to warning for MCP spec compliance
75
+ const mcpLevel = level === 'WARN' ? 'warning' : level.toLowerCase();
76
+ // Send notification to MCP client
77
+ await this.mcpServer.server.notification({
78
+ method: 'notifications/message',
79
+ params: {
80
+ level: mcpLevel,
81
+ data: logData,
82
+ logger: 'falkordb-mcp'
83
+ }
84
+ });
85
+ }
86
+ catch {
87
+ // If MCP notification fails, just continue - file logging is our fallback
88
+ // Don't log this error to avoid infinite loops
89
+ }
90
+ }
91
+ async log(level, message, context) {
92
+ // Always write to file
93
+ this.writeLog(level, message, context);
94
+ // Try to send to MCP client if server is available
95
+ await this.sendMcpLog(level, message, context);
96
+ }
97
+ async info(message, context) {
98
+ await this.log('INFO', message, context);
99
+ }
100
+ async warn(message, context) {
101
+ await this.log('WARN', message, context);
102
+ }
103
+ async error(message, error, context) {
104
+ const errorContext = error ? {
105
+ name: error.name,
106
+ message: error.message,
107
+ stack: error.stack,
108
+ ...error.isOperational !== undefined && {
109
+ isOperational: error.isOperational
110
+ },
111
+ ...context
112
+ } : context;
113
+ await this.log('ERROR', message, errorContext);
114
+ }
115
+ async debug(message, context) {
116
+ if (process.env.NODE_ENV === 'development') {
117
+ await this.log('DEBUG', message, context);
118
+ }
119
+ }
120
+ // Synchronous versions for backward compatibility in cases where async isn't possible
121
+ infoSync(message, context) {
122
+ this.writeLog('INFO', message, context);
123
+ // Fire and forget for MCP notification
124
+ this.sendMcpLog('INFO', message, context).catch(() => { });
125
+ }
126
+ warnSync(message, context) {
127
+ this.writeLog('WARN', message, context);
128
+ this.sendMcpLog('WARN', message, context).catch(() => { });
129
+ }
130
+ errorSync(message, error, context) {
131
+ const errorContext = error ? {
132
+ name: error.name,
133
+ message: error.message,
134
+ stack: error.stack,
135
+ ...error.isOperational !== undefined && {
136
+ isOperational: error.isOperational
137
+ },
138
+ ...context
139
+ } : context;
140
+ this.writeLog('ERROR', message, errorContext);
141
+ this.sendMcpLog('ERROR', message, errorContext).catch(() => { });
142
+ }
143
+ debugSync(message, context) {
144
+ if (process.env.NODE_ENV === 'development') {
145
+ this.writeLog('DEBUG', message, context);
146
+ this.sendMcpLog('DEBUG', message, context).catch(() => { });
147
+ }
148
+ }
149
+ }
150
+ // Export singleton instance
151
+ export const logger = new Logger();