Skip to content

Commit 72bae51

Browse files
committed
fix(v0.2.1): add Grafana auth, human-readable errors, and port overrides for all monitoring tools
Grafana auth: supports HOMELAB_GRAFANA_TOKEN (API key) or HOMELAB_GRAFANA_USER/PASSWORD (basic auth). Error messages: all 5 monitoring tools now explain connection failures instead of showing raw curl exit codes. Port overrides: HOMELAB_PROMETHEUS_PORT, HOMELAB_GRAFANA_PORT, HOMELAB_ALERTMANAGER_PORT, HOMELAB_UPTIME_KUMA_PORT, HOMELAB_SPEEDTEST_PORT. Made-with: Cursor
1 parent a2f19f8 commit 72bae51

10 files changed

Lines changed: 168 additions & 8 deletions

File tree

.env.example

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,17 @@ HOMELAB_COMPOSE_DIR=/opt/homelab/docker
88

99
# Backup repo path on the Pi
1010
HOMELAB_BACKUP_REPO=/mnt/backup/restic
11+
12+
# Grafana authentication (pick one method)
13+
# Option 1: API token (preferred)
14+
# HOMELAB_GRAFANA_TOKEN=glsa_xxxxxxxxxxxx
15+
# Option 2: Basic auth (defaults to admin/admin if neither is set)
16+
# HOMELAB_GRAFANA_USER=admin
17+
# HOMELAB_GRAFANA_PASSWORD=your-password
18+
19+
# Service port overrides (defaults shown -- only set if you remap ports)
20+
# HOMELAB_PROMETHEUS_PORT=9090
21+
# HOMELAB_GRAFANA_PORT=3000
22+
# HOMELAB_ALERTMANAGER_PORT=9093
23+
# HOMELAB_UPTIME_KUMA_PORT=3001
24+
# HOMELAB_SPEEDTEST_PORT=8765

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.1] - 2026-04-05
9+
10+
### Fixed
11+
12+
- `grafanaSnapshot` now supports authentication via `HOMELAB_GRAFANA_TOKEN` (API key) or `HOMELAB_GRAFANA_USER`/`HOMELAB_GRAFANA_PASSWORD` (basic auth)
13+
- All 5 v0.2.0 tools now return human-readable error messages when a service is unreachable (e.g. "Could not connect to Alertmanager on port 9093. Is it running?") instead of raw curl exit codes
14+
- All 5 v0.2.0 tools now support optional port overrides via env vars (`HOMELAB_PROMETHEUS_PORT`, `HOMELAB_GRAFANA_PORT`, `HOMELAB_ALERTMANAGER_PORT`, `HOMELAB_UPTIME_KUMA_PORT`, `HOMELAB_SPEEDTEST_PORT`)
15+
- Updated `.env.example` and `mcp-server/README.md` with new env var documentation
16+
817
## [0.2.0] - 2026-04-05
918

1019
### Added
@@ -45,5 +54,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4554
- Full documentation (README, CLAUDE.md, CONTRIBUTING, ROADMAP, SECURITY)
4655
- Project logo (assets/logo.png)
4756

57+
[0.2.1]: https://github.com/TMHSDigital/Home-Lab-Developer-Tools/releases/tag/v0.2.1
4858
[0.2.0]: https://github.com/TMHSDigital/Home-Lab-Developer-Tools/releases/tag/v0.2.0
4959
[0.1.0]: https://github.com/TMHSDigital/Home-Lab-Developer-Tools/releases/tag/v0.1.0

mcp-server/README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Home Lab MCP Server
22

3-
MCP (Model Context Protocol) server for home lab operations. Connects to a Raspberry Pi via SSH and provides 15 tools for system management, Docker Compose stacks, service monitoring, networking, and backups.
3+
MCP (Model Context Protocol) server for home lab operations. Connects to a Raspberry Pi via SSH and provides 20 tools for system management, Docker Compose stacks, service monitoring, networking, and backups.
44

55
## Tools
66

@@ -20,6 +20,11 @@ MCP (Model Context Protocol) server for home lab operations. Connects to a Raspb
2020
| Network | `homelab_networkInfo` | IP addresses, DNS, Tailscale status |
2121
| Backup | `homelab_backupStatus` | Check latest restic snapshots |
2222
| Backup | `homelab_backupRun` | Trigger restic backup |
23+
| Monitoring | `homelab_prometheusQuery` | Run PromQL queries against Prometheus |
24+
| Monitoring | `homelab_grafanaSnapshot` | Export Grafana dashboard config by UID |
25+
| Monitoring | `homelab_uptimeKumaStatus` | Get Uptime Kuma monitor statuses |
26+
| Monitoring | `homelab_alertList` | List Alertmanager alerts by state |
27+
| Monitoring | `homelab_speedtestResults` | Get recent Speedtest Tracker results |
2328
| SSH | `homelab_sshTest` | Test SSH connectivity |
2429

2530
## Setup
@@ -41,6 +46,14 @@ Set environment variables:
4146
| `HOMELAB_PI_KEY_PATH` | (empty) | Path to SSH private key |
4247
| `HOMELAB_COMPOSE_DIR` | `/opt/homelab/docker` | Compose project directory on Pi |
4348
| `HOMELAB_BACKUP_REPO` | `/mnt/backup/restic` | Restic backup repo path on Pi |
49+
| `HOMELAB_GRAFANA_TOKEN` | (empty) | Grafana API token (preferred auth method) |
50+
| `HOMELAB_GRAFANA_USER` | `admin` | Grafana basic auth username |
51+
| `HOMELAB_GRAFANA_PASSWORD` | (empty) | Grafana basic auth password (falls back to admin/admin) |
52+
| `HOMELAB_PROMETHEUS_PORT` | `9090` | Prometheus port override |
53+
| `HOMELAB_GRAFANA_PORT` | `3000` | Grafana port override |
54+
| `HOMELAB_ALERTMANAGER_PORT` | `9093` | Alertmanager port override |
55+
| `HOMELAB_UPTIME_KUMA_PORT` | `3001` | Uptime Kuma port override |
56+
| `HOMELAB_SPEEDTEST_PORT` | `8765` | Speedtest Tracker port override |
4457

4558
## Usage with Cursor
4659

mcp-server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tmhs/homelab-mcp",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"description": "MCP server for home lab operations via SSH - 20 tools for system status, Docker Compose management, service health, monitoring, networking, backups, and administration on a Raspberry Pi.",
55
"type": "module",
66
"main": "dist/index.js",

mcp-server/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { register as registerSpeedtestResults } from "./tools/speedtestResults.j
2626

2727
const server = new McpServer({
2828
name: "homelab-mcp",
29-
version: "0.2.0",
29+
version: "0.2.1",
3030
});
3131

3232
registerPiStatus(server);

mcp-server/src/tools/alertList.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { z } from "zod";
22
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33
import { execSSH, errorResponse } from "../utils/ssh-api.js";
4+
import { CommandFailedError } from "../utils/errors.js";
5+
6+
const DEFAULT_PORT = 9093;
7+
const SERVICE_NAME = "Alertmanager";
48

