diff --git a/README.md b/README.md index c9b37a6..d31074d 100644 --- a/README.md +++ b/README.md @@ -149,9 +149,38 @@ 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), +}; + +const handler = createMcpHandler( + (server) => { + // Your server setup + }, + {}, + { + logger: customLogger, // Custom logger takes precedence over verboseLogs + verboseLogs: false, // This will be ignored when logger is provided + } +); +``` + ## 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. diff --git a/src/handler/mcp-api-handler.ts b/src/handler/mcp-api-handler.ts index 78e7805..c134c62 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,13 @@ 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. + */ + logger?: Logger; }; /** @@ -256,7 +235,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..7c5394b --- /dev/null +++ b/src/types/logger.ts @@ -0,0 +1,73 @@ +/** + * Log levels supported by the MCP handler + */ +export type LogLevel = "log" | "error" | "warn" | "info" | "debug"; + +/** + * Logger interface for custom logging implementations + */ +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 + */ +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 diff --git a/tests/logger.test.ts b/tests/logger.test.ts new file mode 100644 index 0000000..0ebf93b --- /dev/null +++ b/tests/logger.test.ts @@ -0,0 +1,112 @@ +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'); + }); + }); + + 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