Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
41 changes: 10 additions & 31 deletions src/handler/mcp-api-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = {
/**
Expand Down Expand Up @@ -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;
};

/**
Expand Down Expand Up @@ -256,7 +235,7 @@ export function initializeMcpApiHandler(
sseMessageEndpoint: explicitSseMessageEndpoint,
});

const logger = createLogger(verboseLogs);
const logger = config.logger || createDefaultLogger({ verboseLogs });

let servers: McpServer[] = [];

Expand Down
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,10 @@ export {
generateProtectedResourceMetadata,
metadataCorsOptionsRequestHandler,
} from "./auth/auth-metadata";

export {
Logger,
LogLevel,
DefaultLoggerOptions,
createDefaultLogger,
} from "./types/logger";
73 changes: 73 additions & 0 deletions src/types/logger.ts
Original file line number Diff line number Diff line change
@@ -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);
},
};
}
112 changes: 112 additions & 0 deletions tests/logger.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof vi.spyOn> };

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);
});
});
});
});