59
const inputSchema = {
610
state: z
@@ -9,20 +13,36 @@ const inputSchema = {
913
.describe("Filter alerts by state. Returns all states if omitted"),
1014
};
1115

16+
function getPort(): number {
17+
const override = process.env.HOMELAB_ALERTMANAGER_PORT;
18+
return override ? parseInt(override, 10) : DEFAULT_PORT;
19+
}
20+
1221
export function register(server: McpServer): void {
1322
server.tool(
1423
"homelab_alertList",
1524
"List alerts from Alertmanager, optionally filtered by state",
1625
inputSchema,
1726
async (args) => {
27+
const port = getPort();
1828
try {
1929
const stateParam = args.state ? `?state=${args.state}` : "";
2030
const output = await execSSH(
21-
`curl -sf 'http://localhost:9093/api/v2/alerts${stateParam}'`,
31+
`curl -sf 'http://localhost:${port}/api/v2/alerts${stateParam}'`,
2232
);
2333

2434
return { content: [{ type: "text" as const, text: output }] };
2535
} catch (error) {
36+
if (error instanceof CommandFailedError) {
37+
if (error.exitCode === 7) {
38+
return errorResponse(
39+
new Error(
40+
`Could not connect to ${SERVICE_NAME} on port ${port}. Is it running? ` +
41+
`Set HOMELAB_ALERTMANAGER_PORT if using a non-default port.`,
42+
),
43+
);
44+
}
45+
}
2646
return errorResponse(error);
2747
}
2848
},

mcp-server/src/tools/grafanaSnapshot.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,67 @@
11
import { z } from "zod";
22
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33
import { execSSH, errorResponse } from "../utils/ssh-api.js";
4+
import { CommandFailedError } from "../utils/errors.js";
5+
6+
const DEFAULT_PORT = 3000;
7+
const SERVICE_NAME = "Grafana";
48

59
const inputSchema = {
610
dashboard: z.string().min(1).describe("Dashboard UID to export"),
711
};
812

13+
function getPort(): number {
14+
const override = process.env.HOMELAB_GRAFANA_PORT;
15+
return override ? parseInt(override, 10) : DEFAULT_PORT;
16+
}
17+
18+
function buildAuthHeader(): string {
19+
const token = process.env.HOMELAB_GRAFANA_TOKEN;
20+
if (token) {
21+
return `-H 'Authorization: Bearer ${token}'`;
22+
}
23+
const user = process.env.HOMELAB_GRAFANA_USER || "admin";
24+
const password = process.env.HOMELAB_GRAFANA_PASSWORD;
25+
if (password) {
26+
return `-u '${user}:${password}'`;
27+
}
28+
return `-u 'admin:admin'`;
29+
}
30+
931
export function register(server: McpServer): void {
1032
server.tool(
1133
"homelab_grafanaSnapshot",
1234
"Export a Grafana dashboard configuration by UID",
1335
inputSchema,
1436
async (args) => {
37+
const port = getPort();
1538
try {
39+
const auth = buildAuthHeader();
1640
const output = await execSSH(
17-
`curl -sf 'http://localhost:3000/api/dashboards/uid/${args.dashboard}'`,
41+
`curl -sf ${auth} 'http://localhost:${port}/api/dashboards/uid/${args.dashboard}'`,
1842
);
1943

2044
return { content: [{ type: "text" as const, text: output }] };
2145
} catch (error) {
46+
if (error instanceof CommandFailedError) {
47+
if (error.exitCode === 7) {
48+
return errorResponse(
49+
new Error(
50+
`Could not reach ${SERVICE_NAME} API on port ${port}. Is it running? ` +
51+
`Set HOMELAB_GRAFANA_PORT if using a non-default port.`,
52+
),
53+
);
54+
}
55+
if (error.exitCode === 22) {
56+
return errorResponse(
57+
new Error(
58+
`${SERVICE_NAME} returned an HTTP error. Check authentication -- ` +
59+
`set HOMELAB_GRAFANA_TOKEN (API key) or HOMELAB_GRAFANA_USER/HOMELAB_GRAFANA_PASSWORD. ` +
60+
`Also verify the dashboard UID "${args.dashboard}" exists.`,
61+
),
62+
);
63+
}
64+
}
2265
return errorResponse(error);
2366
}
2467
},

mcp-server/src/tools/prometheusQuery.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { z } from "zod";
22
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33
import { execSSH, errorResponse } from "../utils/ssh-api.js";
4+
import { CommandFailedError } from "../utils/errors.js";
5+
6+
const DEFAULT_PORT = 9090;
7+
const SERVICE_NAME = "Prometheus";
48

59
const inputSchema = {
610
query: z.string().min(1).describe("PromQL expression to evaluate"),
@@ -10,21 +14,37 @@ const inputSchema = {
1014
.describe("Evaluation timestamp (RFC3339 or Unix). Defaults to current time"),
1115
};
1216

17+
function getPort(): number {
18+
const override = process.env.HOMELAB_PROMETHEUS_PORT;
19+
return override ? parseInt(override, 10) : DEFAULT_PORT;
20+
}
21+
1322
export function register(server: McpServer): void {
1423
server.tool(
1524
"homelab_prometheusQuery",
1625
"Run a PromQL query against Prometheus and return the result",
1726
inputSchema,
1827
async (args) => {
28+
const port = getPort();
1929
try {
2030
const encoded = encodeURIComponent(args.query);
2131
const timeParam = args.time ? `&time=${encodeURIComponent(args.time)}` : "";
2232
const output = await execSSH(
23-
`curl -sf 'http://localhost:9090/api/v1/query?query=${encoded}${timeParam}'`,
33+
`curl -sf 'http://localhost:${port}/api/v1/query?query=${encoded}${timeParam}'`,
2434
);
2535

2636
return { content: [{ type: "text" as const, text: output }] };
2737
} catch (error) {
38+
if (error instanceof CommandFailedError) {
39+
if (error.exitCode === 7) {
40+
return errorResponse(
41+
new Error(
42+
`Could not reach ${SERVICE_NAME} on port ${port}. Is it running? ` +
43+
`Set HOMELAB_PROMETHEUS_PORT if using a non-default port.`,
44+
),
45+
);
46+
}
47+
}
2848
return errorResponse(error);
2949
}
3050
},

mcp-server/src/tools/speedtestResults.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { z } from "zod";
22
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33
import { execSSH, errorResponse } from "../utils/ssh-api.js";
4+
import { CommandFailedError } from "../utils/errors.js";
5+
6+
const DEFAULT_PORT = 8765;
7+
const SERVICE_NAME = "Speedtest Tracker";
48

59
const inputSchema = {
610
count: z
@@ -12,19 +16,35 @@ const inputSchema = {
1216
.describe("Number of recent speedtest results to return"),
1317
};
1418

19+
function getPort(): number {
20+
const override = process.env.HOMELAB_SPEEDTEST_PORT;
21+
return override ? parseInt(override, 10) : DEFAULT_PORT;
22+
}
23+
1524
export function register(server: McpServer): void {
1625
server.tool(
1726
"homelab_speedtestResults",
1827
"Get recent speedtest results from Speedtest Tracker",
1928
inputSchema,
2029
async (args) => {
30+
const port = getPort();
2131
try {
2232
const output = await execSSH(
23-
`curl -sf 'http://localhost:8765/api/speedtest/latest?limit=${args.count}'`,
33+
`curl -sf 'http://localhost:${port}/api/speedtest/latest?limit=${args.count}'`,
2434
);
2535

2636
return { content: [{ type: "text" as const, text: output }] };
2737
} catch (error) {
38+
if (error instanceof CommandFailedError) {
39+
if (error.exitCode === 7) {
40+
return errorResponse(
41+
new Error(
42+
`Could not connect to ${SERVICE_NAME} on port ${port}. Is it running? ` +
43+
`Set HOMELAB_SPEEDTEST_PORT if using a non-default port.`,
44+
),
45+
);
46+
}
47+
}
2848
return errorResponse(error);
2949
}
3050
},

mcp-server/src/tools/uptimeKumaStatus.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,39 @@
11
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { execSSH, errorResponse } from "../utils/ssh-api.js";
3+
import { CommandFailedError } from "../utils/errors.js";
4+
5+
const DEFAULT_PORT = 3001;
6+
const SERVICE_NAME = "Uptime Kuma";
7+
8+
function getPort(): number {
9+
const override = process.env.HOMELAB_UPTIME_KUMA_PORT;
10+
return override ? parseInt(override, 10) : DEFAULT_PORT;
11+
}
312

413
export function register(server: McpServer): void {
514
server.tool(
615
"homelab_uptimeKumaStatus",
716
"Get the status of all Uptime Kuma monitors",
817
{},
918
async () => {
19+
const port = getPort();
1020
try {
1121
const output = await execSSH(
12-
`curl -sf 'http://localhost:3001/api/status-page/heartbeat/default'`,
22+
`curl -sf 'http://localhost:${port}/api/status-page/heartbeat/default'`,
1323
);
1424

1525
return { content: [{ type: "text" as const, text: output }] };
1626
} catch (error) {
27+
if (error instanceof CommandFailedError) {
28+
if (error.exitCode === 7) {
29+
return errorResponse(
30+
new Error(
31+
`Could not connect to ${SERVICE_NAME} on port ${port}. Is it running? ` +
32+
`Set HOMELAB_UPTIME_KUMA_PORT if using a non-default port.`,
33+
),
34+
);
35+
}
36+
}
1737
return errorResponse(error);
1838
}
1939
},

0 commit comments

Comments
 (0)