FilteredStdioServerTransport Architecture: How DesktopCommander MCP Buffers Logs

FilteredStdioServerTransport extends the MCP SDK's StdioServerTransport to intercept non-JSON stdout output, buffering logs until the MCP handshake completes and then emitting them as structured JSON-RPC notifications.

The DesktopCommander MCP server relies on this custom transport to prevent crashes in MCP clients that expect strict JSON-RPC protocol adherence. By wrapping raw console output and buffering messages during initialization, the transport ensures no invalid stdout data reaches the client while preserving every log for later delivery.

The Core Architecture

FilteredStdioServerTransport replaces the default behavior of discarding non-JSON console output. Instead, it captures all logging activity and wraps it in proper JSON-RPC notifications/message payloads.

According to the wonderwhy-er/DesktopCommanderMCP source code in src/custom-stdio.ts, the architecture centers on four primary interceptors:

  • Original method capture — Stores references to native console methods and process.stdout.write (lines 19‑27) for later restoration
  • Console redirection — Overrides console.log, info, warn, error, and debug via setupConsoleRedirection() (lines 26‑87)
  • Stdout filtering — Intercepts direct stdout.write calls through setupStdoutFiltering() (lines 89‑122), distinguishing between valid JSON-RPC messages and log output
  • Initialization management — Uses an isInitialized flag and enableNotifications() method (lines 60‑94) to control when buffering stops and transmission begins

How Log Buffering Works

The transport implements a two-phase logging strategy that guarantees message preservation while maintaining protocol compliance.

Pre-Initialization Buffering

Before the MCP client completes its handshake, the transport sets isInitialized to false. During this phase:

  1. Every intercepted console call pushes an entry onto the messageBuffer array (declared at lines 28‑33)
  2. Each entry stores level, args, and timestamp to preserve chronological order
  3. Direct stdout.write calls that contain non-JSON data are similarly buffered rather than emitted

This buffering occurs in setupConsoleRedirection() (lines 45‑52, 61‑68, 77‑84) and setupStdoutFiltering() (lines 93‑100), ensuring that initialization logs never corrupt the JSON-RPC stream.

Flushing the Buffer

When the host calls enableNotifications() after the MCP handshake completes, the transport executes a flush sequence:

  1. Emits an initial "Enhanced FilteredStdioServerTransport initialized" notification
  2. Sorts buffered entries by timestamp (line 84) to maintain chronological integrity
  3. Replays each entry through sendLogNotification() (lines 85‑87)
  4. Clears the messageBuffer array (line 90)

The sendLogNotification() method (lines 124‑176) constructs the JSON-RPC payload and writes it to the original stdout, with fallback handling for serialization failures.

Client-Specific Configuration

The configureForClient() method (lines 98‑110) allows selective disabling of notifications for clients that cannot handle them (such as Cline). When notifications are disabled:

  • The buffer is discarded rather than flushed
  • A summary writes to stderr (lines 66‑73)
  • Subsequent logs are silently dropped rather than wrapped

Key Implementation Details

Message Buffer Structure

The in-memory buffer declared at lines 28‑33 holds objects with the following structure:

interface BufferedMessage {
  level: string;
  args: any[];
  timestamp: number;
}

This structure preserves the original log level and arguments while maintaining sortable timestamps for ordered replay.

Public API Methods

The transport exposes three stable methods for application-wide use:

  • sendLog(level, message, ...args) (lines 178‑227) — Emits structured log notifications with optional metadata
  • sendProgress(token, processed, total) (lines 229‑265) — Sends progress notifications for long-running tasks
  • sendCustomNotification(method, params) (lines 267‑295) — Allows arbitrary JSON-RPC notifications

Cleanup and Restoration

The cleanup() method (lines 297‑307) restores the original console methods and process.stdout.write when the server shuts down, preventing memory leaks and side effects in long-running processes.

Usage Examples

Basic Setup in Entry Point

In src/index.ts, instantiate the transport and configure for the specific MCP client:

import { FilteredStdioServerTransport } from "./custom-stdio.js";

const stdioTransport = new FilteredStdioServerTransport();

// Detect client capabilities
stdioTransport.configureForClient(process.env.MCP_CLIENT ?? "unknown");

// Start server - enableNotifications() flushes buffered logs
await startMcpServer({ transport: stdioTransport });
stdioTransport.enableNotifications();

Structured Logging with Metadata

Use the public API anywhere in the application for type-safe log emission:

import { stdioTransport } from "./index.js";

// Simple info log
stdioTransport.sendLog("info", "Application started");

// Debug with structured data
stdioTransport.sendLog("debug", "Cache miss", { key: "user:1234", ttl: 300 });

// Progress tracking
stdioTransport.sendProgress("import-users", 42, 100);

Disabling Notifications for Incompatible Clients

For clients like Cline that cannot handle custom notifications:

stdioTransport.configureForClient("Cline");
// Buffered logs discarded, future console calls silently suppressed

Summary

  • FilteredStdioServerTransport extends the MCP SDK's stdio transport to wrap non-JSON output in valid JSON-RPC notifications
  • Pre-initialization buffering stores logs in messageBuffer until enableNotifications() flushes them in timestamp order
  • Client detection via configureForClient() allows graceful degradation for incompatible MCP clients
  • Zero data loss is guaranteed through the buffer-then-flush pattern while maintaining strict protocol compliance
  • Cleanup methods restore original stdio handles to prevent process contamination

Frequently Asked Questions

What happens to logs if the MCP client doesn't support notifications?

If configureForClient() identifies an incompatible client (such as Cline), the transport discards the buffer and writes a summary to stderr. All subsequent console output is silently intercepted but not emitted as notifications, preventing client crashes while maintaining a clean stdout stream.

How does the transport distinguish between valid JSON-RPC and log output?

In setupStdoutFiltering() (lines 89‑122), the transport inspects data written to stdout.write. If the content parses as valid JSON-RPC, it passes through unchanged. Non-JSON strings are wrapped in notifications/message payloads or buffered if initialization is pending.

Can I access the buffered messages directly?

No, the messageBuffer array is private to the transport instance. Applications must use the public sendLog() or sendLogNotification() methods to emit messages. The buffer only serves as temporary storage during the pre-initialization phase and clears automatically after flushing.

Where is the transport implementation located?

The core logic resides in src/custom-stdio.ts within the wonderwhy-er/DesktopCommanderMCP repository. Supporting files include src/index.ts for instantiation, src/utils/logger.ts for application-level wrappers, and src/types.ts for TypeScript definitions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →