Add TP-Link Deco network stats tools
This commit is contained in:
@@ -3,3 +3,12 @@ MACMINI_MCP_HOST=127.0.0.1
|
|||||||
MACMINI_MCP_PORT=7331
|
MACMINI_MCP_PORT=7331
|
||||||
# Required whenever MACMINI_MCP_HOST is not loopback.
|
# Required whenever MACMINI_MCP_HOST is not loopback.
|
||||||
# MACMINI_MCP_TOKEN=replace-with-a-long-random-token
|
# MACMINI_MCP_TOKEN=replace-with-a-long-random-token
|
||||||
|
|
||||||
|
# TP-Link Deco read-only network stats.
|
||||||
|
# Use the router/deco LAN IP or hostname and the local web/admin password.
|
||||||
|
# DECO_HOST defaults to the Mac's default gateway when omitted.
|
||||||
|
# DECO_HOST=192.168.68.1
|
||||||
|
# DECO_USERNAME=admin
|
||||||
|
# DECO_PASSWORD=replace-with-router-password
|
||||||
|
# DECO_VERIFY_SSL=false
|
||||||
|
# DECO_TIMEOUT=10
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
.venv/
|
||||||
.env
|
.env
|
||||||
*.log
|
*.log
|
||||||
.logs/
|
.logs/
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ and stays on the local machine.
|
|||||||
| `contacts_search` | Search contact names and organizations without disclosing contact methods |
|
| `contacts_search` | Search contact names and organizations without disclosing contact methods |
|
||||||
| `contacts_read` | Read phone and email details for one selected contact |
|
| `contacts_read` | Read phone and email details for one selected contact |
|
||||||
| `contacts_create` | Create a contact with optional email and phone details |
|
| `contacts_create` | Create a contact with optional email and phone details |
|
||||||
|
| `deco_get_overview` | Read TP-Link Deco overview stats and firmware |
|
||||||
|
| `deco_list_clients` | List online Deco clients and current traffic speeds |
|
||||||
|
| `deco_get_ipv4_status` | Read WAN/LAN IPv4 status |
|
||||||
|
| `deco_get_firmware` | Read Deco model and firmware version |
|
||||||
|
|
||||||
There are no destructive tools in the initial server.
|
There are no destructive tools in the initial server.
|
||||||
|
|
||||||
@@ -34,6 +38,7 @@ Requires macOS and Node.js 20 or newer.
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm install
|
npm install
|
||||||
|
npm run python:install
|
||||||
npm run check
|
npm run check
|
||||||
npm run service:install
|
npm run service:install
|
||||||
```
|
```
|
||||||
@@ -89,6 +94,10 @@ Restart after changing `.env`:
|
|||||||
npm run service:restart
|
npm run service:restart
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For TP-Link Deco tools, install Python dependencies with `npm run
|
||||||
|
python:install`, then set `DECO_HOST`, `DECO_USERNAME`, `DECO_PASSWORD`, and
|
||||||
|
optionally `DECO_VERIFY_SSL=false` in `.env`.
|
||||||
|
|
||||||
For a harness that launches stdio servers, use:
|
For a harness that launches stdio servers, use:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"start:stdio": "node src/stdio.js",
|
"start:stdio": "node src/stdio.js",
|
||||||
"dev": "node --watch src/http.js",
|
"dev": "node --watch src/http.js",
|
||||||
"check": "node --check src/*.js && node --check src/integrations/*.js && node --test",
|
"check": "node --check src/*.js && node --check src/integrations/*.js && node --test",
|
||||||
|
"python:install": "./scripts/setup-python.sh",
|
||||||
"service:install": "./scripts/install-service.sh",
|
"service:install": "./scripts/install-service.sh",
|
||||||
"service:restart": "./scripts/restart-service.sh",
|
"service:restart": "./scripts/restart-service.sh",
|
||||||
"service:status": "./scripts/status-service.sh",
|
"service:status": "./scripts/status-service.sh",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
tplinkrouterc6u==5.21.0
|
||||||
Executable
+156
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from tplinkrouterc6u import TPLinkDecoClient
|
||||||
|
|
||||||
|
|
||||||
|
def default_gateway():
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["/sbin/route", "-n", "get", "default"],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
key, _, value = line.partition(":")
|
||||||
|
if key.strip() == "gateway":
|
||||||
|
return value.strip()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def config():
|
||||||
|
host = os.environ.get("DECO_HOST") or default_gateway()
|
||||||
|
password = os.environ.get("DECO_PASSWORD")
|
||||||
|
if not password:
|
||||||
|
raise RuntimeError("DECO_PASSWORD must be set in .env.")
|
||||||
|
if not host:
|
||||||
|
raise RuntimeError("DECO_HOST must be set in .env; default gateway detection failed.")
|
||||||
|
|
||||||
|
verify_ssl = os.environ.get("DECO_VERIFY_SSL", "true").lower() not in {
|
||||||
|
"0",
|
||||||
|
"false",
|
||||||
|
"no",
|
||||||
|
}
|
||||||
|
timeout = int(os.environ.get("DECO_TIMEOUT", "10"))
|
||||||
|
return {
|
||||||
|
"host": host,
|
||||||
|
"password": password,
|
||||||
|
"username": os.environ.get("DECO_USERNAME", "admin"),
|
||||||
|
"verify_ssl": verify_ssl,
|
||||||
|
"timeout": timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def client():
|
||||||
|
return TPLinkDecoClient(**config())
|
||||||
|
|
||||||
|
|
||||||
|
def device_to_dict(device):
|
||||||
|
return {
|
||||||
|
"hostname": device.hostname,
|
||||||
|
"mac": device.macaddr,
|
||||||
|
"ip": device.ipaddr,
|
||||||
|
"connection": getattr(device.type, "value", str(device.type)),
|
||||||
|
"upSpeed": device.up_speed,
|
||||||
|
"downSpeed": device.down_speed,
|
||||||
|
"active": device.active,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def firmware_to_dict(firmware):
|
||||||
|
return {
|
||||||
|
"model": firmware.model,
|
||||||
|
"hardwareVersion": firmware.hardware_version,
|
||||||
|
"firmwareVersion": firmware.firmware_version,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def ipv4_to_dict(status):
|
||||||
|
return {
|
||||||
|
"wanMac": status.wan_macaddr,
|
||||||
|
"wanIp": status.wan_ipv4_ipaddr,
|
||||||
|
"wanGateway": status.wan_ipv4_gateway,
|
||||||
|
"wanConnectionType": status.wan_ipv4_conntype,
|
||||||
|
"wanNetmask": status.wan_ipv4_netmask,
|
||||||
|
"wanPrimaryDns": status.wan_ipv4_pridns,
|
||||||
|
"wanSecondaryDns": status.wan_ipv4_snddns,
|
||||||
|
"lanMac": status.lan_macaddr,
|
||||||
|
"lanIp": status.lan_ipv4_ipaddr,
|
||||||
|
"lanNetmask": status.lan_ipv4_netmask,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def status_to_dict(status, include_clients=True):
|
||||||
|
data = {
|
||||||
|
"wanMac": status.wan_macaddr,
|
||||||
|
"lanMac": status.lan_macaddr,
|
||||||
|
"wanIp": status.wan_ipv4_addr,
|
||||||
|
"lanIp": status.lan_ipv4_addr,
|
||||||
|
"wanGateway": status.wan_ipv4_gateway,
|
||||||
|
"connectionType": status.conn_type,
|
||||||
|
"cpuUsage": status.cpu_usage,
|
||||||
|
"memoryUsage": status.mem_usage,
|
||||||
|
"clientsTotal": status.clients_total,
|
||||||
|
"wiredClientsTotal": status.wired_total,
|
||||||
|
"wifiClientsTotal": status.wifi_clients_total,
|
||||||
|
"guestClientsTotal": status.guest_clients_total,
|
||||||
|
"iotClientsTotal": status.iot_clients_total,
|
||||||
|
"wifi": {
|
||||||
|
"host2g": status.wifi_2g_enable,
|
||||||
|
"host5g": status.wifi_5g_enable,
|
||||||
|
"host6g": status.wifi_6g_enable,
|
||||||
|
"guest2g": status.guest_2g_enable,
|
||||||
|
"guest5g": status.guest_5g_enable,
|
||||||
|
"guest6g": status.guest_6g_enable,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if include_clients:
|
||||||
|
data["clients"] = [device_to_dict(device) for device in status.devices]
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def run(action):
|
||||||
|
deco = client()
|
||||||
|
try:
|
||||||
|
if action == "overview":
|
||||||
|
status = deco.get_status()
|
||||||
|
firmware = deco.get_firmware()
|
||||||
|
return {
|
||||||
|
"status": status_to_dict(status, include_clients=False),
|
||||||
|
"firmware": firmware_to_dict(firmware),
|
||||||
|
}
|
||||||
|
if action == "clients":
|
||||||
|
status = deco.get_status()
|
||||||
|
return {"clients": [device_to_dict(device) for device in status.devices]}
|
||||||
|
if action == "ipv4":
|
||||||
|
return ipv4_to_dict(deco.get_ipv4_status())
|
||||||
|
if action == "firmware":
|
||||||
|
return firmware_to_dict(deco.get_firmware())
|
||||||
|
raise RuntimeError(f"Unknown action: {action}")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
deco.logout()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
raise RuntimeError("Usage: deco_bridge.py <overview|clients|ipv4|firmware>")
|
||||||
|
print(json.dumps(run(sys.argv[1]), indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as err:
|
||||||
|
print(json.dumps({"error": str(err)}), file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/zsh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
|
||||||
|
if [[ ! -x .venv/bin/python ]]; then
|
||||||
|
python3 -m venv .venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
.venv/bin/python -m pip install -r requirements.txt
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { execFile } from "node:child_process";
|
||||||
|
import { loadEnvFile } from "node:process";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
|
try {
|
||||||
|
loadEnvFile();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "ENOENT") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
const PYTHON = new URL("../../.venv/bin/python", import.meta.url).pathname;
|
||||||
|
const BRIDGE = new URL("../../scripts/deco_bridge.py", import.meta.url).pathname;
|
||||||
|
|
||||||
|
export async function getDecoStats(action) {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execFileAsync(PYTHON, [BRIDGE, action], {
|
||||||
|
timeout: 45_000,
|
||||||
|
maxBuffer: 4 * 1024 * 1024,
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
return JSON.parse(stdout);
|
||||||
|
} catch (error) {
|
||||||
|
const detail = error.stderr?.trim() || error.message;
|
||||||
|
throw new Error(`Deco stats request failed. ${detail}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
readContact,
|
readContact,
|
||||||
searchContacts,
|
searchContacts,
|
||||||
} from "./integrations/contacts.js";
|
} from "./integrations/contacts.js";
|
||||||
|
import { getDecoStats } from "./integrations/deco.js";
|
||||||
import { createNote, listNotes, readNote } from "./integrations/notes.js";
|
import { createNote, listNotes, readNote } from "./integrations/notes.js";
|
||||||
import {
|
import {
|
||||||
createReminder,
|
createReminder,
|
||||||
@@ -190,5 +191,33 @@ export function createMacMiniMcpServer() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
"deco_get_overview",
|
||||||
|
"Read TP-Link Deco overview stats: WAN/LAN addresses, CPU/memory usage, client counts, Wi-Fi enablement, and firmware.",
|
||||||
|
{},
|
||||||
|
handled(() => getDecoStats("overview")),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
"deco_list_clients",
|
||||||
|
"List online TP-Link Deco clients with hostname, IP, MAC, connection type, and current up/down speeds.",
|
||||||
|
{},
|
||||||
|
handled(() => getDecoStats("clients")),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
"deco_get_ipv4_status",
|
||||||
|
"Read TP-Link Deco WAN/LAN IPv4 status including gateway, DNS, netmasks, and connection type.",
|
||||||
|
{},
|
||||||
|
handled(() => getDecoStats("ipv4")),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
"deco_get_firmware",
|
||||||
|
"Read TP-Link Deco model, hardware version, and firmware version.",
|
||||||
|
{},
|
||||||
|
handled(() => getDecoStats("firmware")),
|
||||||
|
);
|
||||||
|
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user