DLI Power Switch MCP Server
# 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.
TDQS
Scored across 8 tools
The tools are largely distinct: switch lifecycle, outlet listing/updating, power actions, and inventory all occupy clear roles. The only mild ambiguity is between power_action and group_power_action, and sync_config_from_hardware overlaps somewhat with add_switch since add_switch also syncs configuration.
Most tools follow a verb_noun snake_case pattern like add_switch, remove_switch, list_outlets, and update_outlet. However, power_action and group_power_action are noun_noun constructions that do not follow the same pattern, creating a minor inconsistency.
Eight tools is a well-scoped set for managing DLI power switches, covering switch administration, outlet operations, power actions, inventory, and configuration sync. Each tool has a clear place and the count feels appropriate rather than bloated or thin.
The surface covers the core lifecycle: add/remove switches, list/update outlets, perform power actions, sync configuration, and read inventory. Minor gaps include no explicit switch update operation and no outlet removal, but these are not critical for the server's apparent purpose.