Skip to main content
Glama
logger.test.ts29.4 kB
import { ConfigurableLogger, createLogger, createMCPLogger, logger, LogEntry } from '../../src/utils/logger.js'; import { LogLevel } from '../../src/types/index.js'; describe('ConfigurableLogger', () => { let testLogger: ConfigurableLogger; let consoleSpy: { debug: jest.SpyInstance; info: jest.SpyInstance; warn: jest.SpyInstance; error: jest.SpyInstance; log: jest.SpyInstance; }; let stderrSpy: jest.SpyInstance; beforeEach(() => { // Mock console methods consoleSpy = { debug: jest.spyOn(console, 'debug').mockImplementation(), info: jest.spyOn(console, 'info').mockImplementation(), warn: jest.spyOn(console, 'warn').mockImplementation(), error: jest.spyOn(console, 'error').mockImplementation(), log: jest.spyOn(console, 'log').mockImplementation() }; // Mock stderr for structured logging stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(); testLogger = new ConfigurableLogger({ level: LogLevel.DEBUG, environment: 'test', enableColors: false, enableTimestamp: false, enableStructured: false }); }); afterEach(() => { // Restore console methods Object.values(consoleSpy).forEach(spy => spy.mockRestore()); stderrSpy.mockRestore(); }); describe('constructor', () => { it('should create logger with default configuration', () => { const defaultLogger = new ConfigurableLogger(); // Test that it doesn't throw and can log expect(() => defaultLogger.info('test')).not.toThrow(); }); it('should create logger with custom configuration', () => { const customLogger = new ConfigurableLogger({ level: LogLevel.WARN, environment: 'production', enableColors: true, enableTimestamp: true, enableStructured: true, context: 'test-context' }); expect(() => customLogger.warn('test')).not.toThrow(); }); it('should use environment-based defaults', () => { const originalEnv = process.env.NODE_ENV; // Test production environment process.env.NODE_ENV = 'production'; const prodLogger = new ConfigurableLogger(); expect(() => prodLogger.info('test')).not.toThrow(); // Test development environment process.env.NODE_ENV = 'development'; const devLogger = new ConfigurableLogger(); expect(() => devLogger.info('test')).not.toThrow(); process.env.NODE_ENV = originalEnv; }); }); describe('logging methods', () => { it('should log debug messages', () => { testLogger.debug('debug message', { data: 'test' }); expect(consoleSpy.debug).toHaveBeenCalledWith( '[DEBUG] debug message', { data: 'test' } ); }); it('should log info messages', () => { testLogger.info('info message', { data: 'test' }); expect(consoleSpy.info).toHaveBeenCalledWith( '[INFO] info message', { data: 'test' } ); }); it('should log warning messages', () => { testLogger.warn('warning message', { data: 'test' }); expect(consoleSpy.warn).toHaveBeenCalledWith( '[WARN] warning message', { data: 'test' } ); }); it('should log error messages', () => { testLogger.error('error message', { data: 'test' }); expect(consoleSpy.error).toHaveBeenCalledWith( '[ERROR] error message', { data: 'test' } ); }); it('should handle multiple arguments', () => { testLogger.info('message', 'arg1', 'arg2', { data: 'test' }); expect(consoleSpy.info).toHaveBeenCalledWith( '[INFO] message', 'arg1', 'arg2', { data: 'test' } ); }); }); describe('log level filtering', () => { it('should respect log level filtering', () => { const warnLogger = new ConfigurableLogger({ level: LogLevel.WARN, enableColors: false, enableTimestamp: false, enableStructured: false }); warnLogger.debug('debug message'); warnLogger.info('info message'); warnLogger.warn('warn message'); warnLogger.error('error message'); expect(consoleSpy.debug).not.toHaveBeenCalled(); expect(consoleSpy.info).not.toHaveBeenCalled(); expect(consoleSpy.warn).toHaveBeenCalledWith('[WARN] warn message'); expect(consoleSpy.error).toHaveBeenCalledWith('[ERROR] error message'); }); it('should silence all logs when level is SILENT', () => { const silentLogger = new ConfigurableLogger({ level: LogLevel.SILENT, enableColors: false, enableTimestamp: false, enableStructured: false }); silentLogger.debug('debug'); silentLogger.info('info'); silentLogger.warn('warn'); silentLogger.error('error'); expect(consoleSpy.debug).not.toHaveBeenCalled(); expect(consoleSpy.info).not.toHaveBeenCalled(); expect(consoleSpy.warn).not.toHaveBeenCalled(); expect(consoleSpy.error).not.toHaveBeenCalled(); }); }); describe('setLevel', () => { it('should set log level using LogLevel enum', () => { testLogger.setLevel(LogLevel.ERROR); testLogger.info('info message'); testLogger.error('error message'); expect(consoleSpy.info).not.toHaveBeenCalled(); expect(consoleSpy.error).toHaveBeenCalledWith('[ERROR] error message'); }); it('should set log level using string', () => { testLogger.setLevel('warn'); testLogger.info('info message'); testLogger.warn('warn message'); expect(consoleSpy.info).not.toHaveBeenCalled(); expect(consoleSpy.warn).toHaveBeenCalledWith('[WARN] warn message'); }); it('should handle case-insensitive string levels', () => { testLogger.setLevel('ERROR'); testLogger.warn('warn message'); testLogger.error('error message'); expect(consoleSpy.warn).not.toHaveBeenCalled(); expect(consoleSpy.error).toHaveBeenCalledWith('[ERROR] error message'); }); it('should warn about invalid string levels', () => { testLogger.setLevel('invalid'); expect(consoleSpy.warn).toHaveBeenCalledWith( expect.stringContaining('Invalid log level: invalid') ); }); }); describe('context handling', () => { it('should set and use context', () => { testLogger.setContext('test-context'); testLogger.info('test message'); expect(consoleSpy.info).toHaveBeenCalledWith( '[test-context] [INFO] test message' ); }); it('should update context', () => { testLogger.setContext('context1'); testLogger.info('message1'); testLogger.setContext('context2'); testLogger.info('message2'); expect(consoleSpy.info).toHaveBeenNthCalledWith(1, '[context1] [INFO] message1'); expect(consoleSpy.info).toHaveBeenNthCalledWith(2, '[context2] [INFO] message2'); }); }); describe('child logger', () => { it('should create child logger with nested context', () => { testLogger.setContext('parent'); const childLogger = testLogger.child('child'); childLogger.info('child message'); expect(consoleSpy.info).toHaveBeenCalledWith( '[parent:child] [INFO] child message' ); }); it('should create child logger without parent context', () => { const childLogger = testLogger.child('child'); childLogger.info('child message'); expect(consoleSpy.info).toHaveBeenCalledWith( '[child] [INFO] child message' ); }); it('should inherit parent configuration', () => { const parentLogger = new ConfigurableLogger({ level: LogLevel.WARN, enableColors: false, enableTimestamp: false, enableStructured: false }); const childLogger = parentLogger.child('child'); childLogger.info('info message'); // Should be filtered out childLogger.warn('warn message'); // Should be logged expect(consoleSpy.info).not.toHaveBeenCalled(); expect(consoleSpy.warn).toHaveBeenCalledWith('[child] [WARN] warn message'); }); }); describe('error logging', () => { it('should log error objects with stack trace', () => { const error = new Error('Test error'); error.stack = 'Error: Test error\n at test'; testLogger.logError(error); expect(consoleSpy.error).toHaveBeenCalledWith( '[ERROR] Test error', { error: { name: 'Error', message: 'Test error', stack: 'Error: Test error\n at test' } } ); }); it('should log error objects with context', () => { const error = new Error('Test error'); testLogger.logError(error, 'test-context'); expect(consoleSpy.error).toHaveBeenCalledWith( '[ERROR] Test error', { error: { name: 'Error', message: 'Test error', stack: error.stack } } ); }); it('should handle errors without message', () => { const error = new Error(); error.name = 'CustomError'; testLogger.logError(error); expect(consoleSpy.error).toHaveBeenCalledWith( '[ERROR] CustomError', { error: { name: 'CustomError', message: '', stack: error.stack } } ); }); }); describe('structured logging', () => { let structuredLogger: ConfigurableLogger; beforeEach(() => { structuredLogger = new ConfigurableLogger({ level: LogLevel.DEBUG, enableStructured: true, context: 'test-context' }); }); it('should log in JSON format when structured logging is enabled', () => { structuredLogger.info('test message', { data: 'test' }); expect(stderrSpy).toHaveBeenCalledWith( expect.stringContaining('"level":"info"') ); expect(stderrSpy).toHaveBeenCalledWith( expect.stringContaining('"message":"test message"') ); expect(stderrSpy).toHaveBeenCalledWith( expect.stringContaining('"context":"test-context"') ); }); it('should include timestamp in structured logs', () => { structuredLogger.info('test message'); const logCall = stderrSpy.mock.calls[0][0]; const logEntry = JSON.parse(logCall); expect(logEntry.timestamp).toBeDefined(); expect(new Date(logEntry.timestamp)).toBeInstanceOf(Date); }); it('should include data in structured logs', () => { structuredLogger.info('test message', { key: 'value' }, 'extra arg'); const logCall = stderrSpy.mock.calls[0][0]; const logEntry = JSON.parse(logCall); expect(logEntry.data).toEqual({ args: [{ key: 'value' }, 'extra arg'] }); }); it('should log errors in structured format', () => { const error = new Error('Test error'); structuredLogger.logError(error, 'error-context'); const logCall = stderrSpy.mock.calls[0][0]; const logEntry = JSON.parse(logCall); expect(logEntry.level).toBe('error'); expect(logEntry.message).toBe('Test error'); expect(logEntry.context).toBe('error-context'); expect(logEntry.data.error).toEqual({ name: 'Error', message: 'Test error', stack: error.stack }); }); }); describe('timestamp formatting', () => { it('should include timestamp when enabled', () => { const timestampLogger = new ConfigurableLogger({ level: LogLevel.INFO, enableTimestamp: true, enableColors: false, enableStructured: false }); timestampLogger.info('test message'); const logCall = consoleSpy.info.mock.calls[0][0]; expect(logCall).toMatch(/^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[INFO\] test message$/); }); it('should exclude timestamp when disabled', () => { const noTimestampLogger = new ConfigurableLogger({ level: LogLevel.INFO, enableTimestamp: false, enableColors: false, enableStructured: false }); noTimestampLogger.info('test message'); expect(consoleSpy.info).toHaveBeenCalledWith('[INFO] test message'); }); }); describe('color formatting', () => { let colorLogger: ConfigurableLogger; beforeEach(() => { colorLogger = new ConfigurableLogger({ level: LogLevel.DEBUG, enableColors: true, enableTimestamp: false, enableStructured: false }); }); it('should apply colors to debug messages', () => { colorLogger.debug('debug message'); const logCall = consoleSpy.debug.mock.calls[0][0]; expect(logCall).toContain('\x1b[90m'); // Gray color expect(logCall).toContain('\x1b[0m'); // Reset color }); it('should apply colors to info messages', () => { colorLogger.info('info message'); const logCall = consoleSpy.info.mock.calls[0][0]; expect(logCall).toContain('\x1b[34m'); // Blue color expect(logCall).toContain('\x1b[0m'); // Reset color }); it('should apply colors to warning messages', () => { colorLogger.warn('warn message'); const logCall = consoleSpy.warn.mock.calls[0][0]; expect(logCall).toContain('\x1b[33m'); // Yellow color expect(logCall).toContain('\x1b[0m'); // Reset color }); it('should apply colors to error messages', () => { colorLogger.error('error message'); const logCall = consoleSpy.error.mock.calls[0][0]; expect(logCall).toContain('\x1b[31m'); // Red color expect(logCall).toContain('\x1b[0m'); // Reset color }); it('should not apply colors when disabled', () => { const noColorLogger = new ConfigurableLogger({ level: LogLevel.INFO, enableColors: false, enableTimestamp: false, enableStructured: false }); noColorLogger.error('error message'); const logCall = consoleSpy.error.mock.calls[0][0]; expect(logCall).not.toContain('\x1b['); expect(logCall).toBe('[ERROR] error message'); }); }); describe('updateConfig', () => { it('should update logger configuration', () => { testLogger.updateConfig({ level: LogLevel.ERROR, enableColors: true, context: 'updated-context' }); testLogger.info('info message'); // Should be filtered testLogger.error('error message'); // Should be logged expect(consoleSpy.info).not.toHaveBeenCalled(); expect(consoleSpy.error).toHaveBeenCalledWith( expect.stringContaining('[updated-context]') ); }); it('should merge configuration partially', () => { const originalConfig = { level: LogLevel.DEBUG, enableColors: false, enableTimestamp: false, enableStructured: false, context: 'original' }; const partialLogger = new ConfigurableLogger(originalConfig); partialLogger.updateConfig({ level: LogLevel.WARN }); partialLogger.info('info message'); // Should be filtered partialLogger.warn('warn message'); // Should be logged expect(consoleSpy.info).not.toHaveBeenCalled(); expect(consoleSpy.warn).toHaveBeenCalledWith('[original] [WARN] warn message'); }); it('should handle empty config update', () => { const originalLevel = testLogger['config'].level; testLogger.updateConfig({}); // Configuration should remain unchanged expect(testLogger['config'].level).toBe(originalLevel); }); it('should handle null config update', () => { expect(() => testLogger.updateConfig(null as any)).not.toThrow(); }); it('should handle undefined config update', () => { expect(() => testLogger.updateConfig(undefined as any)).not.toThrow(); }); }); describe('Performance and Edge Cases', () => { it('should handle very long log messages', () => { const longMessage = 'This is a very long message. '.repeat(1000); expect(() => testLogger.info(longMessage)).not.toThrow(); expect(consoleSpy.info).toHaveBeenCalledWith(`[INFO] ${longMessage}`); }); it('should handle unicode characters in log messages', () => { const unicodeMessage = 'Unicode test: 你好世界 🌍 émojis & symbols!@#$%^&*()'; testLogger.info(unicodeMessage); expect(consoleSpy.info).toHaveBeenCalledWith(`[INFO] ${unicodeMessage}`); }); it('should handle circular references in log data', () => { const circularObj: any = { name: 'test' }; circularObj.self = circularObj; expect(() => testLogger.info('Circular test', circularObj)).not.toThrow(); }); it('should handle null and undefined arguments', () => { expect(() => testLogger.info(null as any)).not.toThrow(); expect(() => testLogger.info(undefined as any)).not.toThrow(); expect(() => testLogger.info('test', null, undefined)).not.toThrow(); }); it('should handle functions as arguments', () => { const testFunction = () => 'test function'; expect(() => testLogger.info('Function test', testFunction)).not.toThrow(); }); it('should handle symbols as arguments', () => { const testSymbol = Symbol('test'); expect(() => testLogger.info('Symbol test', testSymbol)).not.toThrow(); }); it('should handle large number of arguments', () => { const args = Array.from({ length: 100 }, (_, i) => `arg${i}`); expect(() => testLogger.info('Many args', ...args)).not.toThrow(); }); it('should handle concurrent logging', async () => { const promises = Array.from({ length: 100 }, (_, i) => Promise.resolve(testLogger.info(`Concurrent log ${i}`)) ); await Promise.all(promises); expect(consoleSpy.info).toHaveBeenCalledTimes(100); }); it('should handle rapid successive logging', () => { for (let i = 0; i < 1000; i++) { testLogger.info(`Rapid log ${i}`); } expect(consoleSpy.info).toHaveBeenCalledTimes(1000); }); it('should handle logging with Date objects', () => { const now = new Date(); testLogger.info('Date test', { timestamp: now }); expect(consoleSpy.info).toHaveBeenCalledWith('[INFO] Date test', { timestamp: now }); }); it('should handle logging with RegExp objects', () => { const regex = /test-pattern/gi; testLogger.info('RegExp test', { pattern: regex }); expect(consoleSpy.info).toHaveBeenCalledWith('[INFO] RegExp test', { pattern: regex }); }); it('should handle logging with Error objects as data', () => { const error = new Error('Test error'); testLogger.info('Error as data', { error }); expect(consoleSpy.info).toHaveBeenCalledWith('[INFO] Error as data', { error }); }); it('should handle deeply nested objects', () => { const deepObject = { level1: { level2: { level3: { level4: { level5: 'deep value' } } } } }; expect(() => testLogger.info('Deep object', deepObject)).not.toThrow(); }); it('should handle arrays with mixed types', () => { const mixedArray = [ 'string', 123, true, null, undefined, { object: 'value' }, ['nested', 'array'], new Date(), /regex/ ]; expect(() => testLogger.info('Mixed array', mixedArray)).not.toThrow(); }); }); describe('Memory Management', () => { it('should not leak memory with repeated child logger creation', () => { // Create and discard many child loggers for (let i = 0; i < 1000; i++) { const child = testLogger.child(`child-${i}`); child.info(`Child log ${i}`); } // This test mainly ensures no errors are thrown // Memory leak detection would require more sophisticated tooling expect(consoleSpy.info).toHaveBeenCalledTimes(1000); }); it('should handle logger destruction gracefully', () => { const tempLogger = new ConfigurableLogger(); tempLogger.info('Before destruction'); // Simulate logger going out of scope // In a real scenario, this would be handled by garbage collection expect(() => tempLogger.info('After destruction')).not.toThrow(); }); }); }); describe('createLogger', () => { let consoleSpy: jest.SpyInstance; beforeEach(() => { consoleSpy = jest.spyOn(console, 'info').mockImplementation(); }); afterEach(() => { consoleSpy.mockRestore(); }); it('should create logger with default configuration', () => { const newLogger = createLogger(); expect(() => newLogger.info('test')).not.toThrow(); }); it('should create logger with custom configuration', () => { const newLogger = createLogger({ level: LogLevel.WARN, context: 'custom-logger' }); expect(() => newLogger.warn('test')).not.toThrow(); }); }); describe('default logger', () => { let consoleSpy: jest.SpyInstance; beforeEach(() => { consoleSpy = jest.spyOn(console, 'info').mockImplementation(); }); afterEach(() => { consoleSpy.mockRestore(); }); it('should be available as default export', () => { expect(logger).toBeDefined(); expect(() => logger.info('test')).not.toThrow(); }); it('should be an instance of ConfigurableLogger', () => { expect(logger).toBeInstanceOf(ConfigurableLogger); }); }); describe('LogEntry interface', () => { it('should define the correct structure', () => { const entry: LogEntry = { timestamp: '2023-01-01T00:00:00.000Z', level: 'info', message: 'test message', context: 'test-context', data: { key: 'value' }, error: { name: 'Error', message: 'error message', stack: 'stack trace' } }; expect(entry.timestamp).toBe('2023-01-01T00:00:00.000Z'); expect(entry.level).toBe('info'); expect(entry.message).toBe('test message'); expect(entry.context).toBe('test-context'); expect(entry.data).toEqual({ key: 'value' }); expect(entry.error).toEqual({ name: 'Error', message: 'error message', stack: 'stack trace' }); }); }); describe('Enhanced Logger Features', () => { let stderrSpy: jest.SpyInstance; let consoleSpy: { debug: jest.SpyInstance; info: jest.SpyInstance; warn: jest.SpyInstance; error: jest.SpyInstance; }; beforeEach(() => { stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(); consoleSpy = { debug: jest.spyOn(console, 'debug').mockImplementation(), info: jest.spyOn(console, 'info').mockImplementation(), warn: jest.spyOn(console, 'warn').mockImplementation(), error: jest.spyOn(console, 'error').mockImplementation() }; }); afterEach(() => { stderrSpy.mockRestore(); Object.values(consoleSpy).forEach(spy => spy.mockRestore()); }); describe('Safe JSON Serialization', () => { it('should handle circular references', () => { const logger = new ConfigurableLogger({ enableStructured: true, outputStream: 'stderr' }); const obj: any = { name: 'test' }; obj.self = obj; // Create circular reference logger.info('test message', obj); expect(stderrSpy).toHaveBeenCalled(); const logCall = stderrSpy.mock.calls[0][0]; expect(logCall).toContain('[Circular Reference]'); }); it('should handle functions in data', () => { const logger = new ConfigurableLogger({ enableStructured: true, outputStream: 'stderr' }); logger.info('test message', { fn: () => 'test' }); expect(stderrSpy).toHaveBeenCalled(); const logCall = stderrSpy.mock.calls[0][0]; expect(logCall).toContain('[Function]'); }); it('should handle undefined values', () => { const logger = new ConfigurableLogger({ enableStructured: true, outputStream: 'stderr' }); logger.info('test message', { value: undefined }); expect(stderrSpy).toHaveBeenCalled(); const logCall = stderrSpy.mock.calls[0][0]; expect(logCall).toContain('[Undefined]'); }); it('should handle symbols', () => { const logger = new ConfigurableLogger({ enableStructured: true, outputStream: 'stderr' }); const sym = Symbol('test'); logger.info('test message', { symbol: sym }); expect(stderrSpy).toHaveBeenCalled(); const logCall = stderrSpy.mock.calls[0][0]; expect(logCall).toContain('Symbol(test)'); }); it('should handle bigint values', () => { const logger = new ConfigurableLogger({ enableStructured: true, outputStream: 'stderr' }); logger.info('test message', { bigNum: BigInt(123) }); expect(stderrSpy).toHaveBeenCalled(); const logCall = stderrSpy.mock.calls[0][0]; expect(logCall).toContain('"123"'); }); it('should handle Error objects in data', () => { const logger = new ConfigurableLogger({ enableStructured: true, outputStream: 'stderr' }); const error = new Error('test error'); logger.info('test message', { error }); expect(stderrSpy).toHaveBeenCalled(); const logCall = stderrSpy.mock.calls[0][0]; const parsed = JSON.parse(logCall); expect(parsed.data.args[0].error.name).toBe('Error'); expect(parsed.data.args[0].error.message).toBe('test error'); }); it('should respect max depth limit', () => { const logger = new ConfigurableLogger({ enableStructured: true, outputStream: 'stderr', maxDepth: 2 }); const deepObj = { level1: { level2: { level3: { level4: 'deep' } } } }; logger.info('test message', deepObj); expect(stderrSpy).toHaveBeenCalled(); const logCall = stderrSpy.mock.calls[0][0]; expect(logCall).toContain('[Max depth exceeded]'); }); }); describe('Fallback Mechanisms', () => { it('should fallback to console logging when JSON serialization fails', () => { const logger = new ConfigurableLogger({ enableStructured: true, enableFallback: true, outputStream: 'stderr' }); // Mock stderr.write to throw an error stderrSpy.mockImplementation(() => { throw new Error('Write failed'); }); logger.info('test message'); expect(consoleSpy.info).toHaveBeenCalled(); expect(consoleSpy.error).toHaveBeenCalledWith( expect.stringContaining('JSON serialization failed:'), 'Write failed' ); }); it('should not fallback when fallback is disabled', () => { const logger = new ConfigurableLogger({ enableStructured: true, enableFallback: false, outputStream: 'stderr' }); // Mock stderr.write to throw an error stderrSpy.mockImplementation(() => { throw new Error('Write failed'); }); logger.info('test message'); expect(consoleSpy.info).not.toHaveBeenCalled(); }); }); describe('Output Stream Configuration', () => { it('should use stderr by default for structured logging', () => { const logger = new ConfigurableLogger({ enableStructured: true }); logger.info('test message'); expect(stderrSpy).toHaveBeenCalled(); }); it('should use stdout when configured', () => { const stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(); const logger = new ConfigurableLogger({ enableStructured: true, outputStream: 'stdout' }); logger.info('test message'); expect(stdoutSpy).toHaveBeenCalled(); expect(stderrSpy).not.toHaveBeenCalled(); stdoutSpy.mockRestore(); }); }); }); describe('createMCPLogger', () => { let stderrSpy: jest.SpyInstance; beforeEach(() => { stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(); }); afterEach(() => { stderrSpy.mockRestore(); }); it('should create logger with MCP-safe defaults', () => { const mcpLogger = createMCPLogger(); mcpLogger.info('test message'); expect(stderrSpy).toHaveBeenCalled(); const logCall = stderrSpy.mock.calls[0][0]; const parsed = JSON.parse(logCall); expect(parsed.level).toBe('info'); expect(parsed.message).toBe('test message'); }); it('should allow custom configuration while maintaining MCP safety', () => { const mcpLogger = createMCPLogger({ level: LogLevel.WARN, context: 'mcp-server' }); mcpLogger.info('info message'); // Should be filtered mcpLogger.warn('warn message'); // Should be logged expect(stderrSpy).toHaveBeenCalledTimes(1); const logCall = stderrSpy.mock.calls[0][0]; const parsed = JSON.parse(logCall); expect(parsed.level).toBe('warn'); expect(parsed.context).toBe('mcp-server'); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/celeryhq/simplified-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server