- Posted on
- • Artificial Intelligence
MCP for DevOps
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
MCP for DevOps: Make Your Ops Knowledge Programmatically Accessible (from Bash)
If your on-call runbooks, shell scripts, and tribal knowledge live in a dozen repos and six different chat threads, you’re not alone. The problem isn’t a lack of tools—it’s that your valuable operational context is trapped inside them. MCP (Model/Metadata/“Make Context” Protocol—pick your favorite expansion) is a simple idea with outsized impact for DevOps: expose your tools and operational context through a minimal, consistent interface so other systems (including automation and assistants) can discover, invoke, and audit them safely.
This post shows why that matters and how to stand up an MCP-style wrapper around your existing Bash-friendly workflows using only standard Linux tools. You’ll get actionable steps, real-world examples, and copy-paste snippets you can adapt immediately.
Why MCP is worth your time
Unify your tooling: Treat shell scripts, Kubernetes queries, log lookups, and CI diagnostics as first-class, discoverable “tools” with clear inputs and outputs.
Reduce glue code: A small, consistent protocol replaces a pile of bespoke adapters and cron jobs.
Safer by default: You expose least-privilege, allowlisted commands—no arbitrary shell.
Auditable and automatable: JSON in, JSON out means logs, metrics, and policy enforcement become straightforward.
In short: MCP gives your DevOps practice a programmable interface without ripping and replacing your stack.
What we’ll build
We’ll implement a minimal “MCP-style” JSON-RPC-over-stdio wrapper in Python that:
Lists available tools and their descriptions
Executes allowlisted commands with safe parameters
Returns structured output, exit codes, and errors
It’s intentionally small and standard-library only, so you can read it end to end, tweak it, and run it anywhere Bash runs. It’s not a full-blown protocol implementation—it’s a practical, MCP-inspired pattern you can evolve.
Step 1: Install prerequisites
Pick your distro and install common CLI building blocks. These are widely available and safe to put on most DevOps hosts.
- Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y git curl jq python3 python3-venv python3-pip nodejs npm podman
- Fedora/RHEL/CentOS Stream (dnf):
sudo dnf -y install git curl jq python3 python3-pip nodejs npm podman
- openSUSE Leap/Tumbleweed (zypper):
sudo zypper refresh
sudo zypper install -y git curl jq python3 python3-pip nodejs npm podman
Notes:
We use Python’s standard library only—no extra pip modules required.
Podman is included for containerized workflows; swap for Docker if that’s your standard.
jq is used to pretty-print and test JSON responses from Bash.
Step 2: Define safe, discoverable tools (allowlist)
Create a small JSON file describing the commands you want to expose. It includes:
A unique tool name
A human-friendly description
The command and default args
Optional parameter schema with a regex for simple validation
Save as tools.json:
{
"df_h": {
"desc": "Disk free (human readable)",
"cmd": "df",
"args": ["-h"]
},
"tail_syslog": {
"desc": "Tail last 100 lines of system log (systemd/journal or /var/log/syslog)",
"cmd": "bash",
"args": ["-lc", "journalctl -n 100 --no-pager 2>/dev/null || tail -n 100 /var/log/syslog"]
},
"du_path": {
"desc": "Disk usage summary for a given path (safe subset)",
"cmd": "du",
"args": ["-sh", "{path}"],
"params": {
"path": "^/([a-zA-Z0-9._/-]+)$"
}
}
}
You can add more tools over time—think incident commands, artifact lookups, or environment health checks. Keep each one deterministic, documented, and least-privilege.
Step 3: Implement a tiny MCP-style server
This script:
Reads JSON-RPC 2.0 requests from stdin, one per line
Supports two methods:
- tools/list → returns your allowlisted tools
- tools/call → runs a tool by name with validated params
Emits JSON replies to stdout
Save as mcp_server.py:
#!/usr/bin/env python3
import sys, json, re, subprocess, shlex, time
TOOLS = {}
TIMEOUT_SECS = 30
def load_tools(path="tools.json"):
global TOOLS
with open(path, "r") as f:
TOOLS = json.load(f)
def json_reply(req_id, result=None, error=None):
msg = {"jsonrpc": "2.0", "id": req_id}
if error is not None:
msg["error"] = {"code": -32000, "message": error}
else:
msg["result"] = result
sys.stdout.write(json.dumps(msg) + "\n")
sys.stdout.flush()
def list_tools():
out = []
for name, spec in TOOLS.items():
out.append({"name": name, "desc": spec.get("desc", "")})
return out
def safe_args(spec, params):
# Fill template args with validated params (if any)
args = []
for a in spec.get("args", []):
if a.startswith("{") and a.endswith("}"):
key = a[1:-1]
if "params" not in spec or key not in spec["params"]:
raise ValueError(f"param {key} not allowed")
pattern = spec["params"][key]
val = params.get(key, "")
if not re.match(pattern, val):
raise ValueError(f"param {key} failed validation")
args.append(val)
else:
args.append(a)
return args
def call_tool(name, params):
if name not in TOOLS:
raise ValueError("unknown tool")
spec = TOOLS[name]
cmd = spec["cmd"]
args = safe_args(spec, params or {})
start = time.time()
try:
proc = subprocess.run(
[cmd] + args,
capture_output=True,
text=True,
timeout=TIMEOUT_SECS
)
elapsed = time.time() - start
return {
"cmd": [cmd] + args,
"exit_code": proc.returncode,
"stdout": proc.stdout,
"stderr": proc.stderr,
"elapsed_secs": round(elapsed, 3)
}
except subprocess.TimeoutExpired:
raise ValueError(f"command timed out after {TIMEOUT_SECS}s")
def handle_request(req):
req_id = req.get("id")
method = req.get("method")
if method == "tools/list":
return json_reply(req_id, list_tools())
elif method == "tools/call":
name = req.get("name")
params = req.get("params") or {}
try:
result = call_tool(name, params)
return json_reply(req_id, result)
except Exception as e:
return json_reply(req_id, error=str(e))
else:
return json_reply(req_id, error="unknown method")
def main():
load_tools()
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
req = json.loads(line)
handle_request(req)
except Exception as e:
# best-effort error reporting
sys.stdout.write(json.dumps({"jsonrpc":"2.0","id":None,"error":{"code":-32700,"message":str(e)}})+"\n")
sys.stdout.flush()
if __name__ == "__main__":
main()
Make it executable:
chmod +x mcp_server.py
Step 4: Test from Bash
List tools:
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
| ./mcp_server.py | jq .
Call a fixed tool:
printf '%s\n' '{"jsonrpc":"2.0","id":2,"method":"tools/call","name":"df_h"}' \
| ./mcp_server.py | jq -r '.result.stdout'
Call a parameterized tool safely:
printf '%s\n' '{"jsonrpc":"2.0","id":3,"method":"tools/call","name":"du_path","params":{"path":"/var/log"}}' \
| ./mcp_server.py | jq -r '.result.stdout'
Try an invalid path (should error due to regex guard):
printf '%s\n' '{"jsonrpc":"2.0","id":4,"method":"tools/call","name":"du_path","params":{"path":"../../etc"}}' \
| ./mcp_server.py | jq .
Tip: You can daemonize with systemd, supervise with tmux/screen, or containerize with Podman. The interface remains simple: one JSON request per line, one JSON reply per line.
Real-world DevOps examples
Fast incident triage
- Tools: tail_syslog, top five memory consumers, open ports, recent kernel messages.
- Outcome: An on-call co-pilot (human or automated) can request “top_mem” and “tail_syslog” with zero shell access.
Environment-aware K8s snapshots
- Tools: allowlisted kubectl invocations (e.g., get pods -A with a cluster selector you validate).
- Outcome: Assistants can collect consistent cluster state without carte blanche.
CI/CD artifact and rollout checks
- Tools: list latest image digests, check Helm releases, retrieve deployment annotations.
- Outcome: Post-deploy verification becomes a single “tools/call” away.
Compliance and guardrails
- Tools: scan for world-writable files in specified paths, check SSH config, verify SELinux/AppArmor status.
- Outcome: Scheduled or on-demand, with outputs captured as JSON for auditing.
Hardening and production tips
Least privilege
- Run the server as a dedicated, non-root user. Gate each tool with narrow regexes and fixed arguments.
Resource controls
- Apply cgroup limits via systemd or container runtime. Keep TIMEOUT_SECS conservative.
Auditability
- Log requests and responses to a file or central collector. Grep-able JSON makes incident reviews faster.
Separation of concerns
- Group tools by sensitivity and run multiple servers with different Unix users, sockets, or namespaces.
Idempotence and determinism
- Prefer read-only tools and pure queries. For write operations, require explicit parameters and multi-step confirmation in the calling system.
Example systemd unit (stdio over a Unix socket via socat is a common pattern; adapt as needed):
[Unit]
Description=MCP-style DevOps tool server
After=network.target
[Service]
User=devops_tools
Group=devops_tools
WorkingDirectory=/opt/mcp-tools
ExecStart=/opt/mcp-tools/mcp_server.py
Restart=on-failure
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Create the user and directory:
sudo useradd --system --home /opt/mcp-tools --shell /usr/sbin/nologin devops_tools
sudo mkdir -p /opt/mcp-tools
sudo chown -R devops_tools:devops_tools /opt/mcp-tools
sudo cp mcp_server.py tools.json /opt/mcp-tools/
sudo chown devops_tools:devops_tools /opt/mcp-tools/*
# Install the unit
sudo tee /etc/systemd/system/mcp-tools.service >/dev/null <<'EOF'
[Unit]
Description=MCP-style DevOps tool server
After=network.target
[Service]
User=devops_tools
Group=devops_tools
WorkingDirectory=/opt/mcp-tools
ExecStart=/opt/mcp-tools/mcp_server.py
Restart=on-failure
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-tools
sudo systemctl status mcp-tools --no-pager
To containerize with Podman:
cat > Containerfile <<'EOF'
FROM registry.access.redhat.com/ubi9/ubi-minimal
RUN microdnf install -y python3 jq bash coreutils util-linux && microdnf clean all
WORKDIR /app
COPY mcp_server.py tools.json /app/
USER 65532:65532
ENTRYPOINT ["/app/mcp_server.py"]
EOF
podman build -t mcp-tools:local .
podman run --rm -i mcp-tools:local
Extending the pattern
Add “discovery” metadata: version, environment tags, and supported methods.
Emit metrics: count calls per tool, errors, latencies. Ship to Prometheus/StatsD.
Multi-protocol: keep stdio for local, add a Unix domain socket or gRPC/HTTP for remote (with strong auth).
Integrate with your assistant/orchestrator that can speak this simple JSON shape; keep your allowlist the source of truth.
Conclusion and next steps
You don’t need a mega-platform to make your operational context programmable. Start small:
Inventory 5–10 read-only, high-value commands you already trust.
Wrap them in an allowlist like tools.json.
Drop in the tiny server above and test it from Bash with jq.
Iterate: add parameters, validation, and audit logs.
When you’re ready, wire this MCP-style interface into your chatops bot, incident assistant, or pipeline orchestrator. You’ll spend less time spelunking through wikis and more time shipping reliable systems.
If you found this useful, try packaging three of your runbook actions today and share your tools.json with your team. Small steps compound—your future on-calls will thank you.