MCP server for homelab infrastructure. Provides two tools: flux for Docker/Compose/host management and scout for SSH remote operations.
Version: 2.2.3 | Node.js ≥ 24 | MIT
Synapse MCP exposes Docker infrastructure and SSH operations as MCP tools. Connect it to any MCP client (Claude Code, Cursor, Gemini) and manage containers, inspect remote files, read logs, and run diagnostics across multiple hosts.
What it includes:
src/— TypeScript implementation (tools, schemas, services, transport)config/— Config examples and JSON Schemaskills/synapse/— Client-facing skill docstests/— Vitest test suite.claude-plugin/,.codex-plugin/,gemini-extension.json— Client manifests
Routes by action, then subaction. Total: 43 subactions.
| Subaction | Description | Destructive |
|---|---|---|
list |
List containers. Filter by state, name_filter, image_filter, label_filter |
— |
inspect |
Detailed container info. summary=true for basic info |
— |
logs |
Container logs. Supports lines, since, until, grep, stream |
— |
stats |
CPU/memory resource usage. Omit container_id for all containers |
— |
top |
Running processes inside a container | — |
search |
Full-text search across containers | — |
start |
Start a stopped container | — |
stop |
Stop a running container | Yes |
restart |
Stop then start a container | — |
pause |
Freeze container processes via cgroups | — |
resume |
Unfreeze a paused container | — |
pull |
Pull the latest image for a container | — |
recreate |
Delete and recreate container. pull=true by default |
Yes |
exec |
Execute a command inside a running container | Yes |
All subactions accept an optional host field. Destructive subactions require confirmation via MCP elicitation (or SYNAPSE_MCP_ALLOW_DESTRUCTIVE=true).
container:exec parameters: container_id, command, user (optional), workdir (optional), timeout (1000–300000 ms, default 30000).
container:logs parameters: lines (1–500, default 50), since/until (ISO 8601 or relative like "1h"), grep, stream (stdout/stderr/both).
| Subaction | Description | Destructive |
|---|---|---|
list |
List all Compose projects. Filter with name_filter |
— |
status |
Project service status. Filter with service_filter |
— |
logs |
Project logs. Supports service, lines, since, until, grep |
— |
pull |
Pull images for a project or specific service | — |
build |
Build project images. no_cache=true to force rebuild |
— |
up |
Start project. detach=true by default |
— |
down |
Stop and remove containers. remove_volumes=true requires force=true |
Yes |
restart |
Restart all project services | Yes |
recreate |
Recreate all project containers. Requires force=true |
Yes |
refresh |
Rescan filesystem to refresh Compose project cache | — |
compose:down note: remove_volumes=true AND force=true are both required to delete volumes. This prevents accidental data loss.
compose:logs time filter: Accepts durations (30s, 5m, 1.5h, 100ms), dates (2024-01-01), RFC3339 timestamps, or Unix timestamps.
Default Compose search paths (Unraid-centric defaults, override per host):
/compose/mnt/cache/compose/mnt/cache/code
| Subaction | Description | Destructive |
|---|---|---|
info |
Docker daemon information | — |
df |
Docker disk usage | — |
images |
List images. dangling_only=true for untagged only |
— |
networks |
List Docker networks | — |
volumes |
List Docker volumes | — |
pull |
Pull an image by name | — |
build |
Build an image. context must be absolute path |
— |
rmi |
Remove an image. Requires force=true |
Yes |
prune |
Remove unused resources. prune_target required, force=true required |
Yes |
docker:prune targets: containers, images, volumes, networks, buildcache, all.
docker:build parameters: context (absolute path, no ..), tag, dockerfile (relative to context, optional), no_cache.
| Subaction | Description |
|---|---|
status |
Docker connectivity check |
info |
OS, kernel, architecture, hostname |
uptime |
System uptime |
resources |
CPU, memory, disk usage via SSH |
services |
Systemd service status. Filter by service name and state |
network |
Network interfaces and IP addresses |
mounts |
Mounted filesystems |
ports |
All port mappings for containers. Filter by protocol, state, source |
doctor |
Diagnostic checks. Optional checks array: resources, containers, logs, processes, docker, network |
{ "action": "help", "topic": "container:list", "format": "markdown" }Returns auto-generated documentation. topic is optional.
Routes by action. Total: 16 operations.
| Action | Description | Destructive |
|---|---|---|
nodes |
List all configured SSH hosts | — |
peek |
Read file or directory on a remote host. tree=true shows directory tree |
— |
find |
Find files by glob pattern. Parameters: host, path, pattern, depth, limit |
— |
ps |
List processes. Sort by cpu/mem/pid/time. Filter by grep or user |
— |
df |
Disk usage for a remote host. Optional path to target a specific mount |
— |
delta |
Compare files between two remote locations, or a remote file against inline content |
— |
exec |
Run an allowlisted command on a remote host | Yes |
emit |
Run a command on multiple hosts simultaneously | Yes |
beam |
Transfer a file from one remote path to another | Yes |
peek parameters: host, path (safe chars only, no ..), tree (boolean), depth (1–10, default 3).
exec and emit — command allowlist: Commands are validated at runtime. Only these commands are permitted: cat, head, tail, grep, rg, find, ls, tree, wc, sort, uniq, diff, stat, file, du, df, pwd, hostname, uptime, whoami, git. Write commands (rm, mkdir, cp) are blocked.
exec parameters: host, path (absolute, working directory), command, timeout (ms, default 30000, max 300000).
emit parameters: targets (array of { host, path } objects), command, timeout.
beam parameters: source and destination as { host, path } objects.
delta parameters: source ({ host, path }), then either target ({ host, path }) or content (inline string up to 1 MB).
| Subaction | Description |
|---|---|
pools |
List ZFS pools. Filter by pool name or health (online/degraded/faulted) |
datasets |
List datasets. Filter by pool, type (filesystem/volume), recursive |
snapshots |
List snapshots. Filter by pool, dataset, limit |
All zfs subactions require host.
| Subaction | Description |
|---|---|
syslog |
Read /var/log system logs. Supports lines, grep |
journal |
Systemd journal. Supports lines, since, until, unit, priority, grep |
dmesg |
Kernel ring buffer. Requires root or CAP_SYSLOG |
auth |
Authentication logs (/var/log/auth.log or equivalent) |
journal priority levels: emerg, alert, crit, err, warning, notice, info, debug.
journal time formats: ISO 8601 timestamps or relative strings like "2h ago", "yesterday".
All logs subactions accept host, lines (1–500, default 50), and grep.
/plugin marketplace add jmagar/claude-homelab
/plugin install synapse-mcp @jmagar-claude-homelabnpm install
npm run build
npm startThe published binary:
synapse-mcp # stdio transport (default)
synapse-mcp --http # HTTP transportdocker compose up -dSee the full docker-compose.yaml example below.
Synapse MCP loads hosts in this priority order:
SYNAPSE_CONFIG_FILEenv var (explicit path)./synapse.config.json(current directory)~/.config/synapse-mcp/config.json(XDG)~/.synapse-mcp.json(home directory)SYNAPSE_HOSTS_CONFIGenv var (JSON array)~/.ssh/config(auto-discovery)/var/run/docker.sock(local Docker fallback)
You do not need a config file. If ~/.ssh/config lists hosts with HostName and User, they are discovered automatically.
Full example:
{
"$schema": "./config/config.schema.json",
"hosts": [
{
"name": "local",
"host": "localhost",
"protocol": "ssh",
"dockerSocketPath": "/var/run/docker.sock",
"tags": ["local", "development"]
},
{
"name": "nas",
"host": "192.168.1.100",
"port": 22,
"protocol": "ssh",
"sshUser": "admin",
"sshKeyPath": "~/.ssh/id_ed25519",
"dockerSocketPath": "/var/run/docker.sock",
"composeSearchPaths": ["/opt/stacks", "/srv/docker"],
"tags": ["production", "storage"]
},
{
"name": "api-only-host",
"host": "docker.local",
"port": 2375,
"protocol": "http",
"tags": ["api-only"]
}
]
}Host fields:
| Field | Required | Description |
|---|---|---|
name |
Yes | Unique identifier. Alphanumeric, _, - only |
host |
Yes | Hostname, IP, or localhost |
protocol |
Yes | ssh (recommended), http, or https |
sshUser |
If protocol=ssh |
SSH username |
port |
No | SSH port (default 22) or Docker API port (default 2375/2376) |
sshKeyPath |
No | Path to SSH private key. Defaults to SSH agent |
dockerSocketPath |
No | Docker socket path (default /var/run/docker.sock) |
composeSearchPaths |
No | Dirs to search for Compose projects. Overrides Unraid defaults |
tags |
No | String array for grouping and filtering |
Security warning: protocol: "http" exposes Docker over TCP without authentication. Use ssh for all real deployments.
Any entry in ~/.ssh/config with HostName and User is loaded automatically:
Host nas
HostName 192.168.1.100
User admin
IdentityFile ~/.ssh/id_ed25519
Host pi
HostName 192.168.1.50
User pi
IdentityFile ~/.ssh/id_rsaManual synapse.config.json entries override SSH config entries with the same name.
-
Generate a key (if needed):
ssh-keygen -t ed25519 -C "synapse-mcp" -
Copy the public key to each host:
ssh-copy-id [email protected]
-
Test connectivity:
ssh [email protected] "echo ok"
When running in Docker, mount your SSH directory read-only:
volumes:
- ${HOME}/.ssh:/home/node/.ssh:roThe container runs as the node user, so keys must be readable by that user. The mount path /home/node/.ssh is used by the container.
| Variable | Required | Default | Description |
|---|---|---|---|
SYNAPSE_CONFIG_FILE |
No | — | Explicit path to synapse.config.json |
SYNAPSE_HOSTS_CONFIG |
No | — | JSON array of host configs as a fallback |
SYNAPSE_DEFAULT_HOST |
No | — | Default host when none is specified in a request |
SYNAPSE_EXCLUDE_HOSTS |
No | — | Comma-separated host names to skip during discovery |
SYNAPSE_ALLOW_ROOT_LOGIN |
No | false |
Allow SSH as root without elicitation prompt |
SYNAPSE_MCP_ALLOW_DESTRUCTIVE |
No | false |
Skip elicitation prompts for destructive operations |
SYNAPSE_MCP_ALLOW_YOLO |
No | false |
Bypass ALL elicitation confirmation gates (strict =true check only) |
SYNAPSE_DEBUG_ERRORS |
No | false |
Include full error details in responses (do not use in production) |
SYNAPSE_MCP_TRANSPORT |
No | stdio |
Set to http to enable HTTP transport |
SYNAPSE_MCP_PORT |
No | 3000 |
HTTP server listen port |
SYNAPSE_MCP_HOST |
No | 127.0.0.1 |
HTTP server bind address. Set 0.0.0.0 for external access |
SYNAPSE_MCP_TOKEN |
If HTTP | — | Bearer token for HTTP transport. Generate with openssl rand -hex 32 |
SYNAPSE_MCP_NO_AUTH |
No | — | Set true to disable bearer token auth. Not recommended |
SYNAPSE_MCP_SESSION_TTL_MS |
No | 1800000 |
Session idle timeout in ms (30 minutes) |
SYNAPSE_MCP_ALLOWED_HOSTS |
No | — | Comma-separated allowed Host header values (DNS rebinding protection) |
DOCKER_SSH_CONNECT_TIMEOUT_MS |
No | 5000 |
Docker-over-SSH connection timeout in ms |
COMPOSE_HOST_RESOLUTION_TIMEOUT_MS |
No | 30000 |
Compose project host auto-resolution timeout in ms |
PUID |
No | 1000 |
User ID for Docker container process |
PGID |
No | 1000 |
Group ID for Docker container process |
DOCKER_NETWORK |
No | mcp-net |
Docker network name for Compose |
SYNAPSE_ALLOW_ROOT_LOGIN: When false, SSH operations that would run as root require confirmation via MCP elicitation. If your client does not support elicitation, the operation is blocked.
SYNAPSE_MCP_ALLOW_DESTRUCTIVE: When false, these operations require MCP elicitation confirmation: container:stop, container:recreate, container:exec, compose:down, compose:restart, compose:recreate, docker:rmi, docker:prune, scout:exec, scout:emit, scout:beam. When your client does not support elicitation, the operation is blocked entirely.
SYNAPSE_MCP_ALLOW_YOLO: When true, bypasses ALL elicitation confirmation gates — both confirmDestructiveAction and confirmRootLogin return immediately without prompting. Only the exact string "true" activates this; 1, yes, and TRUE do not. YOLO fires before SYNAPSE_MCP_ALLOW_DESTRUCTIVE when both are set. Does NOT bypass schema-level force: true fields — callers must still supply those explicitly. A startup warning is logged whenever this is active.
SYNAPSE_DEBUG_ERRORS: When true, full stack traces and internal error details appear in tool responses. Do not enable in production — this may expose sensitive host information.
Synapse MCP maintains a per-host SSH connection pool. This avoids repeated handshakes when executing many operations against the same host.
Default pool behavior:
- Max 3 connections per host
- Idle timeout: 60 seconds
- Connection timeout: 5 seconds
- Health checks every 30 seconds
Pool configuration is not currently exposed via environment variables. To change defaults, edit src/services/ssh-pool.ts.
The container needs access to the Docker socket to manage local Docker resources:
volumes:
- /var/run/docker.sock:/var/run/docker.sockThe container runs as user 1000:1000 by default. Add the container to the Docker socket group so it can read the socket without running as root:
group_add:
- "${DOCKER_SOCKET_GID:-981}"Find your Docker socket GID:
stat -c '%g' /var/run/docker.socknetworks:
mcp-net:
name: ${DOCKER_NETWORK:-mcp-net}
external: true
services:
synapse-mcp:
image: ghcr.io/jmagar/synapse-mcp:latest
container_name: synapse-mcp
restart: unless-stopped
user: "${PUID:-1000}:${PGID:-1000}"
group_add:
- "${DOCKER_SOCKET_GID:-981}"
environment:
SYNAPSE_MCP_TRANSPORT: http
SYNAPSE_MCP_PORT: 3000
SYNAPSE_MCP_TOKEN: "${SYNAPSE_MCP_TOKEN}"
SYNAPSE_DEFAULT_HOST: nas
ports:
- "${SYNAPSE_MCP_PORT:-3000}:${SYNAPSE_MCP_PORT:-3000}"
volumes:
- ./config/synapse.config.json:/config/synapse.config.json:ro
- ${HOME}/.ssh:/home/node/.ssh:ro
- /var/run/docker.sock:/var/run/docker.sock
networks:
- mcp-net
deploy:
resources:
limits:
memory: 1024M
cpus: "1"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:${SYNAPSE_MCP_PORT:-3000}/health || exit 1"]
interval: 30s
timeout: 5s
retries: 5
start_period: 30sWhen running with --http or SYNAPSE_MCP_TRANSPORT=http:
| Endpoint | Method | Description |
|---|---|---|
/mcp |
POST | Client-to-server (new session or route existing) |
/mcp |
GET | Server-to-client SSE stream (requires Mcp-Session-Id header) |
/mcp |
DELETE | Terminate a session |
/health |
GET | Liveness check (no auth required) |
/ready |
GET | Readiness check (no auth required) |
For HTTP transport, configure the MCP server in your client:
{
"mcpServers": {
"synapse": {
"type": "http",
"url": "http://synapse-mcp:3000/mcp",
"headers": {
"Authorization": "Bearer <your-token>"
}
}
}
}For stdio transport:
{
"mcpServers": {
"synapse": {
"command": "node",
"args": ["/path/to/synapse-mcp/dist/index.js"]
}
}
}{ "action": "container", "subaction": "list", "state": "running" }Target a specific host:
{ "action": "container", "subaction": "list", "host": "nas", "state": "all" }{
"action": "container",
"subaction": "logs",
"container_id": "nginx",
"host": "nas",
"lines": 100,
"since": "1h",
"grep": "error"
}{ "action": "container", "subaction": "restart", "container_id": "nginx", "host": "nas" }{ "action": "container", "subaction": "pull", "container_id": "nginx", "host": "nas" }{ "action": "container", "subaction": "recreate", "container_id": "nginx", "host": "nas", "pull": false }{ "action": "compose", "subaction": "pull", "project": "media-stack", "host": "nas" }{ "action": "compose", "subaction": "recreate", "project": "media-stack", "host": "nas", "force": true }{ "action": "host", "subaction": "resources", "host": "nas" }{
"action": "host",
"subaction": "doctor",
"host": "nas",
"checks": ["resources", "containers", "docker"]
}{ "action": "nodes" }{ "action": "peek", "host": "nas", "path": "/etc/docker/daemon.json" }{ "action": "peek", "host": "nas", "path": "/opt/stacks", "tree": true, "depth": 2 }{ "action": "find", "host": "nas", "path": "/opt/stacks", "pattern": "*.yaml", "depth": 3 }{ "action": "df", "host": "nas" }{
"action": "logs",
"subaction": "journal",
"host": "nas",
"unit": "docker.service",
"priority": "err",
"since": "2h ago",
"lines": 100
}{ "action": "zfs", "subaction": "pools", "host": "nas" }{ "action": "zfs", "subaction": "datasets", "host": "nas", "pool": "tank", "recursive": true }{
"action": "emit",
"targets": [
{ "host": "nas", "path": "/home" },
{ "host": "pi", "path": "/home" }
],
"command": "df -h"
}All remote paths are validated against a safe character set ([a-zA-Z0-9._\-/]) and checked for path traversal (.. as a path component). Shell metacharacters are rejected to prevent command injection (CWE-78).
scout:exec and scout:emit run commands through ALLOWED_READ_COMMANDS — a static read-only allowlist (see the full list under exec above). mkdir, rm, and all other write commands are blocked. cp is write-capable and intentionally excluded from the public surface; it is only available to internal services (e.g., scout:beam file transfer).
SSH usernames and key paths are validated before shell interpolation. Non-alphanumeric characters (with limited exceptions) are rejected.
HTTP transport requires a Bearer token set via SYNAPSE_MCP_TOKEN. To generate one:
openssl rand -hex 32If neither SYNAPSE_MCP_TOKEN nor SYNAPSE_MCP_NO_AUTH=true is set, the server refuses to start.
Set SYNAPSE_MCP_ALLOWED_HOSTS to a comma-separated list of allowed Host header values. Requests with other Host values are rejected.
npm run build # compile TypeScript
npm run typecheck # type-check without emitting
npm test # run unit tests
npm run test:coverage # run tests with coverage report
npm run test:integration # run integration tests
npm run lint # lint with Biome
npm run format # format with BiomeVia Justfile:
just build
just test
just lint
just up # docker compose up -d
just down # docker compose down
just logs # docker compose logs -f
just health # curl /health endpoint
just gen-token # generate a new bearer tokennpm run typecheck
npm test
npm run lintIntegration tests require live SSH/Docker access to configured hosts:
npm run test:integrationHealth check (HTTP transport):
curl -sf http://localhost:3000/health | jq .| Plugin | Category | Description |
|---|---|---|
| homelab-core | core | Core agents, commands, skills, and setup/health workflows for homelab management. |
| overseerr-mcp | media | Search movies and TV shows, submit requests, and monitor failed requests via Overseerr. |
| unraid-mcp | infrastructure | Query, monitor, and manage Unraid servers: Docker, VMs, array, parity, and live telemetry. |
| unifi-mcp | infrastructure | Monitor and manage UniFi devices, clients, firewall rules, and network health. |
| gotify-mcp | utilities | Send and manage push notifications via a self-hosted Gotify server. |
| swag-mcp | infrastructure | Create, edit, and manage SWAG nginx reverse proxy configurations. |
| arcane-mcp | infrastructure | Manage Docker environments, containers, images, volumes, networks, and GitOps via Arcane. |
| syslog-mcp | infrastructure | Receive, index, and search syslog streams from all homelab hosts via SQLite FTS5. |
| plugin-lab | dev-tools | Scaffold, review, align, and deploy homelab MCP plugins with agents and canonical templates. |
MIT