# DLI Power Switch MCP Server
## Overview
This project implements a **Model Context Protocol (MCP)** server that allows an AI agent to control [**Digital Loggers (DLI)** Web Power Switches](https://www.digital-loggers.com/LPC9.html). The system provides tools for discovering hardware, querying outlet status, and performing power operations (On/Off/Cycle).
**Crucial Constraint:** This system interacts with physical hardware. Strict safety protocols are enforced to prevent accidental power loss to critical infrastructure.
## Key Files
* **`server.py`**: The main entry point. Contains the FastMCP server implementation, tool definitions, and hardware interaction logic using the [`power-switch-pro`](https://pypi.org/project/power-switch-pro/) library.
* **`switches_config.json`**: The source of truth for device configuration. Defines IP addresses, authentication, outlet aliases, and safety types (`standard`, `critical`, `prohibited`).
* **`requirements.txt`**: Python dependencies (`power-switch-pro`).
* **`tests/`**: Unit tests for the server logic.
## Architecture & Performance
This server is designed for responsiveness and safety:
* **Asynchronous Core:** Built on Python's `asyncio` to handle multiple operations efficiently.
* **Non-Blocking I/O:** Interactions with physical hardware (which can be slow) are offloaded to background threads, ensuring the main server loop remains responsive.
* **Parallel Discovery:** The `get_inventory` tool fetches status from all configured switches concurrently, significantly reducing latency in systems with multiple devices.
## Installation
Install this server from PyPI using pip:
```bash
pip install dli-mcp-server
```
### Configuring with Gemini CLI (and Antigravity)
Once installed, register the server with the Gemini CLI (or Antigravity) using the `mcp add` command. This ensures the server starts automatically.
```bash
gemini mcp add dli-mcp-server dli-mcp-server -e DLI_MCP_CONFIG="/path/to/your/config.json"
```
**Parameters:**
* The first `dli-mcp-server` is the name you assign to this server instance.
* The second `dli-mcp-server` is the command that runs the server (made available by `pip install`).
* `-e DLI_MCP_CONFIG="..."`: (Optional) Sets the environment variable for the configuration file path. If omitted, it defaults to `switches_config.json` in the current directory.
* `-s user` or `-s project`: (Optional) Sets the configuration scope. Defaults to `project`.
## Command-Line Usage
The `server.py` script can be used directly from the command line to control the power switches.
### `inventory`
Lists all switches and their outlet statuses.
```bash
python server.py inventory
```
### `power_action`
Performs a power action (on, off, cycle) on a specific outlet.
```bash
python server.py power_action <switch_id> <outlet_id> <action> [--confirmation YES]
```
- `switch_id`: Alias or IP address of the switch.
* `outlet_id`: Index or name of the outlet.
* `action`: `on`, `off`, or `cycle`.
* `--confirmation`: Required for critical outlets.
### `group_power_action`
Performs a power action on a group of outlets.
```bash
python server.py group_power_action <group_id> <action>
```
### `sync_config_from_hardware`
Synchronizes outlet names from the hardware.
```bash
python server.py sync_config_from_hardware <switch_id>
```
### `list_outlets`
Lists all outlets on a given switch.
```bash
python server.py list_outlets <switch_id>
```
### `add_switch`
Adds a new DLI power switch to the configuration. If the configuration file does not exist, it will be automatically created.
```bash
python server.py add_switch <ip_address> <username> <password>
```
- `ip_address`: IP address of the new switch.
* `username`: Username for the new switch.
* `password`: Password for the new switch.
### `remove_switch`
Removes a DLI power switch from the configuration.
```bash
python server.py remove_switch <switch_id>
```
- `switch_id`: Alias or IP address of the switch to remove.
### `update_outlet`
Updates the definition of an outlet.
```bash
python server.py update_outlet <switch_id> <outlet_id> [--name <new_name>] [--description <new_description>] [--type <new_type>]
```
- `switch_id`: Alias or IP address of the switch.
* `outlet_id`: Index or name of the outlet (e.g., "Modem" or "1").
* `--name`: New name for the outlet.
* `--description`: New description for the outlet.
* `--type`: New type for the outlet (`standard`, `critical`, or `prohibited`).
| Type | Agent Permission | Behavior |
| :--- | :--- | :--- |
| **`standard`** | **Full Access** | Can be turned On, Off, or Cycled immediately. |
| **`critical`** | **Restricted** | "Off" or "Cycle" actions require explicit user confirmation (`confirmation="YES"`). |
| **`prohibited`** | **No Access** | **NEVER** modify this outlet. The server will raise a `PermissionError`. |
## Available Tools
### 1. `get_inventory()`
* **Purpose:** The "eyes" of the agent. Call this first to see what switches and outlets are available and their current state (ON/OFF).
* **Returns:** A JSON object containing all switches, outlets, groups, and their descriptions.
### 2. `power_action(switch_id, outlet_id, action, confirmation="NO")`
* **Purpose:** Controls a specific physical outlet.
* **Inputs:**
* `switch_id`: The Alias (e.g., "garage_rack") or IP.
* `outlet_id`: The Name (e.g., "Modem") or Index (e.g., "1").
* `action`: "on", "off", or "cycle".
* `confirmation`: Must be set to "YES" only if the user has explicitly approved a dangerous action on a `critical` outlet.
### 3. `group_power_action(target, action)`
* **Purpose:** Controls a logical group of outlets (e.g., "Restart the Network Stack").
* **Behavior:** Executes sequentially. If *any* member of the group is `prohibited`, the entire operation aborts immediately.
### 4. `sync_config_from_hardware(switch_id)`
* **Purpose:** Updates the `switches_config.json` file with the actual outlet names found on the device.
* **Note:** This does not overwrite safety types (`critical`/`prohibited`) or descriptions.
### 5. `add_switch(ip_address, username, password)`
* **Purpose:** Adds a new DLI power switch to the configuration.
* **Inputs:**
* `ip_address`: IP address of the new switch.
* `username`: Username for the new switch.
* `password`: Password for the new switch.
### 6. `remove_switch(switch_id)`
* **Purpose:** Removes a DLI power switch from the configuration.
* **Inputs:**
* `switch_id`: Alias or IP address of the switch to remove.
### 7. `list_outlets(switch_id)`
* **Purpose:** Lists all outlets and their status for a given switch.
* **Inputs:**
* `switch_id`: The Alias or IP address of the switch.
* **Returns:** A JSON array of outlet information.
### 8. `update_outlet(switch_id, outlet_id, new_name=None, new_description=None, new_type=None)`
* **Purpose:** Updates the definition of an outlet in the configuration file and writes the new name to the hardware.
* **Inputs:**
* `switch_id`: The Alias or IP address of the switch.
* `outlet_id`: The Name or Index (e.g., "Modem" or "1").
* `new_name` (optional): The new name for the outlet. This is written to both the config file and the hardware.
* `new_description` (optional): The new description for the outlet. This is only written to the config file.
* `new_type` (optional): The new type for the outlet (`standard`, `critical`, or `prohibited`). This is only written to the config file.
* **Returns:** A success message.
## Operational Guidelines for the Agent
1. **Always Check Inventory First:** Before assuming an outlet exists or knowing its status, run `get_inventory`.
2. **Respect "Prohibited" Outlets:** If a user asks to turn off a prohibited device (e.g., "Security DVR"), explain that you cannot do so because it is restricted in the configuration.
3. **Handle "Critical" Warnings:** If `power_action` returns a "SAFETY LOCK" message, stop and ask the user: *"This is a critical device. Are you sure you want to turn it off?"*. Only proceed if they say "Yes".
4. **Use Aliases:** Prefer using the friendly `alias` and `name` (e.g., "garage_rack", "Modem") over IP addresses and indices when communicating with the user.
## Testing
The project includes a suite of unit tests to ensure the server logic is correct. The tests are located in the `tests/` directory.
To run the tests, first install the testing dependencies:
```bash
pip install -r tests/requirements.txt
```
Then, use the following command to run the tests:
```bash
python tests/test_server.py
```
The tests are designed to run without a physical DLI power switch. They use mocking to simulate the hardware and its behavior.
### Automated Testing
The project uses GitHub Actions for continuous integration. Tests are automatically executed on every push and pull request to the `main` branch. The workflow runs on:
* **Operating Systems:** Windows, Linux (Ubuntu), and macOS.
* **Python Versions:** 3.10, 3.11, and 3.12.
This ensures cross-platform compatibility and stability across supported Python versions.
### Test Coverage
To check the test coverage, you can use the `coverage` package (which is included in `tests/requirements.txt`).
Run the tests with coverage and generate a report:
```bash
coverage run tests/test_server.py
coverage report -m
```
The project aims for a high test coverage to ensure reliability.
### Configuring with Gemini CLI (and Antigravity)
You can easily register this MCP server with the Gemini CLI (or Antigravity) using the `mcp add` command. This ensures the server starts automatically.
**Windows:**
```bash
gemini mcp add dli-mcp-server python "C:\Path\To\dli-mcp-server\server.py" -e DLI_MCP_CONFIG="C:\Path\To\your\config.json" -s user
```
**Linux / macOS:**
```bash
gemini mcp add dli-mcp-server python "/path/to/dli-mcp-server/server.py" -e DLI_MCP_CONFIG="/path/to/your/config.json" -s user
```
**Parameters:**
* `dli-mcp-server`: The name you assign to the server.
* `python "..."`: The command to start the server. Ensure you provide the full absolute path to `server.py`.
* `-e DLI_MCP_CONFIG="..."`: (Optional) Sets the environment variable for the configuration file path. If omitted, it defaults to `switches_config.json` in the server's directory.
* `-s user`: Saves the configuration to your user settings (global), making it available across all projects.
* `-s project` (Default): Saves the configuration to the current project's `.gemini/settings.json`. Use this if you want the server configuration to be specific to the current workspace.
### Configuration
By default, the server uses the `switches_config.json` file in the same directory. You can override this by setting the `DLI_MCP_CONFIG` environment variable to the path of your configuration file.
**Example:**
```bash
export DLI_MCP_CONFIG=/path/to/your/custom_config.json
python server.py inventory
```
This is particularly useful for testing with different configurations without modifying the main `switches_config.json` file.
## Example Interactions
**User:** "Turn off the Router."
**Agent Action:**
1. Calls `get_inventory` (internal) -> sees Router is `critical`.
2. Calls `power_action("garage_rack", "Router", "off")`.
3. **Result:** Returns "SAFETY LOCK...".
4. **Agent Response:** "The Router is marked as a critical device. Are you sure you want to turn it off?"
**User:** "Yes, do it."
**Agent Action:**
1. Calls `power_action("garage_rack", "Router", "off", confirmation="YES")`.
2. **Result:** "Success..."
3. **Agent Response:** "The Router has been turned off."
## Development Information
This MCP server was developed using Gemini CLI and Gemini 3.0 models.