npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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-logger

Optional Dependencies

For remote/HTTP servers, we recommend you install Pino and Pino-Pretty for performance and transport options:

pnpm add pino pino-pretty

For OpenTelemetry logging support:

pnpm add pino-opentelemetry-transport

For log file rotation support:

pnpm add pino-roll

These 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_MODE environment variable
  • Supported values: local or remote
  • Legacy aliases: stdiolocal, http/sseremote

2. Strong Signals (High Confidence)

  • Remote mode: PORT environment 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=production alone 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 detection

MCP 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-RPC

HTTP 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

  1. Request Scoping: Create a child logger for each request with request ID
  2. Service Context: Add service name to all logs from a module
  3. User Context: Track user actions with user ID
  4. 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-roll

Then 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.destination if pino/file fails
  • Enhanced Error Handling: Detailed error messages for debugging
  • Performance Optimized: Uses Pino's high-performance file transports
  • Debug Mode: Set DEBUG_LOGGING=1 to 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 stderr only
  • stdout is 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

  1. Don't use console.log() - Always use the logger instead
  2. Log levels - Use appropriate levels (debug for development, info for production)
  3. Structured logging - Pass objects as second parameter for better searchability
  4. 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.js

Not seeing any logs?

Check your log level:

LOG_LEVEL=debug node your-server.js

File transport not working?

Enable debug logging to see what's happening:

DEBUG_LOGGING=1 node your-server.js

This will show:

  • File transport creation attempts
  • Pino transport configuration
  • Fallback mechanisms
  • Error details if transport creation fails

Log rotation not working?

  1. Ensure pino-roll is installed:

    pnpm add pino-roll
  2. Check file permissions and directory access

  3. 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', ... }] not transports: { file: { ... } }
  • Path permissions: Ensure the process can write to the log directory
  • Missing directories: Set mkdir: true in 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 transport
  • OpenTelemetryTransport - 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