@toolprint/mcp-logger
v0.0.7
Published
Transport-aware logging library for MCP (Model Context Protocol) servers
Readme
mcp-logger
A transport-aware logging library for MCP (Model Context Protocol) servers that automatically adapts to your deployment environment.
MCP servers face a unique challenge: when using STDIO transport, the stdout stream is reserved for JSON-RPC protocol communication. Any accidental console.log() will corrupt the protocol and crash your server. This library solves that problem automatically.
Features
- 🔍 Auto-detection - Automatically detects transport mode to optimize logging
- 🚫 No stdout pollution - Guarantees your STDIO transport won't break
- 🚀 High performance - Uses Pino for remote servers, minimal overhead for local
- 📦 Near-Zero config - Works out of the box with sensible defaults
- 🔧 TypeScript first - Full type safety
- 🎯 Drop-in replacement - Familiar logging API and console-like adapter
- 🏃 Lightweight - Minimal dependencies, tree-shakeable
Installation
npm install @toolprint/mcp-logger
# or
pnpm add @toolprint/mcp-logger
# or
yarn add @toolprint/mcp-loggerOptional Dependencies
For remote/HTTP servers, we recommend you install Pino and Pino-Pretty for performance and transport options:
pnpm add pino pino-prettyFor OpenTelemetry logging support:
pnpm add pino-opentelemetry-transportFor log file rotation support:
pnpm add pino-rollThese are optional peer dependencies and will be used automatically if available.
Getting Started (Recommended)
For full functionality including Pino integration and auto-detection:
import { createLogger } from '@toolprint/mcp-logger';
async function startServer() {
// Initialize logger as early as possible - respects environment config and detects Pino
const logger = await createLogger();
// Now build your MCP server
const server = new McpServer({
name: 'my-server',
version: '1.0.0',
});
logger.info('Server starting...');
// ... rest of server setup
}
startServer().catch(console.error);Quick Start (Limited Functionality)
import { stderrLogger as logger } from '@toolprint/mcp-logger';
// Immediate availability but stderr-only, no Pino integration
logger.info('MCP server started');
logger.debug('Processing request', { id: '123' });
logger.error('Connection failed', new Error('Timeout'));Transport Detection
The library automatically detects whether your MCP server is running in:
- Local mode (STDIO transport) - All logs go to stderr, stdout is never touched
- Remote mode (HTTP/SSE transport) - Uses high-performance Pino logger
Detection Rules
The library uses a priority-based detection system to determine the transport mode:
1. Explicit Configuration (Highest Priority)
MCP_SERVER_MODEenvironment variable- Supported values:
localorremote - Legacy aliases:
stdio→local,http/sse→remote
2. Strong Signals (High Confidence)
- Remote mode:
PORTenvironment variable present - Local mode: Piped stdio streams (non-TTY) without HTTP environment variables
- Local mode: Known MCP client parent processes (
claude,vscode,cursor,mcp-inspector,mcptools,code)
3. Medium Confidence Signals
- Remote mode: HTTP framework modules detected (
express,fastify,koa,next,@hapi/hapi,hapi,restify,micro,@nestjs/core,connect) - Remote mode: HTTP environment variables (
HOST,BIND,LISTEN,LISTEN_ADDRESS) - Remote mode: Docker/Kubernetes environments (
DOCKER_CONTAINER,KUBERNETES_SERVICE_HOST)
4. Safe Default
- Defaults to local mode when no strong indicators are present
- Note:
NODE_ENV=productionalone is not decisive (many local MCP servers run in production mode)
Usage Examples
Basic Usage
import { createLogger } from '@toolprint/mcp-logger';
// Initialize logger (async)
const logger = await createLogger();
// Standard log levels
logger.trace('Detailed trace message');
logger.debug('Debug information');
logger.info('General information');
logger.warn('Warning message');
logger.error('Error message');
// With additional context
logger.info('User action', { userId: '123', action: 'login' });
// Error logging with stack traces
try {
await riskyOperation();
} catch (error) {
logger.error('Operation failed', error);
}Custom Configuration
import { createLogger } from '@toolprint/mcp-logger';
// Explicitly set transport mode
const logger = createLogger({
mode: 'local', // or 'remote'
level: 'debug',
format: 'pretty', // for remote mode only
});
// Custom transports configuration
const logger = createLogger({
transports: [
{
type: 'stderr',
enabled: true,
},
{
type: 'file',
enabled: true,
options: {
path: '/var/log/mcp-server.log',
mkdir: true,
append: true,
},
},
{
type: 'opentelemetry',
enabled: true,
options: {
serviceName: 'my-mcp-server',
serviceVersion: '1.0.0',
url: 'http://localhost:4317',
resourceAttributes: {
'service.name': 'my-mcp-server',
'deployment.environment': 'production',
},
},
},
],
});
// Check detected transport
import { getTransportInfo } from '@toolprint/mcp-logger';
const info = getTransportInfo();
console.error('Transport:', info.mode);
console.error('Confidence:', info.confidence);
console.error('Reasons:', info.reasons);Environment Variables
Configure the logger without code changes:
# Set transport mode explicitly
MCP_SERVER_MODE=local # or remote
# Set log level
LOG_LEVEL=debug # trace, debug, info, warn, error
# Set format for remote mode
LOG_FORMAT=pretty # or json (default: pretty in dev, json in prod)
# Production mode auto-detected
NODE_ENV=production # Influences format and detectionMCP Server Examples
STDIO Transport Server
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { createLogger } from '@toolprint/mcp-logger';
// Initialize logger first
const logger = await createLogger();
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0',
});
// Logger automatically detects STDIO transport
logger.info('Starting MCP server...');
server.setRequestHandler('ping', async (request) => {
logger.debug('Received ping request', request);
return { pong: true };
});
const transport = new StdioServerTransport();
await server.connect(transport);
logger.info('MCP server running in STDIO mode');
// All logs go to stderr, stdout is safe for JSON-RPCHTTP Transport Server
import express from 'express';
import { createLogger } from '@toolprint/mcp-logger';
// Initialize logger first
const logger = await createLogger();
const app = express();
const PORT = process.env.PORT || 3000;
// Logger automatically detects HTTP transport from PORT env var
logger.info('Starting HTTP server...');
app.post('/mcp', (req, res) => {
logger.debug('Received request', {
method: req.body.method,
id: req.body.id,
});
// Handle MCP request
res.json({ result: 'ok' });
});
app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
});Child Loggers
Child loggers allow you to add persistent context to log messages, making it easier to trace operations through your application.
Creating Child Loggers
The child() method accepts either a simple context object or a full ChildLoggerOptions configuration:
const logger = await createLogger();
// Method 1: Simple context object (most common usage)
const userLogger = logger.child({ userId: '123', sessionId: 'abc' });
userLogger.info('User logged in');
// Output includes: { userId: '123', sessionId: 'abc' }
// Method 2: Full ChildLoggerOptions for advanced control
const requestLogger = logger.child({
context: { requestId: 'req-456', method: 'POST' }, // Context fields to include
level: 'debug', // Override parent's log level
metadata: { internal: true } // Store metadata (not logged)
});
// Nested child loggers inherit parent context
const serviceLogger = logger.child({ service: 'auth' });
const authRequestLogger = serviceLogger.child({
requestId: 'req-789',
action: 'login'
});
// Output includes all context: { service: 'auth', requestId: 'req-789', action: 'login' }ChildLoggerOptions Interface
interface ChildLoggerOptions {
// Additional context fields to include in all log messages
context?: Record<string, any>;
// Override the parent logger's minimum log level
level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
// Metadata that can be accessed but won't appear in logs
// Useful for storing internal state or configuration
metadata?: Record<string, any>;
}Context Merging
Child loggers merge their context with parent context, with child values taking precedence:
const parentLogger = logger.child({ userId: '123', role: 'user' });
const childLogger = parentLogger.child({ userId: '456', sessionId: 'xyz' });
childLogger.info('Action performed');
// Output includes: { userId: '456', role: 'user', sessionId: 'xyz' }
// Note: userId '456' overrides parent's '123'Use Cases
- Request Scoping: Create a child logger for each request with request ID
- Service Context: Add service name to all logs from a module
- User Context: Track user actions with user ID
- Operation Tracking: Add operation IDs for distributed tracing
Example: MCP Server with Request Scoping
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { createLogger } from '@toolprint/mcp-logger';
class MyMCPServer {
private logger: Logger;
async init() {
this.logger = await createLogger();
}
async handleRequest(request: any) {
// Create request-scoped logger
const reqLogger = this.logger.child({
requestId: request.id,
method: request.method
});
reqLogger.info('Processing request');
try {
// Pass child logger to handlers
const result = await this.processRequest(request, reqLogger);
reqLogger.info('Request completed');
return result;
} catch (error) {
reqLogger.error('Request failed', error);
throw error;
}
}
}Example: Express Middleware
import express from 'express';
import { createLogger, Logger } from '@toolprint/mcp-logger';
function createLoggingMiddleware(logger: Logger) {
return (req: any, res: any, next: any) => {
// Attach child logger to request
req.logger = logger.child({
requestId: req.id,
method: req.method,
path: req.path,
ip: req.ip
});
req.logger.info('Request started');
// Log response
res.on('finish', () => {
req.logger.info('Request completed', {
status: res.statusCode,
duration: Date.now() - req.startTime
});
});
next();
};
}
const app = express();
const logger = await createLogger();
app.use(createLoggingMiddleware(logger));Performance
- Child loggers are lightweight - they share the parent's transports
- Context is merged at log time (Pino) or stored (StdErr)
- No significant overhead for creating child loggers
Advanced Usage
Additional Pino v7+ Transports
This library supports any Pino v7+ compatible transport, but comes with the following built-in:
- Built-in transports (part of Pino core):
pino/file- File logging (used by our file transport)
- Community transports:
pino-opentelemetry-transport- OpenTelemetry logging (used by our opentelemetry transport)pino-pretty- Pretty printing (used by our console transport)pino-roll- Log file rotation (used by our file transport rotation feature)- Many others available in the Pino ecosystem
You may register additional transports if you have specific log forwarding requirements.
File Transport & Log Rotation
The file transport supports both simple file logging and automatic log rotation via pino-roll.
Basic File Logging:
import { createLogger } from '@toolprint/mcp-logger';
const logger = await createLogger({
transports: [
{
type: 'file',
enabled: true,
options: {
path: '/var/log/mcp-server.log',
mkdir: true, // Create directories if they don't exist (default: true)
append: true, // Append to existing file (default: true)
}
}
]
});Log Rotation with pino-roll:
First install the optional peer dependency:
pnpm add pino-roll
# or
npm install pino-rollThen configure rotation:
const logger = await createLogger({
transports: [
{
type: 'file',
enabled: true,
options: {
path: '/var/log/mcp-server.log',
rotation: {
frequency: 'daily', // 'daily' | 'hourly' | 'size'
mkdir: true, // Create log directories
symlink: true, // Create 'current.log' symlink (default: true)
// Additional pino-roll options...
}
}
}
]
});Rotation Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| frequency | 'daily' | 'hourly' | 'size' | 'daily' | Rotation frequency |
| mkdir | boolean | true | Create directories if needed |
| symlink | boolean | true | Create symlink to current log file |
| size | string | - | Max file size (e.g., '10M', '100K') when using 'size' frequency |
| limit | number | - | Max number of rotated files to keep |
File Rotation Examples:
// Daily rotation with cleanup
{
type: 'file',
enabled: true,
options: {
path: '/var/log/app.log',
rotation: {
frequency: 'daily',
limit: 7, // Keep 7 days of logs
symlink: true, // Creates 'current.log' pointing to today's log
}
}
}
// Size-based rotation
{
type: 'file',
enabled: true,
options: {
path: '/var/log/app.log',
rotation: {
frequency: 'size',
size: '10M', // Rotate when file exceeds 10MB
limit: 5, // Keep 5 rotated files
}
}
}
// Hourly rotation for high-volume logging
{
type: 'file',
enabled: true,
options: {
path: '/var/log/app.log',
rotation: {
frequency: 'hourly',
limit: 24, // Keep 24 hours of logs
}
}
}File Transport Features:
- ✅ ESM/CommonJS Compatible: Works with both module systems
- ✅ Automatic Directory Creation: Creates log directories as needed
- ✅ Graceful Fallback: Falls back to
pino.destinationifpino/filefails - ✅ Enhanced Error Handling: Detailed error messages for debugging
- ✅ Performance Optimized: Uses Pino's high-performance file transports
- ✅ Debug Mode: Set
DEBUG_LOGGING=1to see transport creation details
Custom Logger Classes
import { StdErrLogger, PinoLogger } from '@toolprint/mcp-logger';
// Use specific logger implementation
const stderrLogger = new StdErrLogger({ level: 'debug' });
const pinoLogger = new PinoLogger({ format: 'json' });Transport Detection API
import { TransportDetector } from '@toolprint/mcp-logger';
const detector = new TransportDetector();
const result = detector.detect();
if (result.confidence === 'low') {
// Maybe prompt user to set MCP_SERVER_MODE explicitly
console.error('Unable to reliably detect transport mode');
}How It Works
Local Mode (STDIO Transport)
When STDIO transport is detected:
- All output goes to
stderronly stdoutis never touched (preventing protocol corruption)- Synchronous, minimal overhead logging
- No external dependencies
Remote Mode (HTTP/SSE Transport)
When HTTP/SSE transport is detected:
- Uses Pino for high-performance logging
- Pretty printing in development
- JSON structured logs in production
- Automatic fallback if Pino unavailable
Best Practices
- Don't use console.log() - Always use the logger instead
- Log levels - Use appropriate levels (debug for development, info for production)
- Structured logging - Pass objects as second parameter for better searchability
- Error objects - Pass Error instances directly to preserve stack traces
// Good
logger.info('User action', { userId: user.id, action: 'login' });
logger.error('Database error', error);
// Less optimal
logger.info(`User ${user.id} logged in`);
logger.error('Error: ' + error.message);Troubleshooting
Logger using wrong transport mode?
Set the transport explicitly:
MCP_SERVER_MODE=local node your-server.jsNot seeing any logs?
Check your log level:
LOG_LEVEL=debug node your-server.jsFile transport not working?
Enable debug logging to see what's happening:
DEBUG_LOGGING=1 node your-server.jsThis will show:
- File transport creation attempts
- Pino transport configuration
- Fallback mechanisms
- Error details if transport creation fails
Log rotation not working?
Ensure
pino-rollis installed:pnpm add pino-rollCheck file permissions and directory access
Verify rotation configuration is correct:
options: { path: '/path/to/logs/app.log', rotation: { frequency: 'daily', // Must be 'daily', 'hourly', or 'size' mkdir: true, // Ensure directories can be created } }
Log files empty or not created?
Common issues:
- Incorrect array format: Use
transports: [{ type: 'file', ... }]nottransports: { file: { ... } } - Path permissions: Ensure the process can write to the log directory
- Missing directories: Set
mkdir: truein file transport options - Silent fallback: Check if logger fell back to stderr due to file transport failure
Want to verify detection?
import { getTransportInfo } from '@toolprint/mcp-logger';
const info = getTransportInfo();
console.error('Detection:', JSON.stringify(info, null, 2));Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
API Reference
Exported Functions
createLogger(options?: LoggerOptions): Promise<Logger>
Creates a logger instance asynchronously with full Pino integration and auto-detection.
const logger = await createLogger({
mode: 'auto', // 'local' | 'remote' | 'auto' (default: 'auto')
level: 'info', // 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' (default: 'info')
format: 'pretty', // 'pretty' | 'json' (default: 'pretty' in dev, 'json' in prod)
transports: [...] // Custom transport configuration (optional)
});createLoggerSync(options?: LoggerOptions): Logger
Creates a logger instance synchronously. Always returns StdErrLogger for immediate use.
const logger = createLoggerSync({ level: 'debug' });getTransportInfo(): DetectionResult
Gets information about the detected transport mode. Useful for debugging.
const info = getTransportInfo();
console.log(info);
// { mode: 'local', confidence: 'high', reasons: ['piped stdio', 'parent is mcp client'] }Exported Types
Logger
The main logger interface with all standard logging methods plus child logger creation.
interface Logger {
trace(message?: any, ...args: any[]): void;
debug(message?: any, ...args: any[]): void;
info(message?: any, ...args: any[]): void;
warn(message?: any, ...args: any[]): void;
error(message?: any, ...args: any[]): void;
child(options: ChildLoggerOptions | Record<string, any>): Logger;
}LoggerOptions
Configuration options for logger creation.
interface LoggerOptions {
mode?: 'local' | 'remote' | 'auto'; // Transport mode (default: 'auto')
level?: LogLevel; // Log level (default: 'info')
format?: 'pretty' | 'json'; // Output format (default: depends on NODE_ENV)
transports?: TransportConfig[]; // Custom transports (optional)
}ChildLoggerOptions
Options for creating child loggers.
interface ChildLoggerOptions {
context?: Record<string, any>; // Additional context fields
level?: LogLevel; // Override parent's log level
metadata?: Record<string, any>; // Non-logged metadata
}TransportConfig
Configuration for individual transports.
interface TransportConfig {
type: 'stderr' | 'console' | 'file' | 'opentelemetry';
enabled: boolean;
options?: Record<string, any>; // Transport-specific options
}Other Types
LogLevel:'trace' | 'debug' | 'info' | 'warn' | 'error'InternalLogLevel:LogLevel | 'fatal'(Pino supports 'fatal')ServerMode:'local' | 'remote'ConfidenceLevel:'high' | 'medium' | 'low'
Configuration Options
Logger Options Defaults
| Option | Default | Description |
|--------|---------|-------------|
| mode | 'auto' | Transport mode detection |
| level | 'info' or process.env.LOG_LEVEL | Minimum log level |
| format | 'pretty' in development, 'json' in production | Output format |
| transports | Auto-configured based on mode | Transport configuration |
Transport Defaults
Local mode (STDIO):
- Default:
[{ type: 'stderr', enabled: true }] - Console transport automatically disabled to prevent protocol corruption
Remote mode (HTTP/SSE):
- Default:
[{ type: 'console', enabled: true, options: { pretty: true } }] - Uses pino-pretty for formatted output
Environment Variables
| Variable | Description | Values | Default |
|----------|-------------|--------|---------|
| MCP_SERVER_MODE | Force transport mode | local, remote | Auto-detect |
| LOG_LEVEL | Set minimum log level | trace, debug, info, warn, error, fatal | info |
| LOG_FORMAT | Set output format | pretty, json | Based on NODE_ENV |
| PINO_ENABLED | Enable/disable Pino | true, false, 0, 1, yes, no | true |
| NODE_ENV | Environment mode | production, development, etc. | - |
| PORT | HTTP server port (affects detection) | Any port number | - |
| MCP_LOG_DEBUG | Enable debug logging for detection | Any truthy value | - |
Exported Classes
StdErrLogger
Simple stderr-only logger that serves as a fallback when Pino is not available.
const logger = new StdErrLogger({ level: 'debug' });PinoLogger
High-performance logger using Pino with configurable transports.
const logger = new PinoLogger({
format: 'json',
transports: [{ type: 'file', enabled: true, options: { path: 'app.log' } }]
});Transport Classes
StderrTransport- Outputs to stderr (always safe)ConsoleTransport- Outputs to stdout (disabled in STDIO mode)FileTransport- Logs to file using Pino's built-in file transportOpenTelemetryTransport- Sends logs to OpenTelemetry collectors
Utility Classes
TransportDetector
Detects the appropriate transport mode based on environment signals.
const detector = new TransportDetector();
const result = detector.detect();
// { mode: 'local', confidence: 'high', reasons: [...] }TransportRegistry
Registry for custom transport implementations.
TransportRegistry.register('custom', new CustomTransport());
const transport = TransportRegistry.get('custom');License
MIT License - see LICENSE file for details.
Acknowledgments
- Built specifically for Model Context Protocol servers
- Inspired by the MCP community's best practices
- Uses Pino for high-performance logging
- Uses ts-log for the logger interface
