docker-mcp-server
docker-mcp-server is an MCP server that lets AI agents fully manage Docker environments — containers, images, networks, volumes, builds, Compose projects, Swarm clusters, and more.
Container Management
Run, create, start, stop, restart, pause, unpause, remove, rename, and kill containers
Execute commands inside running containers, fetch logs (one-shot or streaming), and view resource stats
Inspect details, top processes, filesystem diffs, port bindings, and healthcheck state
Commit container state as a new image; update resource limits; wait for stop/healthy
Export filesystems as tar archives; upload/download archives to/from containers
Image Management
Build (classic builder or Buildx/BuildKit), pull, push, tag, remove, prune, save, load, and search images
Inspect layer history and retrieve registry data without pulling
Network & Volume Management
Create, list, inspect, connect, disconnect, and remove networks
Create, list, inspect, prune, remove, and backup/restore volumes
Docker Compose
up,down,start,stop,restart,pause,unpause,kill,build,pull,run,exec,ps,logs,images,port,top,wait,cp,lsRender canonical config with variable substitution and profile merging
Buildx (BuildKit)
Build multi-platform images with cache, SBOM/provenance attestations, secrets, and SSH
Bake multiple targets from HCL/JSON/Compose bake files
Inspect/create manifest lists; manage builder instances; view and prune build history and cache
Swarm Orchestration
Init, join, update, leave, and unlock Swarm clusters
Create, list, scale, update, rollback, inspect, and remove services
Manage secrets, configs, nodes, and join tokens
Docker Scout
CVE scanning, quickview, recommendations, compare, and SBOM generation
Registry Interaction
List tags, inspect manifests, get image config (env/entrypoint/labels) without pulling
Docker Hub tags, repo info, and rate limit queries; login/logout
Docker Contexts
List, inspect, create, use, and remove Docker CLI contexts
System & Daemon Operations
Ping, version, info, disk usage, real-time event streaming, login/logout
Multi-daemon support (local socket, remote SSH/TCP/TLS) with per-host read-only guards
Close/reconnect pooled connections; list configured hosts
Key Configuration Options
Read-only / no-destructive modes to restrict the tool surface
Domain disabling to hide entire feature sets (e.g., swarm, scout) from agents
Provenance labels: every created object is stamped with
docker-mcp-server.managed=trueNo telemetry — runs entirely locally
Provides tools for managing Docker containers, images, networks, volumes, swarm services, secrets, configs, nodes, plugins, and more, enabling AI agents to interact with Docker environments.
docker-mcp-server
More than just a fully featured MCP server that lets AI agents manage Docker - containers, images, networks, volumes, swarm services, secrets, configs, nodes, plugins, etc., it helps you create workflows to easily manage your Docker environments.
It gives you much more control and flexibility than calling the Docker CLI directly: each operation is exposed as its own typed tool, marked read-only or not, with destructive actions separately flagged. This means a client can auto-approve reads while always confirming anything destructive, and the whole server can also be switched into a read-only or no-destructive mode as a blanket safeguard. Output is bounded rather than left to grow unboundedly - capped with a truncated flag instead of silently overflowing the agent's context.
For simple cases, you can just install and go with no configuration required - once loaded it will discover your local Docker socket and expose the full command surface to your AI agent. For more advanced users it can manage multiple Docker daemons, e.g. both your local dev environment and also a remote production environment over TCP, TLS or SSH in a single session. It can also be configured to mark some daemons as read-only, so you can monitor them without the risk of making accidental changes.
It can even be run on a machine without Docker installed and manage remote daemons over SSH, TLS or TCP (some features require SSH). The AI itself does not require shell or SSH access.
The MCP server also exposes things like logs and stats as resources so that you can monitor and triage, enabling you to answer questions like 'why did my container crash?', 'what is the state of my swarm?', 'am I suffering memory pressure?', 'what is the disk usage of my volumes?', 'what differences are there between my test and production systems?', and more...
Documentation is built for the agent, not just the person configuring it: an MCP resource exposes the Docker SDK reference in-session (with a tool-callable fallback for clients that can't read resources), and a live tool-catalog resource reports exactly what's registered under the current configuration. Each tool's own description names its nearest siblings and when to prefer each, states preconditions and side effects in plain language, and is honest about when it can still fail - so an agent can pick the right tool on the first try among 150+ options, not guess.
docker-mcp-server is optimized to work efficiently with the new generation of MCP clients that support lazy tool loading. For clients that still eagerly load all tools, the server can optionally be configured to exclude tools from a subset of domains (e.g. exclude 'swarm' and 'scout' tools) to reduce the tool list size. It's also possible to put the MCP server into 'read-only' or 'no-destructive' modes that prevent any tools with write or destructive capabilities from being registered, which again reduces the footprint.
The server runs entirely on your machine, either natively, as an mcpb bundle, as a Claude plugin, or containerized, and sends no telemetry. You are entirely in control - see the Privacy Policy.
Requirements
Note: If you're using the containerized MCP server or MCPB bundle, the Python and uv requirements are taken care of for you.
A running Docker daemon reachable from the host that runs the server (the standard
DOCKER_HOST/ unix socket conventions apply)uv for dependency management
Intel (x86_64) macOS only: installing natively (via
uvx/pip, the Claude plugin, or the.mcpbbundle, all of which resolve dependencies locally) requires Rust and OpenSSL 3.x, becausecryptography- a transitive dependency, viamcp->pyjwt[crypto]- has shipped no x86_64 macOS wheel since version 49.0.0 and must be built from source there. If you'd rather not install a build toolchain, use the container image instead - it runs the same prebuilt Linux binary regardless of your Mac's CPU architecture, so this doesn't apply to it. See Security considerations for more.
Related MCP server: Docker MCP Server
Using the server
The server is published to PyPI as docker-mcp-server. Add an entry to your AI tool's MCP configuration (commonly mcp.json or the equivalent in your client) pointing uvx at it - uv will fetch and cache the package on first use:
{
"mcpServers": {
"docker-mcp-server": {
"command": "uvx",
"args": ["docker-mcp-server"],
"env": {}
}
}
}To pin a specific version, append ==<version> to the package name (e.g. docker-mcp-server==1.5.0). If you'd rather install it onto your PATH, pipx install docker-mcp-server gives you the docker-mcp-server console script (a docker-mcp alias is also installed).
Installing from git instead. To run an unreleased revision straight from this repository:
{
"mcpServers": {
"docker-mcp-server": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/L337-org/docker-mcp.git",
"docker-mcp-server"
],
"env": {}
}
}
}To pin a specific revision, append @<tag-or-commit> to the git URL.
Install as a Desktop Extension (.mcpb)
For Claude Desktop, a one-click bundle is attached to each
GitHub Release as
docker-mcp-server-<version>.mcpb (with a matching .sha256). Download it and drag it into
Settings > Extensions, or use Settings > Extensions > Advanced settings > Install extension...
and pick the file. The install dialog surfaces a Docker host(s) field and the read-only /
no-destructive / disabled-domain switches, so no manual JSON editing is needed.
It's a uv-type bundle: Claude Desktop's managed
uv resolves the dependencies and runs the server, so the only host prerequisite is Docker itself -
no separate Python, uv, or git. Leave the Docker host(s) field blank to use your default
Docker context; set one endpoint (ssh://user@host) for a remote daemon, or list several (see
Managing several daemons).
Install as a Claude plugin
The claude-plugin/ folder packages the server as a plugin for Claude Code and
Cowork, for Anthropic's plugin directory. It runs uvx docker-mcp-server==<version>, pinned to
the release the plugin was built from, so it needs uv on the host.
It ships a lockfile pinning every dependency by hash, which the directory describes its locked
launch as installing exactly; loaded any other way, uvx resolves the dependencies at install time.
It offers the same settings as the .mcpb install dialog (Docker host(s), read-only,
no-destructive and disabled domains), set in the plugin's settings in Claude Code (/plugin). The
release pipeline updates the plugin the directory serves only after a release has been published
to PyPI and the plugin has been installed and tested from it.
Run as a container
Running the server as a container removes the Python / uv / git prerequisites entirely - the only
thing the host needs is Docker, which you already have. Prebuilt multi-arch images (linux/amd64 +
linux/arm64) are published on each release to Docker Hub (gavinlucas/docker-mcp-server) and
GHCR (ghcr.io/l337-org/docker-mcp-server) - the two are identical. Point your MCP client at
docker run:
{
"mcpServers": {
"docker-mcp-server": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-v", "/var/run/docker.sock:/var/run/docker.sock",
"gavinlucas/docker-mcp-server:latest"
],
"env": {}
}
}
}-i is required (the server speaks MCP over stdio); --rm cleans up when the client disconnects. To
pin a version, replace :latest with a release tag (e.g. :1.5.1). To pull from GHCR instead, use
ghcr.io/l337-org/docker-mcp-server:latest.
Image renamed. As of 1.5.0 the image is published as
docker-mcp-server(matching the PyPI name). The oldghcr.io/gavinlucas/docker-mcpimage is frozen at 1.4.0 and no longer updated - point new pulls atghcr.io/l337-org/docker-mcp-server.
Image variants. Two variants are published to both registries (gavinlucas/docker-mcp-server on
Docker Hub and ghcr.io/l337-org/docker-mcp-server on GHCR), both built from one Dockerfile. The
CLI-backed domains (Compose, Stack, Buildx, Scout, Context) shell out to the docker CLI and its
plugins; in Stack, only stack_deploy does.
Variant | Tags | Approx. size | Includes |
|
| ~510 MB | docker CLI + compose + buildx + scout |
|
| ~315 MB | docker CLI + compose + buildx |
Scout's plugin binary alone accounts for the ~195 MB jump from no-scout to full. The no-scout
image also defaults DOCKER_MCP_SERVER_DISABLE=scout, so the scout tools don't register - the agent is
never offered tools whose CLI plugin isn't present (it sees a smaller, fully-working tool list rather
than scout tools that error on every call). Override at runtime with -e DOCKER_MCP_SERVER_DISABLE=... if you
ever need to change the disabled set (note it replaces, not appends).
Building it yourself. All variants build from the repo's Dockerfile via build args:
docker build -t docker-mcp-server:full . # full (default)
docker build --build-arg INSTALL_SCOUT=0 --build-arg DISABLE_DOMAINS=scout \
-t docker-mcp-server:no-scout . # no-scout
docker build --build-arg INSTALL_CLI=0 -t docker-mcp-server:lite . # lite (SDK-only, ~165 MB)The lite image (docker-py SDK tools only - Compose/Buildx/Scout/Context degrade to "plugin
unavailable") is buildable but not published.
Reaching the daemon from inside the container. The image defaults DOCKER_HOST to
unix:///var/run/docker.sock, so mounting your host's socket onto that path is all that's needed.
Where the host socket is, however, varies - and the server prints a platform-aware hint to stderr
if it can't connect at startup:
Linux:
-v /var/run/docker.sock:/var/run/docker.sock(rootless:-v $XDG_RUNTIME_DIR/docker.sock:/var/run/docker.sock).macOS (Docker Desktop): the real socket is usually
~/.docker/run/docker.sock- mount it onto the in-container path:-v $HOME/.docker/run/docker.sock:/var/run/docker.sock(or enable Settings > Advanced > Allow the default Docker socket and use/var/run/docker.sock).Windows (Docker Desktop / WSL2): the engine uses a named pipe, not a Unix socket - prefer
-e DOCKER_HOST=tcp://host.docker.internal:2375(enable the TCP endpoint in Docker Desktop). That endpoint is unauthenticated and unencrypted - keep it bound to localhost, disable it when you're not using it, and use TLS orDOCKER_HOST=ssh://...for any remote daemon.Remote / TLS / SSH daemon: skip the socket mount and pass
-e DOCKER_HOST=...(plus the TLS vars below) - see Talking to a remote daemon.
Host filesystem access. Inside a container, the file-path tools (image_save / container_export
with dest_path, image_load / container_archive_put with from_file, container_archive_get_to_file,
and compose project_dir / files) resolve paths inside the container,
not on your host. Bind-mount any directory you want to exchange files through - using the same
path inside and out keeps host and container paths identical:
-v $HOME/docker-work:$HOME/docker-workIf you call one of these tools with a path that isn't on a bind mount, the server refuses up front
with a message telling you exactly which -v to add - a write to an unmapped path would otherwise be
silently discarded when the container exits. (The in-band byte tools, capped at 32 MiB, need no
mount.) Configuration env vars (DOCKER_MCP_SERVER_READONLY, DOCKER_HOST, etc.) go in the client's env
block exactly as for the uvx install.
Talking to a remote daemon
When DOCKER_HOST is set the server uses it directly (via docker.from_env(), so DOCKER_TLS_VERIFY / DOCKER_CERT_PATH are honoured too). Common overrides via env:
"env": {
"DOCKER_HOST": "tcp://remote-host:2375",
"DOCKER_TLS_VERIFY": "1",
"DOCKER_CERT_PATH": "/path/to/certs"
}Default daemon (no DOCKER_HOST). With DOCKER_HOST unset, the server resolves the daemon the way the docker CLI does, rather than assuming /var/run/docker.sock: it follows the active Docker context (DOCKER_CONTEXT, else currentContext from ~/.docker/config.json, reading the endpoint from that context's meta.json), and if that yields nothing it probes the well-known socket locations (~/.docker/run/docker.sock for Docker Desktop 4.13+, $XDG_RUNTIME_DIR/docker.sock for rootless, then /var/run/docker.sock). This matters because docker.from_env() alone ignores contexts and would fall back to /var/run/docker.sock - which Docker Desktop 4.13+ no longer creates by default (it uses the desktop-linux context unless you enable Settings > Advanced > Allow the default Docker socket), so a stock Desktop install reachable by your CLI would otherwise fail here. Precedence: a non-empty DOCKER_HOST always wins and goes straight through docker.from_env() (which ignores contexts); DOCKER_CONTEXT / currentContext is consulted only when DOCKER_HOST is unset, and the socket probe only when neither resolves. TLS material attached to a remote context is not applied automatically - a tcp:// + TLS context still needs DOCKER_HOST / DOCKER_CERT_PATH.
Over SSH
DOCKER_HOST=ssh://user@remote-host is supported via a pure-Python transport (paramiko, pulled in by the docker[ssh] dependency) - there is no system ssh binary requirement, so it works the same on the host install and inside the container images. It authenticates with your normal SSH setup:
Keys / agent. Use key-based auth; load the key into your agent (
ssh-add) and make sureSSH_AUTH_SOCKis set in the server's environment (or place the key at the default~/.ssh/id_*path).Known hosts. paramiko verifies the host key against
~/.ssh/known_hostsand rejects an unknown host. Add the host key only after verifying its fingerprint through a trusted channel - connect once interactively withssh user@remote-hostand confirm the prompt, or comparessh-keyscan remote-host | ssh-keygen -lf -against a known-good fingerprint before appending it. Avoid blindly pipingssh-keyscanstraight intoknown_hosts, which trusts whatever key is returned (including a MITM's).In a container. Mount your SSH material read-only -
-v $HOME/.ssh:/root/.ssh:ro(key +known_hosts) - or forward your agent socket; no socket mount and nosshpackage needed.
CLI-backed tools (Compose, Stack, Buildx, Scout, Context) shell out to the docker CLI, which would otherwise use the system ssh binary over an ssh:// endpoint. Instead, run_docker() detects DOCKER_HOST=ssh://... and transparently starts a per-call local TCP proxy (docker_mcp/tools/_ssh_proxy.py) that opens the same paramiko connection docker-py would, runs docker system dial-stdio over it, and points the CLI subprocess at tcp://127.0.0.1:<ephemeral port> for the duration of that one call. So the CLI-backed tools authenticate with the exact same credentials and host-key policy as the docker-py-backed tools above - no system ssh binary for the direct connection, identical on the host install and inside the container images. The one exception is a ProxyCommand in ~/.ssh/config (bastion/jump-host setups): paramiko runs that command as-given, and it's commonly ssh -W %h:%p ..., so a jump-host hop still shells out to the system ssh client even though the direct connection does not.
That ephemeral 127.0.0.1 listener bridges to the remote (root-equivalent) daemon with your SSH credentials for the duration of a single CLI call, so any process sharing the same loopback could reach it during that brief window. The exposure is narrow - localhost-only and torn down when the call returns - and inside a container it's narrower still, reachable only by processes within that container's network namespace. The daemon remains the trust boundary either way (see Security considerations).
No local Docker
If Docker is not installed locally - or the plugin a call needs is missing - most tools will still operate on a remote host exactly as before. A few tool families (Compose, Buildx, Scout, and stack_deploy) will be run on the target host itself, over SSH. For these commands TCP and TLS connections will not work without a local Docker install.
Worth knowing:
It's a fallback, never a preference. A local CLI that can serve the call - binary plus the plugin that call needs - is always used instead, so nothing changes for a normal install.
Local files a command reads are copied onto the remote first - a Compose project directory, a bake file, a build context (honouring
.dockerignore) - into a private0700temp directory that is removed when the call returns - best-effort, since a dropped SSH connection leaves nothing able to run the cleanup. A survivor is nameddocker-mcp-server.stage.*and the failure is logged.Variables travel on the SSH channel, not the command line.
varsand theenv/build_argsvalues described under Security considerations are sent on the remote command's stdin and set in its environment there, so other users of the remote host cannot read them from its process list, and they are never written to its disk.The remote host's credentials apply. Registry logins come from its
~/.docker/config.json, so a private-registrycompose_pull, astack deploy --with-registry-auth, or most Scout operations may needdocker loginthere.system_logintalks to the daemon and does not write the remote CLI's config.The whole working directory is copied, because nothing can tell which files a Compose file references (
build:,env_file:,include:all name arbitrary paths). Point tools at a project directory rather than a large parent, or the call is refused with a size-limit error (200 MiB / 50 000 entries).compose_cprelays whichever side of the copy is local, over the same SSH connection: a host source is staged like any other input, and a container result is fetched back to the real destination once the copy succeeds - the actual copy still runs through the realdocker compose cpon the far host, so every parameter behaves as it does locally. The one difference: a container-to-host copy is refused if the local destination already exists, since only this host knows that.A few things are refused rather than half-done: for
buildx_builda filesystemdest=inoutput/cache_to, a localsrc=incache_from, or anyssh=- each would resolve on the remote machine.The remote must present a POSIX shell. Any Linux/macOS/BSD host qualifies, and sshd running inside a WSL distro is supported (it is a Linux target - and with Docker Desktop's WSL2 backend the daemon lives there anyway). A Windows-side
cmd/PowerShell sshd is refused with an explanatory error, as is aunamefrom MSYS/MinGW/Cygwin; a Windows dialect is architected but not implemented. A Windows sshd whose shell iswsl.exeruns commands in WSL while its SFTP subsystem stays on the Windows side, so file-copying tools are refused there - the ones that only run a command still work.context_*tools never fall back. They manage this host's CLI context registry, which a remote host knows nothing about.
Managing several daemons
Everything above targets one daemon. To manage several in a single session - e.g. local dev plus a remote production daemon - set DOCKER_MCP_SERVER_HOSTS to a comma-separated list of name=endpoint pairs:
"env": {
"DOCKER_MCP_SERVER_HOSTS": "local=auto, prod=ssh://ops@prod.example.com(ro)"
}endpointisauto(your default context/socket, as above),local(the platform-local socket, ignoring contexts), or aunix:///tcp:///ssh:///npipe://URL.ssh://is the recommended remote transport (per-host auth via your SSH keys, no TLS cert plumbing). Atcp://daemon over TLS takes a(tls=<dir>)marker pointing at a cert directory, e.g.prod=tcp://prod:2376(tls=/etc/docker/prod). That directory must holdca.pem(the daemon is always verified against it - so a self-signed daemon works, you just pin its cert here); addcert.pemandkey.pemonly if the daemon requires a client certificate (mutual TLS). There is no unverified-TLS mode - a TLS connection always authenticates the daemon, so encryption never comes without verification.(ro)after an endpoint marks that host read-only: mutating and destructive tools refuse to act on it. This is a per-host guard enforced at call time, independent of the server-wideDOCKER_MCP_SERVER_READONLYswitch - mark production(ro)and the agent can inspect it all day but can't change it, while local stays read-write.(nd)marks a host non-destructive: only destructive tools (removals, prunes, kills) refuse to act on it - reads and ordinary mutations (start/stop/create) still work. This mirrors the server-wideDOCKER_MCP_SERVER_NO_DESTRUCTIVEswitch, per host.(ro)already implies it, so(ro)(nd)combines without error but is redundant.(redact)marks a host for credential redaction, so a production daemon can be redacted while a local one stays unfiltered. Redaction reduces accidental exposure only; it is not access control, and an agent that can run commands in a container can still read a credential. ReadREDACTION.mdbefore relying on it. Mark a redacted host(ro,redact): the server warns at startup about a host markedredactwithoutro.Writing options. A host's options go in one comma-separated bracket,
(ro,redact), or one per bracket,(ro)(redact), and the two forms mix. They are case-insensitive and in any order, except thattls=takes the rest of its bracket (so a certificate directory containing a comma still works) and so must come last in a list:(ro,redact,tls=/etc/docker/prod). Options go at the end of the endpoint; a malformed bracket stops the server at startup with an error naming the host.Single daemon, simpler form. A bare value with no
name=is shorthand for one host -DOCKER_MCP_SERVER_HOSTS=ssh://ops@prod(orauto, or blank). So this one field also covers the single-remote case.DOCKER_HOSTkeeps working whenDOCKER_MCP_SERVER_HOSTSis unset, butDOCKER_MCP_SERVER_HOSTStakes over when set (DOCKER_HOSTis then ignored, with a one-time notice to stderr).
How the agent drives it. With two or more hosts, every daemon-targeting tool gains an optional host argument constrained to your configured names: read-only tools default to the first host when you omit it, while mutating and destructive tools require an explicit host (so the agent can't change the wrong daemon by accident). host_list (and the docker-mcp://hosts resource) report the configured hosts and which is the default; the container/service observability resources become host-aware - the default host's log tail is docker-logs:///{id} (note the empty authority) and a named host's is docker-logs://{host}/{id}, with the same pattern for docker-stats://, service-logs:// and service-tasks://; the single-host bare forms (docker-logs://{id}, ...) are not registered once several hosts are configured. The survey_hosts prompt sweeps every host read-only. The auto/local endpoints are resolved to concrete URLs and pinned at startup, so the SDK and CLI always agree on which daemon a name means - restart to re-resolve after changing a Docker context.
What the agent can do
Once loaded, the agent gets MCP tools grouped by Docker domain. A few examples:
Containers -
container_run,container_list(managed_only=Trueto list only what this server created - see Provenance labels),container_exec,container_logs,container_stop,container_commit,container_export/container_archive_get_to_file/container_archive_put(stream tar archives to/from a host path)Images -
image_build,image_pull,image_push,image_tag,image_prune(withbuild_cache=True, the daemon's build cache too - a separate resource from dangling images),image_save/image_load(stream image tarballs to/from a host path viadest_path/from_file),image_import(build a single-layer image from a flat rootfs tarball, URL, or existing image -docker import, not to be confused withimage_load)Plugins -
plugin_install(pull from a registry;dry_run=Trueshows what host access the plugin demands before installing it - the daemon grants it non-interactively),plugin_enable/plugin_disable,plugin_configure,plugin_upgrade(with the samedry_run),plugin_list/plugin_inspect,plugin_remove(managed engine plugins - volume/network/logging drivers - not the CLI plugins that extend thedockercommand itself)Networks / Volumes -
network_create,network_connect,volume_create,volume_pruneSwarm -
swarm_init,swarm_join_tokens(close the init-to-join loop),swarm_update(rotate join tokens / unlock key),service_create,service_scale,service_rollback(re-apply the previous service spec),service_ps(one service's tasks, or with no service every task in the cluster, filterable by node or desired state - no single CLI command does this),service_logs(a service's logs, or one task's),node_list,node_remove,secret_create,config_createSystem -
system_wait(the one wait: until a container exits, is healthy or logs a line, a service or stack converges, a stack is removed, a node is ready, a compose project is running, healthy or exited, or a tag lands in a registry - every wait has a timeout, 600 s by default, and reports one rather than raising),system_ping,system_info,system_version,system_df,system_events,host_list(the configured daemons and which is the default - see Managing several daemons),system_login/system_logout(cache or clear registry credentials),system_reconnect(rebuild a host's SDK client to recover a wedged connection)Compose -
compose_up,compose_down,compose_stop,compose_start,compose_restart,compose_pause/compose_unpause,compose_kill,compose_ps,compose_list,compose_images,compose_top,compose_port,compose_logs,compose_config,compose_build,compose_pull,compose_run,compose_exec,compose_cp(wraps thedocker composeCLI plugin)Stacks -
stack_deploy,stack_list,stack_ps,stack_services,stack_remove(deploy a Compose file to a swarm as a stack -stack_deploywrapsdocker stack deploy, which translates the Compose file; the rest use the Engine API - requires a swarm manager)Contexts -
context_list,context_inspect,context_create,context_use,context_remove(wraps thedocker contextCLI)Registry / Hub -
registry_tags,registry_manifest,registry_image_config(read an image's env/entrypoint/labels without pulling),hub_tags,hub_repo_info,hub_rate_limit(remaining pull budget) (HTTPS to OCI v2 registries and the Docker Hub API - no daemon required; transparent retry on a brief 429)Buildx -
buildx_build,buildx_bake,buildx_imagetools_inspect,buildx_imagetools_create,buildx_list,buildx_inspect,buildx_du,buildx_history_list/buildx_history_inspect(drill into past build records),buildx_prune,buildx_create,buildx_use,buildx_remove(wraps thedocker buildxCLI plugin). Usebuildx_imagetools_*in place ofdocker manifest- that command is in maintenance mode and lacks support for OCI image indexes and attestations.Scout -
scout_cves,scout_quickview,scout_recommendations,scout_compare,scout_sbom(wraps thedocker scoutCLI plugin; every tool butscout_sbomneedsdocker loginon the host running the CLI).
The SDK-backed surface mirrors the Docker SDK reference - if it's documented there, it's available here. The Compose and Context surfaces follow the Compose CLI and docker context references.
The server also publishes the Docker SDK for Python reference and selected Docker CLI / registry references as MCP resources so the agent can consult them at runtime: read docker-docs://contents for the section index, then docker-docs://<section> (e.g. docker-docs://containers, docker-docs://compose, docker-docs://oci-distribution-spec, docker-docs://dockerfile, docker-docs://build-best-practices, docker-docs://engine-security, docker-docs://engine-api) for the rendered page. For MCP clients that can't read resources (e.g. Claude Desktop, Cursor), the docs_lookup tool mirrors the same content - call it with no arguments for the section index, or docs_lookup(section=...) for a page; it's always available regardless of DOCKER_MCP_SERVER_DISABLE. A further resource, docker-mcp://tool-catalog, lists every tool this server knows about with its domain, mutation category, and whether the active configuration registered it - useful for confirming the blast radius of a tool, or why one is absent from the live list. The tool_list tool mirrors it for clients that can't read resources, and adds filtering: tool_list(domain="buildx") for a one-line-per-tool briefing on an area, tool_list(category="destructive") to see what can destroy data, or tool_list(keyword="logs one task") to search names, summaries and parameter names - every tool matching any of the words comes back, those matching the most first. It lists only what the current configuration registered, and returns an explicit empty result when nothing matches. Like docs_lookup it is always available regardless of DOCKER_MCP_SERVER_DISABLE.
Container and service observability is also exposed as resources, so a client can attach live state as context without a tool call: docker-logs://<id-or-name> for a bounded tail of a container's logs (readable even after it exits - handy for diagnosing why) and docker-stats://<id-or-name> for a computed resource-usage summary (CPU %, memory, network and block I/O) of a running container. Swarm services get the same pattern - service-logs://<id-or-name> for a bounded log tail and service-tasks://<id-or-name> for a computed task/rollout summary (running vs. desired task counts, failing tasks, and the current rolling-update state). Enumerate with container_list / service_list to find the id or name to plug in. These complement the equivalent tools (container_logs/container_stats, service_logs) and the service ones are hidden when services is disabled (containers cannot be).
Example prompts
Many AI clients let you invoke registered MCP prompts directly (in Claude Code, type / to see them). The server ships a small library of templates in docker_mcp/tools/prompts.py that scaffold multi-step workflows - they emit a structured plan that the agent then carries out using the docker tools.
Looking things up in the SDK docs
/lookup_docker_docs section=services
/verify_docker_method method=containers.run section=containers...or just ask in plain English:
Read
docker-docs://networksand tell me the difference betweencreateandconnect. Before changing any code, checkdocker-docs://containersand confirmrunaccepts arestart_policyargument.
Creating and managing containers
/deploy_container image=nginx:1.27 name=web
/monitor_container_fleet
/triage_incident window_minutes=30
/troubleshoot_container container=api-1
/migrate_container container=api-1 new_image=myorg/api:v2
/inspect_stack label=com.example.app=web
/clean_environment scope=stopped
/plan_compose_stack description="wordpress + mysql sharing a named volume"Compose, contexts, and registries
/deploy_compose_project project_dir=/srv/myapp
/troubleshoot_compose_project project_dir=/srv/myapp
/deploy_swarm_stack stack_name=web compose_file=/srv/myapp/docker-stack.yml
/audit_docker_contexts
/find_latest_image_tag image=ghcr.io/org/repoAuditing, security, and host operations
/review_dockerfile dockerfile_path=/srv/myapp/Dockerfile
/audit_container_security
/debug_container_networking source=web target=db
/investigate_disk_usage
/backup_volume volume=pgdata dest_path=/backups/pgdata.tar
/restore_volume volume=pgdata source_path=/backups/pgdata.tar
/audit_swarm_healthBuildx, Scout, and multi-arch manifests
/plan_multiarch_build image=ghcr.io/org/app:v1 platforms=linux/amd64,linux/arm64
/audit_image_cves image=alpine:3.19
/compare_image_versions old_image=org/app:v1 new_image=org/app:v2
/recommend_base_image image=org/app:v1
/inspect_multiarch_manifest image=alpine:3.19
/create_multiarch_manifest target_tag=org/app:v1 source_tags=org/app:v1-amd64,org/app:v1-arm64
/migrate_from_docker_manifest...or in plain English:
Pull
redis:7-alpineand run it as a container calledcacheon a newapp-netnetwork, exposing port 6379 only inside that network. Containerapi-1keeps restarting - grab the last 200 log lines, inspect its state and exit code, and tell me what's wrong before changing anything. Replace the runningwebcontainer withnginx:1.27while keeping its current ports, mounts, and restart policy. Plan a wordpress + mysql stack on a private network with a named volume for the database. Show me the plan before creating anything. Show every container, network, and volume taggedcom.example.app=webas one table. Don't change anything. We're tight on disk - showsystem_df, prune stopped containers and dangling images, then showsystem_dfagain. Skip volumes. Bring up the compose project in/srv/myapp, but show me the rendered config and pull the images before starting anything. List my Docker contexts and tell me which daemon this MCP server is currently talking to. Find the most recent stable tag forghcr.io/org/repowithout pulling it, and tell me which platforms it supports.
Configuration
Env var naming. The server's environment variables are namespaced
DOCKER_MCP_SERVER_*to match the published package name. The pre-2.0DOCKER_MCP_*alias spellings are no longer honoured.
To choose which daemon(s) the server talks to, see Talking to a remote daemon and Managing several daemons (DOCKER_MCP_SERVER_HOSTS / DOCKER_HOST). The variables below instead restrict which tools are registered.
Three environment variables restrict which tools are registered when the server starts. Because they drop tools at registration time, a disabled tool never appears in the client's tool list - this is a server-side guarantee, not a client-side prompt. Set the two boolean switches to 1 / true / yes / on:
DOCKER_MCP_SERVER_READONLY- register only read-only tools (queries, log/data reads, scans). Every tool that changes state is omitted. Use this for monitoring or inspection agents that must not be able to modify anything.DOCKER_MCP_SERVER_NO_DESTRUCTIVE- register everything except destructive tools (remove_*,prune_*,container_kill,compose_down,swarm_leave,context_remove,buildx_prune,buildx_remove). A "no data loss" mode that still allows creating and starting resources.DOCKER_MCP_SERVER_READONLYis stricter and wins if both are set.DOCKER_MCP_SERVER_DISABLE- a comma-separated list of domains (feature areas) to drop wholesale, regardless of category: e.g.DOCKER_MCP_SERVER_DISABLE=swarm,services,nodes,configs,secretsremoves the entire swarm surface from a single-host server, andDOCKER_MCP_SERVER_DISABLE=scout,buildxtrims build/scan tooling an agent will never use. A domain is a tool module's name -images,networks,volumes,compose,stack,context,buildx,scout,registry,swarm,services,nodes,plugins,configs,secrets.systemandcontainerscannot be disabled, so naming either is ignored:systemholds what an agent needs to orient itself and recover, and the one wait tool;containersis what a Docker server is for, and the containers the compose, stack and service tools start can only be waited on, read and removed through it. Disablingcontainersnever stopped the agent running code anyway (compose, stack,service_createandimage_buildall do);DOCKER_MCP_SERVER_READONLYis the switch that does, and that keeps it from logging in to a registry. Disabling a domain also drops its targets fromsystem_wait. Names are case-insensitive; an unrecognised name disables nothing. Either mistake - a name that matches no domain, or one that cannot be disabled - is reported three ways, because an MCP client rarely shows a server's stderr to anyone: aWARNINGline on stderr at startup, aconfiguration_warningsentry intool_list's result (andunknown_disabled_domains/ignored_disabled_domainsin the tool catalog), and a line at the top of the server instructions telling the agent to checktool_listand tell the user. This stacks with the category switches - a tool registers only if its category survives and its domain is enabled. Disabling a domain drops more than its tools: the matching workflow prompts are skipped (so the agent isn't handed a prompt that drives a feature area this server no longer exposes - e.g. disablingscoutremoves theaudit_image_cvesprompt that would otherwise tell the agent to call a tool that isn't registered) and the matching documentation resources are hidden fromdocker-docs://contents(e.g. thescout/scout-clisections). The tool catalog'spromptslist anddisabled_doc_sectionsfield make both auditable. Trimming domains an agent doesn't need also cuts the tool-list size the client has to reason about, which matters at this server's size.
None of the three is a network control. DOCKER_MCP_SERVER_READONLY restricts what the agent can change, not what the server can reach: the registry and Hub tools are read-only, so a read-only server still makes outbound HTTPS requests to whichever registry host a tool argument names - from wherever the server runs, which may be a machine with more network reach than the daemon it manages. Those requests carry no credentials unless the registry answers with a token challenge, and a credential-bearing token endpoint is validated first (scheme, no plaintext to a public host, and no public registry pointing its realm at a private or loopback address), but the request itself is still made and its response is returned to the agent. docs_lookup and the docker-docs:// resources likewise fetch from a fixed list of documentation hosts. If outbound requests are what you need to stop, DOCKER_MCP_SERVER_DISABLE=registry removes that domain entirely, and network policy on the host is the control for the rest - see PRIVACY.md for the full list of what this server contacts.
Independently, every registered tool carries MCP ToolAnnotations - readOnlyHint on queries and destructiveHint on destructive operations (plus idempotentHint on the prune family) - so a client like Claude Code can auto-allow safe reads and gate destructive calls. The classification lives in TOOL_CATEGORIES in docker_mcp/server.py. To see the full picture at runtime - every tool with its domain, category, and whether the active switches registered it - read the docker-mcp://tool-catalog MCP resource.
Credential redaction is configured separately, because it changes what tools return rather than which are registered:
DOCKER_MCP_SERVER_REDACT- redact credentials in everything the server returns, from every host;(redact)on a host inDOCKER_MCP_SERVER_HOSTSdoes the same for that host alone. This reduces accidental exposure only; it is not access control, and it cannot catch every credential. ReadREDACTION.mdfor what it does, what it costs and what it misses, and run it withDOCKER_MCP_SERVER_READONLY.DOCKER_MCP_SERVER_REDACT_NAMES- extra variable names whose values redaction treats as credentials, comma-separated and case-insensitive, with*/?globs (UPSTREAM_*), and!to exclude a name the built-in patterns would otherwise catch (!SORT_KEY). Names only, never values. SeeREDACTION.md.
For private registries, the HTTPS-backed registry_* tools fall back to DOCKER_MCP_SERVER_REGISTRY_USERNAME / DOCKER_MCP_SERVER_REGISTRY_PASSWORD from the server's environment when no explicit username/password arguments are passed (explicit arguments win; the env pair is only used when both arguments are unset). Setting credentials in the environment keeps them out of tool arguments, which many MCP clients log verbatim - the password may be a personal-access token.
Provenance labels
Every Docker object the agent creates through this server - containers, networks, volumes, swarm services, configs, and secrets - is stamped with a small set of docker-mcp-server.* labels recording that this server made it (docker-mcp-server.managed=true), the server version, the originating tool, and a creation timestamp. This lets you (or a cleanup job) later enumerate exactly the footprint the agent created with a single docker ... --filter label=docker-mcp-server.managed=true; the managed_only=True argument on container_list, network_list, volume_list, and service_list is the in-tool shortcut (it combines with any other filters you pass). The stamping is additive (a label you pass yourself always wins on a key collision) and uniquely namespaced, so it's safe by default; DOCKER_MCP_SERVER_NO_LABELS=1 turns it off entirely. Image builds are deliberately not stamped, because a build label changes the resulting image digest.
To tear down only what the server created - and nothing else - use the prune_managed workflow prompt, which scopes every removal step to the docker-mcp-server.managed=true label (volumes only when you pass include_volumes=True, and only after confirmation).
Example: a read-only monitoring server
All of these go in the env block of the server entry in your MCP client config (the same place as DOCKER_HOST above). For example, a read-only inspection server against a remote daemon:
{
"mcpServers": {
"docker-mcp-server-readonly": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/L337-org/docker-mcp.git",
"docker-mcp-server"
],
"env": {
"DOCKER_HOST": "tcp://staging-host:2376",
"DOCKER_TLS_VERIFY": "1",
"DOCKER_MCP_SERVER_READONLY": "1"
}
}
}
}Swap DOCKER_MCP_SERVER_READONLY for DOCKER_MCP_SERVER_NO_DESTRUCTIVE to allow create/start/deploy while still making remove_* / prune_* / container_kill impossible. You can also register the same server twice under different names - a full-access entry you enable when needed and a read-only entry for everyday use. With claude mcp (Claude Code), the equivalent is:
claude mcp add docker-mcp-server-readonly \
--env DOCKER_MCP_SERVER_READONLY=1 \
-- uvx --from git+https://github.com/L337-org/docker-mcp.git docker-mcp-serverOr don't use a server at all: the l337-docker agent skill
A fair question about any MCP server is whether it needs to be one. This repo answers it in the open: skills/l337-docker/ is a Claude Code agent skill that drives Docker purely through the docker CLI - no server process, no Python, nothing beyond a docker binary (plus jq and curl for a few registry recipes).
It exists to mark the ceiling of the skill approach honestly, so you can judge the trade rather than take our word for it. It is written to be as good as that approach gets: a router plus per-domain references and workflows, every command verified against a real daemon, and its shell snippets executed by CI so they cannot drift. For a single local daemon and everyday work, it may well be all you need - if so, use it.
Where it stops is not coverage but enforcement. The skill's safety rules are instructions in a prompt; the server's are refusals in code:
MCP server |
| |
Read-only / no-destructive modes | Enforced - the tools are never registered | A written rule the agent is asked to follow |
Per-daemon write protection ( | Enforced at the call boundary | Not available |
Self-termination guard | Enforced | A written rule |
Output bounding | Enforced, with a | A written rule ( |
Multi-daemon targeting | Resolved and pinned at startup | Ambient Docker contexts, which can move mid-session |
Requirements | Python ≥3.14, or a container | A |
The distinction matters most where it is easiest to overlook: a rule in a prompt can be skipped under pressure, or talked around by a prompt injection sitting in a container log the agent just read. A tool that was never registered cannot be called at all. So prefer the server for anything pointed at production, or wherever you need a guarantee rather than an instruction - and prefer the skill when the stakes are low and the simplicity is worth more.
MCP_VS_SKILLS.md is the full, honest comparison: measured token cost for both - the skill is far cheaper on an eager-loading client, especially at idle, while the server is cheaper per task on a lazy-loading one - plus what each genuinely does better and a tool-by-tool coverage map of every tool, prompt and resource.
To install, download the archive from a release and extract it into your skills directory. It is rooted at l337-docker/, so it lands correctly as-is:
# personal (all projects)
tar -xzf l337-docker-skill-<version>.tar.gz -C ~/.claude/skills
# or per-project, checked into version control
tar -xzf l337-docker-skill-<version>.tar.gz -C .claude/skillsVerify it with the accompanying checksum (sha256sum -c l337-docker-skill-<version>.tar.gz.sha256); the archive is byte-reproducible, so rebuilding from the tag gives the same hash. The skill and the server can coexist - they label the resources they create differently and neither reads the other's.
Security considerations
Connecting this server to an AI agent grants it the same level of access as a local Docker CLI session against the configured daemon. That is broad: the daemon's socket is effectively root-equivalent on the host running it. Treat the agent as a privileged user and weigh the risks below before enabling the server.
Use a scoped daemon. Prefer pointing
DOCKER_HOSTat a daemon dedicated to workloads the agent is allowed to touch (a development VM, a remote sandbox, Docker Desktop, a rootless install) rather than your production socket. The daemon is the trust boundary - there is no per-tool authorization layer.Running as a container. Mounting
/var/run/docker.sockinto the container grants it the same root-equivalent access to that daemon as the uvx install has - no more, no less, but now explicit in thedocker runline. The same scoped-daemon advice applies: prefer mounting a socket for, or pointingDOCKER_HOSTat, a daemon the agent is allowed to control. Note that when containerized the file-path tools read and write the container's filesystem, so they can only reach host directories you bind-mount in (see Run as a container). As an accident guard, the destructive container-lifecycle tools (container_remove,container_kill,container_stop,container_restart,container_pause) refuse to act on the server's own container so the agent can't end its own session mid-call; this is convenience, not a security boundary (it's bypassable withDOCKER_MCP_SERVER_ALLOW_SELF_TERMINATE=1, and a human can always recover the container from the host shell), and it does not constrain the many other ways a daemon-privileged agent can affect the host.Privileged containers and host mounts.
container_runacceptsprivileged=Trueand arbitraryvolumes. A privileged container, or one that bind-mounts/from the host, can trivially escape to the host filesystem. Avoid letting the agent set these unless you have reviewed the request. Compose files can declare the same - review the renderedcompose_configoutput before approvingcompose_upon an unfamiliar project.Pass-through
extra_kwargs/updatesbypass the visible schema.container_run,container_create,service_create(extra_kwargs) andcontainer_update,service_update(updates) forward an arbitrary dict straight into the Docker SDK. A client that gates on, say,privileged=Falsein the tool's declared parameters can still be bypassed viaextra_kwargs={"privileged": True, "pid_mode": "host"}. These escape hatches are consistent with the "daemon is the trust boundary" model, but any allow/deny policy you build at the MCP-client layer must account for them rather than trusting the named parameters alone.Registry credentials. Many MCP clients log tool calls verbatim, so treat any password or
auth_configyou pass through a tool as exposed.SDK-backed tools (
system_login,image_push,image_registry_data) accept credentials directly and can reuse credentials cached bydocker loginin~/.docker/config.json. Prefer runningdocker loginonce on the host running this MCP server and leaving the credential parameters unset. (Note: this is the host running the server, not the daemon - relevant whenDOCKER_HOSTpoints at a remote daemon.) A credential passed tosystem_loginis cached in the server's memory for the life of the client;system_logoutclears that in-memory cache (all registries, or one) without touching~/.docker/config.json, andsystem_close/system_reconnectclear it by discarding the client. There is no daemon-side session to end - the Engine's/authendpoint only validates.HTTPS-backed registry tools (
registry_tags,registry_manifest,registry_image_config,hub_tags,hub_repo_info,hub_rate_limit, andsystem_waitfor a registry tag) talk to the registry directly over HTTPS and do NOT read~/.docker/config.json. Theregistry_*tools acceptusername/passwordfor private registries - or, better, readDOCKER_MCP_SERVER_REGISTRY_USERNAME/DOCKER_MCP_SERVER_REGISTRY_PASSWORDfrom the server's environment so credentials never transit tool arguments (see Configuration); thehub_*tools support public Hub repositories only. If passing credentials as arguments, use a per-invocation token with the minimum required scope rather than a long-lived password. When a registry answers with aBearerauth challenge, the server validates the tokenrealmit points at before sending anything: the scheme must be http/https, plaintext http to a non-local host is rejected, and a public registry is not allowed to redirect the credentialed token request at a private/loopback address (an SSRF guard). A genuinely local dev registry (e.g.localhost:5000) may still use a local realm. Pagination is pinned the same way: a Docker HubnextURL must stay on Hub's own origin, and an OCILinkheader's host is discarded in favour of the registry already being queried, so a response body can never redirect this server at a host of its choosing. Redirects themselves are followed across hosts, deliberately - registries answer blob fetches with a redirect to a CDN on another host as normal operation - but the HTTP client strips theAuthorizationheader on any cross-origin redirect, so credentials cannot follow one.
Swarm secret material transits tool calls too. Beyond registry credentials, several swarm tools carry secret material through arguments or return values that MCP clients may log:
secret_create(data=...)andconfig_create(data=...)take the payload as an argument,secret_inspect/config_inspectreturn the stored object,swarm_join(join_token=...)andswarm_unlock(key=...)take cluster join/unlock secrets, andswarm_unlock_keyandswarm_join_tokensreturn cluster credentials (rotation viaswarm_updateinvalidates old tokens) - a manager join token lets its holder join the swarm as a manager (root-equivalent on the cluster). Treat all of these as exposed in any client that records tool traffic, and prefer provisioning swarm secrets and reading join tokens out-of-band on the host rather than through the agent. If an agent never needs to admit nodes, drop the whole surface withDOCKER_MCP_SERVER_DISABLE=swarm(see Configuration).Credentials for the CLI-backed tools go in arguments, never on a command line or on disk.
compose_up,compose_run,compose_build,stack_deploy,buildx_buildandbuildx_baketakevars: values for${VAR}substitution in a Compose file, bake file variables, or the variable a build secret'senv=names. The values ofcompose_run/compose_execenvandbuildx_buildbuild_argstravel the same way. Each reaches the docker CLI in its environment, passed by name, so no process's command line shows it to other users of the machine - nor, over the SSH fallback, of the remote host - and nothing writes it to a file. The exceptions are three kinds of name, whichvarsrefuses andenv/build_argspass on the command line asNAME=VALUE, where the process list shows the value: a name that would change the docker CLI itself (the docker CLI's and its plugins' own settings, the dynamic loader's, git's, SSH's, proxies, shell start-up files, and the variables this server already passes it;is_reserved_env_nameindocker_mcp/tools/_cli.pyholds the list, a best-effort denylist rather than a guarantee); a name POSIX shells treat specially, such asUIDorRANDOM, which could not reach the CLI on a remote host; and a name that is not a plain variable name, such asspring.profiles.active. Two more consequences: anenvvalue forcompose_runis also visible to the Compose file's own${VAR}substitution, and the arguments themselves still transit the MCP client, which may log them. A build argument is recorded in the image's history: usesecretwithenv=for a credential in a build.container_exec,compose_exec, andcompose_runrun arbitrary commands. When any part of the command is derived from agent-controlled input, use an exec-form argv list that does not invoke a shell (e.g.["python", "-V"]). A list like["sh", "-c", template]that invokes a shell will interpret shell metacharacters in the untrusted substrings.Container archive paths.
container_archive_getandcontainer_archive_putforward the supplied path verbatim to the daemon. The container is the trust boundary - if you do not trust its filesystem, do not assume..traversal will be rejected.File-path payload tools read and write the server host's filesystem.
image_save,container_export(withdest_path), andcontainer_archive_get_to_filewrite to adest_pathon the host running this MCP server (refusing to overwrite an existing file unlessoverwrite=True);image_load,image_import, andcontainer_archive_put(withfrom_file) read a host path;compose_cpcopies between a service container and a host path in either direction.image_import(from_url=...)is the one that leaves the host entirely: the daemon fetches the URL the caller named, in the daemon's own network namespace, so it can reach anything that daemon can reach (the same caller-chooses-the-destination footing asimage_pull's registry argument). These run as the server's user, so the agent can write any file that user can write and read any file it can read. Prefer the in-band byte tools (capped at 32 MiB) when you don't trust the agent with host filesystem access.DOCKER_MCP_SERVER_READONLYalso drops the host-writing tools - but note it is not targeted at them: it registers only read-only tools, soimage_loadandcontainer_archive_put(and every other mutating/destructive tool) go too. There is no switch that drops just the file-writers.Destructive operations have no built-in confirmation.
prune_*,remove_*,container_kill,swarm_leave,compose_down(volumes=True),compose_kill,stack_remove(tears down every service in a stack),buildx_prune(always runs with--force), andbuildx_removeexecute immediately. These tools carry thedestructiveHintannotation, so a client like Claude Code can gate them, and the shippedclean_environmentprompt asks the agent to confirm before pruning volumes - but tool calls themselves are not gated by the server. For a hard guarantee, run withDOCKER_MCP_SERVER_NO_DESTRUCTIVE=1(drops them entirely) orDOCKER_MCP_SERVER_READONLY=1(see Configuration); for an approval step, configure it at the MCP client.CLI shell-out attack surface. Compose, Stack, Buildx, Scout, and Context tools spawn
dockersubprocesses on the host running this MCP server. Every invocation passes arguments as a list (no shell, no metacharacter interpretation), resolves the binary viashutil.which, and runs against a scrubbed environment (DOCKER_HOST and related vars, plus the caller'svarsand theenv/build_argsvalues described above). Positional values (image refs, service / context / builder names, build contexts, bake targets) are additionally rejected if they start with-, so an argument can't be smuggled in as a CLI flag (e.g. a service named--output=...); the one deliberate exception is the trailing command incompose_exec/compose_run, which is meant to be an arbitrary argv. Filesystem paths supplied tocompose_*(project_dir, files) are read by the docker CLI on the server host - passing an unfamiliar path can expose any compose file the server's user can read. With no local docker CLI and anssh://target those subprocesses run on the remote host instead (above), which shifts two things: the command executes as the remote SSH user with its registry credentials, and the files a command reads are copied to that host's temp directory first - so abuildx_build--secret src=file, or anything else in a staged directory, exists briefly on the remote disk (mode0700, removed when the call returns; only a dropped connection can leave it behind). Point staging-backed tools at directories you would be content to copy.The daemon set is fixed at startup; pick it deliberately. When
DOCKER_HOST/DOCKER_MCP_SERVER_HOSTSare unset, the server's initial SDK connection follows your active Docker context (DOCKER_CONTEXT/currentContext) - the same daemon yourdockerCLI targets - so if that context points at a remote or production daemon, the agent connects there too. SetDOCKER_MCP_SERVER_HOSTS(orDOCKER_HOST, or select a scoped context) before starting the server to pin the target(s) deliberately; withDOCKER_MCP_SERVER_HOSTStheauto/localendpoints are resolved and pinned at startup, so they can't drift if a context changes later. After startup,context_useonly changes the CLI default for subsequent CLI-backed tools; SDK-backed tools keep using the daemon their pooled client connected to. There is no runtime way to introduce or retarget a daemon at an arbitrary endpoint -system_reconnectonly rebuilds an already-configured host's client (to recover a wedged connection), it can't point it elsewhere; to add or change a daemon, editDOCKER_MCP_SERVER_HOSTSand restart. This deliberately closes a trust-expansion vector (an agent can't move the root-equivalent boundary to an unvetted endpoint mid-session).context_create(skip_tls_verify=True)disables TLS verification for a context; use only against trusted local daemons.Per-host read-only is an accident guard, not a security boundary. A host marked
(ro)inDOCKER_MCP_SERVER_HOSTSmakes mutating/destructive tools refuse to act on it at call time (and, with several hosts, writes require naming the target host explicitly - so the agent can't change the wrong daemon by omission). Likeguard_not_self, this is in-process convenience: it constrains the agent through this server's tools, but the daemon itself is still the trust boundary, so for a host the agent must never modify, prefer pointing it at a genuinely read-only or scoped daemon over relying on the marker alone.Per-host non-destructive is the same accident guard, scoped narrower. A host marked
(nd)refuses only destructive tool calls (kills, removals, prunes) while reads and ordinary mutations still go through. Like(ro), this is in-process convenience enforced by this server's guard, not a daemon-level boundary.cryptography(a transitive dependency, viamcp->pyjwt[crypto]) has shipped no x86_64/universal2 macOS wheel since 49.0.0 - confirmed permanent, not transient - so a native install there (uvx,pip, or the.mcpbbundle) resolves to a current version and builds it from source, needing Rust and OpenSSL 3.x. This project briefly tried to avoid that build step with its owncryptography<49cap scoped to Intel macOS - that cap turned out to hold back every platform, not just the one it named (a platform-scoped upper bound alone doesn't makeuv lockfork the resolution), so it silently left everyone on a version within a later high-severity CVE's range (a PKCS#7 EnvelopedData decryption Bleichenbacher oracle). The cap was removed rather than replaced: this project doesn't decrypt PKCS#7 envelopes itself, but a dependency carrying a known-exploitable flaw is not something to ship knowingly just to keep one platform wheel-only. If you'd rather not install a build toolchain, use the container image instead (built centrally on Linux, so it never hits this).
Packages and listings
Channel | Link |
PyPI | |
GHCR (container) | |
Docker Hub (container) | |
Desktop Extension (.mcpb) | |
| |
Official MCP Registry | |
Glama | |
mcp.so | |
awesome-mcp-servers |
Privacy Policy
docker-mcp-server collects no data, sends no telemetry, and has no author-operated backend. It runs locally and talks only to the Docker daemon and container registries you point it at, as part of the operations you request. The full statement is in PRIVACY.md.
Contributing
Contributions are welcome. The project values a tight mapping between the Docker SDK's public surface and the MCP tools we expose. See CONTRIBUTING.md for the project layout, tool conventions, the checklist for adding a new tool module, and local development setup.
Trademarks
docker-mcp-server is an independent open-source project. It is not affiliated with, endorsed by or sponsored by Docker, Inc. Docker is a trademark or registered trademark of Docker, Inc.
Available Tools
165 toolsbuildx_bakeA
Build multiple targets defined in a bake file (HCL, JSON, or compose).
Use it for multi-target builds declared in docker-bake.hcl/compose files; for a single
Dockerfile target use buildx_build.
Does not raise on a non-zero CLI exit (a missing buildx plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
targets: Bake targets to build (default: the default group)
files: Bake file paths (-f, repeatable)
set_overrides: Per-target overrides, e.g. ["app.platform=linux/amd64"]
push: Push results to the registry
load: Load results into the local image store
no_cache: Do not use cache when building
pull: Always pull a newer base image
builder: Override the active builder
cwd: Working directory containing the bake file (defaults to the server's cwd; copied to the target host if no
local plugin)
timeout_seconds: Subprocess timeout (default 1800s)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: cwd, load, pull, push, +6 more.
buildx_buildA
Build an image with BuildKit via docker buildx build.
Replaces the legacy image_build tool when you need any of: multi-platform output
(platforms), modern cache export (cache_from/cache_to), SBOM or provenance
attestations, build secrets, or multi-stage builds with target. Always runs with
--progress=plain so output is captured rather than redrawn on a TTY.
With no local buildx plugin and an ssh:// target, the build runs on that host: a local context
directory is copied there honouring .dockerignore, as are file, build_contexts and secret
paths. Raises ToolInputError in that case for output/cache_to with a filesystem dest=,
cache_from with a local src=, or any ssh= - each would resolve on the remote machine, losing
the output or silently changing the build.
Does not raise on a non-zero CLI exit (a missing buildx plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
context: Build context: a filesystem path or Git/HTTP URL (verbatim; no ~/glob expansion). The -
stdin-tarball form is NOT supported (stdin isn't forwarded - it'd block on the server's own stdin); serve a
pre-packed tarball over HTTP instead. Copied to the target host when it names a local directory and there is
no local plugin.
tags: Image references to apply (-t, repeatable)
platforms: Target platforms, e.g. ["linux/amd64", "linux/arm64"]
file: Dockerfile path. A relative path resolves against this server's working directory (buildx's own rule), NOT
against context - pass e.g. "ctx/Dockerfile" for a Dockerfile inside the context directory "ctx".
build_args: Build-time variables (each becomes --build-arg KEY=VALUE)
build_contexts: Additional named build contexts (e.g. {"deps": "./vendor"})
labels: Labels to set on the resulting image (each becomes --label KEY=VALUE)
annotations: OCI manifest annotations (passed verbatim, repeatable)
target: Target build stage to stop at
push: Push the result to the registry (mutually exclusive with load)
load: Load the result into the local image store (single-platform builds only)
output: Custom --output specs (e.g. ["type=tar,dest=out.tar"]). A filesystem dest= is refused when the build
has to run on a remote host; dest=- (stdout) is fine.
no_cache: Do not use cache when building
no_cache_filter: Stage names to exclude from caching
pull: Always attempt to pull a newer version of each base image
cache_from: Cache import specs, e.g. ["type=registry,ref=user/img:cache"]
cache_to: Cache export specs
builder: Override the active builder
sbom: Shorthand for --attest=type=sbom; pass "true" or a config string
provenance: Shorthand for --attest=type=provenance; pass "true", "false", or a config string
attest: Custom attestation specs (repeatable)
secret: Secret specs (e.g. ["id=npmrc,src=/home/user/.npmrc"] or ["id=npmrc,env=NPM_TOKEN"]). ~ in src= is
NOT expanded (by this tool or the CLI) - use an absolute path.
ssh: SSH agent socket/key specs (e.g. ["default"], using $SSH_AUTH_SOCK). Refused when the build has to run on a
remote host: the socket read would be that host's.
timeout_seconds: Subprocess timeout (default 1800s)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: context (required), ssh, file, load, +20 more.
buildx_createA
Create a new BuildKit builder instance.
Needed when the default docker driver falls short: multi-platform builds and cache export
require a docker-container (or kubernetes/remote) builder. Pass use=True to make it
the default for later buildx_build calls (else switch with buildx_use); bootstrap=True
starts the builder now rather than on first build.
Does not raise on a non-zero CLI exit (a missing buildx plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
name: Name for the new builder (defaults to a generated name)
driver: BuildKit driver (e.g. "docker-container", "kubernetes", "remote")
driver_opts: Driver-specific options (each becomes --driver-opt KEY=VALUE)
use: Set the new builder as the current one
bootstrap: Boot the builder immediately
platforms: Platforms the builder advertises
config: Path to a buildkitd config file (copied to the target host if no local plugin); passed as
--buildkitd-config, so this argument needs buildx >= 0.17
node_name: Node name within the builder (for multi-node builders)
append: Append a node to an existing builder named name
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: use, name, append, config, +5 more.
buildx_duARead-only
Report BuildKit cache disk usage as a list of records.
A large cache can easily generate more output than MAX_CLI_OUTPUT_BYTES; if that happens the
captured stdout is truncated and this tool drops the final (partial) record before parsing.
For an exhaustive accounting on a busy builder, run docker buildx du --format '{{json .}}'
on the host directly. Reclaim the cache with buildx_prune (system_df covers daemon-side
disk, not builder cache).
Raises RemoteFailureError if the CLI call fails.
Args: builder: Override the active builder
Returns:
list: One dict per cache record (parsed from --format '{{json .}}')
Inputs: builder.
buildx_history_inspectARead-only
Inspect a single build record by ref, parsed from --format json.
Returns the full record for one build - duration, materials, attestations, error (if any) -
for debugging a failed or slow build found via buildx_history_list. Requires buildx >=
v0.13.
Raises RemoteFailureError if the CLI call fails.
Args:
ref: Build record ref. Pass the ref field from buildx_history_list directly - it reports a qualified
"//", but history inspect only accepts the bare id, so this reduces it to the id and
(unless builder is given) targets the builder named in the ref. Empty/omitted inspects the most recent
build; the ^N syntax (e.g. "^0" = latest) is also valid.
builder: Builder instance the build ran on (defaults to the one in ref, else active)
Returns: dict: The parsed build record (or {"raw": } if the output isn't a JSON object)
Inputs: ref, builder.
buildx_history_listARead-only
List recent build records (BuildKit build history), parsed from --format '{{json .}}'.
Each record is a past build with its ref, name, status, step counts, and timestamps - useful for
finding a build to drill into with buildx_history_inspect. Requires buildx >= v0.13 (older
versions have no history subcommand and this raises with the CLI's "unknown command" error).
Raises RemoteFailureError if the CLI call fails.
Args: builder: Builder instance to read history from (defaults to the active builder)
Returns: list: One dict per build record (ref, name, status, total/completed/cached steps, times)
Inputs: builder.
buildx_imagetools_createA
Create a manifest list / OCI image index from existing per-platform tags.
Replaces docker manifest create + docker manifest push - builds the index and pushes it in
one operation. Source tags must already be pushed; this only stitches them together. Verify
the result with buildx_imagetools_inspect.
Does not raise on a non-zero CLI exit (a missing buildx plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
target: Tag for the new manifest list (-t)
sources: Source image references to combine
append: Append to the existing manifest at target rather than replacing
dry_run: Print the resulting manifest without pushing
annotations: OCI annotations (repeatable; passed verbatim)
platforms: Filter source platforms when combining
descriptor_files: Files to read source descriptors from, instead of refs (copied to the target host if no local
plugin)
builder: Override the active builder
timeout_seconds: Subprocess timeout (default 600s)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: target (required), sources (required), append, builder, +5 more.
buildx_imagetools_inspectARead-only
Inspect a manifest in a registry without pulling.
Replaces docker manifest inspect. The standalone docker manifest command is in
maintenance mode and lacks support for OCI image indexes, attestations, and
annotations - buildx imagetools inspect is the path forward and handles both
single-platform manifests and multi-platform manifest lists / OCI indexes. Uses the docker
CLI's credential store; registry_manifest answers the same question over direct HTTPS
with no daemon or plugin.
Does not raise on a non-zero CLI exit (a missing buildx plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
image: Image reference, e.g. "alpine:3.19" or "ghcr.io/org/repo@sha256:..."
raw: Return the raw manifest bytes (a JSON document) instead of the human-rendered tree
format: Go template format string (mutually exclusive with raw)
builder: Override the active builder
Returns:
dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}. With raw=True or
format="{{json .}}", stdout is a JSON document the caller can parse.
Inputs: image (required), raw, format, builder.
buildx_inspectARead-only
Inspect a builder instance.
Human-readable detail (driver, status, supported platforms) for one builder; buildx_list
returns machine-parsed JSON for all builders.
Does not raise on a non-zero CLI exit (a missing buildx plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args: name: Builder name (defaults to the active builder) bootstrap: Boot the builder if it isn't already running
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}. stdout is human-readable; parse with the agent or call buildx_list for JSON.
Inputs: name, bootstrap.
buildx_listARead-only
List builder instances.
Machine-parsed view of every builder; use buildx_inspect for one builder's human-readable
detail and buildx_use to switch the default.
Raises RemoteFailureError if the CLI call fails.
Returns:
list: One dict per builder (parsed from --format '{{json .}}'). If the captured stdout was truncated by
MAX_CLI_OUTPUT_BYTES the last (likely partial) record is dropped before parsing.
Inputs: no named parameters.
buildx_pruneADestructiveIdempotent
Remove BuildKit cache entries.
Destructive: this tool always passes --force because no interactive prompt is
available under MCP. Pair with buildx_du first to inventory what would be removed.
Does not raise on a non-zero CLI exit (a missing buildx plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args: all: Include internal/frontend images filters: Filter by attributes (e.g. {"until": "24h", "type": "exec.cachemount"}) reserved_space: Amount of disk to always keep (e.g. "10GB") max_used_space: Maximum disk space the cache may use (e.g. "20GB") min_free_space: Target amount of free disk after pruning (e.g. "5GB") builder: Override the active builder timeout_seconds: Subprocess timeout (default 600s)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: all, builder, filters, max_used_space, +3 more.
buildx_removeADestructive
Remove a builder instance.
Deletes a builder made by buildx_create, including its build cache unless keep_state=True;
use buildx_prune to reclaim cache while keeping the builder.
Does not raise on a non-zero CLI exit (a missing buildx plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
name: Builder name to remove (mutually exclusive with all_inactive)
all_inactive: Remove every inactive builder
keep_state: Keep the BuildKit state volume
keep_daemon: Keep the BuildKit daemon process running
force: Force removal even if the builder is in use
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: name, force, keep_state, keep_daemon, +1 more.
buildx_useA
Select the active builder for subsequent buildx operations.
Without default or global_default the switch applies only to the current CLI
session. default persists the choice for the current Docker context; global_default
persists across all Docker contexts. Use buildx_list to see available builders and their
current status. To avoid switching the global default, pass a specific builder name
directly via buildx_build's builder parameter instead.
Does not raise on a non-zero CLI exit (a missing buildx plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
name: Builder name to activate (from buildx_list)
default: Persist as default builder for the current Docker context
global_default: Persist as default builder across all Docker contexts
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: name (required), default, global_default.
compose_buildA
Build images for a compose project.
Builds the images declared by the project's build: sections without starting anything -
compose_up(build=True) builds and starts in one step.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
services: Specific services to build (default: all)
pull: Always attempt to pull a newer base image
no_cache: Do not use cache when building
timeout_seconds: Subprocess timeout (default 1800s)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: pull, files, no_cache, services, +3 more.
compose_configARead-only
Render the canonical compose configuration after merges, profiles, and variable substitution.
Use it to validate compose files and see exactly what the CLI will run before compose_up.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises): on a failed
render config may be None - inspect raw.stderr.
Args:
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
profiles: Profiles to activate before rendering
services_only: List service names only (--services)
format: Render as YAML (default) or JSON
Returns:
dict: {"config": str|dict|None, "raw": }; config is a parsed dict when format="json" and
parsing succeeds, otherwise the rendered text from stdout.
Inputs: files, format, profiles, project_dir, +2 more.
compose_cpA
Copy files/folders between a service container and the server host's filesystem.
Exactly one of source/dest is SERVICE:PATH; the other is a path on the host running this
MCP server, read/written as the server's user (same host exposure as the file-path archive
tools - see SECURITY.md). Copying to stdout (dest="-") is unsupported; use
container_archive_get.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises) - inspect
returncode/stderr in the result. With no local compose plugin and an ssh:// target, runs the real
docker compose cp on that host instead and relays whichever side of the copy is local over the same SSH
connection - every parameter above behaves the same either way, since the actual copy always runs through
the real CLI. The one difference: a container->host copy is refused if the local destination already
exists, since only this host (not the remote one) knows that. unix:///tcp://+TLS hosts with no local
plugin are not covered by this fallback (no shell to run the CLI on) and still raise CapabilityError -
use container_archive_put (host to container) or container_archive_get_to_file (container to host)
there instead; both talk to the daemon directly and need no local CLI (compose_ps gives you the
container name).
Args:
source: SERVICE:SRC_PATH or a host path
dest: SERVICE:DEST_PATH or a host path (not "-")
index: Container index when the service has multiple replicas
all_containers: Copy to/from all containers of the service (--all)
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
timeout_seconds: Subprocess timeout (default 300s)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: dest (required), source (required), files, index, +4 more.
compose_downADestructive
Stop and remove containers, networks (and optionally volumes) for a compose project.
Inverse of compose_up. Images are kept; named volumes go only with volumes=True
(destructive). Use compose_stop to stop without removing anything.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
volumes: Also remove named volumes declared by the project (destructive)
remove_orphans: Remove containers not declared in the compose file
timeout_seconds: Subprocess timeout (default 300s)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: files, volumes, profiles, project_dir, +3 more.
compose_execA
Run a command inside an already-running compose service container (see also container_exec).
Always passes -T (no TTY). Pass an exec-form argv (e.g. ["python", "-V"]); a
["sh", "-c", "..."] form interprets shell metacharacters in untrusted substrings.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
service: Service name from the compose file
command: Argv to execute inside the container
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
index: Container index when the service has multiple replicas
workdir: Working directory inside the container
user: User to run as inside the container (uid or name)
env: Environment variables to set for the exec session
timeout_seconds: Subprocess timeout (default 60s)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: command (required), service (required), env, user, +6 more.
compose_imagesARead-only
List the images used by a compose project's services, parsed from --format json.
Answers "what image and tag does each service container actually run?" - the containers must
exist (compose_up first). Use compose_ps for container state and
image_list for daemon-wide images.
Raises RemoteFailureError if the CLI call fails.
Args:
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
services: Restrict to these services (default: all)
Returns: list: One dict per container image (service, container, repository, tag, id, size)
Inputs: files, services, project_dir, project_name.
compose_killADestructive
Send a signal to a compose project's containers (default SIGKILL).
Immediate, with no grace period - prefer compose_stop for a clean shutdown (stop signal,
then kill after a timeout).
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
services: Restrict to these services (default: all)
signal: Signal to send (default "SIGKILL"; e.g. "SIGTERM", "SIGHUP")
remove_orphans: Also remove containers for services not in the compose file
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: files, signal, services, project_dir, +2 more.
compose_listARead-only
List compose projects known to the daemon (across all directories).
Project-level view (one entry per project); compose_ps lists the containers of a single
project.
Raises RemoteFailureError if the CLI call fails.
Args: all: Include stopped projects
Returns:
list: One dict per project (parsed from --format json)
Inputs: all.
compose_logsARead-only
Fetch a bounded slice of logs from a compose project (never follows).
Bounded and non-following by design, so it always returns promptly. For one container's logs
use container_logs; for a swarm service use service_logs. Log text arrives on stdout.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
services: Restrict to these services (default: all)
tail: Lines per container, or the literal "all" (still capped at MAX_CLI_OUTPUT_BYTES)
since: Show logs since this timestamp/duration (e.g. "10m", "2024-01-01T00:00:00")
until: Show logs before this timestamp/duration
timestamps: Include per-line timestamps
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: tail, files, since, until, +4 more.
compose_pauseA
Pause the containers of a compose project (freezes their processes in place).
Paused containers stop consuming CPU but keep memory, network endpoints, and state; resume
with compose_unpause. To actually stop containers (each one's configured stop signal,
freeing resources) use compose_stop; to stop and delete them use compose_down.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
services: Restrict to these services (default: all)
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: files, services, project_dir, project_name.
compose_portARead-only
Resolve the host binding for a service's container port.
The compose equivalent of docker port: which host address/port a service's private port is
published on. published is None when the port isn't published. For non-compose containers
read container_inspect's NetworkSettings.Ports instead.
Raises RemoteFailureError if the CLI call fails.
Args:
service: Service name from the compose file
private_port: The container-internal port to look up
protocol: "tcp" (default) or "udp"
index: Container index when the service has multiple replicas
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
Returns:
dict: {"service", "private_port", "protocol", "published": "host:port"|None, "host": str|None, "port": int|None,
"bindings": list[str]}. published/host/port describe the first binding; bindings lists every line (a
port can be published on more than one address, e.g. IPv4 and IPv6).
Inputs: service (required), private_port (required), files, index, +3 more.
compose_psARead-only
List containers in a compose project, parsed from --format json.
Container-level view of one project (state, health, publishers); compose_list enumerates
projects, and container_list covers non-compose containers.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises): services
comes back empty - inspect raw.stderr.
Args:
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
services: Restrict output to these services
all: Include stopped containers as well
Returns:
dict: {"services": list[dict], "raw": }; on non-zero exit services is an empty list and the
caller should inspect raw.stderr.
Inputs: all, files, services, project_dir, +1 more.
compose_pullA
Pre-fetch images for a compose project's services without starting them.
Use this to stage images before an outage window, to refresh cached images before
compose_up, or to verify images are accessible without starting containers. For
registry-authenticated pulls ensure the daemon is logged in first with system_login.
compose_up --pull always does the same as part of startup; use this tool when you
want to separate the pull step.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f; overrides auto-discovery)
project_name: Override the compose project name
services: Pull only these services; omit to pull all
ignore_pull_failures: Continue if an individual image pull fails
timeout_seconds: Subprocess timeout (default 1800s for large image pulls)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: files, services, project_dir, project_name, +2 more.
compose_restartA
Stop then start services without recreating containers or applying config changes.
Use this to bounce a service (e.g. to pick up a runtime file change or clear an
in-memory state). If the compose file has changed (new image, environment, volumes,
ports) use compose_up instead - it recreates affected containers to apply the diff.
stop_timeout_seconds controls the SIGTERM grace period before Docker sends SIGKILL.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Override the compose project name
services: Restart only these services; omit to restart all
stop_timeout_seconds: Seconds to wait for graceful stop before SIGKILL
timeout_seconds: Subprocess timeout (default 300s)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: files, services, project_dir, project_name, +2 more.
compose_runA
Run a one-off command against a compose service.
Always passes -T (no TTY under MCP). Defaults to detached with --rm so the call returns
promptly. Unlike compose_exec, this starts a NEW container for the service rather than
running inside the existing one.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
service: Service name from the compose file
command: Command + args to run (exec-form; no shell unless you invoke one)
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
detach: Run detached
rm: Remove the container after the run
no_deps: Don't start linked services
workdir: Working directory inside the container
user: User to run as inside the container (uid or name)
env: Environment variables to set inside the container
name: Container name
timeout_seconds: Subprocess timeout (default 600s)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: service (required), rm, env, name, +9 more.
compose_startA
Start existing (stopped) containers of a compose project.
Counterpart to compose_stop: starts existing containers without recreating them. Use
compose_up to (re)create containers from the compose file.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
services: Specific services to start (default: all)
timeout_seconds: Subprocess timeout (default 600s)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: files, services, project_dir, project_name, +1 more.
compose_stopA
Stop services in a compose project without removing their containers.
Unlike compose_down, containers/networks/volumes survive - use compose_start to bring them back.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
services: Specific services to stop (default: all)
stop_timeout_seconds: Grace period before SIGKILL (passed as --timeout)
timeout_seconds: Subprocess timeout (default 300s)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: files, services, project_dir, project_name, +2 more.
compose_topARead-only
Show the running processes of a compose project's containers.
Output is the ps-style process table per service (not JSON); read it from stdout. The
per-container equivalent is container_top.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
services: Restrict to these services (default: all)
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: files, services, project_dir, project_name.
compose_unpauseA
Unpause the containers of a compose project (resumes paused processes).
Reverse of compose_pause: processes continue from where they were frozen (no restart).
compose_start is the counterpart for stopped containers.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
services: Restrict to these services (default: all)
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: files, services, project_dir, project_name.
compose_upA
Bring up a Docker Compose project, detached.
Always runs detached (-d) so it can't block the server. Use compose_ps to confirm
services are running, or wait=True to block until they're healthy.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
project_dir: Dir with the compose file (default: server cwd, copied to the target host if no local plugin; paths
verbatim, no shell expansion)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
services: Specific services to bring up (default: all)
build: Build images before starting
pull: Pull strategy; omit to use each service's own pull_policy
remove_orphans: Remove containers for services not in the compose file
wait: Block until services are healthy (adds --wait)
timeout_seconds: Subprocess timeout (default 600s)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: pull, wait, build, files, +6 more.
compose_waitARead-only
Block until the named service containers stop, then return their exit codes.
For one-shot / batch services. A long-running service that never exits blocks until
timeout_seconds, then the subprocess is killed (TimeoutExpired) - bound it sensibly.
Exit codes are on stdout. For a single container use container_wait; for swarm services
use service_wait.
Does not raise on a non-zero CLI exit (a missing compose plugin or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
services: One or more services to wait on. At least one is required.
project_dir: Dir with the compose file (default: server cwd; copied to the target host if no local plugin)
files: Explicit compose file paths (repeatable, -f)
project_name: Compose project name override
timeout_seconds: Subprocess timeout (default 300s)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: services (required), files, project_dir, project_name, +1 more.
config_createA
Create an immutable Swarm config object; requires a swarm manager.
Configs store non-sensitive configuration files (nginx.conf, app.yaml, etc.) and mount
them into service containers at a specified path. Unlike secrets, config data is not
encrypted at rest - use secret_create for credentials or keys. data is raw bytes;
encode strings first (e.g. "my config".encode()). Once created, a config is immutable:
to update it, create a new config with a new name and update the service to reference it,
then remove the old config with config_remove.
Args: name: Unique config name within the swarm data: Raw bytes content of the config file labels: Labels to set on the config templating: Templating driver config (e.g. {"Name": "golang"} for Go template syntax)
Returns: dict: The created config's full document ({"ID", "Version", "CreatedAt", "Spec", ...})
Inputs: data (required), name (required), labels, templating.
config_inspectARead-only
Get a swarm config's full inspect payload by id or name.
Requires a swarm manager. Unlike a secret, a config's payload IS readable after creation:
Spec.Data in the result holds the base64-encoded contents. Use config_list to enumerate
configs; use this to read one config's contents and metadata.
Returns: dict: The config's full document (ID, CreatedAt, UpdatedAt, Spec{Name, Labels, Data base64})
Inputs: id_or_name (required).
config_listARead-only
List swarm configs; requires a swarm manager.
Unlike secrets, config attrs include the actual config data (Spec.Data, base64-encoded)
since configs are not treated as sensitive. Valid filter keys: id, name, names,
label (key or key=value). Fetch a single config by id/name with config_inspect.
Args: filters: Narrow the list; omit to return every config
Returns: list: One full config document ({"ID", "Spec", ...}) per config
Inputs: filters.
config_removeADestructive
Remove a swarm config.
Requires a swarm manager, and fails while any service still references the config - update or
remove those services first. The last step of the rotation flow described in config_create.
Returns: bool: True after removal
Inputs: id_or_name (required). Returns: result
container_archive_getARead-only
Retrieve a file or directory from a container as a tar archive, returned in band.
In-band bytes are capped (default 32 MiB) because MCP base64-encodes them;
container_archive_get_to_file streams to a host path instead.
Args: path: Path inside the container max_bytes: Abort with ToolInputError if the archive exceeds this many bytes (defaults to 32 MiB)
Returns: dict: Mapping with archive (bytes) and stat (dict) keys
Inputs: path (required), id_or_name (required), max_bytes.
container_archive_get_to_fileA
Retrieve a file or directory from a container as a tar archive written to a file on the server host.
File-writing variant of container_archive_get - prefer it for anything large, since in-band
bytes are base64-encoded by MCP. For the whole filesystem use container_export. Streams
straight to disk (no in-band byte cap). The file is written by the server's user; ~ is
expanded and an existing file is refused unless overwrite=True.
Args: path: Path inside the container dest_path: Destination path on the server host for the tarball overwrite: Replace dest_path if it already exists
Returns: dict: {"path": , "bytes_written": int, "stat": dict}
Inputs: path (required), dest_path (required), id_or_name (required), overwrite.
container_archive_putA
Upload a tar archive to a path inside a container, from in-band bytes or a file on the server host.
Inverse of container_archive_get: the archive is extracted at path inside the container.
Pass exactly one of data (tar bytes in band) or from_file (a path on the server host, streamed
straight to the daemon - preferred for large archives, since in-band bytes are base64-encoded by
MCP). from_file is read by the server's user; ~ is expanded.
Args: path: Destination path inside the container (must already exist) data: Tar archive bytes; exactly one of data/from_file from_file: Path on the server host to the tar archive to upload; exactly one of data/from_file
Returns: bool: True if the upload succeeded
Inputs: path (required), id_or_name (required), data, from_file. Returns: result
container_commitA
Snapshot a container's current filesystem state as a new image.
Useful for capturing a debugging state or saving manual changes made inside a container.
For repeatable builds use image_build with a Dockerfile instead; publish the result with
image_tag + image_push. The container is paused by default during
the snapshot to ensure filesystem consistency - set pause=False only if the container
cannot be paused. changes accepts Dockerfile instructions to apply on top of the
snapshot, e.g. ["CMD ["python", "app.py"]", "ENV FOO=bar"].
Args: id_or_name: Container id or name to snapshot repository: Repository name for the new image, e.g. "myorg/myimage" tag: Tag for the new image (default: "latest") message: Commit message stored in the image metadata author: Author string stored in the image metadata pause: Pause the container during commit for consistency changes: Dockerfile instructions (CMD, ENV, EXPOSE, etc.) to apply to the image conf: Additional image configuration overrides as a dict
Returns: dict: The new image's full inspect payload (Id is the new image id)
Inputs: id_or_name (required), tag, conf, pause, +4 more.
container_createA
Create a container from an image without starting it.
Use this when you need to configure a container (with extra_kwargs) before its first
start, or want creation and start as separate observable steps. For the common case of
create-then-start-immediately use container_run instead - it does both in one call.
Start the created container with container_start. Common extra_kwargs keys: name
(str), environment (list of "KEY=VAL" or dict), ports (dict, e.g. {"80/tcp": 8080}),
volumes (dict, e.g. {"/host/path": {"bind": "/container/path", "mode": "rw"}}), labels
(dict). For anything else docker-py's ContainerCollection.create accepts, call
docs_lookup(section="containers") rather than guessing a key name.
Args: image: Image to create the container from, e.g. "nginx:alpine" command: Override the image's default command; string or list of strings extra_kwargs: Additional docker-py ContainerCollection.create keyword arguments
Returns: dict: The created container's full inspect payload (not yet running)
Inputs: image (required), command, extra_kwargs.
container_diffARead-only
List filesystem changes a container has made relative to its image.
Use it to audit what a container wrote before container_commit or container_archive_get,
or to debug unexpected writes. Only the writable container layer is compared - files in
volumes and bind mounts never show up.
Returns: list: Dicts of {"Path", "Kind"}; Kind 0=modified, 1=added, 2=deleted
Inputs: id_or_name (required).
container_execA
Run a command inside a running container (for a compose service, prefer compose_exec).
Security: when any element of cmd is agent-controlled, use an exec-form argv list that does not
invoke a shell (e.g. ["python", "-V"], ["ls", path]). A string cmd, or a shell form like
["sh", "-c", template], interprets shell metacharacters in the untrusted parts.
Args:
cmd: Command to execute (prefer exec-form argv, no shell, when any element is agent-controlled)
tty: Allocate a pseudo-TTY
privileged: Run with extended privileges
user: User to run the command as
detach: Detach from the exec
environment: Environment variables, as {"KEY": "value"} or a list of "KEY=value" strings
workdir: Working directory inside the container
demux: Return stdout and stderr separately
Returns: dict: {"exit_code", "output"}; output is combined stdout+stderr, or a [stdout, stderr] pair with demux=True
Inputs: cmd (required), id_or_name (required), tty, user, +8 more.
container_exportA
Export a container's filesystem as a tar archive: to a file on the server host, or in band.
The tar is a flat filesystem snapshot with no image metadata or layers - use image_save for
an archive that image_load can restore, and container_archive_get for a single file or
directory. With dest_path the archive streams straight to disk (no byte cap), so it handles large
containers - the file is written by the server's user, ~ is expanded, and an existing file is
refused unless overwrite=True. Without dest_path the tar bytes are returned in band, capped
at max_bytes (default 32 MiB) because MCP base64-encodes them - a fallback for when no
writable host path exists (e.g. a containerized server without a bind mount).
Args: dest_path: Destination path on the server host; omit to return the bytes in band overwrite: Replace dest_path if it already exists max_bytes: In-band mode: abort with ToolInputError beyond this many bytes (default 32 MiB)
Returns: bytes | dict: the tar bytes (in band), or {"path": , "bytes_written": int}
Inputs: id_or_name (required), dest_path, max_bytes, overwrite. Returns: result
container_inspectARead-only
Return the full inspect detail for a single container.
Use this when you need complete information about one container - config, state,
network settings, mounts, environment variables, and resource limits. To enumerate many
containers use container_list instead (same payload per container by default; abridged
with sparse=True). For just logs or stats use container_logs / container_stats.
Args: id_or_name: Container id (full or short) or name
Returns:
dict: Full container inspect attrs (equivalent to docker inspect)
Inputs: id_or_name (required).
container_killADestructive
Send a signal to a running container (default SIGKILL - immediate, no graceful shutdown).
Use it to force-kill a container that ignores container_stop, or with signal to poke a
process without stopping it (e.g. SIGHUP for a config reload). For a normal shutdown prefer
container_stop, which sends the container's configured stop signal first. Fails with a
conflict error if the container is not running. When the server runs containerized it refuses
to signal its own container.
Args: signal: Signal name or number as a string (e.g. "SIGHUP", "9"); default SIGKILL
Returns: dict: The container's full inspect payload after the signal
Inputs: id_or_name (required), signal.
container_listARead-only
List containers on the daemon (running only by default).
Pass all=True to include stopped containers. For a compose project compose_ps groups
containers by service; for swarm services use service_ps (tasks may live on other nodes).
Args:
all: Show all containers, including stopped ones (default False: running only)
since: Only show containers created after this id or name
before: Only show containers created before this id or name
limit: Maximum number of results
filters: Filter by attributes (e.g. status, label)
sparse: Skip inspect calls and return less detail
ignore_removed: Ignore containers removed during listing; inert when sparse=True, which
skips the inspect calls that would fail
managed_only: Only return containers created by this MCP server (filters on the docker-mcp-server.managed
label); combines with any filters given
Returns:
list: One dict per container: full inspect payloads by default (each match is inspected, like
container_inspect); sparse=True skips the per-container inspect calls and returns the daemon's abridged
list entries instead
Inputs: all, limit, since, before, +4 more.
container_logsARead-only
Get the logs of a container: a one-shot snapshot by default, or a bounded live tail with follow=True.
Follow mode returns when limit_lines lines are collected, timeout_seconds elapses, or the
container exits, whichever comes first - so the agent can watch live output without blocking
forever. limit_lines/timeout_seconds apply only in follow mode; until only in snapshot mode.
Snapshot mode is capped at 32 MiB and raises ToolInputError past it, so a noisy container can't
exhaust the server's memory; service_logs caps the same way and lets the caller raise it.
Prefer an integer tail, or since, over tail="all" on a long-running container: "all" is
safe but will abort on the cap rather than returning a partial answer, and a large result can
still exceed the agent's context.
Caveat for ssh:// daemons: docker-py can't cancel an SSH stream, so in follow mode the
timeout_seconds watchdog can't interrupt a fully silent container - use the snapshot mode
there if you need a hard time bound.
Args: tail: Number of lines from the end, or the literal "all" for everything since: Only return logs created after this unix timestamp until: Only return logs created before this unix timestamp (snapshot mode only) follow: Follow the live log stream instead of returning a snapshot limit_lines: Follow mode: max lines to collect before returning timeout_seconds: Follow mode: max wall-clock seconds before returning what was collected
Returns:
str: Decoded log output (up to limit_lines lines in follow mode). Raises ToolInputError in snapshot mode if
the logs exceed 32 MiB.
Inputs: id_or_name (required), tail, since, until, +6 more. Returns: result
container_pauseA
Suspend all processes in a container using the kernel freezer cgroup.
Unlike sending SIGSTOP, the freezer cgroup suspends processes without their being able
to observe or intercept the suspension. A paused container keeps its resources (memory,
open file descriptors) but consumes no CPU. Resume with container_unpause -
container_exec fails against a paused container until it is unpaused.
Returns: dict: The container's full inspect payload after pause (State.Paused true)
Inputs: id_or_name (required).
container_pruneADestructiveIdempotent
Remove all stopped containers to reclaim disk space.
Only removes containers that are not running - running containers are never affected.
Use container_list(all=True) to preview what would be removed before calling this.
Valid filter keys: until (RFC3339 timestamp or duration like "24h" - removes containers
stopped before that point), label (key or key=value). For a broader cleanup of
containers plus unused images, networks, and volumes see the prune_managed prompt.
Args: filters: Narrow which stopped containers to remove; omit to remove all stopped
Returns: dict: {"ContainersDeleted": [...], "SpaceReclaimed": }
Inputs: filters.
container_removeADestructive
Remove a container, deleting its writable layer.
The image is untouched (image_remove deletes images); named volumes are never removed -
volumes=True only covers anonymous ones. A running container is refused unless force=True,
which kills it first. When the server runs containerized it refuses to remove its own
container.
Args:
volumes: Also remove anonymous volumes (the CLI's --volumes); named volumes persist
link: Remove the specified link
force: Kill a running container before removing it (default False: running is an error)
Returns: bool: True after removal completes
Inputs: id_or_name (required), link, force, volumes. Returns: result
container_renameA
Rename a container in place; its id, state, and configuration are unchanged.
Use it to free up or claim a container name (names are unique per daemon) - e.g. before
starting a replacement under the old name. Fails with a conflict error if the new name is
already taken. Not related to image_tag, which names images.
Args: name: The new name; must not be in use by any other container
Returns: dict: The container's full inspect payload after the rename
Inputs: name (required), id_or_name (required).
container_restartA
Restart a container: stop then start again in one call.
The container receives its configured stop signal (STOPSIGNAL, default SIGTERM), SIGKILL
after stop_timeout_seconds, and is then started. Use container_stop/container_start to do
the halves separately. When the server runs containerized it refuses to restart its own
container.
Args: stop_timeout_seconds: Seconds between the stop signal and SIGKILL
Returns: dict: The container's full inspect payload after the restart
Inputs: id_or_name (required), stop_timeout_seconds.
container_runA
Run a container from an image (create and start in one call, like docker run).
Use container_create to prepare a container without starting it, or container_exec to run
a command in a container that already exists. With detach=False the call blocks until the
container exits and returns its output, so long-running images need detach=True. Created
containers are stamped with provenance labels.
Args:
command: The command to run in the container
name: Name to assign to the container
detach: Run in the background and return container info
environment: Environment variables, as {"KEY": "value"} or a list of "KEY=value" strings
ports: Port mappings, e.g. {'2222/tcp': 3333}
volumes: Volumes to mount, as {"/host/path": {"bind": "/in/container", "mode": "rw"}}
or a list of "host:container:mode" strings
network: Name of the network to attach
hostname: Hostname for the container
user: Username or UID to run as
working_dir: Working directory inside the container
entrypoint: Entrypoint to override the image default
restart_policy: Restart policy, e.g. {'Name': 'on-failure', 'MaximumRetryCount': 3}
labels: Labels to set on the container
remove: Remove the container when it exits (only with detach=False)
auto_remove: Enable auto-removal of the container on daemon side
privileged: Give extended privileges to the container
tty: Allocate a pseudo-TTY
mem_limit: Memory limit: bytes as an int, or a units string ("100000b", "1000k", "128m", "1g")
cpu_count: Number of CPUs
extra_kwargs: Additional keyword arguments forwarded to ContainerCollection.run (call
docs_lookup(section="containers") for the full accepted set)
Returns: dict | str: The container's full inspect payload when detach=True, else stdout/stderr as a string
Inputs: image (required), tty, name, user, +18 more. Returns: result
container_startA
Start an existing stopped container.
Use this to restart a container that was previously created or stopped without removing it.
To create and start a new container in one step use container_run instead. Calling on
an already-running container has no effect (the daemon returns 304 and no error is
raised). To stop then start a running container use container_restart.
Args: id_or_name: Container id (full or short) or name
Returns: dict: The container's full inspect payload after starting
Inputs: id_or_name (required).
container_statsARead-only
Get one point-in-time resource-usage snapshot for a container (non-streaming).
Returns the raw engine stats payload; CPU percent must be computed from the delta between
cpu_stats and precpu_stats. By default the daemon collects two cycles before answering,
which is what fills precpu_stats - so the call takes about a second. one_shot=True returns
after a single collection instead, leaving precpu_stats zeroed and CPU percent uncomputable;
use it when only memory_stats/pids_stats matter. For a pre-computed human-readable summary
prefer the docker-stats://{id_or_name} resource; for a process listing use container_top.
Args:
one_shot: Skip the second collection cycle for a faster answer, at the cost of an empty
precpu_stats (so no CPU percent); needs daemon API v1.41+
Returns: dict: Engine stats payload (read, cpu_stats, precpu_stats, memory_stats, networks, pids_stats, ...)
Inputs: id_or_name (required), one_shot.
container_stopA
Gracefully stop a running container (its configured stop signal, then SIGKILL after a timeout).
Prefer this over container_kill for a clean shutdown: the main process receives the
container's stop signal (STOPSIGNAL, default SIGTERM) and has stop_timeout_seconds to exit
before the daemon force-kills it. Use container_restart to stop and start again in one call,
or container_pause to freeze processes without stopping. When the server runs containerized
it refuses to stop its own container.
Args: stop_timeout_seconds: Seconds between the stop signal and SIGKILL
Returns: dict: The container's full inspect payload after the stop (exit code under State.ExitCode)
Inputs: id_or_name (required), stop_timeout_seconds.
container_topARead-only
List the processes running inside a container (the daemon runs ps on the host).
Works on any running container without executing anything in it, so it needs no shell or ps
binary in the image - unlike container_exec with ps. Use container_stats for resource
usage rather than process lists. Fails if the container is not running.
Args: ps_args: Extra ps arguments (e.g. "aux"); default is the daemon's standard ps invocation
Returns: dict: {"Titles": [ps column names], "Processes": [[one row of values per process]]}
Inputs: id_or_name (required), ps_args.
container_unpauseA
Resume all processes in a paused container (the reverse of container_pause).
Only valid on a paused container - it fails if the container is merely stopped; use
container_start for stopped containers. Processes continue from where they were frozen.
Returns: dict: The container's full inspect payload after unpause (State.Paused becomes false)
Inputs: id_or_name (required).
container_updateA
Update resource limits on a container without recreating it.
Changes take effect immediately on Linux (cgroups); not all fields are updatable on
every platform. Common updates keys: mem_limit (bytes, e.g. 134217728 for 128 MB),
memswap_limit (memory+swap in bytes; -1 = unlimited), cpu_shares (relative weight,
default 1024), cpu_period / cpu_quota (microseconds for CFS throttling),
cpuset_cpus (e.g. "0-1"), restart_policy (dict with Name such as
"on-failure"/"always"/"unless-stopped" and optional MaximumRetryCount). To change
image, env, or volumes the container must be recreated (container_remove +
container_run).
Args: id_or_name: Container id or name to update updates: Resource fields to update; see description for valid keys
Returns: dict: The container's full inspect payload after the update
Inputs: updates (required), id_or_name (required).
container_waitARead-only
Block until a container reaches a condition: stopped, "healthy", or its logs contain a pattern.
One contract for every mode: never raises on timeout - the result always carries met (condition
reached) and timed_out. The stop conditions ("not-running"/"next-exit"/"removed") use the
daemon's blocking wait and fill status_code/error (the container's exit info); "healthy" polls
the container's HEALTHCHECK every poll_intervals and fills health/status; "log-match" polls
recent logs every poll_intervals for pattern and fills matched_line. For a compose
project use compose_wait; for swarm services use service_wait.
Health semantics: with no HEALTHCHECK defined, once the container is running the tool returns
promptly with health: null and met: false (false = "not confirmed healthy", not "unhealthy" -
check health to tell them apart). A container that exits before becoming healthy returns its
terminal status and met: false.
Log-match semantics: pattern is matched as a plain substring by default - safe against any
input, including adversarial ones. Pass regex=True to match pattern as a regular expression
(via re.search) instead; only do this with patterns you trust, since a regex with catastrophic
backtracking run against attacker-influenced log content can exhaust CPU (ReDoS). Checks stdout
and stderr, most recent lines first within each poll. If the container exits/dies before the
pattern ever appears, returns promptly with met=false (not timed_out) - no further logs can
arrive, so there's nothing to keep polling for.
Args:
until: Condition to wait for: "not-running" (default), "next-exit", "removed", "healthy", or "log-match"
(requires pattern)
timeout_seconds: Max seconds to wait before returning with timed_out=true
poll_interval: "healthy"/"log-match" only: seconds between re-checks (default 2, > 0); capped by the time left
so a large value can't push the total wait past the timeout
pattern: "log-match" only: substring (or, with regex=True, a regular expression) to look for in the
container's logs
regex: "log-match" only: treat pattern as a regular expression instead of a plain substring
Returns: dict: {"container", "until", "met", "timed_out", "status_code", "error", "health", "status", "matched_line", "waited_seconds"}; stop modes fill status_code/error, "healthy" fills health ("starting"/"healthy"/"unhealthy", or null with no healthcheck) and status, "log-match" fills matched_line when met and status if the container exited without matching.
Inputs: id_or_name (required), regex, until, pattern, +2 more.
context_createA
Create a new Docker CLI context pointing at a daemon endpoint.
Registers a named endpoint for the CLI; switch with context_use, enumerate with
context_list. It does not retarget this server's docker-py client (pinned at startup).
Does not raise on a non-zero CLI exit (a missing docker binary or a timeout still raises) - inspect
returncode/stderr in the result. It does raise ToolInputError before running anything if docker_host
or a TLS path contains a comma, which would inject extra keys (including skip-tls-verify) into the
endpoint spec.
Args:
name: Name for the new context (must not already exist)
docker_host: Daemon URL, e.g. "tcp://10.0.0.5:2376" or "unix:///var/run/docker.sock"; no commas
description: Human description shown in context ls
tls_ca: Path on the local host to the CA cert (for TLS daemons); no commas
tls_cert: Path on the local host to the client cert; no commas
tls_key: Path on the local host to the client key; no commas
skip_tls_verify: Disable TLS verification (insecure; for testing only). The only way to set it: it cannot be
smuggled through docker_host
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: name (required), docker_host (required), tls_ca, tls_key, +3 more.
context_inspectARead-only
Return the full configuration for a single Docker context.
Full endpoint/TLS detail for one context; context_list gives the one-line summary of all.
Raises RemoteFailureError if the CLI call fails.
Args:
name: Context name (use the Name field from context_list)
Returns:
dict: The parsed docker context inspect entry (keys include "Name" and "Endpoints" with the daemon URL)
Inputs: name (required).
context_listARead-only
List Docker CLI contexts known to the host running this MCP server.
Contexts are a CLI concept (stored in the docker config dir) letting one CLI target multiple
daemons. This server uses whatever DOCKER_HOST / current-context resolved to at startup, so
changing contexts only affects future subprocess-based tools, not the docker-py SDK client.
Use context_inspect for one context's full config and context_use to switch.
Raises RemoteFailureError if the CLI call fails.
Returns:
list: One dict per context, keyed as the CLI emits them - Name, Description,
DockerEndpoint and Current, the last being true for the context the CLI
would use by default
Inputs: no named parameters.
context_removeADestructive
Remove a Docker CLI context.
Deletes only the CLI's connection metadata - the daemon it pointed at is untouched. The
current context needs force=True (or context_use another first).
Does not raise on a non-zero CLI exit (a missing docker binary or a timeout still raises) - inspect
returncode/stderr in the result.
Args: name: Context name to remove force: Force removal even if the context is the current one
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: name (required), force.
context_useA
Set the active Docker context for the CLI on the host running this MCP server.
Note: this does not retarget the long-lived docker-py client - SDK-backed tools keep using the
endpoint they connected to at startup. To retarget those, restart the server with a different
DOCKER_HOST / DOCKER_CONTEXT. Create contexts with context_create; list them with
context_list.
Does not raise on a non-zero CLI exit (a missing docker binary or a timeout still raises) - inspect
returncode/stderr in the result.
Args: name: Existing context name to set as default
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: name (required).
docs_lookupARead-only
Look up Docker SDK/CLI/registry reference documentation by section.
A tool-callable mirror of the docker-docs:// resources, for clients that can't read MCP
resources (e.g. Claude Desktop, Cursor). Always registered regardless of
DOCKER_MCP_SERVER_DISABLE - looking something up costs nothing and isn't tied to any single
Docker feature area - but an individual section still refuses if the domain it documents is
disabled, matching the equivalent docker-docs://{section} resource exactly.
Omit section to list every available section with its source URL (same as
docker-docs://contents); pass a section name to fetch that page's content (same as
docker-docs://{section}). Most useful before constructing an extra_kwargs-style passthrough
dict for a tool like container_run/container_create/service_create (their docstrings only
list common keys, not every key docker-py accepts), or before writing Compose/Dockerfile/buildx
bake-file syntax, which no tool generates.
Args: section: Section name (from a no-argument call's index); omit to list all sections instead
Returns:
str: JSON section index (no section) or that section's raw HTML/Markdown content
Inputs: section. Returns: result
host_listARead-only
List the Docker hosts configured via DOCKER_MCP_SERVER_HOSTS.
With a single host (or the var unset) this is the one resolved daemon; with several it is
the set that the host argument selects from. The default entry is the one used when
host is omitted; pass a name as the host argument of daemon-backed tools
(system_ping(host=...) checks one entry). The docker-mcp://hosts resource mirrors this
tool.
Returns: list[dict]: one per host: name; url (resolved daemon URL, null = docker-py platform default); read_only; non_destructive (blocks destructive calls only); tls (whether a per-host cert dir is configured); default (the omitted-host fallback)
Inputs: no named parameters. Returns: result
hub_rate_limitARead-only
Report the caller's remaining Docker Hub pull-rate-limit budget.
Sends a HEAD to the ratelimitpreview/test manifest (a HEAD isn't metered as a pull, so the
check costs no budget) and reads the RateLimit-Limit / RateLimit-Remaining headers. Call it
before a large compose_pull / image_pull to avoid hitting the cap mid-deploy. Credentials
raise the limit and switch metering from per-IP to per-account; falls back to
DOCKER_MCP_SERVER_REGISTRY_USERNAME / DOCKER_MCP_SERVER_REGISTRY_PASSWORD, does NOT read ~/.docker/config.json.
Plans with no limit return no headers - reported as "unlimited": true.
Args: username: Hub username (overrides DOCKER_MCP_SERVER_REGISTRY_USERNAME) password: Hub password/token (overrides DOCKER_MCP_SERVER_REGISTRY_PASSWORD)
Returns: dict: {"authenticated", "limit", "remaining", "window_seconds", "unlimited"}
Inputs: password, username.
hub_repo_infoARead-only
Fetch Docker Hub metadata for a repository.
Public repos only: sends no auth and does NOT read the local Docker credential store;
private repos return 404/401. Hub-only metadata (stars, pulls, description) - use
registry_tags for tag lists on any OCI registry and hub_tags for Hub tag details.
Args: repository: Hub repository, e.g. "library/alpine" or "myorg/myimage"
Returns: dict: The Hub /v2/repositories// response (description, star_count, pull_count, last_updated, is_private, etc.)
Inputs: repository (required).
hub_tagsARead-only
List tags on a Docker Hub repository with Hub-specific metadata.
Hits the Hub UI API (hub.docker.com) for richer per-tag data than registry_tags -
last pushed date, per-platform sizes, digest. Public repos only: sends no auth and does NOT
read ~/.docker/config.json; private repos return 404/401 (use registry_tags against
registry-1.docker.io with credentials).
Args: repository: Hub repository, e.g. "library/alpine" or "myorg/myimage" limit: Max tags to return (default 100, >= 1); pagination capped at 50 pages
Returns: dict: {"name": , "tags": [{name, full_size, last_updated, digest, images}, ...], "truncated": bool}
Inputs: repository (required), limit.
image_buildA
Build an image from a Dockerfile using the daemon's classic builder.
Use this for simple single-platform builds from a local context. For multi-platform
builds, BuildKit cache export/import, or advanced build features prefer buildx_build.
path must be a directory accessible on the host running this server (it is the build
context sent to the daemon). dockerfile is normally relative to path; omit to use the
default Dockerfile.
dockerfile is not confined to the context, despite the usual relative form: docker-py detects
an absolute path, or a relative one escaping via .., reads that file from the server host's
filesystem and injects its contents into the build. So it can read any file the server user can,
like the other host-filesystem parameters (dest_path, from_file), and unlike them it is easy
to mistake for a context-relative name. buildx_build's --file resolves differently again
(against the CLI's working directory) - see its docstring.
Args:
path: Build context directory path on the server host
tag: Name and optional tag in "name:tag" format to apply to the built image
quiet: Suppress verbose build output (final image id still returned)
nocache: Ignore the layer cache and rebuild all layers
rm: Remove intermediate containers on success
pull: Always pull a newer version of each FROM base image before building
forcerm: Remove intermediate containers even on build failure
dockerfile: Dockerfile filename relative to path (default: "Dockerfile"); an absolute path or one containing
".." reads that file from the server host instead of the context
buildargs: Build-time variables passed as --build-arg; dict of str to str
container_limits: Resource limits for the build container, e.g. {"memory": 134217728}
shmsize: Size of /dev/shm in bytes for build steps that need shared memory
labels: Labels to set on the resulting image (dict of str to str)
cache_from: List of image references to use as layer cache sources
target: Stop at this named build stage (multi-stage Dockerfiles)
network_mode: Network mode for RUN instructions during build (e.g. "host", "none")
squash: Squash all new layers into one (experimental; requires daemon flag)
extra_hosts: Additional /etc/hosts entries during build; dict of hostname to IP
platform: Target platform, e.g. "linux/amd64" (single platform only; use buildx for multi)
isolation: Isolation technology, passed to the daemon as given; platform-dependent, so not validated here
(Windows documents "default", "process", "hyperv")
use_config_proxy: Forward proxy env vars from Docker client config to build
Returns:
dict: The built image's full inspect payload (as docker inspect)
Inputs: rm, tag, path, pull, +16 more.
image_historyARead-only
Return the layer history of an image.
Useful for auditing what commands built each layer and diagnosing image size. Each entry
includes Id (layer digest or "" for imported layers), Created (unix
timestamp), CreatedBy (the Dockerfile command that produced the layer, e.g. a RUN or
COPY), Size (bytes added by that layer), and Comment. For full image metadata use
image_inspect instead.
Args: id_or_name: Image name (with optional tag/digest) or id
Returns: list: Layer history entries, newest first
Inputs: id_or_name (required).
image_importA
Create an image from a flat root-filesystem tarball, like docker import.
Imports a filesystem archive as a new single-layer image with no build history - not the same
thing as image_load, which restores a docker save archive complete with its layers, tags and
history, so prefer image_load for anything image_save produced. Use this for a rootfs that
came from somewhere else: a container_export archive, a distro base tarball, a VM image dump.
The result has an empty config - no CMD/ENTRYPOINT/ENV - unless you supply changes, so an
imported image is usually not runnable until you set at least a command. Pass exactly one source
(from_file, data, from_url or from_image); ToolInputError otherwise. from_url and
from_image are fetched by the daemon, from_file/data are read here and uploaded; a
from_file path that is not a readable file raises rather than being retried as a URL. Unlike
the other image-creating tools this stamps no provenance labels: the Engine's import call accepts
no labels field, and changes does not cover LABEL.
Args:
repository: Repository name to give the new image, e.g. "myorg/rootfs"; may include a tag (myorg/rootfs:v1),
and defaults to :latest when it does not. Omit to import untagged, addressable only by the id in the
returned progress (omit it entirely -- a blank string is a ToolInputError, not a shorthand for untagged). A
digest reference is refused by the daemon. Required if tag is given
tag: Tag to apply, e.g. "v1". Overrides a tag already in repository rather than being ignored, so passing
repository="myorg/rootfs:v1" with tag="v2" yields :v2. Requires repository (ToolInputError without
it - the daemon would otherwise silently drop the tag and import untagged). Blank is also a ToolInputError,
not a shorthand for the default: the daemon would substitute latest without saying so
from_file: Path to a rootfs tarball on the server host (~ expanded), read by the server's user; refused if it
is not an existing regular file; exactly one source
data: Rootfs tarball contents in band (base64-encoded by MCP, so prefer from_file for anything but small
archives); exactly one source
from_url: URL the daemon fetches the tarball from; exactly one source
from_image: Name of an existing image to import from, like a Dockerfile FROM; exactly one source
changes: Dockerfile instructions applied to the new image, e.g. ['CMD ["/bin/sh"]']; only CMD, ENTRYPOINT, ENV,
EXPOSE, ONBUILD, USER, VOLUME and WORKDIR are supported. Parsed as real Dockerfile syntax, so shell form is
wrapped exactly as a Dockerfile would wrap it (CMD /bin/sh is stored as ["/bin/sh","-c","/bin/sh"]) -
use the exec form CMD ["/bin/sh"] to store a bare argv
Returns:
str: The daemon's raw newline-delimited JSON progress records; the final record carries the new image id as its
status
Inputs: tag, data, changes, from_url, +3 more. Returns: result
image_inspectARead-only
Return the full inspect detail for a single local image.
Includes config (env, entrypoint, exposed ports), size, layer digests (RootFS.Layers),
and all tags/digests referencing it (RepoTags/RepoDigests). For a quick overview of
many images use image_list instead. For the per-layer build history (which command
produced each layer) use image_history. Only inspects images already present locally -
for a remote image's manifest without pulling it use image_registry_data or
registry_manifest.
Args: id_or_name: Image name (with optional tag/digest) or id
Returns:
dict: Full image inspect attrs (equivalent to docker inspect on an image)
Inputs: id_or_name (required).
image_listARead-only
List images in the daemon's local store.
Local only - for a registry's contents use registry_tags / hub_tags, and image_search
to find images on Docker Hub. Dangling (untagged) build leftovers show with
filters={"dangling": True}.
Args: repository: Only show images of this repository all: Show intermediate image layers filters: Filter by attributes (label, dangling, before, since, etc.)
Returns:
list: One summary dict per image ({"Id", "RepoTags", "RepoDigests", "Created", "Size", "Labels", ...}); use
image_inspect for a full inspect payload
Inputs: all, filters, repository.
image_loadA
Load an image from a tarball produced by image_save, from in-band bytes or a file on the server host.
Counterpart of image_save; when the image lives in a registry, image_pull is the normal
route, and for a flat rootfs archive that is not a docker save bundle use image_import.
Pass exactly one of data (tarball bytes in band) or from_file (a path on the server host,
streamed straight to the daemon - preferred for anything but small images, since in-band bytes are
base64-encoded by MCP). from_file is read by the server's user; ~ is expanded.
Args:
data: Tarball contents; exactly one of data/from_file
from_file: Path to a tarball produced by docker save / image_save; exactly one of data/from_file
Returns: list: One full inspect payload per loaded image
Inputs: data, from_file.
image_pruneADestructiveIdempotent
Remove unused local images to reclaim disk space.
Without filters removes only "dangling" images - untagged layers not referenced by any
tag or container. To remove all images not used by any container (including tagged ones)
pass filters={"dangling": False}. Valid filter keys: dangling (bool as string
"true"/"false"), until (RFC3339 timestamp or duration like "24h"), label
(key or key=value). Use system_df first to see how much space is reclaimable.
Args: filters: Narrow which images to remove; omit to remove dangling images only
Returns: dict: {"ImagesDeleted": [...], "SpaceReclaimed": }
Inputs: filters.
image_prune_buildsADestructiveIdempotent
Delete the daemon's build cache to reclaim disk space.
Prunes the build cache - a separate Engine resource from the images image_prune removes,
so run both to reclaim everything a build leaves behind. Prefer buildx_prune when the build
ran on a non-default buildx builder (that builder keeps its own cache, invisible here) or when
you need any disk ceiling at all - reserved_space, max_used_space or min_free_space, none of
which the docker-py path can send; this tool needs no CLI plugin and works over any
transport, including a daemon with no local docker binary. Inventory first with system_df
(its BuildCache entry) or buildx_du. Destructive and immediate: later builds must re-run
the steps whose cache was removed. Needs Docker API v1.31+; passing either filters or all
needs v1.39+ and raises InvalidVersion on an older daemon - omit both to prune with the
daemon's own defaults.
Args:
filters: Narrow which cache records to remove, e.g. {"until": "24h"} (a duration or timestamp relative to the
daemon's clock); also accepts id, parent, type, description, inuse, shared, private; omit to
let the daemon prune unused cache
all: Remove all types of build cache, not just the unused records
Returns: dict: {"CachesDeleted": [...], "SpaceReclaimed": }
Inputs: all, filters.
image_pullA
Pull an image from a registry to the daemon's local store.
Private repositories need credentials - system_login (or docker login on the host) first,
or auth_config to authenticate this call alone. Use image_load for tarballs, and
registry_manifest / image_registry_data to inspect a remote image without pulling it.
Security: auth_config carries registry credentials, which many MCP clients log verbatim. Prefer
docker login on the host so the docker module reuses credentials cached in
~/.docker/config.json, and leave auth_config unset.
Args:
tag: The image tag (ignored when all_tags=True)
all_tags: Pull all tags from the repository
platform: Platform in os/arch format
auth_config: Per-call registry credentials under the keys username and password;
overrides the cached credential for this pull only
Returns: dict | list: The pulled image's full inspect payload, or one per image if all_tags=True
Inputs: repository (required), tag, all_tags, platform, +1 more. Returns: result
image_pushA
Push an image or repository to a registry.
The local image must already bear the target name - image_tag it with the
registry-qualified repository[:tag] first; a bare name pushes to Docker Hub. Private
registries need credentials (system_login, or docker login on the host).
Security: auth_config carries registry credentials, which many MCP clients log verbatim. Prefer
docker login on the host so the docker module reuses credentials cached in
~/.docker/config.json, and leave auth_config unset.
Args: auth_config: Registry authentication config
Returns: str: Push output as a string
Inputs: repository (required), tag, auth_config. Returns: result
image_registry_dataARead-only
Get registry data for an image without pulling it, via the daemon's distribution endpoint.
Uses the daemon (and its cached credentials) to resolve the remote descriptor and platform
list. For direct registry access without a daemon use registry_manifest.
Security: auth_config carries registry credentials, which many MCP clients log verbatim. Prefer
docker login on the host so the docker module reuses credentials cached in
~/.docker/config.json, and leave auth_config unset.
Args: repository: Image reference auth_config: Registry authentication config
Returns: dict: The registry data document {"Descriptor", "Platforms"} - the OCI descriptor and the platforms available for the reference
Inputs: repository (required), auth_config.
image_removeADestructive
Remove a local image by name or id.
Fails without force if the image is tagged by multiple names (untag first with
image_tag) or if stopped containers reference it. Running containers always block
removal regardless of force. noprune keeps untagged parent layers that would
otherwise be removed as a side-effect; leave False unless you need to preserve
the parent layers for another purpose.
Args: id_or_name: Image name (with optional tag/digest) or id to remove force: Remove even if referenced by stopped containers or multiple tags noprune: Do not delete untagged intermediate parent layers
Returns: bool: True after removal completes
Inputs: id_or_name (required), force, noprune. Returns: result
image_saveA
Save an image as a tar archive: to a file on the server host, or in band.
The archive keeps layers, tags, and metadata so image_load can restore it - different from
container_export, which flattens one container's filesystem. With dest_path the archive
streams straight to disk (no byte cap), so it handles large images - the file is written by
the server's user, ~ is expanded, and an existing file is refused unless
overwrite=True. Without dest_path the tar bytes are returned in band, capped at max_bytes
(default 32 MiB) because MCP base64-encodes them - a fallback for when no writable host path
exists (e.g. a containerized server without a bind mount).
Args: dest_path: Destination path on the server host; omit to return the bytes in band named: Whether to retain repository/tag names in the saved archive overwrite: Replace dest_path if it already exists max_bytes: In-band mode: abort with ToolInputError beyond this many bytes (default 32 MiB)
Returns: bytes | dict: the tarball bytes (in band), or {"path": , "bytes_written": int}
Inputs: id_or_name (required), named, dest_path, max_bytes, +1 more. Returns: result
image_searchARead-only
Search Docker Hub for public images matching a term.
Searches Docker Hub only - not GHCR, ECR, or other registries. For listing tags on a
specific image from any OCI registry use registry_tags instead.
Args: term: Search keyword, e.g. "nginx" or "python" limit: Maximum number of results to return (Docker Hub default is 25)
Returns:
list: Result dicts: {"name", "description", "star_count", "is_official", "is_automated"} - is_automated is
deprecated in the Engine API and is always false, so rank on star_count/is_official instead
Inputs: term (required), limit.
image_tagA
Tag an image into a repository (add a name to an existing local image).
The image id stays the same and no data is copied - a tag is an alias. Typical flow: tag with
the registry-qualified name, then image_push. image_remove on a tag merely untags while
other names remain. Tagging over a name that already exists repoints it, without asking.
Args: id_or_name: The source image name or id repository: Target repository name (registry-qualified for pushing, e.g. "ghcr.io/o/r") tag: Tag for the new image (default "latest")
Returns: bool: True if the image was tagged
Inputs: id_or_name (required), repository (required), tag. Returns: result
network_connectA
Attach a running container to an additional network without restarting it.
Use this to give a container access to services on a network it was not started with.
aliases sets extra DNS names for this container within the network (other containers
can reach it by those names in addition to its container name). ipv4_address /
ipv6_address assign a specific IP on the network; omit to let the driver assign one.
links is a legacy feature (deprecated; prefer DNS aliases). Use network_disconnect
to undo.
Args: id_or_name: Network id or name to connect the container to container: Container id or name to attach aliases: Additional DNS names for this container within the network links: Legacy container links (deprecated) ipv4_address: Static IPv4 address to assign on this network ipv6_address: Static IPv6 address to assign on this network link_local_ips: Link-local IP addresses to assign driver_opt: Driver-specific endpoint options mac_address: Static MAC address for this endpoint, e.g. "02:42:ac:11:00:04"; per-network rather than per-container, and a driver is free to ignore it
Returns: bool: True after the container is connected
Inputs: container (required), id_or_name (required), links, aliases, +5 more. Returns: result
network_createA
Create a network.
The daemon default driver is bridge (single-host); use overlay for swarm-wide networks.
Creating a network attaches nothing - connect containers afterwards with network_connect or
at start via container_run(network=...). Created networks carry provenance labels.
A duplicate name is always rejected,
so creating is not idempotent - check network_list first when the network may already exist.
Args:
driver: Driver name (daemon default bridge; overlay for swarm scope)
options: Driver-specific options dict
ipam: IPAM configuration as a dict (engine shape: {"Driver", "Config": [{"Subnet", "Gateway", ...}]})
internal: Restrict external access
labels: Labels to set on the network
enable_ipv6: Enable IPv6 networking
attachable: Allow standalone containers to attach (swarm overlay networks)
scope: Network scope; the driver picks a sensible default when omitted
ingress: Make this an ingress network for swarm routing-mesh
Returns: dict: The created network's full inspect payload
Inputs: name (required), ipam, scope, driver, +6 more.
network_disconnectA
Disconnect a container from a network.
The container keeps running with its other network attachments; only this endpoint is removed
(the reverse of network_connect). A network with connected containers cannot be deleted, so
disconnect them before network_remove.
Args: container: The container id or name to disconnect force: Force the disconnect; use to clear a stale endpoint (e.g. from a deleted container)
Returns: bool: True after the container is disconnected
Inputs: container (required), id_or_name (required), force. Returns: result
network_inspectARead-only
Return the full inspect detail for a single network.
Includes the connected containers (Containers, keyed by container id, with each
entry's assigned IP), IPAM config, and driver options. For a quick overview of many
networks use network_list instead - its default (non-greedy) response omits the
per-network Containers detail for speed.
Returns:
dict: Full network inspect attrs (equivalent to docker network inspect)
Inputs: id_or_name (required).
network_listARead-only
List networks.
Valid filter keys: driver (driver name), label (key or key=value), type
("custom" or "builtin"). names/ids are a separate shorthand for filtering by exact
name/id, applied in addition to filters. Set greedy to fetch each network's attrs
individually (adds the connected-containers detail that network_inspect returns, at
the cost of one extra daemon call per network) - leave it False for a fast summary list.
Args:
names: Filter by exact network names
ids: Filter by exact network ids
filters: Additional server-side filters; see description for valid keys
greedy: Fetch extended per-network details (including connected containers)
managed_only: Only return networks created by this MCP server (filters on the docker-mcp-server.managed label);
combines with any filters given
Returns: list: One dict ({"Id", "Name", "Driver", "Scope", ...}) per network: summary attrs by default, full inspect attrs (adding "Containers") when greedy=True
Inputs: ids, names, greedy, filters, +1 more.
network_pruneADestructiveIdempotent
Remove networks that have no active container endpoints.
Built-in networks (bridge, host, none) are never removed. Only networks with zero
connected containers are eligible. Valid filter keys: until (RFC3339 timestamp or
duration - removes networks created before that point), label (key or key=value). Use
network_remove to delete one specific network instead.
Args: filters: Narrow which networks to remove; omit to remove all unused custom networks
Returns: dict: {"NetworksDeleted": [...]}
Inputs: filters.
network_removeADestructive
Remove a single custom network by id or name.
Fails if any container is still attached (disconnect with network_disconnect or stop
the containers first). Built-in networks (bridge, host, none) can never be removed
and return an error regardless of attachment state. For bulk cleanup of every unused
custom network at once use network_prune instead.
Returns: bool: True after removal
Inputs: id_or_name (required). Returns: result
node_inspectARead-only
Get a swarm node's full inspect payload by id or name.
Must run against a swarm manager. Shows role, availability, status, and manager reachability -
use node_list to enumerate nodes first; service_ps(filters={"node": ...}) shows what a
service runs on one node.
Args:
id_or_name: The node id or hostname (as shown by node_list)
Returns: dict: The node's full document (Spec{Role, Availability}, Status, ManagerStatus for managers)
Inputs: id_or_name (required).
node_listARead-only
List swarm nodes.
Must run against a swarm manager. The fleet view of membership, role, and state; drill into
one node with node_inspect.
Args: filters: Filter by attributes (id, name, membership, role)
Returns: list: One full node document per node (Spec, Status, ManagerStatus for managers)
Inputs: filters.
node_removeADestructive
Remove a node from the swarm.
A node should normally be drained (node_update with Availability "drain") and have left the
swarm first, so its tasks reschedule cleanly. Removing an active/reachable node requires force=True.
Args: id_or_name: The node id or name to remove force: Force removal of an active/reachable node
Returns: bool: True after the node is removed
Inputs: id_or_name (required), force. Returns: result
node_updateA
Replace a node's spec (availability, name, role, labels).
Replacement, not a merge: spec becomes the node's entire spec, and omitted keys are cleared.
Fetch the current spec via node_inspect (its Spec key), modify it, and resubmit the whole
dict - e.g. sending just {"Availability": "drain"} would also wipe the node's role and labels.
Args: spec: The complete new node spec (see description - omitted keys are cleared)
Returns: bool: True after the update
Inputs: spec (required), id_or_name (required). Returns: result
node_waitARead-only
Block until a swarm node's Status.State reaches a target value.
Never raises on timeout - the result always carries met and timed_out. Polls
Status.State (one of "unknown"/"down"/"ready"/"disconnected") every poll_intervals.
Common uses: until="ready" after a newly joined node, or until="down" while draining a
node before removal. Does not track task placement - for "has this drained node's workload
fully moved off", inspect the relevant services' tasks directly; no single cheap call spans
every service in the swarm, so that check isn't built into this tool. service_wait covers
service convergence; node_list shows every node's state at once.
Args: until: Target Status.State to wait for: "ready" (default), "down", "disconnected", "unknown" timeout_seconds: Max seconds to wait before returning with timed_out=true poll_interval: Seconds between re-inspections (default 2, > 0); capped by the time left so a large value can't push the total wait past the timeout
Returns: dict: {"node", "until", "met", "timed_out", "state", "availability", "waited_seconds"}
Inputs: id_or_name (required), until, poll_interval, timeout_seconds.
plugin_configureA
Set runtime configuration options on an installed plugin.
Use plugin_inspect first to see which keys the plugin exposes under Settings.Env; pass
those same keys as a plain dict, e.g. {"DEBUG": "1", "SOCKET": "/run/x.sock"}. The
plugin must be disabled before reconfiguring - call plugin_disable first if it is
currently active, then plugin_enable afterwards to apply the new settings.
Args: name: Plugin name, e.g. "vieux/sshfs:latest" options: Key/value settings to apply, matching the plugin's declared env keys
Returns: bool: True after configuration
Inputs: name (required), options (required). Returns: result
plugin_createA
Build a plugin from a local plugin data directory and install it under name.
The counterpart to plugin_install, which pulls an already-published plugin from a registry:
use this only for a plugin rootfs you built yourself, and plugin_install for anything on a
registry. plugin_data_dir is read on the machine running this server (not on the daemon
host), must already contain a config.json manifest and a rootfs directory, and is tarred
client-side and posted to the daemon - in a container it must be a bind mount or the path
resolves to nothing. The new plugin is created disabled: call plugin_configure for any
settings it declares, then plugin_enable to activate it. Raises if the directory is missing
or lacks config.json/rootfs, or if name is already installed (remove it first with
plugin_remove). Unlike the other create tools, this stamps no provenance labels - the Engine
API's plugin-create call accepts none.
Args:
name: Local name for the plugin, author/name:tag; the :latest tag is optional and is the default if omitted
plugin_data_dir: Path on this server's filesystem to the plugin data directory (containing config.json and
rootfs)
gzip: Compress the uploaded directory with gzip
Returns: dict: The created plugin's full document ({"Id", "Name", "Enabled", "Settings", "Config"})
Inputs: name (required), plugin_data_dir (required), gzip.
plugin_disableA
Disable a plugin so it stops intercepting Docker API calls; the plugin remains installed.
A disabled plugin cannot be used by new containers but existing containers that already
have it attached are unaffected. Use force=True to disable even if active containers
are still using it - this may cause those containers to lose access to plugin-provided
resources (e.g. a volume driver). Re-enable with plugin_enable.
Args: force: Disable even if active containers are using the plugin (may disrupt them)
Returns: bool: True after the plugin is disabled
Inputs: name (required), force. Returns: result
plugin_enableA
Activate an installed plugin so Docker routes relevant API calls through it.
Activates a plugin that is currently disabled - either freshly installed or previously
disabled via plugin_disable. If the plugin exposes configuration (check via
plugin_inspect), call plugin_configure while it is still disabled before enabling it.
timeout_seconds controls how long Docker waits for the plugin process to become healthy;
0 means wait indefinitely.
Args: name: The plugin name to enable timeout_seconds: Seconds to wait for the plugin to become healthy (0 = no timeout)
Returns: bool: True after the plugin is enabled
Inputs: name (required), timeout_seconds. Returns: result
plugin_inspectARead-only
Return the full attrs for a single installed plugin.
Use this to check a plugin's Enabled state before calling plugin_enable /
plugin_disable, or to read the config keys it exposes under Settings.Env before
calling plugin_configure. For the set of all installed plugins use plugin_list.
Args: name: Plugin name, e.g. "vieux/sshfs:latest"
Returns: dict: The plugin's full document (Id, Name, Enabled, Settings, Config)
Inputs: name (required).
plugin_installA
Install a plugin from Docker Hub.
remote is a Docker Hub reference in author/name:tag form, e.g.
vieux/sshfs:latest. The daemon handles permission grants non-interactively - call
plugin_privileges first to see what host access the plugin is asking for.
After installation use plugin_inspect to confirm the plugin's enabled state, then call
plugin_enable to activate it if needed, and optionally plugin_configure first if
it requires settings. Use plugin_list to list all plugins, or plugin_remove to
uninstall.
Args: remote: Docker Hub plugin reference, e.g. "vieux/sshfs:latest" local_name: Alias to refer to the plugin locally; defaults to remote
Returns: dict: The installed plugin's full document ({"Id", "Name", "Enabled", "Settings", "Config"})
Inputs: remote (required), local_name.
plugin_listARead-only
List installed engine plugins with their full attrs.
Covers managed engine plugins (volume/network/logging drivers installed via plugin_install)
not docker CLI plugins such as compose, buildx, or scout. Use it to find exact plugin names for
plugin_inspect/plugin_enable/plugin_disable/plugin_remove; theEnabledkey shows each plugin's state.
Returns: list: One full document per installed plugin (Id, Name, Enabled, Settings, Config)
Inputs: no named parameters.
plugin_privilegesARead-only
Ask the registry which host privileges a not-yet-installed plugin demands.
The review step before plugin_install, which grants these privileges non-interactively (the
daemon never prompts) - so this is the only chance to see what a plugin wants before it has it.
Worth checking for anything not already trusted: plugins routinely request host mounts, devices,
and elevated capabilities, and a granted privilege is host-level access, not container-scoped.
Reads the remote plugin from its registry and installs nothing; for the privileges of a plugin
already installed, read Config from plugin_inspect instead. Credentials come from
system_login, or from ~/.docker/config.json if the host ran docker login. Raises if the
reference cannot be resolved in the registry.
Args:
remote: Registry plugin reference, author/name:tag; the :latest tag is optional and is the default if
omitted
Returns: list: One dict per requested privilege ({"Name", "Description", "Value"}), e.g. Name "mount" with Value ["/data"], or "capabilities" with Value ["CAP_SYS_ADMIN"]; empty if the plugin requests none
Inputs: remote (required).
plugin_pushA
Push an installed plugin to its registry.
The write-side counterpart to plugin_install (which pulls) and the publish step after
plugin_create builds a plugin locally: name must already be the registry-qualified name the
plugin is installed under, since - unlike image_push - there is no plugin equivalent of
image_tag to rename it first, so create it under the target name. The plugin does not need to
be enabled. Credentials come from system_login, or from ~/.docker/config.json if the host
ran docker login. Does NOT raise when the registry rejects the push: an authentication or
quota failure arrives as a final progress record and is surfaced as the error key, so check
that key rather than assuming success. Raises CapabilityError if the installed docker-py is too
old to expose the internals below, and reports the daemon's own error if the plugin isn't installed.
Bypasses docker-py's Plugin.push()/APIClient.push_plugin(), which cannot work: both POST to
/plugins/{name}/pull, a route the Engine does not define (push is /plugins/{name}/push), so
they 404 against any daemon. Bug present since the method was written in 2017 and still in
docker-py main; it survives because upstream has no test covering it. This calls the correct
endpoint through docker-py's private request helpers, in the manner of system_logout's
api._auth_configs reach-in, and fails loudly if those internals change shape.
Caveat for ssh:// daemons: docker-py can't cancel an SSH stream, so the timeout_seconds
watchdog can't interrupt a push that stalls with the connection still open - the same limitation
container_logs carries in follow mode. The call still returns normally once the registry
answers or the stream ends.
Args:
name: Installed plugin name to push, [registry/]author/name:tag; :latest if the tag is omitted. A bare
author/name pushes to Docker Hub
timeout_seconds: Max wall-clock seconds to wait on the push stream before returning what was collected (default
300); raise it for a large plugin over a slow link
Returns:
dict: {"name", "progress": [], "truncated": bool, "error": str or None} - error is
non-None only when the registry reported a failure
Inputs: name (required), timeout_seconds.
plugin_removeADestructive
Uninstall an engine plugin from the daemon.
Permanent removal - to deactivate but keep a plugin installed use plugin_disable instead. An
enabled plugin must be disabled first unless force=True. Plugin names come from
plugin_list.
Args: name: The plugin name (e.g. "vieux/sshfs:latest") force: Remove even if the plugin is enabled
Returns: bool: True after removal
Inputs: name (required), force. Returns: result
plugin_upgradeA
Upgrade an installed plugin to a newer version.
The plugin must be disabled first - call plugin_disable before this, then
plugin_enable afterwards to bring it back up. remote lets you upgrade to a
different reference (e.g. a newer tag) than the plugin's current name; omit it to
re-pull the same reference. Existing settings and volumes created by the plugin
persist across the upgrade.
Args: name: The plugin name to upgrade remote: Reference to upgrade to, e.g. "vieux/sshfs:next" (default: same as name)
Returns: bool: True after the upgrade completes
Inputs: name (required), remote. Returns: result
registry_image_configARead-only
Fetch and parse an image's config blob from a registry without pulling.
Answers "what's inside this image?" - env vars, entrypoint/cmd, workdir, exposed ports, user,
labels, layer history (what registry_manifest only points at via config.digest).
Resolves in up to three hops: manifest -> (if multi-platform) the platform entry's manifest
-> the config blob. Fails where that resolves to no config descriptor, which means the
reference is a manifest list rather than an image - registry_manifest reads those.
Args:
repository: Image/repository ref, e.g. "ghcr.io/org/repo"; :tag/@digest is stripped - pass via reference
reference: Tag or digest
platform: Platform to select from a multi-platform image, "os/arch[/variant]"; ignored
for single-platform images
username: Registry username (overrides DOCKER_MCP_SERVER_REGISTRY_USERNAME)
password: Registry password/token (overrides DOCKER_MCP_SERVER_REGISTRY_PASSWORD)
Returns:
dict: {"name", "registry", "reference", "platform", "config_digest", "config": }; platform is the
selected platform (None if single-platform)
Inputs: repository (required), password, platform, username, +1 more.
registry_manifestARead-only
Fetch a repository's manifest without pulling.
May return a single-platform image manifest or a multi-platform manifest list / OCI image
index, depending on what the registry serves for that tag. Talks HTTPS directly - no daemon
or CLI needed. Alternatives for the same question: buildx_imagetools_inspect (uses the
docker CLI and its credential store) and image_registry_data (asks the daemon).
Args:
repository: Image/repository ref, e.g. "ghcr.io/org/repo"; :tag/@digest is stripped - pass via reference
reference: Tag or digest
username: Registry username (overrides DOCKER_MCP_SERVER_REGISTRY_USERNAME; no config.json)
password: Registry password/token (overrides DOCKER_MCP_SERVER_REGISTRY_PASSWORD)
Returns: dict: {"name", "registry", "reference", "media_type", "digest", "manifest": }
Inputs: repository (required), password, username, reference.
registry_tagsARead-only
List tags for a repository in an OCI v2 registry without pulling.
Works against Docker Hub, GHCR, ECR, GAR, and any OCI-compliant registry; anonymous if no
credentials are passed. Talks directly to the registry over HTTPS and does NOT read
~/.docker/config.json - for private registries prefer the DOCKER_MCP_SERVER_REGISTRY_USERNAME /
DOCKER_MCP_SERVER_REGISTRY_PASSWORD env vars (keeps secrets out of tool args, which clients
often log). Fetch one tag's manifest with registry_manifest; hub_tags adds Hub-specific
tag metadata (sizes, push dates).
Args:
repository: Image/repository ref, e.g. "alpine", "ghcr.io/org/repo"; any :tag/@digest is stripped
username: Registry username (overrides DOCKER_MCP_SERVER_REGISTRY_USERNAME)
password: Registry password/token (overrides DOCKER_MCP_SERVER_REGISTRY_PASSWORD)
limit: Max tags to return (default 1000, >= 1); pagination capped at 50 pages
Returns: dict: {"name": , "registry": , "tags": [..], "truncated": bool}
Inputs: repository (required), limit, password, username.
registry_tag_waitARead-only
Block until a specific tag appears in a repository (e.g. waiting for a CI push to land).
Never raises on timeout - the result always carries met and timed_out. Polls registry_tags
every poll_intervals and checks whether tag is in its result. Works against Docker Hub too
(registry_tags' own scope covers it), so there is no separate Hub variant. Unlike every other
wait tool, this has no host argument - registry tools talk HTTPS directly to the registry, not
a Docker daemon.
Caveat: registry_tags paginates up to 50 pages (or limit tags, whichever comes first); if
tag would only appear beyond that window it is never found, even once it exists. Raise limit
if you expect a very large tag list.
Args:
repository: Image/repository ref, e.g. "alpine", "ghcr.io/org/repo"; any :tag/@digest is stripped
tag: The exact tag name to wait for
username: Registry username (overrides DOCKER_MCP_SERVER_REGISTRY_USERNAME)
password: Registry password/token (overrides DOCKER_MCP_SERVER_REGISTRY_PASSWORD)
limit: Max tags to scan per poll (default 1000, >= 1); forwarded to registry_tags
timeout_seconds: Max seconds to wait before returning with timed_out=true
poll_interval: Seconds between re-checks (default 5, > 0); capped by the time left so a large value can't push
the total wait past the timeout
Returns: dict: {"repository", "tag", "met", "timed_out", "waited_seconds"}
Inputs: tag (required), repository (required), limit, password, +3 more.
scout_compareARead-only
Compare two image references and report the CVE delta.
Exactly one of to, to_env, or to_latest=True must be supplied to identify the comparison
target. Use it after a rebuild to check the new image against the old (scout_cves scans a
single image).
Does not raise on a non-zero CLI exit (a missing scout plugin or a timeout still raises) - inspect
raw.stderr. Raises ToolInputError if to names a local directory/archive while the call has to run
on a remote ssh:// host (no local scout plugin): the file is not staged, so it would resolve
against that host's filesystem instead.
Args:
image: The new / candidate image reference
to: Compare against this image reference, directory, or archive (a local directory/archive only when the CLI
runs on this host - see above)
to_env: Compare against an image associated with this Scout environment
to_latest: Compare against the latest scan of image
only_severity: Filter to these severities (omit for all)
ignore_unchanged: Exclude unchanged packages from the diff
format: Output format; only "json" (the default) is parsed into result
platform: Platform of the image to analyze
Returns: dict: {"format": , "result": , "raw": }
Inputs: image (required), to, format, to_env, +4 more.
scout_cvesARead-only
List vulnerabilities (CVEs) in an image via Docker Scout.
Anonymous scans work for public images; Hub policy enforcement and richer recommendations need
docker login on the host that runs the CLI - this server's host, or the target ssh:// host
itself when no local scout plugin is installed. Start with scout_quickview for a
per-severity summary; scout_sbom inventories packages without vulnerability matching.
Does not raise on a non-zero CLI exit (a missing scout plugin or a timeout still raises) - inspect
raw.stderr.
Args:
image: Image reference (a tag or a digest)
only_fixed: Only report CVEs with a fixed version available
only_severity: Filter to these severities (omit for all)
ignore_base: Exclude CVEs introduced by the base image
format: Parsed into result as JSON: "sarif" (default, the standard vulnerability-report schema), "spdx",
"gitlab", "sbom". Returned verbatim as text: "packages" (Scout's own default, grouped by package),
"markdown". There is no plain "json" for this subcommand
platform: Platform of the image to analyze, e.g. "linux/amd64"
Returns: dict: {"format": , "result": , "raw": }
Inputs: image (required), format, platform, only_fixed, +2 more.
scout_quickviewARead-only
Render a compact summary of an image's CVE posture.
The fastest triage step - counts per severity plus base-image status. Drill into individual
findings with scout_cves, which unlike this tool can emit machine-readable JSON; get upgrade
suggestions with scout_recommendations.
Output is plain text only: docker scout quickview has no output-format option, so result
is always the rendered text rather than a parsed document.
Does not raise on a non-zero CLI exit (a missing scout plugin or a timeout still raises) - inspect
raw.stderr.
Args: platform: Platform of the image to analyze, e.g. "linux/amd64"
Returns: dict: {"result": , "raw": }
Inputs: image (required), platform.
scout_recommendationsARead-only
Suggest base-image upgrades for an image.
Computed against Docker Scout's catalog; generally needs docker login on the host that runs the
CLI (the target ssh:// host itself when no local scout plugin is installed) to return useful
results for private or rarely-scanned base images. The natural follow-up to scout_cves when the
fix is a newer base image.
Output is plain text only: docker scout recommendations has no output-format option, so
result is always the rendered text rather than a parsed document.
Does not raise on a non-zero CLI exit (a missing scout plugin or a timeout still raises) - inspect
raw.stderr.
Args: only_refresh: Only show "refresh" recommendations (same major/minor) only_update: Only show "update" recommendations (newer minor/major) tag: Restrict to suggestions matching this tag pattern platform: Platform of the image to analyze
Returns: dict: {"result": , "raw": }
Inputs: image (required), tag, platform, only_update, +1 more.
scout_sbomARead-only
Generate a Software Bill of Materials (SBOM) for an image.
Package inventory only - scout_cves adds vulnerability matching on top. SBOMs can be large;
captured stdout is subject to MAX_CLI_OUTPUT_BYTES and may be truncated for big images. If
that's a concern, run docker scout sbom -o file.json ... on the host and load the file
separately.
Does not raise on a non-zero CLI exit (a missing scout plugin or a timeout still raises) - inspect
raw.stderr.
Args: format: "spdx" (default, SPDX JSON), "cyclonedx" (CycloneDX JSON), "json" (Scout's native JSON), or "list" (plain-text package list) platform: Platform of the image to analyze
Returns:
dict: {"format", "result", "raw": }. result is a parsed dict when format is
"spdx"/"cyclonedx"/"json" and stdout parses cleanly; for "list" or a parse failure it's the raw text.
Inputs: image (required), format, platform.
secret_createA
Create a swarm secret; requires a swarm manager.
Write-once: the payload can never be read back through the API (secret_inspect returns
metadata only) and cannot be changed later - to rotate, create a new secret and update the
consuming services, keeping your own copy of the value. For non-sensitive data that should
stay readable, use config_create instead. Created secrets are stamped with provenance
labels.
Args: name: Name for the secret (unique within the swarm) data: The secret payload (max 500 KB; must be empty when driver is set) labels: Labels to set on the secret driver: Secret-driver config for values held in an external store
Returns: dict: The created secret's full document (ID and Spec metadata; never the payload)
Inputs: data (required), name (required), driver, labels.
secret_inspectARead-only
Get a swarm secret's metadata by id or name; requires a swarm manager.
The returned attrs never include the secret's actual data (Spec.Data is write-only -
the daemon accepts it on secret_create but never returns it back, by design). Use this
to check a secret's CreatedAt, Labels, or which driver created it, not to read its
contents. To see which services reference it, inspect each service's spec via
service_inspect (there is no server-side filter for "services using this secret").
Returns: dict: The secret's full document, excluding the actual secret data
Inputs: id_or_name (required).
secret_listARead-only
List swarm secrets' metadata; requires a swarm manager.
Like secret_inspect, results never include secret data, only metadata (name, id,
labels, timestamps). Valid filter keys: id, name, names, label (key or
key=value).
Args: filters: Narrow the list; omit to return every secret
Returns: list: One full secret document per secret (data-free)
Inputs: filters.
secret_removeADestructive
Remove a Swarm secret; requires a swarm manager.
Removing a secret does not immediately affect running service tasks - tasks that already
have the secret mounted retain access until they are restarted or the service is updated.
Use service_list and inspect each service's spec via service_inspect to identify
services that mount the secret before removing it (service filters do not support
filtering by secret reference).
Args: id_or_name: The secret id or name to remove
Returns: bool: True after removal
Inputs: id_or_name (required). Returns: result
service_createA
Create a Swarm service; requires a swarm manager node.
Use this instead of container_run when you need replicated or global scheduling,
rolling updates, or automatic restart across the swarm. Common extra_kwargs keys:
name (str), env (list of "KEY=VAL"), mode ({"Replicated": {"Replicas": N}} or
{"Global": {}}), networks (list of network names/ids), endpoint_spec
({"Ports": [{"PublishedPort": 80, "TargetPort": 8080}]}), labels (dict),
restart_policy ({"Condition": "on-failure", "MaxAttempts": 3}),
resources ({"Limits": {"NanoCPUs": 500000000, "MemoryBytes": 134217728}}). For anything else
docker-py's ServiceCollection.create accepts, call docs_lookup(section="services") rather
than guessing a key name.
Args: image: Image to run service tasks from (e.g. "nginx:alpine") command: Override the image's default command; string or list of strings extra_kwargs: Additional docker-py ServiceCollection.create keyword arguments
Returns: dict: The created service's full document ({"ID", "Version", "Spec", ...})
Inputs: image (required), command, extra_kwargs.
service_inspectARead-only
Get a swarm service by id or name.
Must run against a swarm manager. Returns the desired-state spec and rollout status - for the
actually-running tasks use service_ps, or the service-tasks://{id_or_name} resource for a
computed rollout summary.
Args: insert_defaults: Merge default values into the output
Returns: dict: The full service document ({"ID", "Version", "Spec", "Endpoint", ...}; "UpdateStatus" during a rolling update)
Inputs: id_or_name (required), insert_defaults.
service_listARead-only
List swarm services.
Must run against a swarm manager. One entry per service (the desired state); service_ps
lists a service's tasks, and stack_services groups services by stack.
Args:
filters: Filter by attributes (id, name, label, mode)
managed_only: Only return services created by this MCP server (filters on the docker-mcp-server.managed label);
combines with any filters given
Returns: list: One full service document ({"ID", "Spec", ...}) per service
Inputs: filters, managed_only.
service_logsARead-only
Get a bounded snapshot of a swarm service's logs (never follows).
follow is intentionally not exposed: the stream is joined into one string before returning, so
following would block forever and grow unbounded. Collection is capped at max_bytes (ToolInputError
if exceeded) so a noisy service can't OOM the server. The default is a bounded tail=200;
tail="all" returns the whole buffer, which can be huge on long-running services and exceed
the agent's context - prefer an integer, or since, to constrain output. Logs aggregate across
all the service's tasks: use swarm_task_logs for one task, container_logs for one container,
and the service-logs://{id_or_name} resource for the resource-flavored equivalent of this
tool.
Args: details: Show extra details since: Show logs since this Unix timestamp tail: Number of lines from the end, or the literal "all" for everything max_bytes: Abort with ToolInputError if the buffered logs exceed this many bytes (default 32 MiB)
Returns: str: Decoded log output
Inputs: id_or_name (required), tail, since, stderr, +4 more. Returns: result
service_psARead-only
List a swarm service's tasks (per-replica scheduling units), like docker service ps.
Shows where replicas run and why they fail: each task carries Status
(State/Message/ContainerStatus), DesiredState, NodeID, and Slot. Prefer this over
container_list for services (tasks may run on other nodes), stack_ps for a whole stack,
and the service-tasks://{id_or_name} resource for a computed rollout summary. Requires a
swarm manager.
Args: filters: Filter dict; keys: id, name, node, label, desired-state (running|shutdown|accepted)
Returns: list: Task dicts (ID, Slot, NodeID, Status, DesiredState, Spec)
Inputs: id_or_name (required), filters.
service_removeADestructive
Stop and remove a swarm service.
Requires a swarm manager. Deletes the service definition and shuts down its tasks - no
confirmation, no undo. To stop work but keep the definition, service_scale to 0 replicas.
Returns: bool: True after the service is removed
Inputs: id_or_name (required). Returns: result
service_rollbackA
Roll a swarm service back to its previous spec (the docker service rollback equivalent).
Re-applies the service's PreviousSpec - the spec from before the most recent service_update /
service_scale. Raises ToolInputError if the service has no PreviousSpec
(it has never been updated, or was already rolled back). The high-level SDK exposes no rollback,
so this reads the current version and previous spec via the low-level APIClient and submits them
with the low-level update_service API call.
Returns: dict: The daemon response (a dict with a "Warnings" key)
Inputs: id_or_name (required).
service_scaleA
Set the desired replica count for a Replicated-mode swarm service.
Only applies to services in Replicated mode; a Global service runs one task per
eligible node and has no replica count to set. The swarm scheduler places or removes
tasks asynchronously to converge on the new count - this call returns once the update
is accepted, not once every task is running. Check progress with service_ps or
service_inspect. For any other spec change (image, env, resources) use
service_update instead.
Args: replicas: The desired number of running task replicas
Returns: bool: True once the scale request is accepted
Inputs: replicas (required), id_or_name (required). Returns: result
service_updateA
Update a swarm service's configuration, or force a redeploy with no spec change.
Pass exactly one of updates (fields to change, same parameters as service_create) or
force=True (the docker service update --force equivalent: bumps the ForceUpdate counter so
the service's tasks redeploy with an unchanged spec - e.g. to reschedule after a node change or
re-pull a mutable tag).
Args: updates: Fields to update on the service; exactly one of updates/force force: Redeploy the service without changing its spec; exactly one of updates/force
Returns: bool: True after the update
Inputs: id_or_name (required), force, updates. Returns: result
service_waitARead-only
Block until a swarm service's tasks converge, or a rolling update finishes.
One contract for both modes: never raises on timeout - the result always carries met and
timed_out. "running" polls task state via the same task-counting logic as
service-tasks://{id_or_name} (not the unconfirmed daemon ServiceStatus field) until running
tasks reach the desired count (Replicated mode) or every returned task is running (Global mode,
which has no fixed target). "update-converged" polls UpdateStatus.State until it reaches a
terminal value (completed or rollback_completed); if the service has never been updated (no
UpdateStatus at all), returns promptly with met=false - there's nothing to converge to, same
as container_wait's no-healthcheck case.
Args:
until: Condition to wait for: "running" (default) or "update-converged"
replicas: "running" mode only: override the desired replica count (e.g. right after a same-turn service_scale
call, before polling reflects the new target)
timeout_seconds: Max seconds to wait before returning with timed_out=true
poll_interval: Seconds between re-checks (default 2, > 0); capped by the time left so a large value can't push
the total wait past the timeout
Returns: dict: {"service", "until", "met", "timed_out", "running_tasks", "desired_tasks", "failed_tasks", "update_state", "waited_seconds"}
Inputs: id_or_name (required), until, replicas, poll_interval, +1 more.
stack_deployA
Deploy (or update) a stack to the swarm from one or more Compose files.
Requires the target daemon to be a swarm manager. Re-running with the same name updates the
stack in place. Defaults to detach=True (returns once specs are submitted, not on
convergence); set detach=False to wait for the rollout (give it a generous
timeout_seconds). The swarm analogue of compose_up; watch the rollout with
stack_services / stack_ps.
Does not raise on a non-zero CLI exit (a missing docker binary or a timeout still raises) - inspect
returncode/stderr in the result.
Args:
name: Name of the stack to create or update
compose_files: One or more Compose file paths (repeated -c; later override earlier). At least one required.
with_registry_auth: Send registry credentials to swarm agents (needed for private images)
prune: Remove services no longer defined in the Compose file
resolve_image: Image-digest resolution; omit for the CLI default ("always")
detach: Return immediately after submitting specs (True) vs wait for convergence (False)
cwd: Working directory for resolving relative Compose paths (defaults to the server's cwd; copied to the target
host if no local docker CLI)
timeout_seconds: Subprocess timeout (default 1800s)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: name (required), compose_files (required), cwd, prune, +4 more.
stack_listARead-only
List the stacks deployed to the swarm, parsed from --format '{{json .}}'.
Requires the target daemon to be a swarm manager. compose_list is the non-swarm equivalent;
drill into one stack with stack_services.
Raises RemoteFailureError if the CLI call fails.
Returns: list: One dict per stack (name, services count, orchestrator)
Inputs: no named parameters.
stack_psARead-only
List the tasks of a stack, parsed from --format '{{json .}}'.
Task-level view across every service in the stack (service_ps covers one service): where
each task runs and why it failed. Requires a swarm manager.
Raises RemoteFailureError if the CLI call fails.
Args: name: The stack to list tasks for no_trunc: Do not truncate task IDs / errors in the output filters: Filter by attributes, e.g. {"desired-state": "running"}; a list value repeats the filter
Returns: list: One dict per task (id, name, node, image, desired/current state, error)
Inputs: name (required), filters, no_trunc.
stack_removeADestructive
Remove one or more stacks from the swarm (tears down their services, networks, and secrets).
Destructive: this stops and deletes every service in the named stack(s) - the reverse of
stack_deploy and the swarm analogue of compose_down. Defaults to detach=True so the call
returns once removal is requested rather than waiting for teardown.
Does not raise on a non-zero CLI exit (a missing docker binary or a timeout still raises) - inspect
returncode/stderr in the result.
Args: names: One or more stack names to remove. At least one is required. detach: Return immediately (True) vs wait for the stack(s) to be fully removed (False) timeout_seconds: Subprocess timeout (default 300s)
Returns: dict: {"returncode": int, "stdout": str, "stderr": str, "truncated": bool}
Inputs: names (required), detach, timeout_seconds.
stack_servicesARead-only
List the services of a stack, parsed from --format '{{json .}}'.
Service-level rollup (replicas ready per service); use stack_ps for individual tasks and
service_inspect for one service's full spec. Requires a swarm manager.
Raises RemoteFailureError if the CLI call fails.
Args: name: The stack to list services for filters: Filter by attributes, e.g. {"name": "web"}; a list value repeats the filter
Returns: list: One dict per service (id, name, mode, replicas, image, ports)
Inputs: name (required), filters.
swarm_initA
Initialize a new swarm, making this Engine its first manager node.
Fails if the Engine is already part of a swarm - call swarm_leave first to reset it.
advertise_addr only needs setting when the host has multiple network interfaces or is
behind NAT (otherwise it is auto-detected); it must be reachable by every other node
that will join. To add more nodes afterwards, retrieve join tokens with
swarm_join_tokens and call swarm_join on each one. Set autolock_managers=True to
require the unlock key (swarm_unlock_key) on every manager restart - store that key
securely immediately, since it is only shown once autolock is enabled.
Args: name: Name for the swarm cluster itself, not for the node running init advertise_addr: Externally reachable address advertised to other nodes listen_addr: Listen address used for inter-manager communication force_new_cluster: Force a new single-node cluster from this node's current state (disaster recovery when a majority of managers is lost) default_addr_pool: IP address pools for swarm overlay networks subnet_size: Subnet size for the IP pool data_path_addr: Address to use for data path traffic data_path_port: Port number for data path traffic labels: Labels to set on the swarm autolock_managers: Require the unlock key after every manager restart log_driver: Default log driver configuration
Returns: str: The node id of the newly created swarm manager
Inputs: name, labels, log_driver, listen_addr, +7 more. Returns: result
swarm_inspectARead-only
Inspect the swarm this daemon belongs to (id, spec, join-token config, CA info).
Works on a manager node only. Cluster-level configuration - for per-node state use
node_list; for the tokens new nodes need, swarm_join_tokens.
Returns: dict: The swarm's attrs, as returned by the daemon's swarm inspect endpoint
Inputs: no named parameters.
swarm_joinA
Join this Engine to an existing swarm as a worker or manager.
Fails if the Engine is already part of a swarm. Whether this node joins as a worker or
a manager is determined entirely by which token is passed - join_token must be one of
the two tokens from swarm_join_tokens, called against any existing manager. advertise_addr
only needs setting when this host has multiple network interfaces or is behind NAT
(otherwise it is auto-detected from the interface used to reach remote_addrs); it must
be reachable by every other node in the swarm.
Args:
remote_addrs: Address(es) of existing swarm managers to connect to
join_token: The worker or manager join token (from swarm_join_tokens) - determines the role this node joins as
listen_addr: Listen address for inter-manager communication
advertise_addr: Externally reachable address advertised to other nodes
data_path_addr: Address to use for data path traffic
Returns: bool: True after the engine joins the swarm
Inputs: join_token (required), remote_addrs (required), listen_addr, advertise_addr, +1 more. Returns: result
swarm_join_tokensARead-only
Return the swarm's worker and manager join tokens.
These are the tokens a new node passes to swarm_join - without one, swarm_join cannot be
called, so this closes the init -> join loop. The tokens are secret bearer credentials (anyone
holding the manager token can join as a manager); treat the result as sensitive and avoid logging
it. Reads swarm.attrs["JoinTokens"] after a reload, so it always reflects the current tokens.
Returns: dict: {"Worker": , "Manager": }
Inputs: no named parameters.
swarm_leaveADestructive
Leave the current swarm.
The daemon's service tasks are rescheduled to the remaining nodes. A manager refuses to leave
without force=True, since leaving can break raft quorum. The departed node lingers as "down"
in node_list until a manager runs node_remove.
Args: force: Force leave even if the node is a manager
Returns: bool: True after leaving the swarm
Inputs: force. Returns: result
swarm_task_inspectARead-only
Inspect a single swarm task, like docker inspect --type task.
For when you already hold a task reference -- from a swarm_task_list or service_ps row, a
service event, or an error message -- and want just that task. swarm_task_list returns the
same document for every task, so prefer it when scanning; this is the single-object fetch.
To reach the container behind a running task, read Status.ContainerStatus.ContainerID and pass
it to container_inspect / container_logs -- but note the container may be on another node,
where those tools cannot see it, and service_logs aggregates across tasks instead;
swarm_task_logs reads that one task's output wherever it landed. Read-only.
Requires a swarm manager; reports the daemon's own error if the task does not exist, if a
prefix matches more than one task, or if this node is not a manager.
Args:
id_or_name: The task id, an unambiguous id prefix, or the task's full name -- which is the container-name form
<service>.<slot>.<taskid> (<service>.<nodeid>.<taskid> for a global service), NOT the shorter
<service>.<slot> that docker service ps prints in its NAME column, which does not resolve. The daemon
tries full id, then full name, then prefix, and rejects an ambiguous prefix rather than picking a match
Returns:
dict: Full task inspect payload, as docker inspect --type task. Carries no name field of its own; compose one
from ServiceID/Slot if you need it
Inputs: id_or_name (required).
swarm_task_listARead-only
List tasks across the whole swarm, like docker service ps with no service to scope it.
The cluster-wide view of what is actually scheduled. service_ps covers one service and
stack_ps one stack, so answering "what is failing anywhere" or "what is running on this node"
through those means looping over every service; this is one call, and swarm_task_logs reads
what a failing one printed. Filter by node for a node's workload (the CLI's docker node ps),
desired-state to separate what should be running from what is shutting down, or service for
a single service -- for which service_ps is the simpler call. Each task carries its full
Spec, including the ContainerSpec (image, command, env), so this returns much more per task
than the service-tasks://{id_or_name} resource's computed rollout summary. Read-only. Requires
a swarm manager: on any other node the daemon refuses, and its refusal is what comes back.
Args: filters: Filter dict; keys: id, name, service, node, label, desired-state (running|shutdown|accepted); omit for every task in the cluster
Returns:
list: One full task document per task (ID, ServiceID, NodeID, Slot, Spec, Status, DesiredState), the same shape
service_ps returns
Inputs: filters.
swarm_task_logsARead-only
Get a bounded snapshot of one swarm task's logs (never follows).
The per-replica counterpart to service_logs, which interleaves every task in the service: use
this to read the replica that actually failed, found with swarm_task_list or service_ps.
container_logs is no substitute on a multi-node swarm - the task's container lives on
whichever node the scheduler placed it on, and this server talks to one daemon.
As with service_logs, follow is not exposed (the stream is joined into one string before
returning, so following would never finish) and collection is capped at max_bytes. The Engine
offers no until bound here, unlike container_logs, so narrow with since or an integer
tail.
docker-py has no task collection and no APIClient.task_logs, so this drives its private
request helpers against the published GET /tasks/{id}/logs, raising CapabilityError if those
internals move. Drop the reach-in if docker-py grows a public method.
Args:
id_or_name: The task id, an unambiguous id prefix, or its full <service>.<slot>.<taskid> name; see
swarm_task_inspect for how the daemon resolves these and which name forms do not work
since: Show logs since this Unix timestamp
tail: Number of lines from the end, or the literal "all" for everything
max_bytes: Abort with ToolInputError if the buffered logs exceed this many bytes (default 32 MiB)
Returns: str: Decoded log output
Inputs: id_or_name (required), tail, since, stderr, +4 more. Returns: result
swarm_unlockA
Unlock a manager node that is locked after restart due to autolock being enabled.
When autolock is enabled (via swarm_init or swarm_update), manager nodes require
the unlock key after every restart before they can rejoin the swarm and resume
scheduling. Must be called on the locked manager node directly. Retrieve the current
unlock key with swarm_unlock_key from any unlocked manager - store it securely
when enabling autolock. A locked node cannot serve API requests and cannot return its
own key while locked; other unlocked managers in the swarm can still serve the key.
Once unlocked the manager resumes automatically.
Args:
key: The swarm unlock key (from swarm_unlock_key)
Returns: bool: True after the swarm is unlocked
Inputs: key (required). Returns: result
swarm_unlock_keyARead-only
Return the swarm's current unlock key.
The key only serves a purpose when autolock is enabled (see swarm_init's /
swarm_update's autolock_managers / rotate_manager_unlock_key). Must be called
against an unlocked manager - a locked manager cannot serve API requests, including
this one. Feed the result's key to swarm_unlock to unlock a manager after restart.
Treat the key as a sensitive credential.
Returns: dict: {"UnlockKey": }
Inputs: no named parameters.
swarm_updateA
Update swarm-wide settings: the single home for join-token rotation and cluster spec changes.
Must be called on a swarm manager node. Token rotation invalidates the old join token
immediately - nodes that have not yet joined using the old token must use the new one.
Existing joined nodes are unaffected. Use swarm_join_tokens to retrieve the new
tokens after rotation. Rotating the unlock key requires all managers to be re-unlocked
on restart with the new key; retrieve it immediately via swarm_unlock_key. Rotation and
updates are independent and may be combined in one call; swarm_init sets these same
fields when the swarm is first created, and swarm_inspect reads the current values back.
The Engine replaces the whole cluster spec on every update, so this reads the current spec
first and resubmits it, merging updates over it - omitting updates therefore changes
nothing but the requested rotation.
Args:
rotate_worker_token: Issue a new worker join token, invalidating the current one
rotate_manager_token: Issue a new manager join token, invalidating the current one
rotate_manager_unlock_key: Issue a new autolock unlock key for manager restart
updates: Engine SwarmSpec fields to change, merged over the current spec one top-level key at a time, so a named
block is replaced whole rather than field by field: keys are "Name", "Labels", "Orchestration", "Raft",
"Dispatcher", "CAConfig", "EncryptionConfig" and "TaskDefaults", e.g. {"EncryptionConfig":
{"AutoLockManagers": True}} to turn manager autolock on. Read the current blocks from swarm_inspect
Returns: bool: True after the update completes
Inputs: updates, rotate_worker_token, rotate_manager_token, rotate_manager_unlock_key. Returns: result
system_closeA
Close and drop pooled Docker client connection(s); each is rebuilt lazily on next use.
Use this to force a stale or errored connection to be discarded. Prefer system_reconnect when you
want to immediately re-establish the connection rather than wait for the next tool call to
trigger a lazy rebuild. With host omitted every pooled client is closed (unlike other tools,
where omitting it means the default host). Closing clients does not affect running containers.
Returns: bool: True once closed
Inputs: no named parameters. Returns: result
system_dfARead-only
Summarize Docker disk usage: layer storage plus per-object sizes for images, containers, volumes, build cache.
Equivalent to docker system df. Use it to find what to reclaim before image_prune /
container_prune / volume_prune / buildx_prune; use system_info for daemon config and
counts rather than sizes. The reply enumerates every object on the daemon, so expect a large
payload on busy hosts.
Returns: dict: {"LayersSize", "Images", "Containers", "Volumes", "BuildCache"} with per-object size fields
Inputs: no named parameters.
system_eventsARead-only
Stream real-time events from the Docker server, bounded by limit events or timeout_seconds.
Returns when limit events are collected or timeout_seconds elapses, whichever comes first
(limit caps memory; timeout_seconds caps how long the call blocks - without it a quiet daemon
would block indefinitely, since the stream only yields on an actual event).
Caveat for ssh:// daemons: docker-py can't cancel an SSH stream, so the timeout_seconds
watchdog can't interrupt a fully idle stream - bound with until/limit (or a non-SSH endpoint).
"Wait for the next matching event" idiom: pass limit=1 with filters narrowed to what you
care about (e.g. {"type": "container", "event": "health_status"}) and a generous
timeout_seconds. This blocks until that one event arrives (or the timeout elapses, returning an
empty list) instead of re-polling a snapshot on a timer - there's no separate wait tool for this
since the filtering this call already does covers it.
Args: since: Show events created since this timestamp until: Show events created until this timestamp filters: Filters to apply to the event stream limit: Max events to return timeout_seconds: Max wall-clock seconds before returning what was collected
Returns: list: A list of decoded event dicts (length <= limit)
Inputs: limit, since, until, filters, +1 more.
system_infoARead-only
Return system-wide Docker information, like docker info.
Daemon runtime state: container/image counts, storage and logging drivers, swarm role, and
daemon warnings. Use system_version for version/API level and system_df for disk usage.
Returns: dict: {"Containers", "Images", "Driver", "ServerVersion", "Swarm", "Warnings", ...}
Inputs: no named parameters.
system_loginA
Authenticate with a Docker registry.
Security: the password is sent as a tool argument, which many MCP clients log verbatim. Prefer
running docker login once on the host so the docker module reuses the credentials cached in
~/.docker/config.json, and avoid calling this tool from an agent loop. Credentials let
image_pull / image_push reach private repositories; system_logout clears them.
Args: username: Registry username password: Registry password or token email: Registry account email registry: URL to the registry (defaults to Docker Hub) reauth: Force re-authentication even if valid credentials exist dockercfg_path: Path to a custom dockercfg file
Returns: dict: The login response: {"Status"} always; "IdentityToken" only when the registry issues one
Inputs: password (required), username (required), email, reauth, +2 more.
system_logoutA
Clear cached registry credentials from this server's in-memory Docker client.
docker-py / the Engine have no true logout: system_login validates against the registry (the daemon's
/auth is stateless) and caches credentials in-process. This drops that in-memory cache; it does
NOT contact the daemon or touch the host's ~/.docker/config.json. With no registry, clears
every cached credential; pass one to clear just that entry (key must match system_login; Docker Hub
is cached under "docker.io"). system_close/system_reconnect also clear it by discarding the client.
Reaches into a private docker-py attribute (api._auth_configs); degrades to clearing nothing if
that internal shape changes.
Args: registry: Registry key to clear, or None to clear every cached credential
Returns: dict: {"cleared": []}
Inputs: registry.
system_pingARead-only
Check that the Docker server is responsive.
The cheapest daemon health check. A failure here usually means connection config rather than
daemon load - system_reconnect rebuilds a wedged client, host_list shows the configured
endpoints. For daemon details use system_version / system_info.
Returns: bool: True if the daemon responded successfully
Inputs: no named parameters. Returns: result
system_reconnectA
Rebuild a pooled Docker client from its configured endpoint, to recover a wedged connection.
Validates the rebuilt client before swapping in (and only then closes the old one), so a failed
rebuild leaves the working client in place. Rebuilds the default host's client when host is
omitted. It CANNOT retarget to a different daemon - to add or change a daemon, edit
DOCKER_MCP_SERVER_HOSTS and restart. system_close closes pooled clients without rebuilding;
host_list shows the configured endpoints.
Returns:
dict: the rebuilt host's version info (same shape as system_version), confirming connectivity
Inputs: no named parameters.
system_versionARead-only
Return Docker server version information.
Engine version, API level, and per-component versions - the first thing to check for feature
availability. system_info reports runtime state (counts, drivers, swarm role) instead.
Returns: dict: {"Version", "ApiVersion", "MinAPIVersion", "Os", "Arch", "Components", ...}
Inputs: no named parameters.
tool_listARead-only
List this server's registered tools as compact rows, filtered by domain, category or keyword.
A tool-callable mirror of docker-mcp://tool-catalog for clients that can't read MCP resources
(e.g. Claude Desktop, Cursor), and the only way to ask what no per-tool description search can
express: which tools are destructive, which accept a host, what this server actually
registered. Use it to brief on an unfamiliar area (domain="buildx" returns one line per tool
rather than ~13 full definitions), to check blast radius (category="destructive"), or to
establish that nothing matches - matched: 0 is a definitive negative, which a client's fuzzy
search cannot give. Covers this server's own surface; docs_lookup covers external Docker
reference documentation. Rows are summaries, not definitions - fetch a tool's own definition for
its parameters. Read-only, never raises on a query matching nothing, and always registered even
when DOCKER_MCP_SERVER_DISABLE drops every domain. A tool dropped by a switch or a disabled
domain is absent rather than flagged; hidden_by_configuration reports how many each domain
hides.
Args:
domain: Exact domain name (see any result's domains key); omit for every domain
category: Exact category; omit for all three
keyword: Case-insensitive substring over tool names, summaries and parameter names
Returns:
dict: {"matched": int, "tools": [{"name", "domain", "category", "summary"}], "domains": {domain: count},
"no_domain": int, "hidden_by_configuration": {domain: count}, "switches", "filters"}. Every domains key is
a value domain accepts; no_domain counts the domain-less tools, whose rows carry domain: null and
which no domain value selects.
Inputs: domain, keyword, category.
volume_createA
Create a volume managed by Docker.
Named volumes persist after their containers stop or are removed; use them for
databases, uploads, or any data that must outlive a container. Anonymous volumes
(no name) are only removed automatically when the container was started with --rm
or removed with docker rm -v; otherwise they accumulate and must be pruned manually.
Common driver_opts for the default local driver: bind-mount an existing host path
with {"type": "none", "device": "/host/path", "o": "bind"}, or mount an NFS share
with {"type": "nfs", "device": "server:/export", "o": "addr=server,rw"}. Third-party
drivers (e.g. rexray, convoy) accept their own option keys. List existing volumes with
volume_list; reclaim unused ones with volume_prune. Created volumes are stamped with provenance
labels.
Args: name: Volume name; auto-generated if omitted (creates an anonymous volume) driver: Volume driver to use (default: "local") driver_opts: Driver-specific options dict labels: Labels to set on the volume
Returns: dict: The created volume's full document (keys include Name, Driver, Mountpoint, CreatedAt, Labels, Options, Scope)
Inputs: name, driver, labels, driver_opts.
volume_inspectARead-only
Get a volume's full inspect payload by name.
Use it after volume_list to see a volume's on-disk location, driver, and labels - e.g.
before a backup or volume_remove. Volumes are addressed purely by name; they have no
separate id.
Args: name: The volume name (volumes have no ids)
Returns: dict: The volume's full document (keys include Name, Driver, Mountpoint, CreatedAt, Labels, Options, Scope)
Inputs: name (required).
volume_listARead-only
List volumes.
Volumes are addressed by name only - feed a Name to volume_inspect for detail or
volume_remove / volume_prune to clean up. filters={"dangling": True} finds volumes that no
container references.
Args:
filters: Filter by attributes (e.g. dangling, name, label)
managed_only: Only return volumes created by this MCP server (filters on the docker-mcp-server.managed label);
combines with any filters given
Returns: list: One volume document ({"Name", "Driver", "Mountpoint", ...}) per volume
Inputs: filters, managed_only.
volume_pruneADestructiveIdempotent
Remove volumes not referenced by any container, running or stopped.
A volume used by even one stopped container is not "unused" and survives the prune -
remove the container first (or use container_prune, then this) to reclaim its
volumes. Valid filter keys: label (key or key=value), all ("true" as a string -
without it only anonymous volumes are eligible, matching docker volume prune's
default). Use volume_list first to see what currently exists.
Args: filters: Narrow which unused volumes to remove; omit to remove all anonymous ones
Returns: dict: {"VolumesDeleted": [...], "SpaceReclaimed": }
Inputs: filters.
volume_removeADestructive
Remove a single volume by name.
Fails if any container, running or stopped, still references the volume - remove or
recreate those containers first, or pass force=True to remove it anyway (the
containers keep their reference but lose the underlying data). For bulk cleanup of
volumes with no container references at all, use volume_prune instead.
Args: name: Volume name to remove force: Remove even if a container still references the volume
Returns: bool: True after removal
Inputs: name (required), force. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 165 tools
Most tools target a distinct resource+action (container_*, image_*, network_*, volume_*), but at 165 tools there are several genuinely overlapping-purpose families: four log tools, three registry/manifest inspection tools, two tag-listing tools (registry_tags vs hub_tags), and five wait tools. The descriptions are exemplary at cross-referencing siblings and stating each tool's exact scope, which compensates substantially, but an agent can still realistically misselect within the registry and log clusters.
The dominant `domain_action` snake_case pattern is applied consistently across ~17 domains (container_run, image_remove, volume_prune, service_scale, secret_create, buildx_build, registry_tags). Minor deviations are mostly CLI-faithful (`compose_cp`, `compose_ps`, `swarm_init`, `image_tag`) plus noun-style scout_*/hub_*/registry_manifest, which are predictable rather than chaotic.
165 tools is far beyond any reasonable agent surface — roughly three times the rubric's 'extreme mismatch' threshold. The scope is genuinely broad (Docker engine + compose + swarm + buildx + registries + scout) and the set is well-organized, but the container_*/compose_*/service_* triads triple near-identical operations and the server even ships meta-tools (tool_list, docs_lookup) to help agents navigate its own size.
This is effectively the entire Docker CLI surface: full lifecycle coverage for containers, images, networks, volumes, compose, swarm/services/stacks, secrets, configs, plugins, buildx, registries, and Scout — each with create/read/update/delete or equivalent. The only gaps are trivial (no standalone container_port or docker attach) and each has a documented workaround via container_inspect, so workflows have no dead ends.
Maintenance
Related MCP Connectors
Linux server administration over OAuth MCP: shell, Docker, systemd, files and monitoring.
A MCP server built for developers enabling Git based project management with project and personal…
Toolstile is a managed MCP gateway that connects remote MCP servers behind one endpoint, with per-agent permissions, shared accounts and activity logs. Connect at https://mcp.toolstile.com/mcp over Streamable HTTP with OAuth; sign in at https://toolstile.com, select a workspace and connect your upstream servers. Available tools, resources and prompts depend on that workspace and the agent's granted access.
MCP gateway and cloud hosting for MCP servers, agents and apps like RAG, managed from your agent.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables remote Docker management over SSH via a local MCP server, providing tools to manage containers, images, Compose, and system resources.1GPL 3.0
- AlicenseNot gradedqualityDmaintenanceA comprehensive MCP server that provides advanced Docker operations through a unified interface with 16 MCP tools and 25+ CLI aliases, enabling secure container lifecycle management, multi-container orchestration, registry publishing, and system maintenance.57 npm15ISC
- AlicenseBqualityDmaintenanceAn AI-powered MCP server for managing Docker infrastructure, enabling creation, deployment, and lifecycle management of containerized services with automated DNS and SSL.10MIT
- FlicenseNot gradedqualityCmaintenanceMCP server that routes Docker operations across a fleet of Docker hosts via the Scotty API.-