Posted on
Artificial Intelligence

MCP Case Studies

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

MCP Case Studies: Bash-first automation that lets AI call your Linux tools

If you’ve ever copy-pasted a 300-line log into an AI assistant and thought “there has to be a better way,” you’re not alone. The Model Context Protocol (MCP) is emerging as a practical way to let AI systems call your tools directly—safely, with guardrails, and using the CLI you already know.

This post is a Bash-first guide to MCP case studies: realistic Linux scenarios where small, composable scripts do the heavy lifting and an MCP server simply exposes them as callable tools. You’ll get install steps, drop-in scripts, and battle-tested tips, so you can go from idea to proof-of-concept in an afternoon.

What problem does MCP solve?

  • Repeatability: Instead of pasting volatile terminal output, the model invokes the same script every time with explicit parameters.

  • Security and control: Tools run in your environment. You choose what’s callable, validate inputs, and log every call.

  • Composability: Your existing Bash, Python, or Go utilities become a single toolbelt the model can orchestrate.

  • Interop: MCP relies on a simple, transport-agnostic pattern (commonly JSON over stdio or a socket) so you can use any language and grow incrementally.

In other words: keep your Linux muscle memory, add a protocol to let AI ask for exactly what it needs, and no more.

Prerequisites: install the basics

These case studies use common command-line tools. Install the following using your distro’s package manager. You can always trim or add as your use case evolves.

Ubuntu/Debian (apt):

sudo apt update
sudo apt install -y \
  python3 python3-pip python3-venv \
  nodejs npm \
  jq ripgrep fd-find bat \
  curl traceroute dnsutils iproute2 git

# Optional: convenience alias for fd on Debian/Ubuntu (binary is "fdfind")
if ! command -v fd >/dev/null 2>&1 && command -v fdfind >/dev/null 2>&1; then
  echo 'alias fd=fdfind' | sudo tee -a /etc/bash.bashrc
  source /etc/bash.bashrc
fi

Fedora/RHEL/CentOS (dnf):

sudo dnf install -y \
  python3 python3-pip \
  nodejs npm \
  jq ripgrep fd-find bat \
  curl traceroute bind-utils iproute git
# Note: on RHEL, you may need to enable EPEL for ripgrep/fd/bat.

openSUSE/SLE (zypper):

sudo zypper refresh
sudo zypper install -y \
  python3 python3-pip \
  nodejs npm \
  jq ripgrep fd bat \
  curl traceroute bind-utils iproute2 git

Why Python and Node? Many MCP servers are written in Python or TypeScript. Even if you stay 100% Bash, these runtimes make it easy to add a thin adapter later.


Case study 1: On-call log triage (grep the right lines, fast)

When incidents strike, context matters. Instead of dumping logs, expose a controlled “grep logs” tool the model can call with a pattern, path, and time window.

Drop-in script: loggrep.sh

#!/usr/bin/env bash
set -Eeuo pipefail

# Dependencies: ripgrep (rg), jq, optionally systemd's journalctl
# Purpose: search files or journal for a regex and return structured results.

usage() {
  cat <<'EOF'
Usage:
  loggrep.sh file <pattern> <path> [--icase] [--max N]
  loggrep.sh journal <pattern> [--unit UNIT] [--since "TIME"] [--until "TIME"] [--icase] [--max N]

Examples:
  loggrep.sh file "ERROR|WARN" /var/log/nginx/access.log --max 100
  loggrep.sh journal "oom-kill" --unit kubelet --since "1 hour ago" --icase
EOF
}

[[ $# -lt 2 ]] && { usage; exit 1; }

mode="$1"; shift
pattern="$1"; shift

icase_flag=""
max_count=""
unit=""
since=""
until=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --icase) icase_flag="-i";;
    --max) shift; max_count="$1";;
    --unit) shift; unit="$1";;
    --since) shift; since="$1";;
    --until) shift; until="$1";;
    *) arg_rest+=("$1");;
  esac
  shift || true
done

jq_safe() { jq -R -s .; }

