Command Palette

Search for a command to run...

UnylyUnyly
Весь каталог

Vpsobs

БесплатноНе проверен

Enables read-only observability of a Linux host via MCP, exposing allowlisted systemd, docker, nginx, logs, disk, and cert info without shell access.

GitHubEmbed

Описание

Enables read-only observability of a Linux host via MCP, exposing allowlisted systemd, docker, nginx, logs, disk, and cert info without shell access.

README

An AI agent that is troubleshooting a production incident wants to look at logs, check whether a service is up, and see how full a disk is. The usual way to give it that is to hand it a shell: SSH credentials, or a "run this command" tool with a string it can pass to subprocess. That solves the immediate problem and creates a much bigger one -- the agent (and anything that can make it say the wrong thing, from a poisoned log line to a compromised MCP client) now has full read/write access to a host that keeps other people's systems running.

mcp-vpsobs is a Model Context Protocol server that gives an agent eyes on a single Linux host without giving it hands. It does not accept a command string. It does not have a write, restart, kill or exec path anywhere in the code. Every unit, container, log and directory it can see is named ahead of time in a YAML allowlist; anything not named is denied before a subprocess is even started. Every byte it returns has already passed through a redaction pass that strips bearer tokens, password= assignments, provider-shaped API keys and e-mail addresses.

Why not just give the agent SSH

The honest argument for a narrower surface:

  • The blast radius of a mistake is bounded. If the agent (or the model behind it, or a prompt-injected log line it read) decides to do something destructive, there is no rm, no systemctl restart, no docker exec for it to reach for. The collectors in this repo only ever call backend.run with a fixed, read-only argv or backend.read_file; there is no code path that writes to the host.
  • The allowlist is explicit and inspectable. A YAML file is the entire answer to "what can this agent see on my host", instead of "whatever the SSH key's permissions and the model's judgement allow at any given moment."
  • Denial happens before any host access. Config.check_unit, check_container, site_log_path and resolve_directory raise DeniedError before a collector builds an argv or opens a file -- tests/test_attack.py proves this with a backend fixture that fails the test if it is ever called.
  • Secrets get a chance to not leak. Every line handed back to the agent passes through redact().

What this design does not protect against, stated plainly rather than hand-waved:

  • Resource exhaustion by a well-behaved-looking client. Each call has a timeout and a byte cap, but nothing here rate-limits repeated calls. An agent that calls journal_tail in a tight loop can still generate load proportional to how fast it can issue MCP requests. There is no token-bucket or per-minute quota in this codebase.
  • Novel secret shapes. The redaction patterns are shaped (bearer tokens, key=value assignments, known provider key formats, e-mails, optionally private IPv4) rather than a generic entropy detector, on purpose -- a blanket "redact anything that looks random" rule would also eat git SHAs, UUIDs and request IDs and make the logs useless. A secret in a format none of the patterns anticipate will pass through unredacted.
  • An operator who allowlists too much. If you put / in directories or a unit that happens to log credentials in units, this server will faithfully show an agent everything inside it. The allowlist moves the trust decision to the config file; it does not make the decision for you.
  • Vulnerabilities in the tools it shells out to. This server is only as read-only as systemctl, journalctl, docker, nginx and openssl are. A bug in one of those binaries, or in the mcp SDK itself, is outside what an allowlist can fix.
  • Audit logging. There is no record kept of which tool was called with which arguments. Everything here is visibility of the host, not visibility into how the agent used this server. If you need "who asked what and when," it is not implemented -- add logging in front of the transport before relying on this in a setting where that matters.

Architecture

