A Docker container that downloads and pre-builds MCP (Model Context Protocol) servers, making them ready for use with Claude Code.
During image build, the container:
- Reads tool definitions from
config/servers.json - Clones each tool's repository from GitHub
- Installs dependencies (npm, pip, cargo, go modules)
- Builds/compiles each tool
- Packages everything at
/app/tools/<tool-name>/
The container stays running so tools can be invoked via docker exec. MCP tools use stdio transport - they read JSON-RPC from stdin and write to stdout, so each invocation is a fresh process (no persistent daemons).
Tools with docker_volume: true are stored in the servers/ directory on the host, allowing persistent data and native execution outside Docker.
| Tool | Type | Description |
|---|---|---|
| mcp-nixos | Python | NixOS package and configuration search |
| tailwind-svelte-assistant | Node.js | Tailwind CSS and SvelteKit documentation |
| context7 | Node.js | Up-to-date code documentation for any library |
| agent-framework | Node.js | AI-powered code quality: check, confirm, commit |
# Build and run
make build && make run
# Check available tools
make status
# Test a tool
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | \
docker exec -i mcp-toolbox /app/tools/mcp-nixos/venv/bin/python3 -m mcp_nixos.serverRegister the pre-built tools with Claude Code using claude mcp add:
# Tools running inside Docker container
claude mcp add nixos-search -- docker exec -i mcp-toolbox /app/tools/mcp-nixos/venv/bin/python3 -m mcp_nixos.server
claude mcp add tailwind-svelte -- docker exec -i mcp-toolbox node /app/tools/tailwind-svelte-assistant/run.mjs
claude mcp add context7 -- docker exec -i mcp-toolbox npx -y @upstash/context7-mcp
# agent-framework can run natively from the volume (docker_volume: true)
claude mcp add agent-framework -- node /path/to/mcp-server-host/servers/agent-framework/dist/mcp/server.js
# Or via Docker:
# claude mcp add agent-framework -- docker exec -i mcp-toolbox node /app/tools/agent-framework/dist/mcp/server.jsOr add to your Claude Code MCP settings (~/.claude/settings.json):
{
"mcpServers": {
"nixos-search": {
"command": "docker",
"args": [
"exec",
"-i",
"mcp-toolbox",
"/app/tools/mcp-nixos/venv/bin/python3",
"-m",
"mcp_nixos.server"
]
},
"context7": {
"command": "docker",
"args": [
"exec",
"-i",
"mcp-toolbox",
"npx",
"-y",
"@upstash/context7-mcp"
]
}
}
}This setup demonstrates declarative management of Claude Code hooks and MCP servers using NixOS + home-manager, with agent-framework providing code quality tooling.
Claude Code
├── Hooks (triggered on events)
│ ├── PreToolUse → agent-framework pre-tool-use.js
│ ├── PostToolUse → agent-framework post-tool-use.js
│ └── Stop → agent-framework stop-off-topic-check.js
│
├── MCP Servers (callable tools)
│ ├── agent-framework → check, confirm, commit
│ ├── nixos-search → NixOS package/option search
│ ├── context7 → Documentation lookup
│ └── tailwind-svelte → Frontend assistance
│
└── Environment
└── env.sh → Loads secrets (API keys, webhooks)
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"alwaysThinkingEnabled": true,
"permissions": {
"allow": [
"Bash(grep:*)",
"Bash(find:*)",
"WebFetch(domain:github.com)",
"WebFetch(domain:docs.rs)",
"WebFetch(domain:nixos.org)",
"WebFetch(domain:tailwindcss.com)",
"WebFetch(domain:svelte.dev)",
"WebFetch(domain:tauri.app)"
],
"deny": []
},
"hooks": {
"PreToolUse": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "$HOME/.claude/hooks/pre-tool-use.sh",
"timeout": 30
}
]
}
],
"PostToolUse": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "$HOME/.claude/hooks/post-tool-use.sh",
"timeout": 30
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "$HOME/.claude/hooks/stop-off-topic-check.sh",
"timeout": 30
}
]
}
]
}
}#!/usr/bin/env bash
# Shared environment setup for Claude Code hooks, MCP servers, and commands
# Called via run-with-env.sh wrapper
# Source API keys from SOPS secrets (with auto-export)
if [[ -f /run/secrets/mcpToolboxENV ]]; then
set -a # Enable auto-export
source /run/secrets/mcpToolboxENV
set +a # Disable auto-export
fi
# Export webhook secrets for hook scripts
if [[ -f /run/secrets/webhook_id_agent_logs ]]; then
export WEBHOOK_ID_AGENT_LOGS=$(cat /run/secrets/webhook_id_agent_logs)
fi#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/env.sh"
exec "$@"~/.claude/hooks/pre-tool-use.sh
#!/usr/bin/env bash
exec "$(dirname "$0")/../run-with-env.sh" node /mnt/docker-data/volumes/mcp-toolbox/agent-framework/dist/hooks/pre-tool-use.js~/.claude/hooks/post-tool-use.sh
#!/usr/bin/env bash
exec "$(dirname "$0")/../run-with-env.sh" node /mnt/docker-data/volumes/mcp-toolbox/agent-framework/dist/hooks/post-tool-use.js~/.claude/hooks/stop-off-topic-check.sh
#!/usr/bin/env bash
exec "$(dirname "$0")/../run-with-env.sh" node /mnt/docker-data/volumes/mcp-toolbox/agent-framework/dist/hooks/stop-off-topic-check.js{
config,
pkgs,
inputs,
lib,
...
}: let
dockerBin = "${pkgs.docker}/bin/docker";
mcpToolboxImage =
if pkgs.stdenv.hostPlatform.system == "aarch64-linux"
then "ghcr.io/timlisemer/mcp-toolbox/mcp-toolbox-linux-arm64:latest"
else "ghcr.io/timlisemer/mcp-toolbox/mcp-toolbox-linux-amd64:latest";
unstable = import inputs.nixpkgs-unstable {
config = {allowUnfree = true;};
system = pkgs.stdenv.hostPlatform.system;
};
in {
##########################################################################
## MCP Toolbox Docker Container ##
##########################################################################
virtualisation.oci-containers.containers.mcp-toolbox = {
image = mcpToolboxImage;
autoStart = true;
autoRemoveOnStop = true;
extraOptions = ["--network=docker-network" "--ip=172.18.0.15"];
volumes = [
"/mnt/docker-data/volumes/mcp-toolbox:/app/servers:rw"
];
environmentFiles = [
"/run/secrets/mcpToolboxENV"
];
};
##########################################################################
## MCP Toolbox volume permissions - make accessible to users ##
##########################################################################
systemd.services.mcp-toolbox-permissions = {
description = "Set permissions on mcp-toolbox volume for user access";
after = ["docker.service" "docker-mcp-toolbox.service"];
wantedBy = ["multi-user.target"];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
set -euo pipefail
VOLUME_PATH="/mnt/docker-data/volumes/mcp-toolbox"
if [ -d "$VOLUME_PATH" ]; then
echo "Setting permissions on $VOLUME_PATH..."
# Set execute-only ACL on parent directories for traversal (no read access)
${pkgs.acl}/bin/setfacl -m u:tim:x /mnt/docker-data
${pkgs.acl}/bin/setfacl -m u:tim:x /mnt/docker-data/volumes
# Set full access on the volume directory and contents
${pkgs.acl}/bin/setfacl -R -m u:tim:rwX "$VOLUME_PATH"
# Set default ACL so new files inherit permissions
${pkgs.acl}/bin/setfacl -R -d -m u:tim:rwX "$VOLUME_PATH"
echo "MCP Toolbox volume permissions set successfully"
else
echo "Warning: $VOLUME_PATH does not exist yet"
fi
'';
};
##########################################################################
## Setup Claude MCP servers ##
##########################################################################
system.activationScripts.claudeMcpSetup = {
text = ''
echo "[claude-mcp] Setting up MCP servers..."
# Run as tim user since claude config is per-user
${pkgs.sudo}/bin/sudo -u tim ${unstable.claude-code}/bin/claude mcp list 2>/dev/null | ${pkgs.gawk}/bin/awk -F: '/^[a-zA-Z0-9_-]+:/ {print $1}' | while read -r server; do
echo "[claude-mcp] Removing server: $server"
${pkgs.sudo}/bin/sudo -u tim ${unstable.claude-code}/bin/claude mcp remove --scope user "$server" 2>/dev/null || true
done
echo "[claude-mcp] Adding nixos-search server..."
${pkgs.sudo}/bin/sudo -u tim ${unstable.claude-code}/bin/claude mcp add nixos-search --scope user -- ${dockerBin} exec -i mcp-toolbox sh -c 'exec 2>/dev/null; /app/tools/mcp-nixos/venv/bin/python3 -m mcp_nixos.server'
echo "[claude-mcp] Adding tailwind-svelte server..."
${pkgs.sudo}/bin/sudo -u tim ${unstable.claude-code}/bin/claude mcp add tailwind-svelte --scope user -- ${dockerBin} exec -i mcp-toolbox node /app/tools/tailwind-svelte-assistant/run.mjs
echo "[claude-mcp] Adding context7 server..."
${pkgs.sudo}/bin/sudo -u tim ${unstable.claude-code}/bin/claude mcp add context7 --scope user -- ${dockerBin} exec -i mcp-toolbox npx -y @upstash/context7-mcp
echo "[claude-mcp] Adding agent-framework server..."
${pkgs.sudo}/bin/sudo -u tim ${unstable.claude-code}/bin/claude mcp add agent-framework --scope user -- \
/home/tim/.claude/run-with-env.sh ${pkgs.nodejs}/bin/node /mnt/docker-data/volumes/mcp-toolbox/agent-framework/dist/mcp/server.js
echo "[claude-mcp] MCP servers setup complete"
'';
};
##########################################################################
## Claude Code shared environment and hooks ##
##########################################################################
home-manager.sharedModules = [
{
home.file = {
# Claude Code shared environment
".claude/env.sh" = {
source = builtins.toPath ../files/.claude/env.sh;
executable = true;
};
".claude/hooks/pre-tool-use.sh" = {
source = builtins.toPath ../files/.claude/hooks/pre-tool-use.sh;
executable = true;
};
".claude/hooks/stop-off-topic-check.sh" = {
source = builtins.toPath ../files/.claude/hooks/stop-off-topic-check.sh;
executable = true;
};
".claude/hooks/post-tool-use.sh" = {
source = builtins.toPath ../files/.claude/hooks/post-tool-use.sh;
executable = true;
};
".claude/run-with-env.sh" = {
source = builtins.toPath ../files/.claude/run-with-env.sh;
executable = true;
};
# Claude Code commands
".claude/commands/commit.md" = {
source = builtins.toPath ../files/.claude/commands/commit.md;
};
".claude/commands/push.md" = {
source = builtins.toPath ../files/.claude/commands/push.md;
};
};
}
];
}~/.claude/
├── env.sh # Environment variables (secrets)
├── run-with-env.sh # Wrapper that sources env.sh
├── settings.json # Claude Code settings + hooks
├── hooks/
│ ├── pre-tool-use.sh # PreToolUse hook
│ ├── post-tool-use.sh # PostToolUse hook
│ └── stop-off-topic-check.sh # Stop hook
└── commands/
├── commit.md # /commit slash command
└── push.md # /push slash command
/mnt/docker-data/volumes/mcp-toolbox/agent-framework/
└── dist/
├── hooks/
│ ├── pre-tool-use.js
│ ├── post-tool-use.js
│ └── stop-off-topic-check.js
└── mcp/
└── server.js # MCP server (check, confirm, commit tools)
- NixOS rebuild deploys hook scripts to
~/.claude/via home-manager - Activation script registers MCP servers with
claude mcp add - Hooks load environment via
run-with-env.sh->env.shbefore calling agent-framework JS - MCP tools (check, confirm, commit) are available to Claude during conversations
- Secrets are loaded from SOPS-managed files at runtime
- Edit
config/servers.json- add your tool definition - Run
make rebuild
{
"tools": {
"my-tool": {
"enabled": true,
"docker_volume": false,
"type": "node",
"description": "What the tool does",
"repository": "https://github.com/user/repo",
"build_command": "npm install && npm run build",
"binary_path": "dist/index.js",
"capabilities": ["feature1", "feature2"]
}
}
}Configuration Options:
| Option | Type | Description |
|---|---|---|
enabled |
boolean | Whether to build and enable this tool |
docker_volume |
boolean | If true, tool data persists in servers/<name>/ and can run natively |
type |
string | Runtime type: node, python, go, rust |
repository |
string | Git repository URL to clone |
build_command |
string | Command to build the tool after cloning |
binary_path |
string | Path to the executable relative to tool directory |
capabilities |
array | List of tool capabilities (documentation only) |
When docker_volume: true is set for a tool:
- Build time: Tool is built normally, then moved to
/app/tools-builtin/<name>/ - First run: Built artifacts are copied to
/app/servers/<name>/(mounted volume) - Runtime: A symlink
/app/tools/<name>/->/app/servers/<name>/is created
This allows:
- Persistent data: Tool data survives container rebuilds
- Native execution: Tools can be run directly from
servers/<name>/on the host without Docker - Easy updates: Modify tool code directly in the volume
Currently enabled for: agent-framework
mcp-toolbox/
├── Dockerfile # Build environment with Node/Python/Go/Rust
├── docker-compose.yml # Container configuration
├── Makefile # Management commands
├── config/
│ └── servers.json # Tool definitions
├── scripts/
│ ├── install.sh # Build script for all tools
│ └── entrypoint.sh # Runtime initialization (symlinks, volume setup)
└── servers/ # Persistent storage for docker_volume tools (git-ignored)
└── <tool-name>/ # Tool data (e.g., servers/agent-framework/)
make build # Build Docker image
make run # Run container (foreground, Ctrl+C to stop)
make stop # Stop container
make restart # Restart container
make logs # View container logs
make shell # Open container shell
make status # List available MCP tools
make test # Test MCP tools respond
make clean # Remove container and image
make rebuild # Clean rebuildThe agent-framework tool requires API credentials. Two options are supported:
Option A: Direct Anthropic API
ANTHROPIC_API_KEY=sk-ant-...Option B: OpenRouter (Anthropic-compatible)
ANTHROPIC_API_KEY= # Leave empty
ANTHROPIC_BASE_URL=https://openrouter.ai/api
ANTHROPIC_AUTH_TOKEN=sk-or-... # Your OpenRouter keySee .env.example for the full template.
# Enter the container
docker exec -it mcp-toolbox /bin/bash
# Test mcp-nixos (inside container)
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | \
/app/tools/mcp-nixos/venv/bin/python3 -m mcp_nixos.server
# Test agent-framework (inside container via symlink)
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | \
node /app/tools/agent-framework/dist/mcp/server.js
# Test agent-framework natively (from host, docker_volume: true)
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | \
node /path/to/mcp-server-host/agent-framework/dist/mcp/server.js# Check all tools
docker exec mcp-toolbox ls -la /app/tools/
# Check volume-enabled tools (should show symlinks)
docker exec mcp-toolbox ls -la /app/tools/agent-framework