Skip to content

Repository files navigation

beszel-mcp

CI License: MIT

A read-only MCP server for a Beszel monitoring hub. Twelve tools let an agent answer questions about your servers — load, containers, services, disk health, alerts — without knowing anything about Beszel's API.

An independent project, not affiliated with or endorsed by the Beszel maintainers.

Quick start

docker run -p 3000:3000 \
  -e BESZEL_URL=https://beszel.example.com \
  -e BESZEL_EMAIL=you@example.com \
  -e BESZEL_PASSWORD=your-password \
  -e MCP_AUTH_TOKEN=some-long-random-string \
  ghcr.io/maxbth/beszel-mcp

The MCP endpoint is POST http://localhost:3000/mcp (Streamable HTTP).

stdio

For local clients that speak stdio:

docker run -i --rm \
  -e BESZEL_URL=https://beszel.example.com \
  -e BESZEL_EMAIL=you@example.com \
  -e BESZEL_PASSWORD=your-password \
  ghcr.io/maxbth/beszel-mcp --stdio

Wired into an MCP client — Claude Desktop, Claude Code, or anything else that launches a subprocess. -i is required (the client talks to the container's stdin) and --rm keeps a container from being left behind on every restart:

{
  "mcpServers": {
    "beszel": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "BESZEL_URL",
        "-e", "BESZEL_EMAIL",
        "-e", "BESZEL_PASSWORD",
        "ghcr.io/maxbth/beszel-mcp", "--stdio"
      ],
      "env": {
        "BESZEL_URL": "https://beszel.example.com",
        "BESZEL_EMAIL": "you@example.com",
        "BESZEL_PASSWORD": "your-password"
      }
    }
  }
}

MCP_AUTH_TOKEN has no role here: there is no port to reach, so the only thing that can talk to the server is the process that spawned it.

Image tags

Tag Points at
latest, 1.2.3, 1.2, 1 the most recent tagged release
dev the current tip of main, rebuilt on every push

dev moves. It is the right tag for trying unreleased changes and the wrong one for anything you depend on, since the digest behind it changes without warning. Pull a version tag for that.

Tools

Tool What it answers
get_hub_info Can I reach the hub, and what is it running?
list_systems What machines exist and how are they doing right now?
get_system Full hardware and current state for one machine
get_system_metrics How has this machine behaved over the last hour/day/week?
list_containers What containers are running, and are any unhealthy?
get_container_metrics Which container is eating the CPU?
get_container_logs What is this container saying?
list_services Which systemd units are failed?
get_service_details Why is this unit unhappy?
list_smart_devices Is any disk dying?
list_alerts What alerts are configured, and which are firing?
get_alert_history Is this problem recurring?

Every tool is read-only. This server never writes to your hub.

Configuration

Variable Default Notes
BESZEL_URL required
BESZEL_EMAIL / BESZEL_PASSWORD required unless BESZEL_TOKEN
BESZEL_TOKEN skips login
BESZEL_SUPERUSER false authenticate against _superusers
BESZEL_TIMEOUT_MS 15000 per-request timeout
MCP_TRANSPORT http http or stdio; --stdio overrides
MCP_HOST 127.0.0.1 the image sets 0.0.0.0
MCP_PORT 3000
MCP_AUTH_TOKEN unset when set, a matching bearer token is required
MCP_ALLOWED_ORIGINS empty comma-separated hostnames (not full origins), added to the localhost defaults on a localhost bind
LOG_LEVEL info

Securing the endpoint with MCP_AUTH_TOKEN

Set MCP_AUTH_TOKEN and every request to /mcp must carry a matching Authorization: Bearer header. Leave it unset and the endpoint is open to anything that can reach the port. There is no default value and no built-in fallback — an unset token means no authentication, not a weak one.

1. Generate one. Any long random string works; these produce 32 bytes of base64:

openssl rand -base64 32

2. Give it to the server. Prefer a file over an inline -e, which lands in your shell history and in docker inspect output:

