Part 6 of the Local-First AI Agents series — what six weeks of homelab exploration actually taught, and what I’d build next.
The gotchas I hit
Over six weeks of building this setup, I hit a lot of walls. Here are the ones most likely to trip you up.
1. Context window is your #1 constraint
Every tool description costs 40-80 tokens. My full setup has 61 tools, consuming ~2,500 tokens before the user says anything. On a 16k context model, that’s 15% of your window gone to overhead.
The fix: Use the tier system. Start with Standard (24 tools, ~1,000 tokens). Only add servers you need for the current task. Restart the gateway with a different --servers line between tasks.
The harder truth: Even 24 tools is generous for a 16k model doing research. Each paper search result can be 500+ tokens. A Wikipedia article is 2,000+ tokens. Three search results and your context is half full. Plan your sessions accordingly.
2. Volume paths are container-relative
This caught me for hours. The gateway validates volume paths against its own filesystem, not the host’s. Your config.yaml must use paths from the gateway’s perspective:
# WRONG — host paths
volumes:
- ~/data.mcp/papers:/app/papers:rw
# CORRECT — gateway container paths
volumes:
- /workspace/papers:/app/papers:rw
The compose file mounts ~/data.mcp to /workspace inside the gateway container. So /workspace/papers inside the container maps to ~/data.mcp/papers on the host. The gateway validates against /workspace/papers (which it can see), and Docker resolves the actual host path.
3. Writable paths use colons
The MCP_GATEWAY_DOCKER_BIND_ALLOW_WRITABLE_PATHS variable separates paths with :, not commas. This is because Go’s filepath.SplitList() uses : on Linux (same as PATH). It’s undocumented and counterintuitive.
# WRONG
- MCP_GATEWAY_DOCKER_BIND_ALLOW_WRITABLE_PATHS=/workspace/papers,/workspace/files
# CORRECT
- MCP_GATEWAY_DOCKER_BIND_ALLOW_WRITABLE_PATHS=/workspace/papers:/workspace/files
4. Parent directories don’t count for writable paths
If you mount /workspace/papers:/app/papers:rw, you must list /workspace/papers exactly in the writable paths variable. Listing /workspace is not sufficient. The gateway does exact matching, not prefix matching.
5. Config overrides require a patched gateway
The stock Docker MCP Gateway ignores your config.yaml overrides for volumes, user, env, and longLived when the catalog has empty (but non-nil) values for those fields. The patch in post 2 fixes this by applying config overrides when catalog Spec fields are empty. Without the patch, your volumes won’t mount, your user ID won’t apply, and your environment variables won’t be passed.
Until an equivalent fix lands upstream (the gateway was on v0.44.x as of publishing — retest on current releases), you need to build the patched image:
git clone https://github.com/docker/mcp-gateway.git
# Apply the clientpool patch from post 2
docker build -t docker/mcp-gateway:config-override .
6. tools.yaml filtering doesn’t work with —servers
The --servers flag registers all server tools at startup, which makes the dynamic tools list non-empty. The isToolEnabled() function checks the dynamic tools list first, and only falls back to tools.yaml when the list is empty. Since the list is never empty with --servers, your filter config is ignored.
Workaround: Control tool exposure by adding/removing servers from the --servers flag. Use the tier system (core/standard/extended/full) instead of tools.yaml filtering.
Upstream fix needed: Check configuration.tools.ServerTools as an additional allowlist, regardless of whether enabledTools is empty.
7. Ephemeral containers need longLived used carefully
longLived: true keeps containers alive between requests, which is essential for stateful servers (memory, sqlite, playwright). But for stateless servers (paper-search, fetch, wikipedia), it wastes resources. Each MCP client connection creates a new container, and with longLived: true, they all stay alive:
# I once had 8 paper-search containers running simultaneously
docker ps --filter "label=docker-mcp-name=paper-search"
# 8 containers, all idle
Guideline: Use longLived: true only for servers that maintain state. Volume mounts handle data persistence for stateless servers.
8. Rate limits from inside Docker
Some APIs rate-limit based on IP address. When MCP servers run as Docker containers on a bridge network, they share the host’s IP but the requests come in bursts. I saw Wikipedia return 429 (Too Many Requests) from inside Docker, even though the same requests worked from the host.
Mitigation: Add delays between requests (my paper-search wrapper uses 5-second delays). Use longLived: true for servers that make frequent API calls so the container stays warm.
9. Docker networking quirks
MCP servers on the mcp-net bridge network can reach the internet, but DNS resolution and connection behavior can differ from the host. If a server can’t reach an external API, test from inside a container:
docker exec mcp-gateway wget -q -O- "https://en.wikipedia.org" 2>&1 | head -5
10. Paper search source format
The paper-search MCP server’s sources parameter takes a string, not a list:
# CORRECT
call_tool("search_papers", {"query": "KD", "sources": "arxiv"})
# WRONG — this returns 0 results
call_tool("search_papers", {"query": "KD", "sources": ["arxiv"]})
Architectural decisions I’d defend
Why Docker Compose, not Kubernetes
Kubernetes is overkill for a homelab. Docker Compose is simpler, has lower overhead, and the gateway already manages container lifecycle. One docker compose up -d and everything is running.
Why bind mounts, not named Docker volumes
Named Docker volumes are harder to inspect and manage. Bind mounts to ~/data.mcp/ mean you can ls, cat, and grep your data directly. They also make backups trivial: tar czf backup.tar.gz ~/data.mcp/.
Why a tier system, not per-tool toggles
The tools.yaml filtering should work, but doesn’t (gotcha #6). Rather than fight it, I designed a tier system that’s simple to understand and switch. Edit one line in compose.yaml, restart, and you have a different tool set.
Why the patched image, not workarounds
I could have used the stock gateway with a static compose.yaml that pre-creates all containers. But that would mean maintaining a separate Dockerfile for each server, losing the catalog’s automatic updates, and making it hard to add servers. The patch is ~100 lines of Go, well-tested, and makes the config override system actually work as documented.
What’s next
Short term
-
Upstream PR: Submit the config override patch to docker/mcp-gateway. The fix is clean, tested, and follows the “catalog wins” principle. (Re-check the current release first — the codebase has moved since this exploration.)
-
Tools filter fix: File an issue about
isToolEnabled()bypassingtools.yamlwhen--serversis used. The fix is straightforward: checkconfiguration.tools.ServerToolsas an additional allowlist. -
Writable paths documentation: File a docs issue about the
:separator inMCP_GATEWAY_DOCKER_BIND_ALLOW_WRITABLE_PATHS.
Medium term
-
Neo4j knowledge graph: The
neo4j-memoryserver provides much richer knowledge graph capabilities than the flatmemoryserver. It requires running a Neo4j instance, which is straightforward in Docker Compose. -
Local embedding/vector search: Add a Chroma or Qdrant server for semantic search over downloaded papers. This would let you query your local knowledge base by meaning, not just keyword.
-
Automated research pipeline: Combine task-orchestrator + paper-search + markdownify + memory into a single script that:
- Takes a research topic
- Searches for papers
- Downloads and converts to markdown
- Extracts key entities and relationships
- Stores everything in the knowledge graph
- Produces a markdown summary
Long term
-
Multi-agent orchestration: Use sequential thinking as a meta-controller that decides which tools to use for each step of a complex research task. This is the “swarm agent” pattern — specialized agents for specialized tasks, coordinated by a planner.
-
Human-in-the-loop review: Build a simple web UI that shows the research progress (tasks completed, papers found, knowledge graph state) and allows the human to steer the direction. The task-orchestrator’s markdown export is a start.
-
Local LLM fine-tuning: Use the collected papers and knowledge graph to create a domain-specific fine-tuning dataset. Train a small model on your research domain and get better results from smaller, faster models.
Resources
- Docker MCP Gateway: github.com/docker/mcp-gateway
- MCP Protocol Spec: modelcontextprotocol.io
- Ollama: ollama.com
- LM Studio: lmstudio.ai
Closing thoughts
Building a local AI agent hub is more achievable than it seems. The MCP protocol provides the standard, Docker provides the isolation, and the gateway provides the orchestration. The challenges are real — config bugs, context windows, Docker networking — but they’re solvable.
The key insight: you don’t need cloud APIs for powerful AI agents. A consumer GPU running a 14B parameter model, connected to a gateway with a few well-chosen servers, can search papers, browse the web, process documents, and build knowledge graphs — all without sending a single byte to an external service.
Your data stays on your machine. Your costs are electricity. Your limits are context windows and creativity.
That’s the promise of local-first AI. And with MCP and Docker, it’s here today.
This concludes the Local-First AI Agents series. Start from Why I Built a Local AI Agent Hub.