#
tokens: 8549/50000 7/7 files
lines: off (toggle) GitHub
raw markdown copy
# 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);

```