mcp-vpsobs

mcp-vpsobs

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

Category
访问服务器

README

mcp-vpsobs

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.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选