If you've been watching the AI tooling space for the past six months, you've probably noticed Model Context Protocol (MCP) quietly becoming the backbone of how developers give AI assistants real power. It's the difference between a chatbot that talks about your codebase and one that actually reads, searches, and acts on it.
The best part? You can build your own MCP server in about 30 minutes. Here's exactly how.
What Is MCP (and Why Should You Care)?
MCP is an open standard (originally by Anthropic, now broadly adopted) that defines how AI models talk to external tools and data sources. Think of it like a USB-C port for AI — instead of every app having its own proprietary integration, MCP gives you a universal protocol.
When your Claude Desktop, Cursor, VS Code, or any MCP-compatible client connects to an MCP server, it gains access to tools — functions the AI can call. Your server defines what those tools do. That could be reading files, querying a database, calling an API, running a script, or anything else you can code.
Client (Claude, Cursor, VS Code)
│
▼ MCP Protocol (stdio / HTTP)
│
MCP Server ──► Your Tools (files, DBs, APIs, scripts)
What We're Building
A lightweight MCP server in Python that exposes three practical tools:
-
read_file— Read any file from allowed directories -
run_command— Execute whitelisted shell commands -
search_logs— Grep through log files with pattern matching
Real homelab stuff. No fluff. Let's build it.
Prerequisites
- Python 3.10+
-
pip install mcp(the official MCP SDK) - Claude Desktop or any MCP-compatible client
Step 1: Install the SDK
pip install mcp
That's it. The mcp package handles all the protocol boilerplate — you just define your tools.
Step 2: Write the Server
Create homelab_mcp.py:
import subprocess
import asyncio
from pathlib import Path
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
app = Server("homelab-tools")
ALLOWED_DIRS = [
Path.home() / "logs",
Path("/var/log"),
Path.home() / "projects",
]
def is_safe_path(path: str) -> bool:
target = Path(path).resolve()
return any(
str(target).startswith(str(d)) for d in ALLOWED_DIRS
)
@app.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="read_file",
description="Read a file from allowed directories",
inputSchema={
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Absolute file path"
}
},
"required": ["path"]
}
),
Tool(
name="run_command",
description="Run a whitelisted shell command",
inputSchema={
"type": "object",
"properties": {
"command": {"type": "string"},
"args": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["command"]
}
),
Tool(
name="search_logs",
description="Search log files for a pattern",
inputSchema={
"type": "object",
"properties": {
"log_path": {"type": "string"},
"pattern": {"type": "string"},
"lines": {"type": "integer", "default": 50}
},
"required": ["log_path", "pattern"]
}
),
]
SAFE_COMMANDS = {"df", "free", "uptime", "docker", "ps"}
@app.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "read_file":
path = arguments["path"]
if not is_safe_path(path):
return [TextContent(
type="text",
text=f"Denied: {path} outside allowed dirs"
)]
content = Path(path).read_text(errors="replace")
return [TextContent(type="text", text=content[:10000])]
elif name == "run_command":
cmd = arguments["command"]
args = arguments.get("args", [])
if cmd not in SAFE_COMMANDS:
return [TextContent(
type="text",
text=f"'{cmd}' is not whitelisted"
)]
result = subprocess.run(
[cmd] + args,
capture_output=True, text=True, timeout=10
)
return [TextContent(
type="text", text=result.stdout or result.stderr
)]
elif name == "search_logs":
log_path = arguments["log_path"]
pattern = arguments["pattern"]
lines = arguments.get("lines", 50)
if not is_safe_path(log_path):
return [TextContent(type="text", text="Access denied")]
result = subprocess.run(
["grep", "-n", pattern, log_path],
capture_output=True, text=True, timeout=5
)
output = "\n".join(result.stdout.splitlines()[-lines:])
return [TextContent(
type="text", text=output or "No matches"
)]
async def main():
async with stdio_server() as streams:
await app.run(
*streams,
app.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
Step 3: Connect It to Claude Desktop
Open your Claude Desktop config file:
-
Mac:
~/Library/Application Support/Claude/claude_desktop_config.json -
Windows:
%APPDATA%\Claude\claude_desktop_config.json -
Linux:
~/.config/Claude/claude_desktop_config.json
Add your server:
{
"mcpServers": {
"homelab": {
"command": "python3",
"args": ["/path/to/homelab_mcp.py"]
}
}
}
Restart Claude Desktop. You'll see a hammer icon in the chat — that's your MCP server connected and ready.
Step 4: Test It
Ask Claude something like:
"Check my disk usage and search ~/logs/app.log for any ERROR lines from today"
Claude will call run_command with df -h, then search_logs with your pattern — and synthesize the results into a coherent answer. No copy-pasting between terminals. No context switching.
Security: Don't Skip This
Three non-negotiable rules for any MCP server that touches your system:
1. Whitelist commands aggressively. The SAFE_COMMANDS set should stay minimal. Never include rm, curl, bash, or sh. If you need destructive ops, build a separate server with extra auth.
2. Sandbox file access. ALLOWED_DIRS is your perimeter. Keep it tight. Adding your entire home directory is asking for trouble.
3. Add auth for remote deployments. If you switch from stdio to HTTP transport (for remote clients), add bearer token verification:
import os
def verify_request(headers: dict):
token = headers.get("Authorization", "")
expected = f"Bearer {os.environ['MCP_SECRET']}"
if token != expected:
raise PermissionError("Unauthorized")
4. Log everything. Every tool call should be logged with timestamp, tool name, and arguments. When something goes wrong — and it will — you want the audit trail.
What to Build Next
Once the basics are working, the fun starts:
-
Docker tools —
docker ps,docker logs, restart containers by name - Home Assistant bridge — query sensor states, trigger automations
- Database reader — safe read-only queries to SQLite or Postgres
-
Git dashboard —
git log,git diff, status across your repos - Network scanner — who's on your LAN right now?
The MCP ecosystem is still young. Anthropic's GitHub has 40+ reference servers (filesystem, GitHub, Postgres, Slack), but the most interesting homelab integrations haven't been built yet. That's the opportunity.
The Bigger Picture
MCP solves a problem that's been bugging builders since GPT-3: how do you give an AI real context about your systems without dumping everything into a prompt? The answer isn't bigger context windows — it's structured tool access with proper security boundaries.
A 100-line Python file and 5 minutes of config turns your AI assistant from a smart chatbot into something that can actually navigate your infrastructure. That's a meaningful upgrade.
Build your own server first. You'll understand the protocol better than any tutorial can teach, and you'll have something genuinely useful running on your homelab by lunch.
SIGNAL is a weekly dispatch for builders working at the intersection of AI, dev tools, and self-hosted infrastructure. New articles every Monday, Wednesday, and Friday.
Top comments (0)