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
consolemethods andprocess.stdout.write(lines 19‑27) for later restoration - Console redirection — Overrides
console.log,info,warn,error, anddebugviasetupConsoleRedirection()(lines 26‑87) - Stdout filtering — Intercepts direct
stdout.writecalls throughsetupStdoutFiltering()(lines 89‑122), distinguishing between valid JSON-RPC messages and log output - Initialization management — Uses an
isInitializedflag andenableNotifications()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:
- Every intercepted console call pushes an entry onto the
messageBufferarray (declared at lines 28‑33) - Each entry stores
level,args, andtimestampto preserve chronological order - Direct
stdout.writecalls 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:
- Emits an initial "Enhanced FilteredStdioServerTransport initialized" notification
- Sorts buffered entries by
timestamp(line 84) to maintain chronological integrity - Replays each entry through
sendLogNotification()(lines 85‑87) - Clears the
messageBufferarray (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 metadatasendProgress(token, processed, total)(lines 229‑265) — Sends progress notifications for long-running taskssendCustomNotification(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
messageBufferuntilenableNotifications()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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →