- Posted on
- • Artificial Intelligence
MCP Servers with Artificial Intelligence
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
MCP Servers with Artificial Intelligence: Give Your Bash Superpowers
If you’ve ever thought “I wish my shell scripts could ask an AI to do the boring parts safely,” you’re ready for MCP. The Model Context Protocol (MCP) lets AI assistants call well-defined, auditable tools you expose as a small “server” process. Instead of giving an AI your entire machine, you give it guarded entry points like “read these logs,” “run this safe command,” or “query this database.” That’s the power of MCP servers on Linux.
This article explains why MCP matters, then walks you through building a minimal MCP-style server in Python that exposes safe Unix tools over JSON-RPC via stdin/stdout—perfect for integration with modern AI assistants or for local experimentation.
Why MCP on Linux is worth your time
Clear boundaries: You define exactly which tools and resources an AI can use—no more, no less.
Reproducible and debuggable: Everything is an explicit request/response you can log and audit.
Least privilege by design: Wrap sensitive operations behind sanitized, allowlisted calls.
Portable: It’s just a process that speaks JSON-RPC over stdio or sockets; easy to version, containerize, and ship.
Extensible: Start with simple commands (uname, grep), grow into org-wide automations (ticketing, CI, fleet queries).
What we’ll build
A tiny MCP-style server that supports two tools:
- system_info: returns kernel and OS basics via uname.
- grep_logs: safely searches common system logs with an allowlist.
CLI-only workflow using Bash and Python, with install steps for apt, dnf, and zypper.
How to run, test, and harden it.
Note: We’ll adhere to the JSON-RPC 2.0 pattern commonly used by MCP clients. Actual MCP clients spawn servers and talk over stdio; that’s exactly what our script does.
1) Prerequisites and installation
We’ll use Python 3, a virtual environment, and jq for pretty-printing JSON. Choose the commands for your distro.
Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y python3 python3-venv python3-pip jq
Fedora/RHEL/CentOS Stream (dnf):
sudo dnf install -y python3 python3-pip python3-virtualenv jq
openSUSE Leap/Tumbleweed (zypper):
sudo zypper refresh
sudo zypper install -y python3 python3-pip python3-virtualenv jq
Set up a project directory and virtual environment:
mkdir -p ~/mcp-demo && cd ~/mcp-demo
python3 -m venv .venv
. .venv/bin/activate
No extra Python packages are required for this minimal server.
2) Scaffold a minimal MCP-style server
Create server.py with the code below. It implements two JSON-RPC methods:
tools/list: enumerate available tools (with simple input schemas).
tools/call: run a requested tool with validated arguments.
#!/usr/bin/env python3
import json
import os
import shlex
import subprocess
import sys
from typing import Any, Dict
JSONRPC = "2.0"
ALLOWED_LOG_FILES = [
"/var/log/syslog", # Debian/Ubuntu
"/var/log/messages", # Fedora/RHEL/openSUSE
]
def first_existing(paths):
for p in paths:
if os.path.isfile(p):
return p
return None
def resp_ok(id_, result):
return {"jsonrpc": JSONRPC, "id": id_, "result": result}
def resp_err(id_, code, message, data=None):
err = {"code": code, "message": message}
if data is not None:
err["data"] = data
return {"jsonrpc": JSONRPC, "id": id_, "error": err}
def list_tools():
return {
"tools": [
{
"name": "system_info",
"description": "Return basic system info (uname -a).",
"input_schema": {
"type": "object",
"properties": {},
},
},
{
"name": "grep_logs",
"description": "Search common system logs for a case-insensitive pattern. Returns up to 200 matches with line numbers.",
"input_schema": {
"type": "object",
"properties": {
"pattern": {"type": "string", "description": "Regex or plain text to grep"},
"path": {"type": "string", "description": "Optional log path; must be allowlisted"}
},
"required": ["pattern"]
},
},
]
}
def tool_system_info(_args: Dict[str, Any]) -> Dict[str, Any]:
cp = subprocess.run(["uname", "-a"], check=False, capture_output=True, text=True)
return {"stdout": cp.stdout.strip(), "stderr": cp.stderr.strip(), "returncode": cp.returncode}
def sanitize_pattern(pat: str) -> str:
# Basic guardrails; reject very long or control-laden patterns.
if not isinstance(pat, str):
raise ValueError("pattern must be a string")
if len(pat) > 256:
raise ValueError("pattern too long")
# Disallow NUL and some control chars
for bad in ["\x00", "\r"]:
if bad in pat:
raise ValueError("invalid characters in pattern")
return pat
def tool_grep_logs(args: Dict[str, Any]) -> Dict[str, Any]:
if "pattern" not in args:
raise ValueError("missing required argument: pattern")
pattern = sanitize_pattern(args["pattern"])
path = args.get("path") or first_existing(ALLOWED_LOG_FILES)
if path not in ALLOWED_LOG_FILES:
# If a user-supplied path matches allowlist strictly, permit it; otherwise deny.
if path is None or path not in ALLOWED_LOG_FILES:
raise ValueError("log path not allowed")
# Use grep safely without invoking a shell; cap matches.
cmd = ["grep", "-i", "-E", pattern, path, "-n", "-m", "200"]
cp = subprocess.run(cmd, check=False, capture_output=True, text=True)
# Truncate extremely large outputs defensively
out = cp.stdout
if len(out) > 64_000:
out = out[:64_000] + "\n...[truncated]"
return {"stdout": out, "stderr": cp.stderr.strip(), "returncode": cp.returncode, "path": path}
def handle_call(params: Dict[str, Any]) -> Dict[str, Any]:
name = params.get("name")
arguments = params.get("arguments", {}) or {}
if name == "system_info":
return tool_system_info(arguments)
elif name == "grep_logs":
return tool_grep_logs(arguments)
else:
raise ValueError(f"unknown tool: {name}")
def main():
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
req = json.loads(line)
method = req.get("method")
id_ = req.get("id")
if method == "tools/list":
result = list_tools()
sys.stdout.write(json.dumps(resp_ok(id_, result)) + "\n")
sys.stdout.flush()
elif method == "tools/call":
result = handle_call(req.get("params", {}))
sys.stdout.write(json.dumps(resp_ok(id_, result)) + "\n")
sys.stdout.flush()
else:
sys.stdout.write(json.dumps(resp_err(id_, -32601, f"Method not found: {method}")) + "\n")
sys.stdout.flush()
except Exception as e:
# Ensure we always return a JSON-RPC error envelope
try:
id_ = req.get("id") if 'req' in locals() else None
except Exception:
id_ = None
sys.stdout.write(json.dumps(resp_err(id_, -32000, "Server error", {"detail": str(e)})) + "\n")
sys.stdout.flush()
if __name__ == "__main__":
main()
Make it executable:
chmod +x server.py
3) Try it out from Bash
Because this server reads JSON from stdin and writes JSON to stdout, we can test it directly.
List tools:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
| ./server.py | jq
Call system_info:
printf '%s\n' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"system_info","arguments":{}}}' \
| ./server.py | jq
Grep logs (pattern = ssh):
printf '%s\n' \
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"grep_logs","arguments":{"pattern":"ssh"}}}' \
| ./server.py | jq -r '.result.stdout' | head -n 20
If your distro uses a different log file, you can supply a path explicitly (must be allowlisted):
printf '%s\n' \
'{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"grep_logs","arguments":{"pattern":"error","path":"/var/log/messages"}}}' \
| ./server.py | jq -r '.result.stdout' | sed -n '1,20p'
Tip: In “real” MCP use, an AI client spawns this process and communicates over stdio using these same methods. Your CLI tests prove the contract works.
4) Harden and run it responsibly
- Least privilege: create a dedicated user so the server can only read what it must.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin mcp
sudo install -m 0755 server.py /usr/local/bin/mcp-demo
sudo chown root:root /usr/local/bin/mcp-demo
Consider log access: journal-based systems may require additional permissions or a different tool wrapper to read via journalctl instead of files.
Resource limits: if you expect heavy queries, add timeouts or cap output sizes (we already cap to 64 KB and 200 matches).
Audit: pipe stdout/stderr through a logger if you wrap this in a supervising process or client.
If you want a user-level systemd entry to make the binary easy to reference (the AI client will still spawn it on demand), you only need the installed executable. A background daemon is not required because stdio is the transport; your AI client starts/stops the server as needed.
5) Real-world patterns you can build next
SRE log triage: expose tools to fetch the last N lines of critical logs, grep by incident ID, and summarize error frequencies.
Service health probes: wrap curl with strict allowlists to hit internal status endpoints and parse JSON.
Git operations: create tools to list open PRs, diff files, or check commit messages in a repo directory that’s safe to expose.
SQL read-only analytics: present a parameterized, whitelisted set of queries against a replica; return CSV/JSON with row caps and timeouts.
Ticketing/ChatOps: expose “create_ticket” and “comment_ticket” tools with strict schemas to keep AI output structured and traceable.
Each tool should:
Validate inputs strictly (length, format, enums).
Avoid shell=True; always pass argv arrays to subprocess.
Limit output and execution time.
Log requests/responses for audits (mind sensitive data).
Troubleshooting
No output? Ensure each JSON request is on a single line and the server flushes stdout. Our code flushes after every response.
Permission denied on logs? Try an allowlisted path that exists on your distro. Debian/Ubuntu: /var/log/syslog; Fedora/RHEL/openSUSE: /var/log/messages.
jq not found? Install it as shown above with apt/dnf/zypper.
Conclusion and next steps
You just built a minimal MCP-style server that exposes safe, auditable Unix powers to an AI—without handing over your entire shell. From here:
Add your own tools with strict input schemas.
Package this in a repo with tests and CI.
Wire it into an AI assistant that supports MCP over stdio and start automating safely.
Your Bash skills + MCP’s guardrails = reliable, repeatable, AI-augmented ops. Try extending the server with one more real tool today—like “tail_logs” or “service_status”—and see how quickly your workflows evolve.