config.yaml --> Config (allowlist + limits)
                    |
                    v
              collectors/*  --uses-->  Backend (Protocol)
                    |                       |
                    v                       +-- SubprocessBackend (real: argv-only, shell=False)
              redact() / redact_lines()     +-- FakeBackend (tests: canned argv -> CommandResult)
                    |
                    v
                server.py (FastMCP resources + tools, stdio)
  • Allowlist first. Config (in src/vpsobs/config.py, not modified by this change) is the only place that knows what is visible. A collector cannot "forget" a check because there is nothing for it to check except by asking Config.
  • Backend injection. Every collector takes a Backend -- run(argv, timeout, max_bytes) or read_file(path, max_bytes, tail) -- so tests run against a FakeBackend fed canned command output. No test in this repo touches a real systemctl, docker, journalctl, nginx or openssl.
  • Redaction is a pipeline, not a suggestion. Anything that reaches an agent goes through redact()/redact_lines() on the way out.

Resources

URI Contents
host://summary uptime, load average, memory, disk usage of /
host://services allowlisted systemd units with active/sub/load state
host://containers allowlisted docker containers (name, image, status, ports)
host://sites nginx server_name -> root/proxy_pass

Tools

Tool Signature
journal_tail (unit, lines=100, since=None, grep=None)
docker_logs (container, lines=100, since=None)
nginx_access_stats (site, window="1h") -- request count, status histogram, top paths, top user agents, error sample
cert_expiry () -- days remaining per configured certificate
disk_hogs (path, depth=1, top=10)
port_map () -- listening sockets with owning process, where known

Every tool result is a JSON string. Failures (denied, not configured, timeout, backend error) come back as {"error": ..., "message": ..., "details": {...}} in the same shape, rather than as a different kind of MCP protocol error -- see "Design notes" below.

Configuration

Copy config.example.yaml to config.yaml and edit it:

  • units -- systemd unit names visible to host://services and journal_tail.
  • containers -- docker container names visible to host://containers and docker_logs.
  • sites -- site label -> access log path, used by nginx_access_stats.
  • directories -- absolute directory roots disk_hogs may inspect.
  • certs -- certificate file paths cert_expiry reports on.
  • redaction.redact_private_ipv4 -- also scrub RFC1918/loopback addresses (off by default).
  • limits.timeout_seconds / limits.max_output_bytes / limits.max_lines -- the hard ceiling every collector runs under.
  • nginx_config_command -- override if nginx -T is not the right command for your setup (default ["nginx", "-T"]).

Design notes

Places where the spec left a decision open and the simpler option was taken:

  • MCP SDK surface: built on mcp.server.fastmcp.FastMCP (decorator-based resources/tools, stdio transport) rather than the low-level mcp.server.Server API, for less boilerplate per resource/tool.
  • Error reporting: VpsobsError subclasses are caught in server.py and serialized as {"error": ...} JSON text in the normal tool/resource result, rather than raised as MCP protocol-level errors. One response shape for both success and failure is simpler for a caller to parse.
  • host://containers filtering: shows only containers already on the containers allowlist, rather than showing every running container annotated with an allowlisted/not flag. Fewer things for an agent to see that it can't act on, and no leaking of the existence of containers the operator never chose to expose.
  • port_map and the unit allowlist: port_map does not cross-reference listening sockets against config.units. It reports whatever ss reports; nothing is denied either way, so filtering would only be cosmetic. Note ss -tulpn needs to run as root (or with equivalent capabilities) to populate the process/pid column at all -- without that, process/pid come back null.
  • host://sites directive scoping: the nginx config parser tracks server { ... } blocks (including nested location blocks) for server_name, root and proxy_pass, but does not model nginx's directive-inheritance rules (e.g. a root set on the surrounding http block applying to a server that declares none itself).
  • Config file location: --config defaults to ./config.yaml in the current working directory; there is no /etc/vpsobs/config.yaml fallback. Simpler to reason about, and the caller (Claude Desktop config, claude mcp add, systemd unit, etc.) already has to specify a working directory or an absolute --config path either way.
  • No audit logging: confirmed absent by reading server.py and every collector -- no tool call is logged anywhere beyond whatever the mcp library itself does. Stated explicitly in "Why not just give the agent SSH" above rather than left implicit.
  • journal_tail/docker_logs grep: implemented as a plain Python substring filter applied to lines already returned by journalctl/docker logs, never passed to a subprocess or treated as a regex -- there is no way for a grep value to become a shell metacharacter problem because it never reaches an argv.

Running it

pip install -e .
cp config.example.yaml config.yaml   # then edit the allowlist for your host
mcp-vpsobs --config config.yaml

The server speaks MCP over stdio. To point Claude Desktop or claude mcp add at it:

claude mcp add vpsobs -- mcp-vpsobs --config /path/to/config.yaml

or, in Claude Desktop's claude_desktop_config.json:

{
  "mcpServers": {
    "vpsobs": {
      "command": "mcp-vpsobs",
      "args": ["--config", "/path/to/config.yaml"]
    }
  }
}

Testing

pip install -e ".[dev]"
pytest -q

Every test runs against a fixture backend (tests/conftest.py's FakeBackend) or a pure string-parsing helper -- none of them shell out to a real systemctl, journalctl, docker, nginx or openssl, so the suite passes in CI (see .github/workflows/ci.yml) with none of those installed.

As of this commit the suite defines 72 test functions across 13 files (grep -roh "def test_" tests/ | wc -l); parametrized cases push the number pytest actually collects higher than that -- run pytest -q for the exact, current count.

from github.com/0mandrock1/mcp-vpsobs

Установить Vpsobs в Claude Desktop, Claude Code, Cursor

Рекомендуется · одна команда, все IDE
unyly install mcp-vpsobs

Ставит в Claude Desktop, Claude Code, Cursor и VS Code — сам разбирается с npx, uvx и сборкой из исходников.

Впервые? Поставь CLI: curl -fsSL https://unyly.org/install | sh

Или настроить вручную

Выполни в терминале:

claude mcp add mcp-vpsobs -- uvx --from git+https://github.com/0mandrock1/mcp-vpsobs mcp-vpsobs

Пошаговые гайды: как установить Vpsobs

FAQ

Vpsobs MCP бесплатный?

Да, Vpsobs MCP бесплатный — установка в пару кликов через Unyly без оплаты.

Нужен ли API-ключ для Vpsobs?

Нет, Vpsobs работает без API-ключей и переменных окружения.

Vpsobs — hosted или self-hosted?

Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.

Как установить Vpsobs в Claude Desktop, Claude Code или Cursor?

Открой Vpsobs на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.

Похожие MCP

Compare Vpsobs with

Не уверен что выбрать?

Найди свой стек за 60 секунд

Автор?

Embed-бейдж для README

Похожее

Все в категории development