豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Latest commit

 

History

History
300 lines (228 loc) · 8.53 KB

File metadata and controls

300 lines (228 loc) · 8.53 KB

Custom Stdio Server - How It Works

Overview

Desktop Commander uses a custom stdio transport layer that wraps standard console output (console.log, console.error, etc.) and raw stdout writes into valid JSON-RPC notification messages. This prevents crashes in MCP clients that expect all stdio communication to be JSON-RPC formatted.

File Locations

1. Custom Transport Implementation

File: src/custom-stdio.ts

This file contains the FilteredStdioServerTransport class that extends the standard MCP SDK's StdioServerTransport.

2. Server Integration

File: src/index.ts

This is where the custom transport is instantiated and connected to the MCP server.

How It Works

Architecture Flow

Application Code
    ↓
console.log() / process.stdout.write()
    ↓
FilteredStdioServerTransport (intercepts)
    ↓
Wraps in JSON-RPC notification format
    ↓
Original stdout (valid JSON-RPC only)
    ↓
MCP Client (Claude Desktop, etc.)

Key Components

1. Console Redirection (setupConsoleRedirection())

Located in src/custom-stdio.ts, this method intercepts all console methods:

console.log = (...args: any[]) => {
  if (this.isInitialized) {
    this.sendLogNotification("info", args);
  } else {
    // Buffer for later replay to client
    this.messageBuffer.push({
      level: "info",
      args,
      timestamp: Date.now()
    });
  }
};

What happens:

  • All console.log(), console.warn(), console.error(), etc. calls are intercepted
  • Before initialization: Messages are buffered in memory
  • After initialization: Messages are converted to JSON-RPC notifications
  • Original console methods are stored and can be restored

2. Stdout Filtering (setupStdoutFiltering())

Also in src/custom-stdio.ts, this method intercepts raw writes to stdout:

process.stdout.write = (buffer: any, encoding?: any, callback?: any): boolean => {
  if (typeof buffer === 'string') {
    const trimmed = buffer.trim();
    
    // Check if this looks like a valid JSON-RPC message
    if (trimmed.startsWith('{') && (
      trimmed.includes('"jsonrpc"') || 
      trimmed.includes('"method"') || 
      trimmed.includes('"id"')
    )) {
      // This looks like a valid JSON-RPC message, allow it
      return this.originalStdoutWrite.call(process.stdout, buffer, encoding, callback);
    } else if (trimmed.length > 0) {
      // Non-JSON-RPC output, wrap it in a log notification
      if (this.isInitialized) {
        this.sendLogNotification("info", [buffer.replace(/\n$/, '')]);
      } else {
        this.messageBuffer.push({
          level: "info",
          args: [buffer.replace(/\n$/, '')],
          timestamp: Date.now()
        });
      }
      if (callback) callback();
      return true;
    }
  }
  
  return this.originalStdoutWrite.call(process.stdout, buffer, encoding, callback);
};

What happens:

  • Intercepts ALL writes to process.stdout.write()
  • Checks if content is already valid JSON-RPC (looks for "jsonrpc", "method", "id")
  • If valid JSON-RPC → passes through unchanged
  • If plain text → wraps in JSON-RPC notification
  • If before initialization → buffers for later

3. JSON-RPC Notification Format (sendLogNotification())

Messages are wrapped in the MCP logging notification format:

const notification: LogNotification = {
  jsonrpc: "2.0",
  method: "notifications/message",
  params: {
    level: level,  // "info", "warning", "error", "debug", etc.
    logger: "desktop-commander",
    data: data     // The actual message content
  }
};

Result: Every log message becomes a proper MCP notification that clients can handle safely.

4. Initialization Flow

The initialization happens in src/index.ts:

