dli-mcp-server 0.0.1
An MCP server to control Digital Loggers (DLI) Web Power Switches.
Requires Python: >=3.10

# 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.