# Directory Structure ``` ├── .gitignore ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── network-discovery.ts │ └── server.ts ├── tsconfig.json └── vite.config.ts ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` dist .claude node_modules ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Eufy RoboVac MCP Server A Model Context Protocol (MCP) server for controlling Eufy RoboVac devices. Built with TypeScript and Vite. ## Setup 1. Install dependencies: ```bash npm install ``` 2. Get your device credentials: - Device ID and Local Key from your Eufy Home app or network analysis - Find your RoboVac's IP address on your network ## Development Run in development mode with hot reload: ```bash npm run dev ``` Type checking: ```bash npm run typecheck ``` ## Production Build the project: ```bash npm run build ``` Run the built server: ```bash npm start ``` ## Available Tools ### Connection & Setup - `robovac_scan_network` - Scan local network for RoboVac devices (🆕 no credentials needed!) - `robovac_connect_discovered` - Connect to a discovered device using its IP - `robovac_connect` - Manual connection using device credentials - `robovac_auto_initialize` - Cloud-based discovery (⚠️ May not work due to API changes) ### Basic Controls - `robovac_start_cleaning` - Start cleaning cycle - `robovac_stop_cleaning` - Stop cleaning cycle - `robovac_return_home` - Return to charging dock - `robovac_play` - Start/resume cleaning - `robovac_pause` - Pause cleaning - `robovac_find_robot` - Make the RoboVac beep to locate it ### Advanced Controls - `robovac_set_work_mode` - Set cleaning mode (AUTO, SMALL_ROOM, SPOT, EDGE, NO_SWEEP) - `robovac_set_clean_speed` - Set suction power (STANDARD, BOOST_IQ, MAX, NO_SUCTION) ### Status Information - `robovac_get_status` - Get current device status (legacy) - `robovac_get_battery` - Get battery level - `robovac_get_error_code` - Get current error code - `robovac_get_work_mode` - Get current cleaning mode - `robovac_get_clean_speed` - Get current suction level - `robovac_get_work_status` - Get detailed work status - `robovac_get_play_pause` - Get play/pause state ### Utility Functions - `robovac_format_status` - Print formatted status to console - `robovac_get_all_statuses` - Get all status information at once ## Usage with MCP Client ### 🆕 Easy Setup with Network Scan (Recommended) 1. **Scan your local network to find RoboVac devices:** ``` robovac_scan_network() ``` This will show you: - All devices with open Tuya/Eufy ports (6668, 6667, 443) - Devices with Anker/Eufy MAC addresses (⭐ likely RoboVacs) - IP addresses of potential devices 2. **Connect to a discovered device:** ``` robovac_connect_discovered(ip="192.168.1.100", deviceId="your_device_id", localKey="your_local_key") ``` ### Getting Device Credentials You still need the device ID and local key: 1. **Try community tools:** - `eufy-security-client` or similar projects - Check GitHub for updated credential grabbers 2. **Network traffic analysis:** - Monitor Eufy app network traffic - Use tools like Wireshark or Charles Proxy 3. **Router/firmware methods:** - Some routers log device information - Check if your RoboVac firmware exposes credentials ### Alternative Methods **Manual connection (if you have all credentials):** ``` robovac_connect(deviceId="your_device_id", localKey="your_local_key", ip="192.168.1.100") ``` **Cloud discovery (may not work due to API changes):** ``` robovac_auto_initialize(email="[email protected]", password="your_password") ``` ### Control Your RoboVac Once connected, use any control tools: ``` robovac_start_cleaning() robovac_get_status() robovac_return_home() robovac_set_work_mode(mode="SPOT") robovac_set_clean_speed(speed="MAX") ``` ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "node", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "allowJs": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "declaration": true, "declarationMap": true, "sourceMap": true, "outDir": "./dist", "rootDir": "./src", "types": ["node"] }, "include": [ "src/**/*" ], "exclude": [ "node_modules", "dist" ] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "sam", "version": "1.0.0", "description": "", "main": "dist/server.es.js", "type": "module", "scripts": { "dev": "tsx src/server.ts", "build": "vite build", "start": "node dist/server.es.js", "typecheck": "tsc --noEmit", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@modelcontextprotocol/sdk": "^1.16.0", "crypto-js": "^4.2.0", "eufy-robovac": "^1.4.6", "node-rsa": "^1.1.1" }, "devDependencies": { "@types/crypto-js": "^4.2.2", "@types/node": "^24.1.0", "@types/node-rsa": "^1.1.4", "tsx": "^4.20.3", "typescript": "^5.8.3", "vite": "^7.0.5" } } ``` -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- ```typescript import { defineConfig } from "vite"; import { resolve } from "path"; export default defineConfig({ build: { lib: { entry: resolve(__dirname, "src/server.ts"), name: "EufyRoboVacMCPServer", fileName: (format) => `server.${format}.js`, formats: ["es"], }, rollupOptions: { external: [ "@modelcontextprotocol/sdk/server/index.js", "@modelcontextprotocol/sdk/server/stdio.js", "@modelcontextprotocol/sdk/types.js", "eufy-robovac", "crypto", "crypto-js", "node-rsa", "net", "child_process", "os", ], }, target: "node18", outDir: "dist", }, resolve: { alias: { "@": resolve(__dirname, "src"), }, }, }); ``` -------------------------------------------------------------------------------- /src/network-discovery.ts: -------------------------------------------------------------------------------- ```typescript import * as net from "net"; import { execSync } from "child_process"; import * as os from "os"; interface NetworkDevice { ip: string; mac?: string; vendor?: string; ports: number[]; isLikelyRoboVac: boolean; } export class NetworkDiscovery { private readonly TUYA_PORTS = [6668, 6667, 443]; private readonly ANKER_EUFY_OUIS = [ "34:ea:34", // Anker Innovations Limited "70:55:82", // Anker Innovations Limited "90:9a:4a", // Anker Innovations Limited "a4:c1:38", // Anker Innovations Limited "2c:aa:8e", // Anker Innovations Limited ]; private getLocalNetworkRange(): string { const interfaces = os.networkInterfaces(); for (const [name, addrs] of Object.entries(interfaces)) { if (!addrs) continue; for (const addr of addrs) { if (addr.family === "IPv4" && !addr.internal) { const parts = addr.address.split("."); if (parts[0] === "192" && parts[1] === "168") { return `${parts[0]}.${parts[1]}.${parts[2]}.0/24`; } else if (parts[0] === "10") { return "10.0.0.0/24"; } else if ( parts[0] === "172" && parseInt(parts[1]) >= 16 && parseInt(parts[1]) <= 31 ) { return `172.${parts[1]}.0.0/16`; } } } } return "192.168.1.0/24"; // Default fallback } private async scanPort( ip: string, port: number, timeout: number = 1000 ): Promise<boolean> { return new Promise((resolve) => { const socket = new net.Socket(); const timer = setTimeout(() => { socket.destroy(); resolve(false); }, timeout); socket.on("connect", () => { clearTimeout(timer); socket.destroy(); resolve(true); }); socket.on("error", () => { clearTimeout(timer); resolve(false); }); socket.connect(port, ip); }); } private async getArpTable(): Promise<Map<string, string>> { const arpMap = new Map<string, string>(); try { let arpOutput: string; const platform = os.platform(); if (platform === "darwin" || platform === "linux") { arpOutput = execSync("arp -a", { encoding: "utf8", timeout: 5000 }); } else if (platform === "win32") { arpOutput = execSync("arp -a", { encoding: "utf8", timeout: 5000 }); } else { return arpMap; } const lines = arpOutput.split("\n"); for (const line of lines) { // Parse different ARP formats let match; if (platform === "darwin") { // macOS format: hostname (192.168.1.100) at aa:bb:cc:dd:ee:ff [ether] on en0 match = line.match(/\((\d+\.\d+\.\d+\.\d+)\) at ([a-fA-F0-9:]{17})/); } else if (platform === "linux") { // Linux format: 192.168.1.100 ether aa:bb:cc:dd:ee:ff C eth0 match = line.match(/(\d+\.\d+\.\d+\.\d+).*?([a-fA-F0-9:]{17})/); } else if (platform === "win32") { // Windows format: 192.168.1.100 aa-bb-cc-dd-ee-ff dynamic match = line.match(/(\d+\.\d+\.\d+\.\d+)\s+([a-fA-F0-9-]{17})/); if (match) { match[2] = match[2].replace(/-/g, ":"); // Convert Windows format to standard } } if (match) { arpMap.set(match[1], match[2].toLowerCase()); } } } catch (error) { console.error("[DEBUG] Failed to get ARP table:", error); } return arpMap; } private isAnkerEufyDevice(mac: string): boolean { const macPrefix = mac.toLowerCase().substring(0, 8); return this.ANKER_EUFY_OUIS.some((oui) => macPrefix.startsWith(oui)); } private async pingHost(ip: string): Promise<boolean> { try { const platform = os.platform(); let pingCommand: string; if (platform === "win32") { pingCommand = `ping -n 1 -w 1000 ${ip}`; } else { pingCommand = `ping -c 1 -W 1 ${ip}`; } execSync(pingCommand, { encoding: "utf8", timeout: 2000, stdio: "pipe", // Suppress output }); return true; } catch { return false; } } async discoverDevices(): Promise<NetworkDevice[]> { console.error("[DEBUG] Starting local network discovery..."); const networkRange = this.getLocalNetworkRange(); console.error(`[DEBUG] Scanning network range: ${networkRange}`); // Get current ARP table const arpTable = await this.getArpTable(); console.error(`[DEBUG] Found ${arpTable.size} devices in ARP table`); // Generate IP range to scan const baseIp = networkRange.split("/")[0]; const ipParts = baseIp.split("."); const ips: string[] = []; // Scan common ranges more efficiently for (let i = 1; i < 255; i++) { ips.push(`${ipParts[0]}.${ipParts[1]}.${ipParts[2]}.${i}`); } const devices: NetworkDevice[] = []; const batchSize = 20; // Process IPs in batches to avoid overwhelming the network for (let i = 0; i < ips.length; i += batchSize) { const batch = ips.slice(i, i + batchSize); const batchPromises = batch.map(async (ip) => { // First check if device responds to ping const isAlive = await this.pingHost(ip); if (!isAlive) return null; console.error(`[DEBUG] Device found at ${ip}, checking ports...`); // Check Tuya/Eufy ports const portResults = await Promise.all( this.TUYA_PORTS.map((port) => this.scanPort(ip, port, 500)) ); const openPorts = this.TUYA_PORTS.filter( (port, index) => portResults[index] ); if (openPorts.length === 0) return null; const mac = arpTable.get(ip); const isAnkerDevice = mac ? this.isAnkerEufyDevice(mac) : false; const device: NetworkDevice = { ip, mac, vendor: isAnkerDevice ? "Anker/Eufy" : undefined, ports: openPorts, isLikelyRoboVac: isAnkerDevice && openPorts.includes(6668), }; console.error(`[DEBUG] Potential device: ${JSON.stringify(device)}`); return device; }); const batchResults = await Promise.all(batchPromises); const validDevices = batchResults.filter( (device): device is NetworkDevice => device !== null ); devices.push(...validDevices); // Progress indicator console.error( `[DEBUG] Scanned ${Math.min(i + batchSize, ips.length)}/${ ips.length } IPs...` ); } // Sort by likelihood of being a RoboVac devices.sort((a, b) => { if (a.isLikelyRoboVac && !b.isLikelyRoboVac) return -1; if (!a.isLikelyRoboVac && b.isLikelyRoboVac) return 1; return 0; }); console.error( `[DEBUG] Network discovery complete. Found ${devices.length} potential devices` ); return devices; } async findRoboVacs(): Promise<NetworkDevice[]> { const allDevices = await this.discoverDevices(); // Filter for devices that are likely RoboVacs return allDevices.filter( (device) => device.isLikelyRoboVac || (device.ports.includes(6668) && device.vendor === "Anker/Eufy") ); } } ``` -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- ```typescript import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, CallToolRequest, Tool, } from "@modelcontextprotocol/sdk/types.js"; import { RoboVac, WorkMode, CleanSpeed } from "eufy-robovac"; import { NetworkDiscovery } from "./network-discovery.js"; interface RoboVacConfig { deviceId: string; localKey: string; ip?: string; } class RoboVacMCPServer { private server: Server; private robovac: RoboVac | null = null; private networkDiscovery: NetworkDiscovery; constructor() { this.server = new Server( { name: "eufy-robovac-mcp-server", version: "0.2.0", }, { capabilities: { tools: { listChanged: false, }, logging: {}, }, } ); this.networkDiscovery = new NetworkDiscovery(); this.setupHandlers(); } private async initializeRoboVac( deviceId: string, localKey: string, ip?: string ): Promise<boolean> { try { this.robovac = new RoboVac({ deviceId: deviceId, localKey: localKey, ip: ip || "192.168.1.100", }); await this.robovac.connect(); return true; } catch (error) { console.error("Failed to initialize RoboVac:", error); return false; } } private ensureRoboVacInitialized(): void { if (!this.robovac) { throw new Error( "RoboVac not initialized. Please run robovac_auto_initialize or robovac_connect first." ); } } private async discoverBestRoboVacIP(): Promise<string | null> { try { console.error("[DEBUG] Auto-discovering RoboVac devices..."); const devices = await this.networkDiscovery.discoverDevices(); if (devices.length === 0) { console.error("[DEBUG] No devices found during auto-discovery"); return null; } // Filter for likely RoboVac devices const likelyRoboVacs = devices.filter((device) => device.isLikelyRoboVac); if (likelyRoboVacs.length > 0) { const bestDevice = likelyRoboVacs[0]; // Take the first likely device console.error( `[DEBUG] Found likely RoboVac at ${bestDevice.ip} (MAC: ${bestDevice.mac}, Vendor: ${bestDevice.vendor})` ); return bestDevice.ip; } // If no likely RoboVacs, try devices with port 6668 open const devicesWithPort6668 = devices.filter((device) => device.ports.includes(6668) ); if (devicesWithPort6668.length > 0) { const potentialDevice = devicesWithPort6668[0]; console.error( `[DEBUG] Found potential RoboVac at ${potentialDevice.ip} with port 6668 open` ); return potentialDevice.ip; } console.error("[DEBUG] No suitable RoboVac candidates found"); return null; } catch (error) { console.error( `[DEBUG] Auto-discovery failed: ${(error as Error).message}` ); return null; } } private setupHandlers(): void { this.server.setRequestHandler( ListToolsRequestSchema, async (): Promise<{ tools: Tool[] }> => ({ tools: [ { name: "robovac_set_work_mode", description: "Set the cleaning mode of the robovac", inputSchema: { type: "object", properties: { mode: { type: "string", description: "The work mode to set", enum: ["AUTO", "SMALL_ROOM", "SPOT", "EDGE", "NO_SWEEP"], }, }, required: ["mode"], }, }, { name: "robovac_set_clean_speed", description: "Set the suction speed of the robovac", inputSchema: { type: "object", properties: { speed: { type: "string", description: "The cleaning speed to set", enum: ["STANDARD", "BOOST_IQ", "MAX", "NO_SUCTION"], }, }, required: ["speed"], }, }, { name: "robovac_play", description: "Start/resume robovac cleaning", inputSchema: { type: "object", properties: {}, }, }, { name: "robovac_pause", description: "Pause robovac cleaning", inputSchema: { type: "object", properties: {}, }, }, { name: "robovac_find_robot", description: "Make the robovac beep to help locate it", inputSchema: { type: "object", properties: { enable: { type: "boolean", description: "Whether to enable or disable find robot mode", default: true, }, }, }, }, { name: "robovac_get_error_code", description: "Get the current error code of the robovac", inputSchema: { type: "object", properties: { force: { type: "boolean", description: "Force refresh of cached data", default: false, }, }, }, }, { name: "robovac_get_work_mode", description: "Get the current work mode of the robovac", inputSchema: { type: "object", properties: { force: { type: "boolean", description: "Force refresh of cached data", default: false, }, }, }, }, { name: "robovac_get_clean_speed", description: "Get the current cleaning speed of the robovac", inputSchema: { type: "object", properties: { force: { type: "boolean", description: "Force refresh of cached data", default: false, }, }, }, }, { name: "robovac_get_work_status", description: "Get the current work status of the robovac", inputSchema: { type: "object", properties: { force: { type: "boolean", description: "Force refresh of cached data", default: false, }, }, }, }, { name: "robovac_get_play_pause", description: "Get the current play/pause state of the robovac", inputSchema: { type: "object", properties: { force: { type: "boolean", description: "Force refresh of cached data", default: false, }, }, }, }, { name: "robovac_format_status", description: "Get a formatted display of all robovac status information", inputSchema: { type: "object", properties: {}, }, }, { name: "robovac_get_all_statuses", description: "Get all status information from the robovac at once", inputSchema: { type: "object", properties: { force: { type: "boolean", description: "Force refresh of cached data", default: false, }, }, }, }, { name: "robovac_auto_initialize", description: "Automatically discover and initialize the first RoboVac device found", inputSchema: { type: "object", properties: { email: { type: "string", description: "Your Eufy account email address", }, password: { type: "string", description: "Your Eufy account password", }, deviceIndex: { type: "number", description: "Index of device to connect to (0 for first device)", default: 0, }, }, required: ["email", "password"], }, }, { name: "robovac_connect_discovered", description: "Connect to a discovered RoboVac device by IP (requires device ID and local key)", inputSchema: { type: "object", properties: { ip: { type: "string", description: "IP address of the discovered device", }, deviceId: { type: "string", description: "The device ID of your Eufy RoboVac", }, localKey: { type: "string", description: "The local key for your Eufy RoboVac", }, }, required: ["ip", "deviceId", "localKey"], }, }, { name: "robovac_connect", description: "Connect to your RoboVac using device credentials (manual setup)", inputSchema: { type: "object", properties: { deviceId: { type: "string", description: "The device ID of your Eufy RoboVac", }, localKey: { type: "string", description: "The local key for your Eufy RoboVac", }, ip: { type: "string", description: "The IP address of your Eufy RoboVac (optional, defaults to 192.168.1.100)", }, }, required: ["deviceId", "localKey"], }, }, { name: "robovac_start_cleaning", description: "Start the robovac cleaning cycle", inputSchema: { type: "object", properties: {}, }, }, { name: "robovac_stop_cleaning", description: "Stop the robovac cleaning cycle", inputSchema: { type: "object", properties: {}, }, }, { name: "robovac_return_home", description: "Send the robovac back to its charging dock", inputSchema: { type: "object", properties: {}, }, }, { name: "robovac_get_status", description: "Get the current status of the robovac", inputSchema: { type: "object", properties: {}, }, }, { name: "robovac_get_battery", description: "Get the battery level of the robovac", inputSchema: { type: "object", properties: {}, }, }, ], }) ); this.server.setRequestHandler( CallToolRequestSchema, async (request: CallToolRequest) => { const { name, arguments: args } = request.params; try { switch (name) { case "robovac_connect_discovered": // Auto-discover best IP if not provided or if connection fails let targetIP = args?.ip as string; let discoveredSuccess = false; if (targetIP) { discoveredSuccess = await this.initializeRoboVac( args?.deviceId as string, args?.localKey as string, targetIP ); } if (!discoveredSuccess) { console.error( "[DEBUG] Initial connection failed or no IP provided, trying auto-discovery..." ); const discoveredIP = await this.discoverBestRoboVacIP(); if (discoveredIP) { targetIP = discoveredIP; discoveredSuccess = await this.initializeRoboVac( args?.deviceId as string, args?.localKey as string, targetIP ); } } return { content: [ { type: "text", text: discoveredSuccess ? `Successfully connected to RoboVac at ${targetIP}!` : `Failed to connect to device. ${ targetIP ? `Tried ${targetIP} but` : "" } Check your device ID and local key, and ensure the RoboVac is on the same network.`, }, ], isError: !discoveredSuccess, }; case "robovac_connect": // Auto-discover best IP if not provided or if connection fails let connectTargetIP = args?.ip as string | undefined; let connectSuccess = false; if (connectTargetIP) { connectSuccess = await this.initializeRoboVac( args?.deviceId as string, args?.localKey as string, connectTargetIP ); } if (!connectSuccess) { console.error( "[DEBUG] Manual connection failed or no IP provided, trying auto-discovery..." ); const discoveredIP = await this.discoverBestRoboVacIP(); if (discoveredIP) { connectTargetIP = discoveredIP; connectSuccess = await this.initializeRoboVac( args?.deviceId as string, args?.localKey as string, connectTargetIP ); } } // Fallback to default IP if still not successful if (!connectSuccess && !connectTargetIP) { connectTargetIP = "192.168.1.100"; connectSuccess = await this.initializeRoboVac( args?.deviceId as string, args?.localKey as string, connectTargetIP ); } return { content: [ { type: "text", text: connectSuccess ? `RoboVac connected successfully at ${connectTargetIP}!` : `Failed to connect to RoboVac. ${ connectTargetIP ? `Tried ${connectTargetIP} but connection failed.` : "" } Check your device ID, local key, and network connection.`, }, ], isError: !connectSuccess, }; case "robovac_auto_initialize": try { const devices = await this.networkDiscovery.discoverDevices(); if (devices.length === 0) { return { content: [ { type: "text", text: "No RoboVac devices found.", }, ], isError: true, }; } const deviceIndex = (args?.deviceIndex as number) || 0; if (deviceIndex >= devices.length) { return { content: [ { type: "text", text: `Device index ${deviceIndex} is out of range. Found ${devices.length} device(s).`, }, ], isError: true, }; } const selectedDevice = devices[deviceIndex]; let autoInitSuccess = await this.initializeRoboVac( selectedDevice.deviceId, selectedDevice.localKey, selectedDevice.ip ); // If direct connection fails, try auto-discovery if (!autoInitSuccess) { console.error( "[DEBUG] Cloud connection failed, trying auto-discovery..." ); const discoveredIP = await this.discoverBestRoboVacIP(); if (discoveredIP) { autoInitSuccess = await this.initializeRoboVac( selectedDevice.deviceId, selectedDevice.localKey, discoveredIP ); } } return { content: [ { type: "text", text: autoInitSuccess ? `Successfully connected to ${selectedDevice.name}!` : `Failed to connect to ${selectedDevice.name}. Check network connection and ensure the device is online.`, }, ], isError: !autoInitSuccess, }; } catch (error) { return { content: [ { type: "text", text: `Auto-initialization failed: ${ (error as Error).message } ⚠️ The Eufy API appears to have changed since this implementation was created. As an alternative, you can: 1. Use the Eufy app to find your device IP address 2. Use a network scanner to find devices on your network 3. Check your router's device list 4. Use tools like eufy-security-client or other community projects Once you have the device credentials, you can use the eufy-robovac library directly.`, }, ], isError: true, }; } case "robovac_start_cleaning": this.ensureRoboVacInitialized(); await this.robovac!.startCleaning(); return { content: [ { type: "text", text: "RoboVac cleaning started!", }, ], }; case "robovac_stop_cleaning": this.ensureRoboVacInitialized(); await this.robovac!.pause(); return { content: [ { type: "text", text: "RoboVac cleaning stopped!", }, ], }; case "robovac_return_home": this.ensureRoboVacInitialized(); await this.robovac!.goHome(); return { content: [ { type: "text", text: "RoboVac returning to charging dock!", }, ], }; case "robovac_get_status": this.ensureRoboVacInitialized(); const status = await this.robovac!.getStatuses(); return { content: [ { type: "text", text: `RoboVac Status:\n${JSON.stringify(status, null, 2)}`, }, ], }; case "robovac_get_battery": this.ensureRoboVacInitialized(); const battery = await this.robovac!.getBatteyLevel(); return { content: [ { type: "text", text: `Battery Level: ${battery}%`, }, ], }; case "robovac_set_work_mode": this.ensureRoboVacInitialized(); await this.robovac!.setWorkMode(args?.mode as WorkMode); return { content: [ { type: "text", text: `Work mode set to: ${args?.mode}`, }, ], }; case "robovac_set_clean_speed": this.ensureRoboVacInitialized(); await this.robovac!.setCleanSpeed(args?.speed as CleanSpeed); return { content: [ { type: "text", text: `Clean speed set to: ${args?.speed}`, }, ], }; case "robovac_play": this.ensureRoboVacInitialized(); await this.robovac!.play(); return { content: [ { type: "text", text: "RoboVac started/resumed cleaning!", }, ], }; case "robovac_pause": this.ensureRoboVacInitialized(); await this.robovac!.pause(); return { content: [ { type: "text", text: "RoboVac paused!", }, ], }; case "robovac_find_robot": this.ensureRoboVacInitialized(); const enableFind = args?.enable !== undefined ? (args?.enable as boolean) : true; await this.robovac!.setFindRobot(enableFind); return { content: [ { type: "text", text: enableFind ? "Find robot enabled - RoboVac should be beeping!" : "Find robot disabled", }, ], }; case "robovac_get_error_code": this.ensureRoboVacInitialized(); const errorCode = await this.robovac!.getErrorCode( args?.force as boolean ); return { content: [ { type: "text", text: `Error Code: ${errorCode}`, }, ], }; case "robovac_get_work_mode": this.ensureRoboVacInitialized(); const workMode = await this.robovac!.getWorkMode( args?.force as boolean ); return { content: [ { type: "text", text: `Work Mode: ${workMode}`, }, ], }; case "robovac_get_clean_speed": this.ensureRoboVacInitialized(); const cleanSpeed = await this.robovac!.getCleanSpeed( args?.force as boolean ); return { content: [ { type: "text", text: `Clean Speed: ${cleanSpeed}`, }, ], }; case "robovac_get_work_status": this.ensureRoboVacInitialized(); const workStatus = await this.robovac!.getWorkStatus( args?.force as boolean ); return { content: [ { type: "text", text: `Work Status: ${workStatus}`, }, ], }; case "robovac_get_play_pause": this.ensureRoboVacInitialized(); const playPause = await this.robovac!.getPlayPause( args?.force as boolean ); return { content: [ { type: "text", text: `Play/Pause State: ${playPause}`, }, ], }; case "robovac_format_status": this.ensureRoboVacInitialized(); await this.robovac!.formatStatus(); return { content: [ { type: "text", text: "Status information has been printed to console. Use robovac_get_all_statuses for structured data.", }, ], }; case "robovac_get_all_statuses": this.ensureRoboVacInitialized(); const allStatuses = await this.robovac!.getStatuses( args?.force as boolean ); return { content: [ { type: "text", text: `All RoboVac Statuses:\n${JSON.stringify( allStatuses, null, 2 )}`, }, ], }; default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: "text", text: `Error: ${(error as Error).message}`, }, ], isError: true, }; } } ); } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Eufy RoboVac MCP server running on stdio"); } } const server = new RoboVacMCPServer(); server.run().catch(console.error); ```