if [[ "$mode" == "file" ]]; then
  path="${arg_rest[0]:-}"
  [[ -z "${path}" || -z "${pattern}" ]] && { usage; exit 2; }
  [[ -r "$path" ]] || { echo "Path not readable: $path" >&2; exit 3; }

  # ripgrep JSON output -> condensed JSON array
  rg ${icase_flag} --json --line-number --no-messages --max-count "${max_count:-0}" -- "$pattern" "$path" \
  | jq -rs '
      [ .[] | select(.type=="match") |
        { file: .data.path.text,
          line: .data.lines.text,
          line_number: .data.line_number } ]'
elif [[ "$mode" == "journal" ]]; then
  command -v journalctl >/dev/null 2>&1 || { echo "journalctl not found" >&2; exit 4; }

  jargs=( -o short-iso )
  [[ -n "$unit" ]] && jargs+=( -u "$unit" )
  [[ -n "$since" ]] && jargs+=( --since "$since" )
  [[ -n "$until" ]] && jargs+=( --until "$until" )

  # Stream journal lines, filter with rg, cap results
  journalctl "${jargs[@]}" \
  | rg ${icase_flag} --line-number --no-messages --max-count "${max_count:-0}" -- "$pattern" \
  | awk -F: '{ln=$1; sub(/^[^ ]+ /,"",$0); print "{\"line_number\":"ln",\"line\":" "\"" $0 "\"" "}"}' \
  | jq -s '.'
else
  usage; exit 5
fi

Why this works well with MCP

  • Deterministic: clearly defined inputs and a stable output shape (JSON).

  • Narrow scope: the tool can only read specific logs you allow.

  • Extensible: add whitelists or path allowlists without changing the protocol.


Case study 2: Package compliance (pending updates and security advisories)

Expose a read-only “what’s pending?” tool that enumerates updates and security advisories in a distro-aware way.

Drop-in script: pkg_audit.sh

#!/usr/bin/env bash
set -Eeuo pipefail

# Reports pending package updates and security advisories across apt/dnf/zypper.
# Output: simple JSON with counts and summaries.

require() { command -v "$1" >/dev/null 2>&1 || { echo "Missing: $1" >&2; exit 1; }; }
to_json_string() { jq -Rn --arg s "$1" '$s'; }

manager=""
if command -v apt-get >/dev/null 2>&1; then
  manager="apt"
elif command -v dnf >/dev/null 2>&1; then
  manager="dnf"
elif command -v zypper >/dev/null 2>&1; then
  manager="zypper"
else
  echo '{"error":"unsupported package manager"}'
  exit 2
fi

updates=""
sec=""
case "$manager" in
  apt)
    sudo apt-get -qq update || true
    # Upgradable list
    updates="$(apt list --upgradable 2>/dev/null | awk 'NR>1{print $0}')"
    # Heuristic: security pocket often tagged with -security
    sec="$(apt-get -s upgrade | awk '/^Inst/ && /-security/ {print $0}')"
    ;;
  dnf)
    updates="$(dnf -q check-update || true)"
    sec="$(dnf -q updateinfo list security all || true)"
    ;;
  zypper)
    updates="$(zypper -q lu || true)"
    sec="$(zypper -q lp --category security || true)"
    ;;
esac

# Count lines with content
u_count=$(printf "%s\n" "$updates" | sed '/^\s*$/d' | wc -l | tr -d ' ')
s_count=$(printf "%s\n" "$sec" | sed '/^\s*$/d' | wc -l | tr -d ' ')

# Emit minimal JSON
jq -n --arg mgr "$manager" \
      --arg u "$updates" --arg s "$sec" \
      --argjson uc "$u_count" --argjson sc "$s_count" \
'{
  manager: $mgr,
  updates_count: $uc,
  security_count: $sc,
  updates_raw: $u,
  security_raw: $s
}'

Why this works well with MCP

  • Read-only by default; easy to audit.

  • The model can ask “are there security patches pending?” and get a yes/no plus details.

  • You can add allowlisted actions later (e.g., “preview only” vs “apply now” guarded by policy).


Case study 3: Network diagnostics on demand

Pack connectivity checks into a single, parameterized tool the model can run when troubleshooting.

Drop-in script: netdiag.sh

#!/usr/bin/env bash
set -Eeuo pipefail

# Multipurpose network diagnostics
# Usage:
#   netdiag.sh ping <host> [count]
#   netdiag.sh trace <host>
#   netdiag.sh ports [--listening] [--numeric]
#   netdiag.sh dns <name> [type=A|AAAA|MX|TXT]

usage() {
  cat <<'EOF'
Examples:
  netdiag.sh ping example.com 4
  netdiag.sh trace 1.1.1.1
  netdiag.sh ports --listening --numeric
  netdiag.sh dns example.com A
EOF
}

[[ $# -lt 1 ]] && { usage; exit 1; }

cmd="$1"; shift

case "$cmd" in
  ping)
    host="${1:-}"; count="${2:-4}"
    [[ -z "$host" ]] && { usage; exit 2; }
    timeout 10 ping -c "$count" -W 2 "$host" || true
    ;;
  trace)
    host="${1:-}"; [[ -z "$host" ]] && { usage; exit 3; }
    if command -v traceroute >/dev/null 2>&1; then
      timeout 20 traceroute -n "$host" || true
    elif command -v tracepath >/dev/null 2>&1; then
      timeout 20 tracepath -n "$host" || true
    else
      echo "traceroute/tracepath not installed" >&2; exit 4
    fi
    ;;
  ports)
    list_flag=""; num_flag=""
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --listening) list_flag="-l";;
        --numeric) num_flag="-n";;
        *) ;;
      esac
      shift
    done
    if command -v ss >/dev/null 2>&1; then
      ss -tulp${list_flag:+l} ${num_flag:+-n} || true
    else
      echo "ss (iproute2) not available" >&2; exit 5
    fi
    ;;
  dns)
    name="${1:-}"; rr="${2:-A}"
    [[ -z "$name" ]] && { usage; exit 6; }
    if command -v dig >/dev/null 2>&1; then
      dig +nocomments +noquestion +nocmd "$name" "$rr"
    else
      echo "dig not installed (dnsutils/bind-utils)" >&2; exit 7
    fi
    ;;
  *)
    usage; exit 8;;
esac

Why this works well with MCP

  • One tool entry point, multiple safe sub-commands.

  • Timeouts avoid hanging sessions.

  • Easy for a model to chain: ping -> trace -> inspect open ports.


Case study 4: Local knowledge search (man pages, READMEs, runbooks)

Expose “read-only knowledge” so the model can cite facts from your environment, not the public internet.

Drop-in script: kb_search.sh

#!/usr/bin/env bash
set -Eeuo pipefail

# Grep a documentation root for patterns and return context lines.
# Usage: kb_search.sh <doc_root> <query> [--icase] [--before N] [--after N] [--max-count N]

[[ $# -lt 2 ]] && { echo "Usage: kb_search.sh <doc_root> <query> [...]" >&2; exit 1; }

doc_root="$1"; shift
query="$1"; shift

icase=""
before=1
after=1
maxc=50

while [[ $# -gt 0 ]]; do
  case "$1" in
    --icase) icase="-i";;
    --before) shift; before="$1";;
    --after) shift; after="$1";;
    --max-count) shift; maxc="$1";;
  esac
  shift || true
done

[[ -d "$doc_root" ]] || { echo "Not a directory: $doc_root" >&2; exit 2; }

# Print filename, line, and context in a simple, model-friendly format
rg ${icase} --no-heading -n -C "${before},${after}" --max-count "${maxc}" -- "$query" "$doc_root" \
| sed 's/\x1b\[[0-9;]*m//g'

Why this works well with MCP

  • Curated scope: point it to manpage exports, runbooks, or config directories.

  • Model can provide citations by filename and line number.

  • Easy to serialize if you prefer JSON later.


How these scripts fit into an MCP server