// In src/index.ts
async function runServer() {
  // ... config loading ...
  
  const transport = new FilteredStdioServerTransport();
  
  // Export transport for global access
  global.mcpTransport = transport;
  
  // Set up event-driven initialization completion handler
  server.oninitialized = () => {
    // CRITICAL: This is called AFTER MCP handshake is complete
    transport.enableNotifications();
    
    // Flush all deferred messages
    while (deferredMessages.length > 0) {
      const msg = deferredMessages.shift()!;
      transport.sendLog('info', msg.message);
    }
    flushDeferredMessages();
    
    transport.sendLog('info', 'Server connected successfully');
  };

  await server.connect(transport);
}

Initialization sequence:

  1. Transport created → Console/stdout interception begins immediately
  2. Messages buffered → Any logs before initialization are stored in memory
  3. MCP handshake → Client and server negotiate protocol version/capabilities
  4. server.oninitialized fires → This is when the handshake completes
  5. enableNotifications() called → Sets isInitialized = true
  6. Buffered messages replayed → All startup logs are sent in chronological order
  7. Normal operation → Future logs are sent immediately as JSON-RPC notifications

Why This Matters

Without buffering:

  • Logs sent during initialization would violate MCP protocol
  • Client might crash or reject the connection
  • Startup messages would be lost

With buffering:

  • All messages are preserved
  • Protocol compliance is maintained
  • Clients receive complete startup history

Key Methods

Public API Methods

The FilteredStdioServerTransport class exposes several useful methods:

// Enable notifications after initialization
public enableNotifications(): void

// Send a log notification (any time after initialization)
public sendLog(
  level: "emergency" | "alert" | "critical" | "error" | "warning" | "notice" | "info" | "debug",
  message: string,
  data?: any
): void

// Send progress updates for long operations
public sendProgress(token: string, value: number, total?: number): void

// Send custom notifications
public sendCustomNotification(method: string, params: any): void

// Cleanup and restore original console/stdout
public cleanup(): void

// Check if notifications are enabled
public get isNotificationsEnabled(): boolean

// Get count of buffered messages
public get bufferedMessageCount(): number

Usage Examples

Example 1: Using Console Methods Anywhere

// In any file in the application
console.log("This will become a JSON-RPC notification");
console.warn("Warning message");
console.error("Error occurred", { details: "some data" });

All of these are automatically wrapped and sent as proper MCP notifications.

Example 2: Direct Logging via Transport

// Access the global transport
const transport = global.mcpTransport;

// Send structured logs
transport.sendLog('info', 'Processing started', {
  fileCount: 42,
  status: 'active'
});

Example 3: Progress Notifications

// For long-running operations
const transport = global.mcpTransport;

for (let i = 0; i < 100; i++) {
  transport.sendProgress('task-123', i, 100);
  // ... do work ...
}
transport.sendProgress('task-123', 100, 100); // Complete

Benefits

  1. Protocol Compliance - All stdout is valid JSON-RPC
  2. No Lost Messages - Buffering preserves startup logs
  3. Developer Experience - Use normal console.log() anywhere
  4. Flexibility - Can send structured data or simple strings
  5. Error Prevention - Prevents client crashes from malformed output

Technical Details

Message Buffer Structure

private messageBuffer: Array<{
  level: "emergency" | "alert" | "critical" | "error" | "warning" | "notice" | "info" | "debug";
  args: any[];
  timestamp: number;
}> = [];

Messages are timestamped and sorted chronologically before replay.

Original References Preserved

private originalConsole: {
  log: typeof console.log;
  warn: typeof console.warn;
  error: typeof console.error;
  debug: typeof console.debug;
  info: typeof console.info;
};

private originalStdoutWrite: typeof process.stdout.write;

This allows cleanup/restoration if needed and enables calling original methods for JSON-RPC output.

Summary

The custom stdio server works by:

  1. Intercepting all console methods and stdout writes at startup
  2. Buffering messages until MCP initialization completes
  3. Wrapping all output in valid JSON-RPC notification format
  4. Replaying buffered messages after initialization
  5. Forwarding all future logs as proper notifications

This ensures Desktop Commander maintains full MCP protocol compliance while still allowing normal console logging throughout the codebase.