UNPKG

@wonderwhy-er/desktop-commander

Version:

MCP server for terminal operations and file editing

185 lines (184 loc) 8.48 kB
import { spawn } from 'child_process'; import path from 'path'; import fs from 'fs/promises'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { fileURLToPath } from 'url'; import { captureRemote } from '../utils/capture.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export class DesktopCommanderIntegration { constructor() { this.mcpClient = null; this.mcpTransport = null; this.isReady = false; } async initialize() { console.debug('[DEBUG] DesktopCommanderIntegration.initialize() called'); const config = await this.resolveMcpConfig(); if (!config) { console.debug('[DEBUG] No MCP config found'); throw new Error('Desktop Commander MCP not found. Please install it globally via `npm install -g @wonderwhy-er/desktop-commander` or build the local project.'); } console.log(` - ⏳ Connecting to Local Desktop Commander MCP using: ${config.command} ${config.args.join(' ')}`); console.debug('[DEBUG] MCP config:', JSON.stringify(config, null, 2)); try { console.debug('[DEBUG] Creating StdioClientTransport'); this.mcpTransport = new StdioClientTransport(config); // Create MCP client console.debug('[DEBUG] Creating MCP Client'); this.mcpClient = new Client({ name: "desktop-commander-client", version: "1.0.0" }, { capabilities: {} }); // Connect to Desktop Commander console.debug('[DEBUG] Connecting MCP client to transport'); await this.mcpClient.connect(this.mcpTransport); this.isReady = true; console.log(' - 🔌 Connected to Desktop Commander MCP'); console.debug('[DEBUG] Desktop Commander MCP connection successful'); } catch (error) { console.error(' - ❌ Failed to connect to Desktop Commander MCP:', error); console.debug('[DEBUG] MCP connection error:', error); await captureRemote('desktop_integration_init_failed', { error }); throw error; } } async resolveMcpConfig() { console.debug('[DEBUG] Resolving MCP config...'); // Option 1: Development/Local Build // Adjusting path resolution since we are now in src/remote-device and dist is in root/dist // Original: path.resolve(__dirname, '../../dist/index.js') const devPath = path.resolve(__dirname, '../../dist/index.js'); console.debug('[DEBUG] Checking local dev path:', devPath); try { await fs.access(devPath); console.debug(' - 🔍 Found local MCP server at:', devPath); return { command: process.execPath, // Use the current node executable args: [devPath], cwd: path.dirname(devPath) }; } catch { console.debug('[DEBUG] Local dev path not found, trying global installation'); // Local file not found, continue... } // Option 2: Global Installation const commandName = 'desktop-commander'; console.debug('[DEBUG] Checking for global command:', commandName); try { await new Promise((resolve, reject) => { // Use platform-appropriate command to check if the command exists in PATH // We can't run it directly as it's an stdio MCP server that waits for input const whichCommand = process.platform === 'win32' ? 'where' : 'which'; console.debug('[DEBUG] Using platform command:', whichCommand, 'on platform:', process.platform); const check = spawn(whichCommand, [commandName], { windowsHide: true }); // Prevent visible console windows on Windows check.on('error', (err) => { console.debug('[DEBUG] Spawn error for', whichCommand, ':', err.message); reject(err); }); check.on('close', (code) => { console.debug('[DEBUG]', whichCommand, 'exited with code:', code); return code === 0 ? resolve() : reject(new Error('Command not found')); }); }); console.debug(' - Found global desktop-commander CLI'); return { command: commandName, args: [] }; } catch (err) { console.debug('[DEBUG] Global command not found:', err); // Global command not found } console.debug('[DEBUG] No MCP config resolved'); return null; } async callClientTool(toolName, args, metadata) { if (!this.isReady || !this.mcpClient) { console.debug('[DEBUG] callClientTool() failed - not ready or no client'); throw new Error('DesktopIntegration not initialized'); } // Proxy other tools to MCP server try { console.debug('[DEBUG] Calling MCP tool:', toolName, 'args:', JSON.stringify(args).substring(0, 100)); const result = await this.mcpClient.callTool({ name: toolName, arguments: args, _meta: { remote: true, ...metadata || {} } }); console.debug('[DEBUG] Tool call successful:', toolName); return result; } catch (error) { console.error(`Error executing tool ${toolName}:`, error); console.debug('[DEBUG] Tool call error details:', error); await captureRemote('desktop_integration_tool_call_failed', { error, toolName }); throw error; } } async listClientTools() { if (!this.mcpClient) return { tools: [] }; try { // List tools from MCP server const mcpTools = await this.mcpClient.listTools(); // Merge tools return { tools: mcpTools.tools || [] }; } catch (error) { console.error('Error fetching capabilities:', error); await captureRemote('desktop_integration_list_tools_failed', { error }); // Fallback to local tools return { tools: [] }; } } async shutdown() { console.debug('[DEBUG] DesktopCommanderIntegration.shutdown() called'); const closeWithTimeout = async (operation, name, timeoutMs = 3000) => { return Promise.race([ operation(), new Promise((_, reject) => setTimeout(() => reject(new Error(`${name} timeout after ${timeoutMs}ms`)), timeoutMs)) ]); }; if (this.mcpClient) { try { console.log(' → Closing MCP client...'); console.debug('[DEBUG] Calling mcpClient.close() with timeout'); await closeWithTimeout(() => this.mcpClient.close(), 'MCP client close'); console.log(' ✓ MCP client closed'); } catch (e) { console.warn(' ⚠️ MCP client close timeout or error:', e.message); console.debug('[DEBUG] MCP client close error:', e); await captureRemote('desktop_integration_shutdown_error', { error: e, component: 'client' }); } this.mcpClient = null; } if (this.mcpTransport) { try { console.log(' → Closing MCP transport...'); console.debug('[DEBUG] Calling mcpTransport.close() with timeout'); await closeWithTimeout(() => this.mcpTransport.close(), 'MCP transport close'); console.log(' ✓ MCP transport closed'); } catch (e) { console.warn(' ⚠️ MCP transport close timeout or error:', e.message); console.debug('[DEBUG] MCP transport close error:', e); await captureRemote('desktop_integration_shutdown_error', { error: e, component: 'transport' }); } this.mcpTransport = null; } this.isReady = false; console.debug('[DEBUG] Desktop Commander integration shutdown complete'); } }