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.