echo "MCP_AUTH_TOKEN=$(openssl rand -base64 32)" >> .env
docker run -p 3000:3000 --env-file .env ghcr.io/maxbth/beszel-mcp

3. Give it to the client. In an MCP client's server config:

{
  "mcpServers": {
    "beszel": {
      "type": "http",
      "url": "https://mcp.example.com/mcp",
      "headers": {"Authorization": "Bearer PASTE_THE_TOKEN_HERE"}
    }
  }
}

To check it by hand — the first should return 401, the second should not:

curl -s -o /dev/null -w '%{http_code}\n' -X POST https://mcp.example.com/mcp
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://mcp.example.com/mcp -H "Authorization: Bearer $MCP_AUTH_TOKEN"

Comparison is constant-time, and the token is never written to a log line or an error message. To rotate it, change the value and restart: there is no session state, so nothing survives the restart and every client simply presents the new token on its next request.

MCP_AUTH_TOKEN protects /mcp only — see below for why /health is deliberately open.

Health checks

GET /health returns 200 {"status":"ok"} and is reachable without a bearer token, from outside the container, through a reverse proxy. It is exempt from the bearer check and from the Host/Origin validation that guards /mcp, so an uptime monitor or tunnel — Pangolin, Traefik, a Kubernetes liveness probe, the image's own HEALTHCHECK — can reach it without being configured as a browser origin or preserving a particular Host header.

That exemption is safe because the route has nothing to protect: it returns no monitoring data, no configuration and no credential, and it changes nothing. Host/Origin validation exists to stop a malicious web page using a victim's browser to read data from a server on their network — and there is no data here to read. Anyone who reaches /health learns only that something is listening, which the TCP handshake already told them.

/mcp keeps every protection. A valid bearer token still does not buy past Origin validation, and vice versa.

For Pangolin specifically, point the health check at /health on the same port you expose:

Health check path: /health
Expected status:   200

A note on exposure

The image binds 0.0.0.0 so the container is reachable from outside itself. A wildcard bind gets no automatic Host-header validation, so set MCP_AUTH_TOKEN — otherwise anything that can reach the port can read every monitored machine, container and disk, and fetch container logs. The server warns on stderr when you do not. Origin validation is always on: on a wildcard bind with MCP_ALLOWED_ORIGINS empty, browser requests are rejected outright and only non-browser clients (which send no Origin) get through.

On a localhost bind (127.0.0.1, localhost, ::1 — the default outside the image) the localhost hostnames localhost, 127.0.0.1 and [::1] are allowed on top of whatever MCP_ALLOWED_ORIGINS lists, so browser-hosted clients such as the MCP Inspector work with no configuration. Non-localhost origins are still rejected.

A note on visibility

Beszel scopes systems per user. If tools report no systems, the configured account is probably not a member of them — either add it to each system, or run the hub with SHARE_ALL_SYSTEMS=true.

get_container_logs additionally requires the hub to have container details enabled (it is, unless the hub sets CONTAINER_DETAILS=false).

Hub compatibility

Built and tested against Beszel 0.19. An older hub still works: 0.19 moved hostname, kernel, CPU model, core/thread counts, OS and podman out of systems.info into a system_details collection, so list_systems joins it and falls back to the legacy blob when the collection answers 404. Decoding likewise accepts both the current byte fields and the deprecated MB/s pairs. Nothing needs configuring either way.

Development

Requires Bun 1.3+.

bun install
bun test
bun run typecheck   # Bun does not typecheck; this is what does
bun run lint:check
bun run dev

Issues and pull requests are welcome. Run those three commands before opening a PR — CI runs exactly the same gate, plus a multi-arch Docker build, and fails on any lint warning. Tests are colocated with what they test (src/config.ts / src/config.test.ts) and every tool is read-only by contract, so a change that writes to the hub will not be accepted.

License

MIT © Maxime Bertheau

About

Read-only MCP server for a Beszel monitoring hub — twelve tools over Streamable HTTP and stdio

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages