mcp-vps-toolkit
Provides safe fast-forward Git deployment for allowlisted checkouts, including deploy status and restarting configured services when HEAD changes.
Provides tools for inspecting and operating a Linux VPS, including system overview, disk usage, process lists, systemd service status and logs, allowlisted log tailing, and audit records.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
mcp-vps-toolkit
A policy-gated MCP server that lets AI assistants inspect and operate your Linux VPS safely: read-only by default, no shell tool, every action allowlisted in YAML and written to a tamper-evident audit log.

The screenshot shows real output from the CLI against the policy on the left. No AI model was involved: mcp-vps-toolkit call drives the same MCP tools an assistant would get.
Why
Asking Claude, Cursor or another AI agent "why is my API returning 502?" works much better when it can look at the server itself. The usual way to give it that access is a generic shell or SSH MCP server, and that hands a language model a root prompt. Prompt injection hidden in a log line, a README or a web page can then turn into rm -rf or curl | sh.
mcp-vps-toolkit takes the opposite approach. It exposes a small set of typed DevOps tools, such as service status, journald logs, log tail, disk, CPU, memory, processes, restart and git deploy. It does not offer a shell tool at all. Every call goes through a YAML policy that you write, and anything the policy does not list is denied.
How it differs from what already exists:
generic SSH / shell MCP servers | read-only Linux MCP servers | mcp-vps-toolkit | |
Arbitrary commands | yes | some offer an opt-in mode | never |
Restart services / deploy | via shell | no | yes, allowlisted, dry-run first |
Single policy file (deny by default, unknown keys rejected) | no | env vars | yes |
Mutating tools invisible in read-only mode | n/a | n/a | yes |
Audit log of every call (allowed, denied, dry-run) | rarely | no | yes, SHA-256 hash-chained |
Secret redaction in logs and process lists | no | no | yes |
Rate limit on mutations | no | n/a | yes |
Related MCP server: vps-ops-mcp
Features
Read-only by default. In
mode: read-only,restart_serviceanddeployare not even registered with the MCP server, so the model never sees them.No shell, ever. Commands are fixed argv lists run with
shell=False. Only four programs are allowed (systemctl,journalctl,git, andsudo -n systemctl), resolved against a fixedPATHwith a minimal environment.YAML allowlists for systemd units (read vs restart), log file globs, disk paths and git deploy targets. Patterns that match every unit, such as
*, are rejected.Strict input validation happens before the policy check: unit names,
sinceexpressions, substring filters (never regex) and absolute paths. Symlinks must resolve inside the allowlist too.Dry-run first. Mutating tools default to
dry_run=trueand return the exact argv they would run.dry_run: truein the policy forces plan-only mode whatever the model asks.Safe git deploy. Deploys only fast-forward: no reset, no force and no merge of diverged history. They refuse a dirty tree or the wrong branch, disable git hooks, block the
ext::transport, and restart only allowlisted units, and only when HEAD changed.Tamper-evident audit log. Each call writes one JSONL line with its args (redacted), decision, argv, result and duration. Every line carries the SHA-256 of the previous line, so
mcp-vps-toolkit audit verifydetects edited or deleted lines.Secret redaction. API keys (OpenAI, Anthropic, AWS, GitHub, Slack, Telegram), JWTs, bearer tokens,
password=style pairs, URL credentials and PEM private keys are masked in all output.Hard limits. Output is capped by line, byte and character count. Commands have timeouts. A per-hour budget applies to executed mutations and survives restarts because it is seeded from the audit log.
Two transports. stdio (best over SSH) and Streamable HTTP. The HTTP transport binds to loopback, requires a bearer token and refuses public binds by default.
Real host metrics. CPU, memory, load and per-process CPU and RSS are read straight from
/proc, with nopsutildependency.Small footprint. Pure Python 3.10+ with three dependencies:
mcp,pydanticandPyYAML.
Quick start
On the server (Ubuntu or Debian, any systemd distro):
# 1. Install (pipx keeps it isolated; plain pip works too)
pipx install git+https://github.com/mohammadhadijavidi/mcp-vps-toolkit
# or: pip install git+https://github.com/mohammadhadijavidi/mcp-vps-toolkit
# 2. Write a policy (starts read-only) and edit the allowlists
sudo mkdir -p /etc/mcp-vps-toolkit
mcp-vps-toolkit init | sudo tee /etc/mcp-vps-toolkit/policy.yaml >/dev/null
sudo chmod 644 /etc/mcp-vps-toolkit/policy.yaml
# 3. Check what the AI will be allowed to do
mcp-vps-toolkit check
mcp-vps-toolkit tools
# 4. Try a tool locally (goes through the real MCP server, and is audited)
mcp-vps-toolkit call system_overview
mcp-vps-toolkit call service_logs service=nginx lines=50 since=1h grep=errorOn your laptop, connect Claude Desktop (or any MCP client) over SSH. The SSH session is the transport, so no port is opened. This is examples/claude_desktop_config.json:
{
"mcpServers": {
"my-vps": {
"command": "ssh",
"args": ["-T", "-o", "BatchMode=yes", "mcp@my-vps.example.com",
"/home/mcp/.local/bin/mcp-vps-toolkit", "serve",
"--policy", "/etc/mcp-vps-toolkit/policy.yaml"]
}
}
}With Claude Code:
claude mcp add my-vps -- ssh -T mcp@my-vps.example.com /home/mcp/.local/bin/mcp-vps-toolkit serveTo try it without a server, run the demo. It starts the real server over stdio in a temporary sandbox and makes a fixed sequence of calls; no AI model is involved:
git clone https://github.com/mohammadhadijavidi/mcp-vps-toolkit && cd mcp-vps-toolkit
pip install -e . && python examples/demo_session.pyConfiguration
Everything lives in one policy file. Unknown keys are an error, so a typo cannot silently widen access.
version: 1
mode: read-only # or read-write
dry_run: false # true = mutations always return a plan, never execute
use_sudo: false # prefix mutating systemctl with `sudo -n`
services:
read: [nginx, postgresql, "myapp-*.service"] # status + journald logs
restart: [myapp-api.service] # read-write mode only; implies read
logs:
files: [/var/log/nginx/*.log, /srv/myapp/logs/*.log]
max_lines: 300
max_bytes: 262144
redact: true
disk_paths: [/, /srv]
deploy:
targets:
api:
path: /srv/myapp
remote: origin
branch: main
restart: [myapp-api.service] # must also be in services.restart
limits:
mutating_per_hour: 10
command_timeout_s: 30
max_output_chars: 20000Key | Default | Meaning |
|
|
|
|
|
|
|
| run mutating |
|
| append-only JSONL, mode |
| empty | unit globs; bare names get |
| empty | absolute file globs for |
|
| mount points reported by |
| empty | named git checkouts the AI may fast-forward |
| see above | output caps, timeouts, mutation budget |
Environment variables (see .env.example):
Variable | Purpose |
| policy path (default |
| override the audit log path |
| bearer token, required for |
There is no LLM provider to configure. This is an MCP server, so the model runs in your client (Claude Desktop, Claude Code, Cursor, VS Code, or an agent built on the OpenAI Agents SDK or LangChain MCP adapters) and the server never calls a model API.
Usage examples
Tools
Tool | Mode | What it does |
| read | the effective allowlists and limits, so the model knows its boundaries |
| read | hostname, uptime, CPU %, load, memory, swap, disks |
| read | usage for |
| read | top N by CPU or memory, with command lines redacted |
| read | allowlisted units and their state |
| read |
|
| read | journald lines for a unit; |
| read | last lines of an allowlisted log file |
| read | HEAD, branch and clean or dirty state of a deploy target |
| read | recent audit records |
| write | restart an allowlisted unit (dry-run by default) |
| write |
|
Things to ask your assistant
"Is anything on the server unhealthy? Check disk, memory and the services you can see."
"nginx is returning 502s. Look at the last hour of nginx and myapp-api logs and tell me why."
"Deploy the api target. Show me the plan first." The model calls
deploy(dry_run=true), shows you the fetch, merge and restart argv, and runs it only after you say yes.
CLI
mcp-vps-toolkit serve # stdio (for SSH / local clients)
mcp-vps-toolkit serve --transport http # 127.0.0.1:8765/mcp, needs MCP_VPS_TOKEN
mcp-vps-toolkit check # validate policy, print effective permissions
mcp-vps-toolkit tools # list exposed tools (read vs WRITE)
mcp-vps-toolkit call restart_service service=myapp-api dry_run=false
mcp-vps-toolkit audit tail -n 50 --commands # who asked for what, and the exact argv that ran
mcp-vps-toolkit audit verify # check the hash chainA denied call shows the model exactly which policy key blocked it:
$ mcp-vps-toolkit call tail_log path=/etc/shadow
Error executing tool tail_log: DENIED by policy: log file '/etc/shadow' is not in the policy allowlist (logs.files)How it works

Tool registry. Only tools the mode allows are registered. In read-only mode there is no write tool to call or to trick the model into calling.
Validation. Strict patterns run before anything else.
nginx;reboot,--host=eviland$(id)are rejected as invalid input before the policy check.Policy. Deny by default, with glob allowlists per capability.
tail_logchecks both the requested path and itsrealpath.Dry-run and budget. Mutations return a plan unless
dry_run=falseis passed and the policy allows execution. Executed mutations are rate-limited per hour.Runner. The only code that starts processes. It accepts an argv allowlist only,
shell=False, fixedPATH, minimal env, closed stdin and a timeout.sudois accepted only assudo -n systemctl ....Redact and cap. Secrets are masked and output is bounded before it reaches the model, which also reaches the provider's logs.
Every path, including errors, writes exactly one audit record.
Security model and threat model
Security is the point of this project, so here is exactly what it does and does not protect against.
Assets
The host itself, its secrets (credentials in logs, environment and config), service availability, and the integrity of deployed code.
Adversaries considered
Prompt injection. Text the model reads (a log line, a commit message, a web page it browsed earlier) tells it to do something harmful.
An over-eager or confused model that restarts the wrong thing, deploys at the wrong time, or loops.
A network attacker reaching the HTTP transport.
Secret exfiltration through tool output that flows to the model provider and the chat transcript.
Mitigations
Threat | Mitigation |
Arbitrary command execution | No shell or exec tool exists. Fixed argv, |
Argument and flag injection | Unit names must start with an alphanumeric character. |
Reading sensitive files |
|
Unwanted changes | Read-only by default, with write tools not registered. Separate |
Destructive deploys | Fast-forward only, refuses a dirty tree or wrong branch, |
Secret leakage to the model | Redaction of common token formats in logs, journald output, process command lines and audit args. Environment variables are never exposed. |
Network access to the server | stdio over SSH is recommended. HTTP requires a 24+ character bearer token (constant-time compare), binds to |
Repudiation ("what did the AI do?") | Every call (allowed, denied, invalid, dry-run, executed, error) is audited with exact argv. The SHA-256 chain detects edited or deleted lines. |
Loosened policy | Unknown keys and match-everything patterns are rejected. A warning is printed if the policy file is group- or world-writable. |
Out of scope and known limits
A compromised host or server user. If an attacker already runs code as the
mcpuser (or root), they can bypass this tool, edit the policy if it is writable, or rewrite the audit log. The hash chain detects edits in the middle of the log, not truncation of the tail or a full rewrite. Ship the audit log to another machine (journald forwarding, Vector or Fluent Bit) if you need stronger guarantees.Redaction is best effort. It uses pattern matching. A secret in an unusual format can get through. Keep high-secret files out of
logs.filesentirely.Allowed actions can still cause harm. Restarting an allowlisted service at a bad moment is still a restart. The policy limits what can happen; the dry-run plus your confirmation decides when. Keep
services.restartshort.The model sees what you allow. Log contents go to your model provider. Choose a provider and retention settings accordingly.
Deploy trusts the remote. The deploy fast-forwards to whatever
origin/maincontains. Protect that branch, require reviews, and consider signed commits.No sandbox. The server is a normal Python process. Run it as a dedicated unprivileged user, optionally with the systemd hardening in
examples/mcp-vps-toolkit-http.service.Not run as root.
checkwarns if it is. Use the sudoers rule below instead.
Least-privilege setup
sudo useradd --system --create-home --shell /bin/bash mcp
sudo usermod -aG systemd-journal,adm mcp # read journald + /var/log without root
# Only if mode: read-write and use_sudo: true. Restart exactly these units, nothing else:
sudo install -m 0440 examples/sudoers.mcp-vps-toolkit /etc/sudoers.d/mcp-vps-toolkit
sudo visudo -cThe sudoers rule matches the exact argv the tool runs (systemctl --no-pager --no-ask-password restart -- <unit>), so even a bug in this project could not use sudo for anything else. Keep /etc/mcp-vps-toolkit/policy.yaml owned by root and not writable by mcp.
Roadmap
Docker tools (
docker ps, logs, restart of allowlisted containers) with the same policy modelHuman approval over Telegram or a webhook for
dry_run=falsecalls (out-of-band confirmation)Ship audit records to syslog or an HTTP sink, and anchor the chain externally
Nginx config test and reload (
nginx -tgate) as a typed toolCertificate expiry and open-port checks
Multi-host mode: one client, several policies
Contributing
Issues and PRs are welcome. Read CONTRIBUTING.md first. The short version: new tools must take typed parameters (never a command), be deny-by-default in the policy, be dry-run-first if they mutate, and come with a test for the denial path.
pip install -e ".[dev]" && pytest && ruff check src tests examplesLicense
MIT © 2026 Mohammad Hadi Javidi
Built by Mohammad Hadi Javidi — AI products & automation, Dubai.
This server cannot be deployed
Maintenance
Related MCP Connectors
Operate Linux, macOS and Windows from your LLM. Every action runs through an auditable allowlist.
Let AI operate servers without SSH. Choose actions, approve risky changes, and audit every step.
Real Linux labs your AI agent deploys, routes and runs, with domains, TLS, DBs and an audit log.
Context and control for any AI. Agent identity, scoped access, and proof across tools.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables AI assistants to perform controlled Linux system administration tasks like reading logs, managing services, cron jobs, WordPress, and executing sandboxed Python code, with strict security constraints.292GPL 2.0
- AlicenseAqualityAmaintenanceA safe, structured MCP server that lets AI inspect and operate a VPS through typed, allowlisted tools for nginx, PM2, SSL, UFW, fail2ban, with read-only defaults and opt-in mutations.614 npm2MIT
- AlicenseNot gradedqualityAmaintenanceEnables AI tools to securely inspect remote servers by listing directories, reading configuration files, and performing authorized actions through permission controls, human-in-the-loop approvals, and audit logging. It acts as a bastion that never reveals raw SSH credentials or root passwords to the model.1MIT
- AlicenseNot gradedqualityFmaintenanceEnables AI clients to operate remote VPS hosts through a jump-host gateway with a fixed whitelist of tools, allowing them to read logs, inspect and edit configuration files, manage services, and deploy or roll back versions. Production writes require a per-client time-limited lease, and all actions are audited, verified, and automatically reverted on failure.MIT