Deployment of Homelab Model Context Protocol (MCP) Servers
Model Context Protocol (MCP) is an open standard that lets AI assistants connect to real tools and data sources. Instead of generating…
Deployment of Homelab Model Context Protocol (MCP) Servers

Model Context Protocol (MCP) is an open standard that lets AI assistants connect to real tools and data sources. Instead of generating guesses, a model with MCP can query your SQLite database, read log files, flip smart home switches, or manage Docker containers, all through a structured, auditable interface. For homelabbers, this opens up a useful tier of local automation that does not depend on cloud services or external APIs.
This article walks through what you need to know to deploy MCP servers in a homelab environment. You will learn how the two primary transport mechanisms, stdio and Streamable HTTP, work and when to use each. You will then see how to connect MCP to Home Assistant for smart home control, how to set up SQLite and filesystem access for local data, and how to coordinate local inference with Ollama. The article closes with security hardening practices, including reverse proxy authentication with Caddy and container isolation with Docker Compose, so your homelab does not accidentally become an open door.
How MCP Communication Works
MCP frames all communication as JSON-RPC 2.0 messages. Every tool call, response, and error follows the same envelope, which keeps integrations predictable regardless of which server or client you are using. The two transport types sit underneath that protocol layer and handle how those messages actually move between the host client and the server process.
Standard input-output (stdio) is the default for local deployments. It works by launching the MCP server as a subprocess directly controlled by the host client. The client writes to the process’s stdin, reads from its stdout, and the protocol runs over newline-delimited messages. There are no ports to open, no firewall rules to write, and no authentication handshakes over the network. The process-per-user model means each client gets its own isolated subprocess, which keeps things simple and avoids cross-user data leakage.
The tradeoff is that stdio cannot serve multiple clients at once and has no built-in authentication. Because any process that can read the environment can see credentials stored there, you need strict file-level permissions on configuration files. Relative paths in client configs also cause silent failures at startup, because the subprocess is spawned without the working directory context you might expect. Always use absolute paths in every MCP configuration file you write.
Streamable HTTP is the right choice when you need to share a single server backend across multiple clients or physical machines. It routes JSON-RPC messages over standard HTTP, using Server-Sent Events to push real-time updates back to the client. A single server can handle many concurrent clients without spawning separate processes for each. The cost is exposure: any unauthenticated port on your local network is an attack surface, because anyone who can reach the port can invoke the tools registered on that server. That means Streamable HTTP setups require a proxy layer with authentication or a VPN tunnel before you put anything real behind them.
Connecting MCP to Home Assistant
Home Assistant ships with an official MCP server integration built on top of its Assist voice interface. It exposes smart devices through standard conversational triggers and keeps the model away from configuration directories and system files. This is safe and easy to set up, but it limits what you can actually do. You cannot create automations, modify dashboards, or inspect config history through the official integration.
The community ha-mcp server takes a different approach. It connects directly to Home Assistant's REST and WebSocket APIs, giving the model access to the full state register. This means the model can read the current state of every entity, trigger any automation, and write configuration files. It can inspect automation histories and construct missing automation files on the fly, which opens up genuinely self-healing smart home behavior.
The tradeoff is a much wider blast radius if something goes wrong. Authentication uses a long-lived access token generated from the Home Assistant user security profile. This token is passed in HTTP request headers and authorizes privileged administrative actions; there are no session cookies, no scope restrictions. If this token is exposed to an untrusted client, that client can do anything your Home Assistant user account can do.
Mitigate this by routing all traffic through a secure local network or VPN tunnel, binding credentials to specific local IP addresses in your configuration, and never exposing the token to clients outside your homelab boundary.
The tools available through the community integration cover both read and write operations:
Tool Scope Type getStates All entities Read-only getState Single entity Read-only searchEntities Search index Read-only listAutomations System config Read-only triggerAutomation Automation execution Read-write
Running the Stack with Docker Compose
The cleanest way to manage multiple MCP servers in a homelab is to centralize them under a single Docker Compose file. This handles container lifecycle, environment variables, and volume mounts in one place, and ensures all services restart automatically on boot.
Here is a working Compose configuration covering Home Assistant, Docker management, and an Ollama bridge:
services:
hass-mcp:
image: voska/hass-mcp:latest
container_name: mcp_homeassistant
restart: unless-stopped
network_mode: host
environment:
- HA_URL=http://127.0.0.1:8123
- HA_TOKEN=eyK17Y0_TOKEN_PLACEHOLDER
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
docker-manager:
image: ckreiling/mcp-server-docker:latest
container_name: mcp_docker_manager
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DOCKER_HOST=unix:///var/run/docker.sock
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
ollama-bridge:
image: jonigl/ollama-mcp-bridge:latest
container_name: mcp_ollama_bridge
restart: unless-stopped
ports:
- "8000:8000"
environment:
- OLLAMA_HOST=http://host.docker.internal:11434
- MAX_TOOL_ROUNDS=5
- SYSTEM_PROMPT=Execute local system modifications only after verifying target arguments.
volumes:
- ./mcp-config.json:/mcp-config.json:ro
extra_hosts:
- "host.docker.internal:host-gateway"
The Home Assistant container runs in host network mode so it can reach loopback services directly without complex bridge mappings. This bypasses NAT overhead but also means the container shares the host’s network namespace, so any vulnerability on the host network is visible to the container.
The Docker manager container mounts the host Docker socket. This is the most dangerous volume in the stack. Any process inside that container with access to the socket can issue Docker API calls with full host privileges. Run this container without elevated capabilities and restrict which tools the model can call against it. Mounting the socket is sometimes unavoidable for Docker management use cases, but you should know what you are accepting when you do it.
Logging is capped at ten megabytes per file with a three-file rotation. This is a small but important detail. Unbounded logging on a busy homelab host will fill your disk eventually, and Docker’s default JSON-file driver has no size limit unless you configure it explicitly.
SQLite and Filesystem Access
SQLite is the natural choice for storing homelab metrics and telemetry locally. Exposing it to a model through MCP means the model can inspect table schemas dynamically and write queries against real data without needing those schemas hardcoded into a prompt.
The risk here is SQL injection. Unvalidated queries passed to a SQLite server can corrupt the database or leak data from tables the model was not intended to read. The standard mitigation is a metadata configuration file that defines a set of pre-authorized canned queries. The model selects from these rather than composing arbitrary SQL. This limits what the model can ask, but it protects the database’s structural integrity.
The Ollama bridge reads its server map from a JSON configuration file:
// File Location: /var/lib/homelab/mcp/mcp-config.json
{
"mcpServers": {
"sqlite-analytics": {
"command": "uvx",
"args": [
"mcp-sqlite",
"/var/lib/homelab/mcp/database/metrics.db",
"--metadata",
"/var/lib/homelab/mcp/database/metadata.yml"
]
},
"filesystem-logs": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/var/log/homelab"
]
}
}
}
Every path in this file is absolute. Relative paths cause silent connection failures during client initialization because the subprocess does not inherit the working directory you expect. This is one of the more common setup mistakes and one of the harder ones to diagnose because there is no explicit error, the client simply never spawns the child process.
Filesystem servers follow the same principle. The server enforces a directory whitelist, and any directory the model can access must be explicitly registered as an individual argument. This prevents path traversal attacks where a model reads outside its intended workspace. If you configure multiple directories, each one gets its own argument entry. There is no glob or wildcard support here.
On the data format side, tab-separated values are worth considering for large database query responses. Because TSV drops the structural overhead of JSON, no repeated keys, no quotes, no colons, payloads can be significantly smaller. For large result sets, this keeps the response inside the model’s context window and reduces token consumption during long multi-turn sessions.
Local Inference with Ollama
Ollama handles offline inference, which means your homelab can process private data without sending anything to an external API. To use tools, Ollama needs structured JSON schemas that define each tool’s parameters. The bridge layer handles this automatically by discovering tool definitions from registered MCP servers and converting them into formats Ollama can parse during its reasoning loop.
The bridge intercepts the model’s function call requests, routes them to the correct MCP server, and feeds the tool output back to the model so it can continue generating. This feedback loop is what makes agentic behavior work. The model can call a tool, read the result, decide to call another tool, and eventually produce a final response, all in a single turn.
The practical constraint is model size. Smaller local models often fail to extract parameter properties correctly, which produces broken tool calls or malformed JSON. If you find tool calls failing inconsistently, the fix is usually to switch to a model that has been fine-tuned for instruction following and structured output, rather than tuning the tool definitions themselves.
Tool filtering is a critical safety mechanism at the bridge layer. The bridge supports explicit allow-lists and deny-lists on registered servers. This means you can permit state queries against Home Assistant while blocking automation triggers, or allow read-only Docker inspection while blocking container deletion. Misconfiguring these lists causes the bridge to exit immediately on startup, so validate your configuration schema before deploying. The tool names in filter lists are case-sensitive and must match exactly.
Securing the Stack with Caddy
Binding MCP server processes to localhost loopback interfaces is the baseline. Any server that binds to 0.0.0.0 on a port is reachable from anywhere on your local network, and since there is no built-in authentication on most MCP transports, any device on the subnet can invoke your tools. Bind to 127.0.0.1 first, then route external access through a proxy that handles authentication.
Caddy is a good fit for this role. It is lightweight, handles automatic TLS certificate generation, and is simple to configure as a reverse proxy with bearer token validation. Here is a Caddyfile that puts a token-authenticated HTTPS gateway in front of the Ollama bridge:
# File Location: /var/lib/homelab/mcp/Caddyfile
mcp.homelab.local {
tls internal
@authenticated {
header Authorization "Bearer your_super_secure_mcp_token"
}
handle @authenticated {
reverse_proxy 127.0.0.1:8000
}
handle {
respond "Unauthorized" 401
}
}
The tls internal directive generates a local certificate authority and signs the domain certificate automatically. This gives you encrypted HTTPS across the local network without depending on Let's Encrypt or any external CA. Clients that do not have the internal CA in their trust store will get a TLS warning, so you will need to distribute the Caddy-generated CA certificate to any machine that connects to this gateway.
The authentication check is simple bearer token validation. Any request without the correct Authorization header gets a 401 response. This is not strong authentication by itself. A token on a local network is still sniffable if the connection is not encrypted, which is why the TLS setup matters. Together, encryption and token validation are enough for most homelab threat models, where the main risk is other devices on your LAN accidentally or intentionally reaching the server ports.
For higher-risk setups, add a VPN tunnel so that the MCP server ports are never exposed to the physical local network at all. WireGuard is a common choice for this in homelab environments.
Client Configuration Paths
Once your servers are running and secured, you need to register them in your AI client. The configuration file location varies by client and operating system:
- Client OS Config Path Claude Desktop — macOS —
~/Library/Application Support/Claude/claude_desktop_config.json - Claude Desktop — Windows —
%APPDATA%\Claude\claude_desktop_config.json - Claude Desktop — Linux —
~/.config/Claude/claude_desktop_config.json - Windsurf Cascade — Cross-platform —
~/.codeium/windsurf/mcp_config.json - Cursor IDE — Cross-platform —
$HOME/.cursor/mcp.json
The format in all cases follows the same pattern: a JSON object with an mcpServers key, each server entry specifying either a command and args array (for stdio) or a URL (for Streamable HTTP). Use absolute paths throughout.
Wrapping Up
Running MCP servers in a homelab gives you a level of local AI tool integration that is hard to achieve any other way. The model can query real data, control real devices, and execute real commands, all without sending anything to an external service. That power comes with real operational risk, which is why the security layer matters as much as the integration layer.
The pattern that works: stdio transports for single-host integrations, Streamable HTTP behind Caddy for anything that needs multi-client access, Docker Compose to manage the lifecycle, and strict path and permission hygiene throughout. The two ongoing challenges are managing custom certificate trust stores across clients and handling network boundary resolution inside Docker networks. Both are solvable, and solving them well is what separates a homelab that actually runs reliably from one that needs constant poking.
메타데이터
- post_id
- c47cedb82b2a
- slug
- deployment-of-homelab-model-context-protocol-mcp-servers-c47cedb82b2a
- url
- https://medium.com/@khabdrick-dev/deployment-of-homelab-model-context-protocol-mcp-servers-c47cedb82b2a
- canonical_url
- https://medium.com/@khabdrick-dev/deployment-of-homelab-model-context-protocol-mcp-servers-c47cedb82b2a
- author_url
- https://medium.com/@khabdrick-dev
- status
- ok
- fetched_at
- 2026-06-14 11:28:49