From 7d65c90b6ba58821f5382636e2cb7152d5116049 Mon Sep 17 00:00:00 2001 From: Stanislav Khromov Date: Fri, 18 Jul 2025 22:37:55 +0200 Subject: [PATCH 1/6] Add support for custom logger in MCP handler Introduces a Logger interface and allows passing a custom logger implementation via the config object. Updates documentation with usage examples and exports logger types and utilities. The default logger logic is moved to a new file, enabling better extensibility and integration with external logging systems. --- README.md | 62 +++++++++++++++++++++++ src/handler/mcp-api-handler.ts | 53 ++++++++----------- src/index.ts | 7 +++ src/types/logger.ts | 93 ++++++++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 31 deletions(-) create mode 100644 src/types/logger.ts diff --git a/README.md b/README.md index c9b37a6..393ec6d 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,68 @@ interface Config { basePath?: string; // Base path for MCP endpoints maxDuration?: number; // Maximum duration for SSE connections in seconds verboseLogs?: boolean; // Log debugging information + logger?: Logger; // Custom logger implementation +} +``` + +### Custom Logger + +You can provide a custom logger implementation to handle all logging from the MCP adapter. This is useful for integrating with existing logging systems or adding custom formatting. + +```typescript +import { createMcpHandler, Logger } from "mcp-handler"; + +// Custom logger with timestamp and prefix +const customLogger: Logger = { + log: (...args) => console.log(`[${new Date().toISOString()}] [MCP]`, ...args), + error: (...args) => console.error(`[${new Date().toISOString()}] [MCP ERROR]`, ...args), + warn: (...args) => console.warn(`[${new Date().toISOString()}] [MCP WARN]`, ...args), + info: (...args) => console.info(`[${new Date().toISOString()}] [MCP INFO]`, ...args), + debug: (...args) => console.debug(`[${new Date().toISOString()}] [MCP DEBUG]`, ...args), +}; + +// Or create a logger that respects verboseLogs setting +function createCustomLogger(verboseLogs: boolean): Logger { + return { + log: (...args) => { + if (verboseLogs) console.log(`[MCP]`, ...args); + }, + error: (...args) => { + if (verboseLogs) console.error(`[MCP ERROR]`, ...args); + }, + warn: (...args) => { + if (verboseLogs) console.warn(`[MCP WARN]`, ...args); + }, + info: (...args) => { + if (verboseLogs) console.info(`[MCP INFO]`, ...args); + }, + debug: (...args) => { + if (verboseLogs) console.debug(`[MCP DEBUG]`, ...args); + }, + }; +} + +const handler = createMcpHandler( + (server) => { + // Your server setup + }, + {}, + { + logger: createCustomLogger(true), // Custom logger takes precedence over verboseLogs + verboseLogs: false, // This will be ignored when logger is provided + } +); +``` + +The Logger interface requires these methods: + +```typescript +interface Logger { + log: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + info: (...args: unknown[]) => void; + debug: (...args: unknown[]) => void; } ``` diff --git a/src/handler/mcp-api-handler.ts b/src/handler/mcp-api-handler.ts index 78e7805..7f76a65 100644 --- a/src/handler/mcp-api-handler.ts +++ b/src/handler/mcp-api-handler.ts @@ -22,6 +22,7 @@ import { EventEmittingResponse } from "../lib/event-emitter.js"; import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types"; import { getAuthContext } from "../auth/auth-context"; import { ServerOptions } from "."; +import { Logger, LogLevel, createDefaultLogger } from "../types/logger"; interface SerializedRequest { requestId: string; @@ -30,42 +31,13 @@ interface SerializedRequest { body: BodyType; headers: IncomingHttpHeaders; } - -type LogLevel = "log" | "error" | "warn" | "info" | "debug"; - -type Logger = { - log: (...args: unknown[]) => void; - error: (...args: unknown[]) => void; - warn: (...args: unknown[]) => void; - info: (...args: unknown[]) => void; - debug: (...args: unknown[]) => void; -}; - -function createLogger(verboseLogs = false): Logger { - return { - log: (...args: unknown[]) => { - if (verboseLogs) console.log(...args); - }, - error: (...args: unknown[]) => { - if (verboseLogs) console.error(...args); - }, - warn: (...args: unknown[]) => { - if (verboseLogs) console.warn(...args); - }, - info: (...args: unknown[]) => { - if (verboseLogs) console.info(...args); - }, - debug: (...args: unknown[]) => { - if (verboseLogs) console.debug(...args); - }, - }; -} /** * Configuration for the MCP handler. * @property redisUrl - The URL of the Redis instance to use for the MCP handler. * @property streamableHttpEndpoint - The endpoint to use for the streamable HTTP transport. * @property sseEndpoint - The endpoint to use for the SSE transport. * @property verboseLogs - If true, enables console logging. + * @property logger - Custom logger implementation. If provided, takes precedence over verboseLogs. */ export type Config = { /** @@ -125,6 +97,25 @@ export type Config = { * @default false */ disableSse?: boolean; + + /** + * Custom logger implementation. + * If provided, this logger will be used instead of the default console logger. + * Takes precedence over the verboseLogs option. + * @example + * ```typescript + * const config = { + * logger: { + * log: (...args) => console.log('[MCP]', ...args), + * error: (...args) => console.error('[MCP ERROR]', ...args), + * warn: (...args) => console.warn('[MCP WARN]', ...args), + * info: (...args) => console.info('[MCP INFO]', ...args), + * debug: (...args) => console.debug('[MCP DEBUG]', ...args), + * } + * }; + * ``` + */ + logger?: Logger; }; /** @@ -256,7 +247,7 @@ export function initializeMcpApiHandler( sseMessageEndpoint: explicitSseMessageEndpoint, }); - const logger = createLogger(verboseLogs); + const logger = config.logger || createDefaultLogger({ verboseLogs }); let servers: McpServer[] = []; diff --git a/src/index.ts b/src/index.ts index 93b3235..0bae0a3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,3 +13,10 @@ export { generateProtectedResourceMetadata, metadataCorsOptionsRequestHandler, } from "./auth/auth-metadata"; + +export { + Logger, + LogLevel, + DefaultLoggerOptions, + createDefaultLogger, +} from "./types/logger"; diff --git a/src/types/logger.ts b/src/types/logger.ts new file mode 100644 index 0000000..932b523 --- /dev/null +++ b/src/types/logger.ts @@ -0,0 +1,93 @@ +/** + * Log levels supported by the MCP handler + */ +export type LogLevel = "log" | "error" | "warn" | "info" | "debug"; + +/** + * Logger interface for custom logging implementations + * + * @example + * ```typescript + * import { Logger } from "mcp-handler"; + * + * const customLogger: Logger = { + * log: (...args) => myLogger.info(...args), + * error: (...args) => myLogger.error(...args), + * warn: (...args) => myLogger.warn(...args), + * info: (...args) => myLogger.info(...args), + * debug: (...args) => myLogger.debug(...args), + * }; + * ``` + */ +export interface Logger { + /** + * Log general information messages + */ + log: (...args: unknown[]) => void; + + /** + * Log error messages + */ + error: (...args: unknown[]) => void; + + /** + * Log warning messages + */ + warn: (...args: unknown[]) => void; + + /** + * Log informational messages + */ + info: (...args: unknown[]) => void; + + /** + * Log debug messages + */ + debug: (...args: unknown[]) => void; +} + +/** + * Options for creating a default console logger + */ +export interface DefaultLoggerOptions { + /** + * Whether to enable verbose logging to console + * @default false + */ + verboseLogs?: boolean; +} + +/** + * Creates a default console-based logger implementation + * + * @param options - Configuration options for the default logger + * @returns A Logger instance that logs to the console + * + * @example + * ```typescript + * import { createDefaultLogger } from "mcp-handler"; + * + * const logger = createDefaultLogger({ verboseLogs: true }); + * ``` + */ +export function createDefaultLogger(options: DefaultLoggerOptions = {}): Logger { + const { verboseLogs = false } = options; + + return { + log: (...args: unknown[]) => { + if (verboseLogs) console.log(...args); + }, + error: (...args: unknown[]) => { + if (verboseLogs) console.error(...args); + }, + warn: (...args: unknown[]) => { + if (verboseLogs) console.warn(...args); + }, + info: (...args: unknown[]) => { + if (verboseLogs) console.info(...args); + }, + debug: (...args: unknown[]) => { + if (verboseLogs) console.debug(...args); + }, + }; +} \ No newline at end of file From 172db04bea2ae628e92d62be7f28128cf3ac0665 Mon Sep 17 00:00:00 2001 From: Stanislav Khromov Date: Fri, 18 Jul 2025 22:44:01 +0200 Subject: [PATCH 2/6] Add tests for logger utilities and types Introduces unit tests for the logger implementation, including createDefaultLogger, custom logger interfaces, and LogLevel type. Tests cover logging behavior based on verbosity, argument handling, and custom logger flexibility. --- tests/logger.test.ts | 131 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 tests/logger.test.ts diff --git a/tests/logger.test.ts b/tests/logger.test.ts new file mode 100644 index 0000000..b7615ba --- /dev/null +++ b/tests/logger.test.ts @@ -0,0 +1,131 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { createDefaultLogger, Logger, LogLevel } from '../src/types/logger'; + +describe('Logger Types and Utilities', () => { + let consoleSpy: { [K in LogLevel]: ReturnType }; + + beforeEach(() => { + // Spy on all console methods + consoleSpy = { + log: vi.spyOn(console, 'log').mockImplementation(() => {}), + error: vi.spyOn(console, 'error').mockImplementation(() => {}), + warn: vi.spyOn(console, 'warn').mockImplementation(() => {}), + info: vi.spyOn(console, 'info').mockImplementation(() => {}), + debug: vi.spyOn(console, 'debug').mockImplementation(() => {}), + }; + }); + + afterEach(() => { + // Restore all console methods + Object.values(consoleSpy).forEach(spy => spy.mockRestore()); + }); + + describe('createDefaultLogger', () => { + it('creates a logger that logs when verboseLogs is true', () => { + const logger = createDefaultLogger({ verboseLogs: true }); + + logger.log('test log'); + logger.error('test error'); + logger.warn('test warn'); + logger.info('test info'); + logger.debug('test debug'); + + expect(consoleSpy.log).toHaveBeenCalledWith('test log'); + expect(consoleSpy.error).toHaveBeenCalledWith('test error'); + expect(consoleSpy.warn).toHaveBeenCalledWith('test warn'); + expect(consoleSpy.info).toHaveBeenCalledWith('test info'); + expect(consoleSpy.debug).toHaveBeenCalledWith('test debug'); + }); + + it('creates a logger that does not log when verboseLogs is false', () => { + const logger = createDefaultLogger({ verboseLogs: false }); + + logger.log('test log'); + logger.error('test error'); + logger.warn('test warn'); + logger.info('test info'); + logger.debug('test debug'); + + expect(consoleSpy.log).not.toHaveBeenCalled(); + expect(consoleSpy.error).not.toHaveBeenCalled(); + expect(consoleSpy.warn).not.toHaveBeenCalled(); + expect(consoleSpy.info).not.toHaveBeenCalled(); + expect(consoleSpy.debug).not.toHaveBeenCalled(); + }); + + it('defaults to verboseLogs false when no options provided', () => { + const logger = createDefaultLogger(); + + logger.log('test log'); + expect(consoleSpy.log).not.toHaveBeenCalled(); + }); + + it('passes multiple arguments correctly', () => { + const logger = createDefaultLogger({ verboseLogs: true }); + + logger.log('message', { data: 'test' }, 123, true); + expect(consoleSpy.log).toHaveBeenCalledWith('message', { data: 'test' }, 123, true); + }); + + it('handles undefined and null arguments', () => { + const logger = createDefaultLogger({ verboseLogs: true }); + + logger.log(undefined, null, ''); + expect(consoleSpy.log).toHaveBeenCalledWith(undefined, null, ''); + }); + }); + + describe('Custom Logger Interface', () => { + it('allows custom logger implementations', () => { + const mockLogger: Logger = { + log: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + debug: vi.fn(), + }; + + mockLogger.log('test'); + mockLogger.error('error'); + mockLogger.warn('warning'); + mockLogger.info('info'); + mockLogger.debug('debug'); + + expect(mockLogger.log).toHaveBeenCalledWith('test'); + expect(mockLogger.error).toHaveBeenCalledWith('error'); + expect(mockLogger.warn).toHaveBeenCalledWith('warning'); + expect(mockLogger.info).toHaveBeenCalledWith('info'); + expect(mockLogger.debug).toHaveBeenCalledWith('debug'); + }); + + it('supports conditional logging in custom implementations', () => { + const conditionalLogger = (enabled: boolean): Logger => ({ + log: (...args) => { if (enabled) console.log(...args); }, + error: (...args) => { if (enabled) console.error(...args); }, + warn: (...args) => { if (enabled) console.warn(...args); }, + info: (...args) => { if (enabled) console.info(...args); }, + debug: (...args) => { if (enabled) console.debug(...args); }, + }); + + const enabledLogger = conditionalLogger(true); + const disabledLogger = conditionalLogger(false); + + enabledLogger.log('enabled log'); + disabledLogger.log('disabled log'); + + expect(consoleSpy.log).toHaveBeenCalledWith('enabled log'); + expect(consoleSpy.log).toHaveBeenCalledTimes(1); + }); + }); + + describe('LogLevel Type', () => { + it('includes all expected log levels', () => { + const levels: LogLevel[] = ['log', 'error', 'warn', 'info', 'debug']; + + // This test ensures the LogLevel type matches our expectations + levels.forEach(level => { + expect(['log', 'error', 'warn', 'info', 'debug']).toContain(level); + }); + }); + }); +}); \ No newline at end of file From e78c3605eda485cd0f604b5cf6e98a21998d24c9 Mon Sep 17 00:00:00 2001 From: Stanislav Khromov Date: Fri, 18 Jul 2025 22:46:48 +0200 Subject: [PATCH 3/6] Update README.md --- README.md | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/README.md b/README.md index 393ec6d..fb4b840 100644 --- a/README.md +++ b/README.md @@ -169,34 +169,13 @@ const customLogger: Logger = { debug: (...args) => console.debug(`[${new Date().toISOString()}] [MCP DEBUG]`, ...args), }; -// Or create a logger that respects verboseLogs setting -function createCustomLogger(verboseLogs: boolean): Logger { - return { - log: (...args) => { - if (verboseLogs) console.log(`[MCP]`, ...args); - }, - error: (...args) => { - if (verboseLogs) console.error(`[MCP ERROR]`, ...args); - }, - warn: (...args) => { - if (verboseLogs) console.warn(`[MCP WARN]`, ...args); - }, - info: (...args) => { - if (verboseLogs) console.info(`[MCP INFO]`, ...args); - }, - debug: (...args) => { - if (verboseLogs) console.debug(`[MCP DEBUG]`, ...args); - }, - }; -} - const handler = createMcpHandler( (server) => { // Your server setup }, {}, { - logger: createCustomLogger(true), // Custom logger takes precedence over verboseLogs + logger: customLogger, // Custom logger takes precedence over verboseLogs verboseLogs: false, // This will be ignored when logger is provided } ); From e8dd04e30efbeca144c8d585d3ebe6b72d25026d Mon Sep 17 00:00:00 2001 From: Stanislav Khromov Date: Fri, 18 Jul 2025 22:47:12 +0200 Subject: [PATCH 4/6] Update README.md --- README.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/README.md b/README.md index fb4b840..d31074d 100644 --- a/README.md +++ b/README.md @@ -181,18 +181,6 @@ const handler = createMcpHandler( ); ``` -The Logger interface requires these methods: - -```typescript -interface Logger { - log: (...args: unknown[]) => void; - error: (...args: unknown[]) => void; - warn: (...args: unknown[]) => void; - info: (...args: unknown[]) => void; - debug: (...args: unknown[]) => void; -} -``` - ## Authorization The MCP adapter supports the [MCP Authorization Specification](https://modelcontextprotocol.io/specification/draft/basic/authorization) per the through the `withMcpAuth` wrapper. This allows you to protect your MCP endpoints and access authentication information in your tools. From 4a9ff46349974280a91b7c1b1a5c303c113af13b Mon Sep 17 00:00:00 2001 From: Stanislav Khromov Date: Fri, 18 Jul 2025 22:48:10 +0200 Subject: [PATCH 5/6] Remove example code from logger type definitions Eliminated inline TypeScript example blocks from the logger-related type definitions and config documentation to streamline code comments and reduce clutter. --- src/handler/mcp-api-handler.ts | 12 ------------ src/types/logger.ts | 20 -------------------- 2 files changed, 32 deletions(-) diff --git a/src/handler/mcp-api-handler.ts b/src/handler/mcp-api-handler.ts index 7f76a65..c134c62 100644 --- a/src/handler/mcp-api-handler.ts +++ b/src/handler/mcp-api-handler.ts @@ -102,18 +102,6 @@ export type Config = { * Custom logger implementation. * If provided, this logger will be used instead of the default console logger. * Takes precedence over the verboseLogs option. - * @example - * ```typescript - * const config = { - * logger: { - * log: (...args) => console.log('[MCP]', ...args), - * error: (...args) => console.error('[MCP ERROR]', ...args), - * warn: (...args) => console.warn('[MCP WARN]', ...args), - * info: (...args) => console.info('[MCP INFO]', ...args), - * debug: (...args) => console.debug('[MCP DEBUG]', ...args), - * } - * }; - * ``` */ logger?: Logger; }; diff --git a/src/types/logger.ts b/src/types/logger.ts index 932b523..7c5394b 100644 --- a/src/types/logger.ts +++ b/src/types/logger.ts @@ -5,19 +5,6 @@ export type LogLevel = "log" | "error" | "warn" | "info" | "debug"; /** * Logger interface for custom logging implementations - * - * @example - * ```typescript - * import { Logger } from "mcp-handler"; - * - * const customLogger: Logger = { - * log: (...args) => myLogger.info(...args), - * error: (...args) => myLogger.error(...args), - * warn: (...args) => myLogger.warn(...args), - * info: (...args) => myLogger.info(...args), - * debug: (...args) => myLogger.debug(...args), - * }; - * ``` */ export interface Logger { /** @@ -62,13 +49,6 @@ export interface DefaultLoggerOptions { * * @param options - Configuration options for the default logger * @returns A Logger instance that logs to the console - * - * @example - * ```typescript - * import { createDefaultLogger } from "mcp-handler"; - * - * const logger = createDefaultLogger({ verboseLogs: true }); - * ``` */ export function createDefaultLogger(options: DefaultLoggerOptions = {}): Logger { const { verboseLogs = false } = options; From 4d9d615f5abce223c5b2a177be65736e40be955a Mon Sep 17 00:00:00 2001 From: Stanislav Khromov Date: Fri, 18 Jul 2025 22:49:28 +0200 Subject: [PATCH 6/6] Update logger.test.ts --- tests/logger.test.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/tests/logger.test.ts b/tests/logger.test.ts index b7615ba..0ebf93b 100644 --- a/tests/logger.test.ts +++ b/tests/logger.test.ts @@ -97,25 +97,6 @@ describe('Logger Types and Utilities', () => { expect(mockLogger.info).toHaveBeenCalledWith('info'); expect(mockLogger.debug).toHaveBeenCalledWith('debug'); }); - - it('supports conditional logging in custom implementations', () => { - const conditionalLogger = (enabled: boolean): Logger => ({ - log: (...args) => { if (enabled) console.log(...args); }, - error: (...args) => { if (enabled) console.error(...args); }, - warn: (...args) => { if (enabled) console.warn(...args); }, - info: (...args) => { if (enabled) console.info(...args); }, - debug: (...args) => { if (enabled) console.debug(...args); }, - }); - - const enabledLogger = conditionalLogger(true); - const disabledLogger = conditionalLogger(false); - - enabledLogger.log('enabled log'); - disabledLogger.log('disabled log'); - - expect(consoleSpy.log).toHaveBeenCalledWith('enabled log'); - expect(consoleSpy.log).toHaveBeenCalledTimes(1); - }); }); describe('LogLevel Type', () => {