feat: Implement NUT client library and initial project scaffolding for UPS communication.
This commit is contained in:
5
.env.example
Normal file
5
.env.example
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# NUT Server Configuration
|
||||||
|
NUT_HOST=localhost
|
||||||
|
NUT_PORT=3493
|
||||||
|
NUT_USERNAME=
|
||||||
|
NUT_PASSWORD=
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
200
README.md
Normal file
200
README.md
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# NUT UPS MCP Server
|
||||||
|
|
||||||
|
An MCP (Model Context Protocol) server that enables LLMs to interact with UPS (Uninterruptible Power Supply) devices through Network UPS Tools (NUT).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **list_ups** - List all available UPS devices
|
||||||
|
- **get_ups_status** - Get comprehensive status of a UPS (battery, load, runtime, voltage, etc.)
|
||||||
|
- **get_ups_var** - Get specific variable from UPS
|
||||||
|
- **list_ups_commands** - List available instant commands
|
||||||
|
- **execute_ups_command** - Execute instant commands (battery test, beeper control, etc.)
|
||||||
|
- **get_ups_description** - Get UPS model and manufacturer information
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### 1. Install Network UPS Tools (NUT)
|
||||||
|
|
||||||
|
**On Debian/Ubuntu:**
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install nut nut-client
|
||||||
|
```
|
||||||
|
|
||||||
|
**On RHEL/CentOS:**
|
||||||
|
```bash
|
||||||
|
sudo yum install nut nut-client
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure NUT Server
|
||||||
|
|
||||||
|
Edit `/etc/nut/ups.conf` to add your UPS:
|
||||||
|
```ini
|
||||||
|
[myups]
|
||||||
|
driver = usbhid-ups
|
||||||
|
port = auto
|
||||||
|
desc = "Main UPS"
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `/etc/nut/upsd.conf`:
|
||||||
|
```ini
|
||||||
|
LISTEN 127.0.0.1 3493
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `/etc/nut/upsd.users` (if authentication is needed):
|
||||||
|
```ini
|
||||||
|
[admin]
|
||||||
|
password = yourpassword
|
||||||
|
actions = SET
|
||||||
|
instcmds = ALL
|
||||||
|
```
|
||||||
|
|
||||||
|
Start NUT services:
|
||||||
|
```bash
|
||||||
|
sudo systemctl start nut-server
|
||||||
|
sudo systemctl enable nut-server
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify UPS is detected:
|
||||||
|
```bash
|
||||||
|
upsc myups
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `.env` with your NUT server details:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NUT_HOST=localhost
|
||||||
|
NUT_PORT=3493
|
||||||
|
NUT_USERNAME=admin
|
||||||
|
NUT_PASSWORD=yourpassword
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** Username and password are optional. Leave empty if your NUT server doesn't require authentication.
|
||||||
|
|
||||||
|
### 3. Build the Project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### With Claude Desktop
|
||||||
|
|
||||||
|
Add to your Claude Desktop configuration (`~/.config/Claude/claude_desktop_config.json` on Linux):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"nut-ups": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["/home/wartana/myApp/nut-ups-mcp/dist/index.js"],
|
||||||
|
"env": {
|
||||||
|
"NUT_HOST": "localhost",
|
||||||
|
"NUT_PORT": "3493",
|
||||||
|
"NUT_USERNAME": "admin",
|
||||||
|
"NUT_PASSWORD": "yourpassword"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Other MCP Clients
|
||||||
|
|
||||||
|
Run the server directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the helper script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x run_server.sh
|
||||||
|
./run_server.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The server communicates via stdio using the MCP protocol.
|
||||||
|
|
||||||
|
## Example Commands
|
||||||
|
|
||||||
|
Once connected, you can ask the LLM things like:
|
||||||
|
|
||||||
|
- "List all UPS devices"
|
||||||
|
- "What's the battery charge level of myups?"
|
||||||
|
- "Show me the complete status of the UPS"
|
||||||
|
- "What's the current load on the UPS?"
|
||||||
|
- "How much runtime is remaining?"
|
||||||
|
- "What commands are available for this UPS?"
|
||||||
|
- "Mute the UPS beeper"
|
||||||
|
- "Start a battery test"
|
||||||
|
- "What's the input voltage?"
|
||||||
|
- "Show me the UPS model and manufacturer"
|
||||||
|
|
||||||
|
## Common UPS Variables
|
||||||
|
|
||||||
|
- `battery.charge` - Battery charge level (%)
|
||||||
|
- `battery.runtime` - Estimated runtime remaining (seconds)
|
||||||
|
- `battery.voltage` - Battery voltage
|
||||||
|
- `input.voltage` - Input line voltage
|
||||||
|
- `output.voltage` - Output voltage
|
||||||
|
- `ups.load` - UPS load (%)
|
||||||
|
- `ups.status` - UPS status (OL=Online, OB=On Battery, LB=Low Battery)
|
||||||
|
- `ups.temperature` - UPS temperature
|
||||||
|
|
||||||
|
## Common UPS Commands
|
||||||
|
|
||||||
|
- `test.battery.start` - Start battery test
|
||||||
|
- `test.battery.stop` - Stop battery test
|
||||||
|
- `beeper.mute` - Mute the beeper
|
||||||
|
- `beeper.enable` - Enable the beeper
|
||||||
|
- `load.off` - Turn off the load
|
||||||
|
- `load.on` - Turn on the load
|
||||||
|
|
||||||
|
> **Warning:** Use instant commands with caution! Commands like `load.off` will power down connected devices.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Watch mode for TypeScript (auto-recompile on changes):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Error: connect ECONNREFUSED"
|
||||||
|
- Make sure NUT server (upsd) is running: `sudo systemctl status nut-server`
|
||||||
|
- Check that NUT is listening on the correct port: `netstat -tlnp | grep 3493`
|
||||||
|
|
||||||
|
### "Error: Not connected to NUT server"
|
||||||
|
- Verify NUT_HOST and NUT_PORT are correctly set
|
||||||
|
- Test connection manually: `upsc -l localhost`
|
||||||
|
|
||||||
|
### "Access Denied" errors
|
||||||
|
- Check NUT_USERNAME and NUT_PASSWORD are correct
|
||||||
|
- Verify user permissions in `/etc/nut/upsd.users`
|
||||||
|
|
||||||
|
### No UPS devices listed
|
||||||
|
- Check UPS is detected by NUT: `upsc -l`
|
||||||
|
- Verify UPS configuration in `/etc/nut/ups.conf`
|
||||||
|
- Check NUT driver is running: `sudo systemctl status nut-driver`
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
62
debug_nut.js
Normal file
62
debug_nut.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug script for NUT connection
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as net from 'net';
|
||||||
|
|
||||||
|
const NUT_HOST = '10.8.0.17';
|
||||||
|
const NUT_PORT = 3493;
|
||||||
|
|
||||||
|
async function sendNUTCommand(command) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const socket = new net.Socket();
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
console.log(`✅ Connected to ${NUT_HOST}:${NUT_PORT}`);
|
||||||
|
console.log(`📤 Sending: ${command}\n`);
|
||||||
|
socket.write(command + '\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('data', (chunk) => {
|
||||||
|
data += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('end', () => {
|
||||||
|
console.log('📥 Received:');
|
||||||
|
console.log(data);
|
||||||
|
console.log('---\n');
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', (err) => {
|
||||||
|
console.error('❌ Error:', err.message);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.connect(NUT_PORT, NUT_HOST);
|
||||||
|
|
||||||
|
// Close connection after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
socket.end();
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function debugNUT() {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Testing NUT commands...\n');
|
||||||
|
|
||||||
|
// Try different commands
|
||||||
|
await sendNUTCommand('LIST UPS');
|
||||||
|
await sendNUTCommand('VER');
|
||||||
|
await sendNUTCommand('HELP');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugNUT();
|
||||||
1190
package-lock.json
generated
Normal file
1190
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
package.json
Normal file
33
package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "nut-ups-mcp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "MCP Server for Network UPS Tools (NUT) integration",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"bin": {
|
||||||
|
"nut-ups-mcp": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "tsc --watch"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"mcp",
|
||||||
|
"nut",
|
||||||
|
"ups",
|
||||||
|
"network-ups-tools",
|
||||||
|
"llm",
|
||||||
|
"ai"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||||
|
"dotenv": "^16.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.10.0",
|
||||||
|
"typescript": "^5.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
run_server.sh
Executable file
10
run_server.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
if [ -f .env ]; then
|
||||||
|
export $(cat .env | grep -v '^#' | xargs)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the server
|
||||||
|
node dist/index.js
|
||||||
243
src/client.ts
Normal file
243
src/client.ts
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
/**
|
||||||
|
* NUT (Network UPS Tools) Client Library
|
||||||
|
* Communicates with NUT daemon (upsd) using the NUT protocol over TCP
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as net from 'net';
|
||||||
|
|
||||||
|
export interface UPSDevice {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UPSVariable {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UPSCommand {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NUTClient {
|
||||||
|
private host: string;
|
||||||
|
private port: number;
|
||||||
|
private username?: string;
|
||||||
|
private password?: string;
|
||||||
|
private socket?: net.Socket;
|
||||||
|
private connected: boolean = false;
|
||||||
|
|
||||||
|
constructor(host: string, port: number, username?: string, password?: string) {
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a command to the NUT server and receive the response
|
||||||
|
*/
|
||||||
|
private async sendCommand(command: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.socket || !this.connected) {
|
||||||
|
return reject(new Error('Not connected to NUT server'));
|
||||||
|
}
|
||||||
|
|
||||||
|
let responseData = '';
|
||||||
|
let timeout: NodeJS.Timeout;
|
||||||
|
|
||||||
|
const onData = (data: Buffer) => {
|
||||||
|
responseData += data.toString();
|
||||||
|
|
||||||
|
// Check if we have a complete response
|
||||||
|
// For LIST commands, wait for END marker
|
||||||
|
// For single line commands, wait for newline
|
||||||
|
const isListCommand = command.startsWith('LIST');
|
||||||
|
const isComplete = isListCommand
|
||||||
|
? responseData.includes('END ')
|
||||||
|
: responseData.includes('\n');
|
||||||
|
|
||||||
|
if (isComplete) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
this.socket?.removeListener('data', onData);
|
||||||
|
this.socket?.removeListener('error', onError);
|
||||||
|
resolve(responseData.trim());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onError = (err: Error) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
this.socket?.removeListener('data', onData);
|
||||||
|
this.socket?.removeListener('error', onError);
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.socket.on('data', onData);
|
||||||
|
this.socket.on('error', onError);
|
||||||
|
|
||||||
|
// Set timeout for response
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
this.socket?.removeListener('data', onData);
|
||||||
|
this.socket?.removeListener('error', onError);
|
||||||
|
if (responseData) {
|
||||||
|
resolve(responseData.trim());
|
||||||
|
} else {
|
||||||
|
reject(new Error('Command timeout'));
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// Send the command
|
||||||
|
this.socket.write(command + '\n');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to the NUT server
|
||||||
|
*/
|
||||||
|
async connect(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.socket = new net.Socket();
|
||||||
|
|
||||||
|
this.socket.on('connect', async () => {
|
||||||
|
this.connected = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Authenticate if credentials are provided
|
||||||
|
if (this.username && this.password) {
|
||||||
|
await this.sendCommand(`USERNAME ${this.username}`);
|
||||||
|
await this.sendCommand(`PASSWORD ${this.password}`);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('error', (err) => {
|
||||||
|
this.connected = false;
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.connect(this.port, this.host);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from the NUT server
|
||||||
|
*/
|
||||||
|
disconnect(): void {
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.end();
|
||||||
|
this.socket.destroy();
|
||||||
|
this.connected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all UPS devices available on the NUT server
|
||||||
|
*/
|
||||||
|
async listUPS(): Promise<UPSDevice[]> {
|
||||||
|
const response = await this.sendCommand('LIST UPS');
|
||||||
|
const devices: UPSDevice[] = [];
|
||||||
|
|
||||||
|
const lines = response.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
// Format: UPS <upsname> "<description>"
|
||||||
|
const match = line.match(/^UPS\s+(\S+)\s+"(.+)"$/);
|
||||||
|
if (match) {
|
||||||
|
devices.push({
|
||||||
|
name: match[1],
|
||||||
|
description: match[2],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all variables for a specific UPS
|
||||||
|
*/
|
||||||
|
async getUPSVars(upsName: string): Promise<UPSVariable[]> {
|
||||||
|
const response = await this.sendCommand(`LIST VAR ${upsName}`);
|
||||||
|
const variables: UPSVariable[] = [];
|
||||||
|
|
||||||
|
const lines = response.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
// Format: VAR <upsname> <varname> "<value>"
|
||||||
|
const match = line.match(/^VAR\s+\S+\s+(\S+)\s+"(.+)"$/);
|
||||||
|
if (match) {
|
||||||
|
variables.push({
|
||||||
|
name: match[1],
|
||||||
|
value: match[2],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific variable from a UPS
|
||||||
|
*/
|
||||||
|
async getUPSVar(upsName: string, varName: string): Promise<string> {
|
||||||
|
const response = await this.sendCommand(`GET VAR ${upsName} ${varName}`);
|
||||||
|
|
||||||
|
// Format: VAR <upsname> <varname> "<value>"
|
||||||
|
const match = response.match(/^VAR\s+\S+\s+\S+\s+"(.+)"$/);
|
||||||
|
if (match) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Failed to get variable ${varName} from ${upsName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List available commands for a UPS
|
||||||
|
*/
|
||||||
|
async listCommands(upsName: string): Promise<UPSCommand[]> {
|
||||||
|
const response = await this.sendCommand(`LIST CMD ${upsName}`);
|
||||||
|
const commands: UPSCommand[] = [];
|
||||||
|
|
||||||
|
const lines = response.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
// Format: CMD <upsname> <cmdname>
|
||||||
|
const match = line.match(/^CMD\s+\S+\s+(\S+)$/);
|
||||||
|
if (match) {
|
||||||
|
commands.push({
|
||||||
|
name: match[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute an instant command on a UPS
|
||||||
|
*/
|
||||||
|
async executeCommand(upsName: string, commandName: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response = await this.sendCommand(`INSTCMD ${upsName} ${commandName}`);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to execute command ${commandName} on ${upsName}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get description of a UPS command
|
||||||
|
*/
|
||||||
|
async getCommandDescription(upsName: string, commandName: string): Promise<string> {
|
||||||
|
const response = await this.sendCommand(`GET CMDDESC ${upsName} ${commandName}`);
|
||||||
|
|
||||||
|
// Format: CMDDESC <upsname> <cmdname> "<description>"
|
||||||
|
const match = response.match(/^CMDDESC\s+\S+\s+\S+\s+"(.+)"$/);
|
||||||
|
if (match) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
311
src/index.ts
Normal file
311
src/index.ts
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network UPS Tools (NUT) MCP Server
|
||||||
|
* Provides tools for LLMs to interact with UPS devices through NUT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
Tool,
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
import { NUTClient } from './client.js';
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
config();
|
||||||
|
|
||||||
|
const NUT_HOST = process.env.NUT_HOST || 'localhost';
|
||||||
|
const NUT_PORT = parseInt(process.env.NUT_PORT || '3493');
|
||||||
|
const NUT_USERNAME = process.env.NUT_USERNAME;
|
||||||
|
const NUT_PASSWORD = process.env.NUT_PASSWORD;
|
||||||
|
|
||||||
|
// Initialize NUT client
|
||||||
|
const nutClient = new NUTClient(NUT_HOST, NUT_PORT, NUT_USERNAME, NUT_PASSWORD);
|
||||||
|
|
||||||
|
// Define available tools
|
||||||
|
const tools: Tool[] = [
|
||||||
|
{
|
||||||
|
name: 'list_ups',
|
||||||
|
description: 'List all available UPS devices connected to the NUT server. Returns the device names and descriptions.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_ups_status',
|
||||||
|
description: 'Get comprehensive status information for a UPS device. Returns battery charge, load, runtime, input/output voltage, and other important metrics.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
ups_name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the UPS device (e.g., "ups", "myups")',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['ups_name'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_ups_var',
|
||||||
|
description: 'Get a specific variable value from a UPS device. Common variables include: battery.charge, battery.runtime, input.voltage, output.voltage, ups.load, ups.status, etc.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
ups_name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the UPS device',
|
||||||
|
},
|
||||||
|
var_name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The variable name to retrieve (e.g., "battery.charge", "ups.status")',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['ups_name', 'var_name'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'list_ups_commands',
|
||||||
|
description: 'List all available instant commands for a UPS device. Common commands include test.battery.start, test.battery.stop, beeper.mute, beeper.enable, etc.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
ups_name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the UPS device',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['ups_name'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'execute_ups_command',
|
||||||
|
description: 'Execute an instant command on a UPS device. WARNING: Some commands may affect UPS operation. Use with caution!',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
ups_name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the UPS device',
|
||||||
|
},
|
||||||
|
command_name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The command to execute (e.g., "test.battery.start", "beeper.mute")',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['ups_name', 'command_name'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_ups_description',
|
||||||
|
description: 'Get detailed description and model information for a UPS device.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
ups_name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the UPS device',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['ups_name'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create server
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: 'nut-ups-mcp',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle list tools request
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return { tools };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle tool calls
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { name, arguments: args } = request.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Connect to NUT server for each request
|
||||||
|
await nutClient.connect();
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case 'list_ups': {
|
||||||
|
const devices = await nutClient.listUPS();
|
||||||
|
nutClient.disconnect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(devices, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'get_ups_status': {
|
||||||
|
const upsName = args?.ups_name as string;
|
||||||
|
const variables = await nutClient.getUPSVars(upsName);
|
||||||
|
nutClient.disconnect();
|
||||||
|
|
||||||
|
// Parse important status variables
|
||||||
|
const status: Record<string, string> = {};
|
||||||
|
for (const v of variables) {
|
||||||
|
status[v.name] = v.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(status, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'get_ups_var': {
|
||||||
|
const upsName = args?.ups_name as string;
|
||||||
|
const varName = args?.var_name as string;
|
||||||
|
|
||||||
|
const value = await nutClient.getUPSVar(upsName, varName);
|
||||||
|
nutClient.disconnect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `${varName}: ${value}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'list_ups_commands': {
|
||||||
|
const upsName = args?.ups_name as string;
|
||||||
|
const commands = await nutClient.listCommands(upsName);
|
||||||
|
|
||||||
|
// Get descriptions for each command
|
||||||
|
const commandsWithDesc = await Promise.all(
|
||||||
|
commands.map(async (cmd) => {
|
||||||
|
try {
|
||||||
|
const desc = await nutClient.getCommandDescription(upsName, cmd.name);
|
||||||
|
return { ...cmd, description: desc };
|
||||||
|
} catch {
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
nutClient.disconnect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(commandsWithDesc, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'execute_ups_command': {
|
||||||
|
const upsName = args?.ups_name as string;
|
||||||
|
const commandName = args?.command_name as string;
|
||||||
|
|
||||||
|
const result = await nutClient.executeCommand(upsName, commandName);
|
||||||
|
nutClient.disconnect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `Command ${commandName} executed on ${upsName}.\nResponse: ${result}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'get_ups_description': {
|
||||||
|
const upsName = args?.ups_name as string;
|
||||||
|
|
||||||
|
// Get relevant description variables
|
||||||
|
const varsToGet = [
|
||||||
|
'device.mfr',
|
||||||
|
'device.model',
|
||||||
|
'device.serial',
|
||||||
|
'device.type',
|
||||||
|
'ups.mfr',
|
||||||
|
'ups.model',
|
||||||
|
'ups.serial',
|
||||||
|
'ups.firmware',
|
||||||
|
];
|
||||||
|
|
||||||
|
const description: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const varName of varsToGet) {
|
||||||
|
try {
|
||||||
|
const value = await nutClient.getUPSVar(upsName, varName);
|
||||||
|
description[varName] = value;
|
||||||
|
} catch {
|
||||||
|
// Variable might not exist for this UPS
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nutClient.disconnect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(description, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
nutClient.disconnect();
|
||||||
|
throw new Error(`Unknown tool: ${name}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
nutClient.disconnect();
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `Error: ${errorMessage}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
async function main() {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
console.error('NUT UPS MCP Server running on stdio');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
93
test_nut.js
Executable file
93
test_nut.js
Executable file
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick test script to verify NUT UPS MCP functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NUTClient } from './dist/client.js';
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
config();
|
||||||
|
|
||||||
|
const NUT_HOST = process.env.NUT_HOST || 'localhost';
|
||||||
|
const NUT_PORT = parseInt(process.env.NUT_PORT || '3493');
|
||||||
|
const NUT_USERNAME = process.env.NUT_USERNAME;
|
||||||
|
const NUT_PASSWORD = process.env.NUT_PASSWORD;
|
||||||
|
|
||||||
|
async function testNUTConnection() {
|
||||||
|
console.log('🔌 Connecting to NUT server...');
|
||||||
|
console.log(` Host: ${NUT_HOST}:${NUT_PORT}`);
|
||||||
|
console.log(` User: ${NUT_USERNAME || '(no auth)'}\n`);
|
||||||
|
|
||||||
|
const client = new NUTClient(NUT_HOST, NUT_PORT, NUT_USERNAME, NUT_PASSWORD);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Connect
|
||||||
|
await client.connect();
|
||||||
|
console.log('✅ Connected successfully!\n');
|
||||||
|
|
||||||
|
// List UPS devices
|
||||||
|
console.log('📋 Listing UPS devices...');
|
||||||
|
const devices = await client.listUPS();
|
||||||
|
console.log(`Found ${devices.length} UPS device(s):\n`);
|
||||||
|
devices.forEach(d => {
|
||||||
|
console.log(` • ${d.name}: ${d.description}`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
if (devices.length > 0) {
|
||||||
|
const upsName = devices[0].name;
|
||||||
|
console.log(`📊 Getting complete status for: ${upsName}\n`);
|
||||||
|
|
||||||
|
// Get all variables
|
||||||
|
const vars = await client.getUPSVars(upsName);
|
||||||
|
|
||||||
|
// Group variables by category
|
||||||
|
const battery = vars.filter(v => v.name.startsWith('battery.'));
|
||||||
|
const input = vars.filter(v => v.name.startsWith('input.'));
|
||||||
|
const output = vars.filter(v => v.name.startsWith('output.'));
|
||||||
|
const ups = vars.filter(v => v.name.startsWith('ups.'));
|
||||||
|
const device = vars.filter(v => v.name.startsWith('device.'));
|
||||||
|
|
||||||
|
console.log('🔋 BATTERY STATUS:');
|
||||||
|
battery.forEach(v => console.log(` ${v.name}: ${v.value}`));
|
||||||
|
|
||||||
|
console.log('\n⚡ INPUT:');
|
||||||
|
input.forEach(v => console.log(` ${v.name}: ${v.value}`));
|
||||||
|
|
||||||
|
console.log('\n🔌 OUTPUT:');
|
||||||
|
output.forEach(v => console.log(` ${v.name}: ${v.value}`));
|
||||||
|
|
||||||
|
console.log('\n🖥️ UPS:');
|
||||||
|
ups.forEach(v => console.log(` ${v.name}: ${v.value}`));
|
||||||
|
|
||||||
|
console.log('\n🔧 DEVICE:');
|
||||||
|
device.forEach(v => console.log(` ${v.name}: ${v.value}`));
|
||||||
|
|
||||||
|
// Show other variables
|
||||||
|
const other = vars.filter(v =>
|
||||||
|
!v.name.startsWith('battery.') &&
|
||||||
|
!v.name.startsWith('input.') &&
|
||||||
|
!v.name.startsWith('output.') &&
|
||||||
|
!v.name.startsWith('ups.') &&
|
||||||
|
!v.name.startsWith('device.')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (other.length > 0) {
|
||||||
|
console.log('\n📝 OTHER:');
|
||||||
|
other.forEach(v => console.log(` ${v.name}: ${v.value}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.disconnect();
|
||||||
|
console.log('\n✅ Test completed successfully!');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ Error:', error.message);
|
||||||
|
client.disconnect();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testNUTConnection();
|
||||||
27
tsconfig.json
Normal file
27
tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ES2020",
|
||||||
|
"lib": [
|
||||||
|
"ES2020"
|
||||||
|
],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user