MCP servers expose discrete “tools.” In practice, that means your server process:

  • Receives a tool name and parameters from the MCP client

  • Validates inputs and maps the call to a local action

  • Executes your script with a whitelist of flags

  • Returns structured output (JSON or plain text) and exit status

  • Logs the call for auditing

A pragmatic path for Bash-first teams: 1) Keep tools as scripts like the ones above. 2) Wrap them with a small adapter in your preferred language that speaks MCP and shells out to the scripts. 3) Constrain and sanitize everything: allowed commands, directories, and maximum durations.

Example: a very thin JSON-in/JSON-out adapter you can evolve into a full MCP server later.

tool_adapter.py (not a full MCP implementation, but illustrates the pattern)

#!/usr/bin/env python3
import json, subprocess, sys, shlex

# Read a single JSON object from stdin:
# {"tool":"loggrep","args":{"mode":"file","pattern":"ERROR","path":"/var/log/syslog","max":100}}
req = json.loads(sys.stdin.read())

tool = req.get("tool")
args = req.get("args", {})

def run(cmd, timeout=20):
    p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
    return {"rc": p.returncode, "stdout": p.stdout, "stderr": p.stderr}

if tool == "loggrep":
    mode = args.get("mode","file")
    pattern = args.get("pattern")
    path = args.get("path","")
    flags = []
    if args.get("icase"): flags.append("--icase")
    if (m := args.get("max")): flags += ["--max", str(m)]
    cmd = ["./loggrep.sh", mode, pattern] + ([path] if path else []) + flags
    res = run(cmd, timeout=30)
elif tool == "pkg_audit":
    res = run(["./pkg_audit.sh"], timeout=60)
elif tool == "netdiag":
    sub = args.get("sub","ping")
    rest = args.get("rest",[])
    cmd = ["./netdiag.sh", sub] + [str(x) for x in rest]
    res = run(cmd, timeout=30)
elif tool == "kb_search":
    doc_root = args.get("doc_root",".")
    query = args.get("query")
    flags = []
    if args.get("icase"): flags.append("--icase")
    if (b := args.get("before")): flags += ["--before", str(b)]
    if (a := args.get("after")):  flags += ["--after",  str(a)]
    if (m := args.get("max_count")): flags += ["--max-count", str(m)]
    cmd = ["./kb_search.sh", doc_root, query] + flags
    res = run(cmd, timeout=30)
else:
    res = {"rc": 127, "stdout": "", "stderr": f"unknown tool: {tool}"}

print(json.dumps(res))

You can now test your adapter by piping JSON:

echo '{"tool":"pkg_audit","args":{}}' | python3 tool_adapter.py | jq

From here, swap the adapter’s I/O for your MCP SDK’s server primitives and map SDK tool handlers to these scripts. The contract and validation stay the same.

Tip: whichever SDK you use, keep stdio small and stable. Return JSON for structured tools (loggrep, pkg_audit) and compact text for exploratory tools (netdiag). Add allowlists, timeouts, and resource limits early.


Actionable checklist

  • Start small: pick one high-signal tool (e.g., loggrep) and make its output model-friendly (JSON, capped length).

  • Add guardrails: input validation, path allowlists, and timeouts in scripts and adapter.

  • Standardize outputs: consistent JSON shapes let the model chain tools without brittle parsing.

  • Log everything: append tool name, args, duration, and exit code to a file for audits.

  • Iterate with real incidents: record prompts, tweak flags, add missing parameters and context.


Conclusion and next steps

MCP doesn’t require you to reinvent your stack. It’s a thin layer that lets AI call the tools you already trust—on your terms. Start with the four case studies above, wire them into a minimal adapter, and you’ll immediately feel the difference during incidents and day‑to‑day ops.

Next steps:

  • Drop these scripts into a dedicated repo and add tests.

  • Choose an MCP SDK in your preferred language and map each tool to a handler.

  • Package the server as a systemd service with a locked-down service user.

  • Add one “write” capability behind an explicit policy gate (e.g., “roll logs” or “restart safe service”) once you’re confident in the read-only flow.

Have a cool Bash-friendly MCP use case or a twist on these scripts? Share it with the community—your future on-call self will thank you.