Sandbox
@strausmann/mcp-dockhand

MCP server for Dockhand Docker management

This repo exposes Dockhand's Docker management API as MCP tools, so an agent can inspect containers, manage stacks, work with images, and read system data through one endpoint. It uses Streamable HTTP, session-based auth, and transport security settings like host, origin, and bearer-token checks.

38 stars5 forksTypeScriptUpdated 7d ago
Who it's for

Builders who want Claude Code or another MCP client to manage Docker infrastructure through Dockhand.

What it delivers

You can manage containers, stacks, images, and related Docker resources from your agent instead of switching to Dockhand by hand.

What it does

Large MCP tool set

Exposes 280+ Dockhand endpoints as MCP tools across containers, stacks, images, environments, networks, volumes, git stacks, auth, audit, notifications, registries, settings, and users.

Streamable HTTP transport

Serves MCP over Streamable HTTP for container-friendly deployment, with SSE support for deploy operations.

Session-based authentication

Keeps MCP sessions alive and can relogin automatically when Dockhand returns 401.

Environment filtering

Enforces an environment filter on container, stack, image, network, and volume endpoints.

Transport security controls

Supports optional host allowlists, origin allowlists, and bearer-token auth for `/mcp`.

Docker-ready packaging

Includes a multi-stage Docker build, non-root runtime, health checks, and `docker-compose.yml`.

How to get it

  1. 1Run
    docker run -d \
      --name mcp-dockhand \
      -p 8080:8080 \
      -e DOCKHAND_URL=https://your-dockhand-server.com \
      -e DOCKHAND_USERNAME=your-username \
      -e DOCKHAND_PASSWORD=your-password \
      ghcr.io/strausmann/mcp-dockhand:latest
  2. 2Run
    git clone https://github.com/strausmann/mcp-dockhand.git
    cd mcp-dockhand
    npm install
    npm run build
    DOCKHAND_URL=https://your-server.com DOCKHAND_USERNAME=admin DOCKHAND_PASSWORD=secret npm start

README

MCP Dockhand

CI License: MIT Docker

An MCP (Model Context Protocol) server that exposes the Dockhand API as MCP tools. Manage your entire Docker infrastructure through AI assistants.

API coverage: 88.7% of in-scope Dockhand endpoints (282/318) have an MCP tool — see docs/coverage.md for the full, auto-updated breakdown by area.

Dockhand is a Docker management server that connects to multiple Docker hosts via Hawser agents. This MCP server provides full programmatic access to all Dockhand features.

Features

  • 280+ MCP Tools covering the Dockhand API — see docs/coverage.md for exact, auto-updated coverage
  • Streamable HTTP Transport (MCP Spec 2025-03-26) for Docker container hosting
  • Session-based Auth with auto-relogin on 401
  • SSE Support for deploy operations (start, stop, down, restart)
  • Environment Filter enforced on all container/stack/image/network/volume endpoints
  • Docker Ready with multi-stage build, non-root user, and health checks

Quick Start

Docker (recommended)

docker run -d \
  --name mcp-dockhand \
  -p 8080:8080 \
  -e DOCKHAND_URL=https://your-dockhand-server.com \
  -e DOCKHAND_USERNAME=your-username \
  -e DOCKHAND_PASSWORD=your-password \
  ghcr.io/strausmann/mcp-dockhand:latest

Docker Compose

services:
  mcp-dockhand:
    image: ghcr.io/strausmann/mcp-dockhand:latest
    container_name: mcp-dockhand
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      - DOCKHAND_URL=https://your-dockhand-server.com
      - DOCKHAND_USERNAME=your-username
      - DOCKHAND_PASSWORD=your-password

From Source

git clone https://github.com/strausmann/mcp-dockhand.git
cd mcp-dockhand
npm install
npm run build
DOCKHAND_URL=https://your-server.com DOCKHAND_USERNAME=admin DOCKHAND_PASSWORD=secret npm start

Configuration

