UNPKG

mcpdog

Version:

MCPDog - Universal MCP Server Manager with Web Interface

147 lines 5.37 kB
/** * Stdio proxy - MCP client connecting to daemon * Acts as a bridge between MCP client and daemon */ import { createInterface } from 'readline'; import { DaemonClient } from './daemon-client.js'; export class StdioProxy { daemonClient; readline; isReady = false; constructor(daemonPort) { this.daemonClient = new DaemonClient({ port: daemonPort || 9999, clientType: 'stdio', reconnect: true, silent: true // Enable silent mode to avoid log pollution in stdio }); this.setupStdio(); this.setupDaemonClient(); } setupStdio() { this.readline = createInterface({ input: process.stdin, output: process.stdout, crlfDelay: Infinity }); this.readline.on('line', (line) => { this.handleStdioInput(line.trim()); }); this.readline.on('close', () => { this.shutdown(); }); // Handle process signals process.on('SIGINT', () => this.shutdown()); process.on('SIGTERM', () => this.shutdown()); } setupDaemonClient() { this.daemonClient.on('connected', () => { // Connection established but handshake not completed yet, don't set as ready for now }); this.daemonClient.on('disconnected', () => { this.isReady = false; }); this.daemonClient.on('error', (error) => { // Only output to stderr for serious errors if (!this.isReady) { process.stderr.write(`MCPDog connection error: ${error.message}\n`); } }); this.daemonClient.on('welcome', (message) => { // Set as ready immediately after receiving welcome message, because daemon is running this.isReady = true; }); this.daemonClient.on('ready', (serverStatus) => { // Handshake completed, ensure ready status this.isReady = true; }); // Listen to daemon events, forward to MCP client this.daemonClient.on('server-started', () => { // Can send notifications to MCP client }); this.daemonClient.on('routes-updated', (data) => { // Tool routes updated, may need to send notifications }); } async handleStdioInput(line) { if (!line) return; try { const request = JSON.parse(line); // Check if it's a notification message (no id field) if (!('id' in request)) { // Notification messages are temporarily ignored or forwarded to daemon return; } // If daemon connection is not ready yet, wait a bit if (!this.isReady) { // Give some time for connection to establish await new Promise(resolve => setTimeout(resolve, 100)); } // If still not ready, try to send request directly if (!this.isReady && !this.daemonClient.connected) { this.sendStdioResponse({ jsonrpc: "2.0", id: request.id, error: { code: -32603, message: "MCPDog daemon not connected" } }); return; } // Forward MCP request to daemon (even if isReady is false, as long as connection exists) const response = await this.daemonClient.sendMCPRequest(request); // Send response back to stdio this.sendStdioResponse(response); } catch (error) { // Don't output error logs to stderr to avoid polluting MCP protocol // Send standard JSON-RPC error response try { const request = JSON.parse(line); if ('id' in request) { this.sendStdioResponse({ jsonrpc: "2.0", id: request.id, error: { code: -32700, message: "Parse error" } }); } } catch { // Unable to parse request, ignore } } } sendStdioResponse(response) { // Write response directly to stdout for MCP protocol const responseStr = JSON.stringify(response) + '\n'; process.stdout.write(responseStr); // Log summary to stderr for debugging (avoid stdout pollution) if (responseStr.length > 1000) { process.stderr.write(`[DEBUG] Response sent (${responseStr.length} chars)\n`); } } async start() { try { await this.daemonClient.connect(); // After successful connection, start processing MCP requests } catch (error) { process.stderr.write(`MCPDog failed to connect to daemon: ${error.message}\n`); process.exit(1); } } shutdown() { // Silent shutdown, no log output if (this.readline) { this.readline.close(); } this.daemonClient.disconnect(); process.exit(0); } } //# sourceMappingURL=stdio-proxy.js.map