VariableRequiredDefaultDescription
DOCKHAND_URLYes-Dockhand server URL
DOCKHAND_USERNAMEYes-Dockhand username
DOCKHAND_PASSWORDYes-Dockhand password
MCP_PORTNo8080Port for the MCP server
MCP_SESSION_TTL_SECONDSNo1800Inactivity timeout before a retained MCP session is expired
MCP_SESSION_CLEANUP_INTERVAL_SECONDSNo300Interval for removing expired sessions (clamped to the session TTL)
MCP_MAX_SESSIONSNo0Maximum retained sessions; 0 keeps the existing unlimited behavior
MCP_MAX_REQUEST_BODY_BYTESNo104857600 (100 MB)Maximum /mcp JSON request-body size in bytes. Ceiling ~512 MiB (Node's max string length — express.json must materialize the whole body as one string, so a larger body would fail regardless). Raise it when load_image needs a larger docker save tar — the base64 MCP payload is ~33% larger than the raw tar. A malformed, non-positive, or too-large value is ignored/capped with a startup warning. Passed through in docker-compose.yml, so setting it in .env reaches the container
MCP_HOSTNo0.0.0.0Listen address. Kept as the wildcard address by default so the published Docker port (-p 8080:8080 / docker-compose.yml) keeps working; see Securing the transport for the recommended way to protect the endpoint instead of binding loopback-only
MCP_ALLOWED_HOSTSNo(unset — Host check disabled)Comma-separated Host header allowlist for /mcp (DNS-rebinding protection). Opt-in: unset means no Host check at all (pre-existing behavior, so existing deployments aren't broken by an update). Recommended once you set it up — see Securing the transport
MCP_ALLOWED_ORIGINSNo(unset — Origin check disabled)Comma-separated Origin header allowlist for /mcp. Opt-in, same as above. Only enforced when a caller actually sends an Origin header at all (non-browser MCP clients typically don't)
MCP_AUTH_TOKENNo(unset — endpoint unauthenticated)Shared secret required as Authorization: Bearer <token> on every /mcp request. Opt-in; recommended once the endpoint is reachable beyond your own loopback — see Securing the transport
LOG_LEVELNoinfoerror, warn, info or debug. debug adds one line per Dockhand request (method, endpoint template, status, duration). For requests through the client the duration spans the full response body and a bytes body-size field is added; the login and self-check probes (which bootstrap the client and so can't route through it) log time-to-headers without a bytes field. Never a path segment or a parameter value. An unrecognised value warns and falls back to info.
TRUSTED_PROXIESNo(empty)Comma-separated addresses or CIDRs allowed to set X-Forwarded-For / X-Real-IP, e.g. 10.0.0.0/8, 100.64.0.0/10. Empty means the headers are ignored and the peer address is used.

Securing the transport

/mcp binds 0.0.0.0:8080 by default (see MCP_HOST above), and out of the box — with none of MCP_ALLOWED_HOSTS, MCP_ALLOWED_ORIGINS, or MCP_AUTH_TOKEN set — it accepts any request with no Host/Origin check and no authentication. This is the same behavior mcp-dockhand has always had, kept as the default deliberately: enabling a check by default would reject requests from any client that doesn't reach the server as localhost/127.0.0.1 (a LAN IP, a reverse proxy, a Docker network alias), breaking existing deployments on a routine update.

You should turn this on once /mcp is reachable beyond your own machine's loopback interface — the server holds one Dockhand admin credential and every tool call acts with that identity, so anyone who can open an MCP session controls Docker (container exec, host bind-mounts via create_container, file read/write, stored git credentials). With no protection configured, the server logs a [security] WARNING at startup as a reminder. Three independent, all-opt-in layers are available:

  1. Host allowlist (MCP_ALLOWED_HOSTS). Once set to a non-empty value, every request to /mcpPOST, GET, and DELETE — is rejected with 403 unless its Host header matches the allowlist. This is the primary defense against DNS-rebinding: a malicious web page cannot make the operator's browser reach the server under a Host value the allowlist accepts. Set it to however your client actually reaches the server — localhost:8080/127.0.0.1:8080 for the documented local setup, or, if you connect directly by address rather than through localhost (including the mcp-proxy remote-server setup below), the exact host:port your client sends, e.g. 100.100.50.40:8222. Get this wrong and every request is rejected with 403 Invalid Host header — check the message, it echoes the Host value it saw.
  2. Origin allowlist (MCP_ALLOWED_ORIGINS). Once set, any request that does send an Origin header not in the list is rejected with 403. A missing Origin header always passes (the SDK's own MCP client and most non-browser tooling never send one), so this is only useful if a browser-based client talks to /mcp directly; the Host allowlist above is what actually stops DNS-rebinding.
  3. Bearer token (MCP_AUTH_TOKEN). Once set, every /mcp request must carry Authorization: Bearer <token> or is rejected with 401; the comparison is constant-time. Recommended alongside the Host allowlist for any deployment reachable from more than the operator's own machine.
# .env — recommended configuration once /mcp is reachable beyond loopback
MCP_ALLOWED_HOSTS=dock-mcp.internal.example.com
# or, connecting directly by address instead of a hostname:
#MCP_ALLOWED_HOSTS=100.100.50.40:8222
MCP_AUTH_TOKEN=<a long random secret, e.g. `openssl rand -hex 32`>

Securing the server with CrowdSec

The server writes an nginx-format access line to stdout for every request, including the ones it rejects, while the structured application log goes to stderr. CrowdSec parses the access lines with its stock collections — no custom parser required.

Add an acquisition file on the host running your CrowdSec agent:

source: docker
container_name:
  - mcp-dockhand
labels:
  type: docker
  program: nginx-mcp

Both labels are required, and neither fails loudly if you forget it. type: docker enables crowdsecurity/docker-logs, which unwraps Docker's JSON envelope. program: nginx-mcp enables crowdsecurity/nginx-logs, which matches on program starting with nginx — the -mcp suffix keeps this source distinguishable from your other nginx sources. With one label missing the chain simply produces nothing, and nothing reports it.

Once wired up, the stock scenarios apply:

ScenarioWhat it means here
LePresidente/http-generic-401-bfRepeated 401 on /mcp — someone is guessing MCP_AUTH_TOKEN
crowdsecurity/http-dos-swithcing-uaRequest floods with rotating user agents

A 403 is worth watching too: it means a request failed the MCP_ALLOWED_HOSTS or MCP_ALLOWED_ORIGINS check, which is what a DNS-rebinding attempt looks like from here.

The stock 401 scenario only counts POST. Its filter is evt.Parsed.verb == 'POST' — one literal, not a list. This server serves POST, GET and DELETE on /mcp, and the bearer check runs ahead of all three, so a wrong token on GET /mcp or DELETE /mcp returns 401 exactly like POST does — and LePresidente/http-generic-401-bf never counts those. Someone guessing MCP_AUTH_TOKEN over GET /mcp is invisible to it.

This is a property of the upstream scenario, shared with every nginx deployment that uses it — not something this server's log format can fix. To close it, add a local scenario that drops the verb filter, or matches the three methods this server answers on. Until then, treat the row above as "repeated 401 on POST /mcp".

Set TRUSTED_PROXIES before you enable this. Behind a reverse proxy every request arrives from the proxy's address. Without TRUSTED_PROXIES that address is what gets logged — so the first ban CrowdSec issues takes out the proxy, and with it every user behind it. Set it to the address or subnet your proxy talks from.

The setting is equally deliberate in the other direction: the forwarding headers are only honoured from a peer on that list. Trusting them unconditionally would let any direct caller name an arbitrary third party and have them banned.

One expected side effect: the structured JSON lines share the container's log stream and carry the same program label, so they fail the nginx pattern and count as unparsed in cscli metrics. That is noise, not a fault — no alert, no decision.

MCP Client Configuration

Claude Desktop / Claude Code

Add to your MCP settings:

{
  "mcpServers": {
    "dockhand": {
      "url": "http://localhost:8080/mcp"
    }
  }
}

If the server enforces a bearer token (MCP_AUTH_TOKEN set — see Securing the transport), the client must send it as an Authorization header, or every request is rejected with 401. In Claude Code's .mcp.json, add a headers block — reference an environment variable so the token never lives in the (often version-controlled) config file:

{
  "mcpServers": {
    "dockhand": {
      "type": "http",
      "url": "http://your-server:8080/mcp",
      "headers": { "Authorization": "Bearer ${DOCKHAND_MCP_TOKEN}" }
    }
  }
}

Send the token only over an encrypted transport. A bearer over plain http:// on a shared network can be sniffed — terminate TLS at a reverse proxy, or reach the server over a WireGuard/Tailscale/VPN link (the app-layer HTTP is then encrypted by the tunnel).

Export DOCKHAND_MCP_TOKEN in the environment Claude Code is launched from (e.g. from a gitignored .env you source before starting). The Host/host:port you connect to must also be in the server's MCP_ALLOWED_HOSTS if that allowlist is set. For Claude Desktop (native config has no headers field), pass the token through the mcp-proxy workaround below — mcp-proxy forwards an Authorization header via its own environment/args.

Claude Desktop with a remote server (mcp-proxy)

Claude Desktop can fail to connect to a remote mcp-dockhand server (not localhost) using the native "url" config above, even though the endpoint itself is reachable. The symptom is a generic "not a valid MCP server" error in Claude Desktop, while a plain browser/curl request to the same URL correctly returns {"error":"Invalid or missing session ID"}. This is a known limitation of Claude Desktop with remote Streamable HTTP servers, not a mcp-dockhand bug.

Workaround: wrap the connection with mcp-proxy, which translates Streamable HTTP to stdio — a transport Claude Desktop handles reliably:

{
  "mcpServers": {
    "dockhand": {
      "command": "/path/to/mcp-proxy",
      "args": ["--transport", "streamablehttp", "http://your-server:8080/mcp"]
    }
  }
}

All tools load and work correctly through the proxy. Thanks to @deadrubberboy for reporting this and sharing the workaround (#90).

Tool Reference

Containers (27 tools)

ToolDescription
list_containersList all containers in an environment
get_containerGet container details
inspect_containerDocker inspect (full details)
get_container_logsGet container logs
get_container_statsGet resource usage stats
get_container_topGet running processes
start_containerStart a container
stop_containerStop a container
restart_containerRestart a container
pause_containerPause a container
unpause_containerUnpause a container
rename_containerRename a container
update_containerUpdate container settings
create_containerCreate a new container
get_container_shellsList available shells
exec_containerCreate a terminal exec session (execId + WS connectionInfo); does NOT run a one-shot command or return output — no such endpoint exists in the Dockhand API
list_container_filesBrowse files inside container
get_container_file_contentRead file from container
create_container_fileCreate an empty file or directory in container (no content — use write_container_file_content for that)
delete_container_fileDelete file in container
rename_container_fileRename file in container
chmod_container_fileChange file permissions
check_container_updatesCheck for image updates
get_pending_updatesGet pending updates
batch_update_containersBatch update containers
execute_batchRun a bulk lifecycle operation (start/stop/restart/remove/etc.) across containers, images, volumes, networks, or stacks
get_container_sizesGet container disk sizes
get_containers_statsGet aggregated stats

Stacks (21 tools)

ToolDescription
list_stacksList all stacks
get_stackGet stack details
create_stackCreate and optionally deploy a stack
start_stackStart a stack (compose up)
stop_stackStop a stack (compose stop)
restart_stackRestart a stack
down_stackTake down a stack (compose down)
delete_stackDelete a stack
get_stack_composeRead compose file
update_stack_composeUpdate compose file
get_stack_envRead environment variables
update_stack_envUpdate environment variables (merge by default — safe for partial updates; use mode="replace" to overwrite all)
get_stack_env_rawRead raw .env file
validate_stack_envValidate env variables
scan_stacksScan filesystem for stacks
adopt_stackAdopt an untracked stack
relocate_stackMove stack to new path
get_stack_sourcesGet stack sources
get_stack_base_pathGet base path
get_stack_path_hintsGet path suggestions
validate_stack_pathValidate a stack path

Images (9 tools)

ToolDescription
list_imagesList all images
get_imageGet image details
get_image_historyGet image layer history
tag_imageTag an image
remove_imageRemove an image
pull_imagePull an image
push_imagePush an image
scan_imageVulnerability scan (Trivy/Grype)
export_imageExport image as tarball

Environments (18 tools)

ToolDescription
list_environmentsList all environments
get_environmentGet environment details
create_environmentCreate an environment
update_environmentUpdate an environment
delete_environmentDelete an environment
test_environmentTest connection
test_environment_connectionTest without saving
detect_docker_socketAuto-detect socket
get_environment_timezoneGet timezone
set_environment_timezoneSet timezone
get_environment_update_checkGet update-check settings
set_environment_update_checkSet update-check settings
get_environment_image_pruneGet image prune settings
set_environment_image_pruneSet image prune settings
list_environment_notificationsList notifications
create_environment_notificationCreate notification
get_environment_notificationGet notification
delete_environment_notificationDelete notification

Networks (7 tools)

ToolDescription
list_networksList all networks
get_networkGet network details
inspect_networkInspect network
create_networkCreate a network
remove_networkRemove a network
connect_container_to_networkConnect container
disconnect_container_from_networkDisconnect container

Volumes (9 tools)

ToolDescription
list_volumesList all volumes
get_volumeGet volume details
inspect_volumeInspect volume
browse_volumeBrowse files in volume
get_volume_file_contentRead file from volume
release_volume_browseRelease browse session
clone_volumeClone a volume
export_volumeExport volume
remove_volumeRemove volume (destructive)

Git Stacks (15 tools)

ToolDescription
list_git_stacksList Git-based stacks
get_git_stackGet Git stack details
deploy_git_stackDeploy a Git stack (SSE)
sync_git_stackSync with remote repo
test_git_stackTest Git connection
get_git_stack_env_filesGet env files
trigger_git_webhookTrigger webhook
get_git_webhookGet webhook details
list_git_credentialsList Git credentials
create_git_credentialCreate Git credential
get_git_credentialGet credential details
update_git_credentialUpdate credential
delete_git_credentialDelete credential
list_git_repositoriesList Git repositories
create_git_repositoryCreate repository config

Dashboard & Activity (8 tools)

ToolDescription
get_dashboard_statsGet dashboard statistics
get_dashboard_preferencesGet display preferences
set_dashboard_preferencesSet display preferences
get_activity_feedGet activity feed
get_container_activityContainer activity
get_activity_eventsActivity events
get_activity_statsActivity statistics
get_merged_logsMerged logs from containers

Auth & Hawser (12 tools)

ToolDescription
get_auth_sessionCheck session status
get_auth_providersList auth providers
get_auth_settingsGet auth settings
create_oidc_providerCreate OIDC provider
get_oidc_providerGet OIDC provider
test_oidc_providerTest OIDC provider
create_ldap_providerCreate LDAP provider
get_ldap_providerGet LDAP provider
test_ldap_providerTest LDAP provider
list_hawser_tokensList Hawser tokens
create_hawser_tokenCreate Hawser token
revoke_hawser_tokenRevoke Hawser token

Audit (4 tools)

ToolDescription
get_audit_logGet audit log
get_audit_eventsGet audit event types
get_audit_usersAudit data by user
export_audit_logExport audit log

Notifications (8 tools)

ToolDescription
list_notificationsList notifications
create_notificationCreate notification
get_notificationGet notification
update_notificationUpdate notification
delete_notificationDelete notification
test_notificationTest notification
test_notification_configTest without saving
trigger_test_notificationTrigger a real test event for a given event type + payload

Registries (10 tools)

ToolDescription
list_registriesList registries
create_registryAdd registry
get_registryGet registry details
update_registryUpdate registry
delete_registryDelete registry
set_default_registrySet as default
search_registrySearch registry
get_registry_catalogGet catalog
get_registry_imageGet image from registry
get_registry_tagsGet image tags

System & Settings (19 tools)

ToolDescription
health_checkServer health
health_check_databaseDatabase health
get_host_infoHost information
get_system_infoSystem information
get_system_diskDisk usage
list_system_filesList system files
get_system_file_contentRead system file
get_changelogChangelog
get_dependenciesDependencies
get_general_settingsGeneral settings
update_general_settingsUpdate settings
get_theme_settingsTheme settings
update_theme_settingsUpdate theme
get_scanner_settingsScanner settings
update_scanner_settingsUpdate scanner
get_licenseLicense info
activate_licenseActivate license by name and key
get_prometheus_metricsPrometheus metrics
prune_allPrune all resources

Users, Roles & Preferences (20 tools)

ToolDescription
list_usersList users
create_userCreate user
get_userGet user details
update_userUpdate user
delete_userDelete user
get_user_mfa_statusMFA status
enable_user_mfaEnable MFA
disable_user_mfaDisable MFA
get_user_rolesGet user roles
add_user_roleAssign one role to a user (no bulk-replace)
remove_user_roleUnassign one role from a user
list_rolesList roles
create_roleCreate role with name + permissions object
get_roleGet role
`upd

Files in the repo

Repository payload25 top-level entries
  • .github
  • .husky
  • docs
  • scripts
  • src
  • tests
  • .commitlintrc.json
  • .dockerignore
  • .env.example
  • .gitignore
  • .npmrc
  • .releaserc.json
  • CHANGELOG.md
  • CLAUDE.md
  • docker-compose.yml
  • Dockerfile
  • eslint.config.js
  • LICENSE
  • package-lock.json
  • package.json
  • README.md
  • renovate.json
  • tsconfig.json
  • tsconfig.tests.json
  • vitest.config.ts

Discussion (0)

Ask about usage, or say what you built with it

Sign in to join the discussion.

No comments yet. Be the first to say what this is good for.

More connectors

High-performance code intelligence MCP server. Indexes codebases into a persistent knowledge graph — average repo in milliseconds. 158 languages, sub-ms queries, 99% fewer tokens. Single static binary, zero dependencies.

43k

Universal provider proxy for OpenAI Codex & Claude Code — use any LLM (Claude, Gemini, Grok, DeepSeek, Ollama…) with Codex CLI, App, SDK, and Claude Code

14k
okf-memory/
okf-agent-memory

Git-native persistent memory for AI coding agents. Implements Google OKF v0.2 with sub-300µs in-memory BM25 search, embedded MCP server, and progressive disclosure. Slashes token bloat by 80% with zero external databases or dependencies. Built in pure Go.

547
tirth8205/
code-review-graph

Local-first code intelligence graph for MCP and CLI. Builds a persistent map of your codebase so AI coding tools read only what matters, with benchmarked context reductions on reviews and large-repo workflows.

31k
2akouwu/
reverify

Stop your AI from making things up — it proposes, deterministic tools decide, every claim checked against ground truth with evidence. Grounded facts and context survive resets. Reverse engineering is the proving ground. MCP server + CLI.

1.1k
t8y2/dbxConnectors

20 MB lightweight cross-platform database client for 90+ databases, including MySQL, PostgreSQL, SQLite, Redis, MongoDB, DuckDB, SQL Server, and Dameng. Built-in AI, MCP Server, CLI, desktop and Docker. | 轻量级跨平台数据库管理工具,支持 MySQL、PostgreSQL、SQLite、Redis、MongoDB、达梦等 90+ 数据库,提供桌面端、Docker、CLI、内置 AI 助手和 MCP Server。

19k