{
  "markdown": "# Docker MCP Server — Docker tools for AI agents\n\n**A Docker MCP server that lets AI agents operate Compose stacks with structured,\nDocker-native tools — instead of brittle shell commands and terminal-shaped output.**\n\nInspect containers, read logs, check health, query databases, take dumps, and bring stacks up or\ndown — on your laptop or on a server across the world by naming its profile.\n\nIt speaks Docker the way you do: your compose files, project names, and service names. Answers\ncome back as named fields with enough context for an agent to act on them, rather than parse an\nASCII table and guess.\n\nWorks with Claude Code, Codex CLI, Cursor, opencode, Gemini CLI, Qwen Code and other MCP clients.\n\n[![MCP Registry](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fregistry.modelcontextprotocol.io%2Fv0%2Fservers%3Fsearch%3Dio.github.hypnosis%2Fdocker-mcp-server%26version%3Dlatest&query=%24.servers%5B0%5D.server.version&style=flat-square&logo=modelcontextprotocol&logoColor=white&label=MCP%20Registry&color=0F172A)](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.hypnosis/docker-mcp-server&version=latest) [![npm version](https://img.shields.io/npm/v/@hypnosis/docker-mcp-server?style=flat-square&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@hypnosis/docker-mcp-server) [![npm downloads](https://img.shields.io/npm/dm/@hypnosis/docker-mcp-server?style=flat-square&logo=npm&logoColor=white&color=2EA043&label=downloads)](https://www.npmjs.com/package/@hypnosis/docker-mcp-server) [![tests](https://img.shields.io/github/actions/workflow/status/hypnosis/docker-mcp-server/test.yml?branch=main&style=flat-square&logo=githubactions&logoColor=white&label=tests)](https://github.com/hypnosis/docker-mcp-server/actions/workflows/test.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-yellow?style=flat-square)](LICENSE)\n\n**[Install](#install-in-30-seconds) · [Tools](#docker-mcp-tools-for-containers-compose-and-databases) · [Setup](#set-up-the-docker-mcp-server) · [Security](#destructive-command-protection-for-ai-agents) · [Docs](docs/tools.md) · [Changelog](CHANGELOG.md)**\n\n---\n\n## Install in 30 seconds\n\nNo global installation required. `npx` downloads the package on first use:\n\n```bash\nnpx -y @hypnosis/docker-mcp-server\n```\n\nAdd it to your MCP client — **Claude Code**, for example — for every project:\n\n```bash\nclaude mcp add docker -s user -- npx -y @hypnosis/docker-mcp-server\n```\n\nThat is the whole setup for the Docker on this machine. No profiles file, no environment\nvariables: the server uses the local Docker socket, and the compose project of your working\ndirectory is the project it answers about.\n\nFor a Docker host on another machine, add one file with the servers you reach over SSH:\n\n```bash\nclaude mcp add docker -s user \\\n  -e DOCKER_MCP_PROFILES_FILE=\"$HOME/.claude/docker-profiles.json\" \\\n  -- npx -y @hypnosis/docker-mcp-server\n```\n\n```json\n{\n  \"default\": \"local\",\n  \"profiles\": {\n    \"local\": { \"mode\": \"local\" },\n    \"production\": {\n      \"host\": \"prod.example.com\",\n      \"username\": \"deployer\",\n      \"privateKeyPath\": \"~/.ssh/id_ed25519\"\n    }\n  }\n}\n```\n\nNow every tool takes a `profile`, and `docker_health({ action: \"profiles\" })` lists what the\nserver was given.\n\nCodex, Cursor, opencode and other clients are covered in\n[Set up the Docker MCP server](#set-up-the-docker-mcp-server).\n\n### Install as a plugin\n\nSome clients — **Claude Code**, for example — can take the whole thing as a plugin instead:\n\n```\n/plugin marketplace add hypnosis/docker-mcp-server\n/plugin install docker-mcp-server@docker-mcp-server\n```\n\nThe plugin brings the server up against the Docker on this machine, with no configuration at\nall. Point it at other hosts the same way as above, with `DOCKER_MCP_PROFILES_FILE`.\n\n### Requirements\n\n[![Node.js](https://img.shields.io/node/v/@hypnosis/docker-mcp-server?style=flat-square&logo=nodedotjs&logoColor=white&color=5FA04E)](https://nodejs.org/) [![TypeScript](https://img.shields.io/npm/dependency-version/@hypnosis/docker-mcp-server/dev/typescript?style=flat-square&logo=typescript&logoColor=white&color=3178C6&label=typescript)](https://www.typescriptlang.org/) [![MCP SDK](https://img.shields.io/npm/dependency-version/@hypnosis/docker-mcp-server/@modelcontextprotocol/sdk?style=flat-square&logo=modelcontextprotocol&logoColor=white&color=0F172A&label=MCP%20SDK)](https://modelcontextprotocol.io/)\n\n**Node.js 18+** and a Docker daemon you can already reach — the local socket, or a remote host\nover SSH with a key.\n\nThe server starts whether or not Docker is up. A call made while the daemon is down answers\n\"start Docker\" and works on the next try, without restarting your MCP client.\n\nPrefer a pinned version, offline work, or one less registry check per launch:\n`npm install -g @hypnosis/docker-mcp-server`, then use `docker-mcp-server` as the command\ninstead of `npx`.\n\n## Who this Docker MCP server is for\n\n- **AI-assisted developers** who build, run, and debug applications with Docker Compose.\n- **Backend and full-stack developers** who need their coding agent to inspect containers, logs,\n  health checks, databases, and Compose services while solving a problem.\n- **Independent developers and small product teams** who deploy Docker applications to their own\n  servers and want one agent workflow for local development and production.\n- **Platform engineers** who maintain Docker environments across development, staging, and\n  production hosts.\n- **DevOps engineers and SREs using AI coding agents** for incident checks, routine container\n  work, and faster investigation of Compose-based services.\n- **Agencies and technical teams maintaining client applications**, where an agent needs to\n  understand an unfamiliar Compose stack before making a change.\n- **QA and release engineers** who need repeatable visibility into test stacks, service health,\n  logs, and databases before a release.\n\n## Why use a Docker MCP server instead of raw Docker CLI?\n\nDocker CLI is excellent for a person at a terminal. An AI agent needs something different: Docker\nand Compose concepts it can query directly, results it can reason about, and clear boundaries\nbetween what happened, what was not found, and what could not be checked.\n\n### Less context, lower AI cost\n\n`docker ps`, `docker stats`, and `docker logs` are designed to be read on a screen. Their output\nmixes labels, values, units, and formatting into text an agent must parse before it can use it.\n\nA Docker MCP tool returns the fields the task needs: service, project, state, health, ports,\ntimestamps, byte counts, exit codes, and log streams. The agent receives less terminal noise,\nspends fewer tokens interpreting it, and keeps more context for the problem it is solving.\n\n### Debug Docker Compose as a system\n\nA Docker application is more than a list of containers. It is a Compose project with services,\ndependencies, health checks, logs, databases, and resources that affect one another.\n\nThe server lets an agent ask about that system in the names you use every day: the project,\nservice, and Compose file. One call can show the state and health of a stack; another can inspect\na bounded log window, measure a service, or query the database already running beside it. The\nagent spends its turns finding the cause, not assembling and decoding shell commands.\n\n### Make decisions from explicit results\n\nA shell command can return an empty table, clipped output, or an error printed alongside ordinary\ntext. For an agent, those are different situations — and treating them as the same leads to\nguesses.\n\nDocker MCP results say what was found, what was not found, what was cut short, and why an action\ndid not run. A missing healthcheck is not a failed healthcheck; an empty list is not a failed\nread; a command with no exit code never started. That gives the agent a reliable basis for its\nnext step, and gives you fewer confident-looking but wrong fixes.\n\n## Manage local and remote Docker hosts over SSH\n\nUse the same Docker MCP tools on the Docker daemon beside you and on a remote server. A remote\nDocker host is a profile: a stable name, its SSH connection, and the authentication it needs.\nThe agent names that profile in a tool call:\n\n```typescript\ndocker_container({ \"action\": \"list\", \"profile\": \"production\" })\n```\n\nLeave `profile` out and the default Docker host answers, so local development stays frictionless.\nName a profile the server does not know and the response lists the profiles it does know instead\nof quietly sending a request to another machine.\n\nRemote Docker Compose projects are resolved by the labels their containers already carry. Ask for\na project by name wherever it lives on that host — no remote working directory or fixed Compose\npath to maintain. For a project that has not run there yet, pass its file explicitly with\n`compose_path`.\n\n## Built for the Model Context Protocol\n\nA native Docker MCP server built on the official MCP SDK, with structured tools designed for\nAI agents rather than terminal automation.\n\nIts behaviour is covered by unit tests and end-to-end tests that run against real Docker\ncontainers, not only mocks.\n\n---\n\n## Docker MCP vs Docker CLI: practical examples\n\nThe Docker CLI is the right interface when a person is driving the terminal. These examples show\nwhat changes when an AI agent needs to inspect a Docker Compose application, understand the\nresult, and choose the next safe step.\n\nEach comparison uses the same real-world task: first the commands and output an agent would have\nto work through, then the structured Docker MCP result it can use directly.\n\n### Diagnose the health of a Docker Compose stack\n\n> **Situation:** A deploy just went out. The site is slow, and you do not know whether a\n> container is down, a healthcheck is failing, or something is eating the memory.\n>\n> **Question:** \"Is this stack healthy?\"\n\n#### Raw docker CLI\n\n```console\n$ docker compose ps\nNAME              IMAGE               STATUS                     PORTS\nshop-api-1        shop-api:latest     Up 4 minutes (healthy)     0.0.0.0:8080->8080/tcp\nshop-postgres-1   postgres:16         Up 4 minutes (unhealthy)   5432/tcp\nshop-worker-1     shop-worker:latest  Exited (1) 2 minutes ago\n$ docker stats --no-stream\nCONTAINER ID   NAME              CPU %   MEM USAGE / LIMIT     MEM %   NET I/O          BLOCK I/O\n1f2c4d5e6a7b   shop-api-1        3.10%   214.8MiB / 7.66GiB    2.74%   12.4MB / 8.9MB   0B / 4.1MB\n9a8b7c6d5e4f   shop-postgres-1   0.42%   1.204GiB / 7.66GiB    15.7%   3.2MB / 44MB     112MB / 890MB\n$ docker inspect --format '{{.State.Health.Status}}' shop-postgres-1\nunhealthy\n$ docker inspect --format '{{.State.Health.Status}}' shop-worker-1\nTemplate parsing error: executing \"\" at <.State.Health.Status>: nil pointer evaluating *types.Health.Status\n```\n\nThree commands, three formats, and the last one fails because the worker image declares no\nhealthcheck at all. Nothing is broken — there is simply nothing to read — but the agent now has\nto tell \"no healthcheck\" apart from \"failing\", and a table gives it no help.\n\n#### Structured MCP result\n\n```typescript\ndocker_container({ \"action\": \"list\", \"project\": \"shop\" })\n```\n\n```json\n{\n  \"action\": \"list\",\n  \"project\": \"shop\",\n  \"containers\": [\n    { \"name\": \"shop-api-1\", \"service\": \"api\", \"project\": \"shop\", \"state\": \"running\",\n      \"health\": \"healthy\", \"image\": \"shop-api:latest\",\n      \"ports\": [\"0.0.0.0:8080->8080/tcp\"], \"created\": \"2026-08-24T09:14:02Z\" },\n    { \"name\": \"shop-postgres-1\", \"service\": \"postgres\", \"project\": \"shop\", \"state\": \"running\",\n      \"health\": \"unhealthy\", \"image\": \"postgres:16\",\n      \"ports\": [\"5432/tcp\"], \"created\": \"2026-08-24T09:14:01Z\" },\n    { \"name\": \"shop-worker-1\", \"service\": \"worker\", \"project\": \"shop\", \"state\": \"exited\",\n      \"health\": null, \"image\": \"shop-worker:latest\",\n      \"ports\": [], \"created\": \"2026-08-24T09:14:01Z\" }\n  ],\n  \"reason\": null,\n  \"legend\": {\n    \"containers[].state=running\": \"Running now.\",\n    \"containers[].state=exited\": \"Stopped. Its logs survive, its processes do not.\",\n    \"containers[].health=healthy\": \"Its own healthcheck passed last run.\",\n    \"containers[].health=unhealthy\": \"Its own healthcheck failed enough times in a row to give up on it.\",\n    \"containers[].health=null\": \"The image declares no healthcheck, so nothing is measured. Not the same as failing.\"\n  }\n}\n```\n\n#### What changes for the agent\n\n| Raw docker CLI | Structured MCP | Your gain |\n|---|---|---|\n| Three commands and three output formats | One call with state, health and ports per service | Fewer round trips |\n| A missing healthcheck errors out or reads as failure | `health: null` is explained in the answer itself | No container blamed for a check nobody wrote |\n| Container names must be mapped back to services | Every row carries `service`, `project` and `name` | The agent speaks compose, not container ids |\n\nThe legend explains only the words this answer used, right next to the fields that used them —\nso the meaning of `unhealthy` is not sitting hundreds of messages back in a tool description.\n\n**Need numbers rather than states?** `docker_container({ action: \"stats\", service: \"postgres\" })`\nreturns CPU, memory against its limit, network and disk as numbers, with the answer saying\nplainly that it is one instant reading and not an average.\n\n**Need the healthchecks themselves?** `docker_health({ action: \"services\" })` reports each\nservice's own verdict, counts its checks and failures, and calls a service with no healthcheck\n`none` rather than counting it as sick.\n\n### Investigate container logs without losing context\n\n> **Situation:** The API started returning 502 about ten minutes ago. The container has been\n> running for a week and has written hundreds of thousands of lines.\n>\n> **Question:** \"What did it print when it broke?\"\n\n#### Raw docker CLI\n\n```console\n$ docker compose logs api --tail 200\napi-1  | 2026-08-24T10:31:07.104Z INFO  request GET /health 200 3ms\napi-1  | 2026-08-24T10:31:07.882Z INFO  request GET /health 200 2ms\n... 196 more lines of the same ...\napi-1  | 2026-08-24T10:39:14.522Z ERROR database connection timed out after 30000ms\n$ docker compose logs api --since 10m | grep -i error\napi-1  | 2026-08-24T10:39:14.522Z ERROR database connection timed out after 30000ms\n```\n\nThe first call spent two hundred lines of context on health-check noise. The second found the\nerror but threw away the lines around it, and nothing in either output says whether the stream\nwas stdout or stderr, or whether anything was dropped on the way.\n\n#### Structured MCP result\n\n```typescript\ndocker_logs({ \"service\": \"api\", \"project\": \"shop\", \"since\": \"10m\", \"lines\": 50 })\n```\n\n```json\n{\n  \"service\": \"api\",\n  \"project\": \"shop\",\n  \"date\": \"2026-08-24\",\n  \"lines\": [\n    { \"stream\": \"stdout\", \"time\": \"10:39:14.518\",\n      \"text\": \"INFO  pool exhausted, waiting for a free connection\" },\n    { \"stream\": \"stderr\", \"time\": \"10:39:14.522\",\n      \"text\": \"ERROR database connection timed out after 30000ms\" },\n    { \"stream\": \"stderr\", \"time\": \"10:39:14.530\",\n      \"text\": \"ERROR upstream 502 while proxying GET /api/orders\" }\n  ],\n  \"returned_lines\": 3,\n  \"since\": \"10m\",\n  \"until\": null,\n  \"truncated\": false,\n  \"truncated_reason\": null,\n  \"clipped_lines\": 0,\n  \"follow\": false,\n  \"reason\": null,\n  \"legend\": {\n    \"lines[].stream=stderr\": \"the stream the container wrote to; many programs write their ordinary progress there, so a line is not an error for being here\"\n  }\n}\n```\n\n#### What changes for the agent\n\n| Raw docker CLI | Structured MCP | Your gain |\n|---|---|---|\n| A tail is a guess: too few lines miss it, too many bury it | `since` and `until` cut the window by time, then `lines` counts | Fewer tokens on noise |\n| One text blob; stream and timestamp are glued into it | Every line carries its own `stream`, `time` and `text` | Errors are found by field, not by grep |\n| A dropped middle looks exactly like a quiet log | `truncated`, `truncated_reason` and `clipped_lines` name every cut | No \"the logs are clean\" from a partial read |\n\nThe day the lines fall on is said once, at the top, and each line then carries only its time —\nthe same stamp repeated on every line is text you would be paying for. Lines spread across more\nthan one day keep their full stamps, and the answer says why.\n\n`since` also takes a date or a UNIX timestamp, and `follow: true` waits for lines still to come,\nreturning what arrived within its own time and size ceiling instead of hanging on an open stream.\n\n### Query a database running in Docker\n\n> **Situation:** Orders stopped appearing on the dashboard. You want to look in the database —\n> which is a container, with a client inside it and no port published to your machine.\n>\n> **Question:** \"What does the table actually hold?\"\n\n#### Raw docker CLI\n\n```console\n$ docker compose exec -T postgres psql -U app -d shop -c \"select status, count(*) from orders group by status\"\n  status   | count\n-----------+-------\n paid      |  1284\n pending   |    17\n(2 rows)\n$ docker compose exec -T redis redis-cli info keyspace\ndb0:keys=41822,expires=41822,avg_ttl=3600000\n```\n\nEvery engine needs its own client, its own flags and its own way of naming the user and the\ndatabase. Get one flag wrong and the shell prints an error the agent has to read as text — the\ncall itself \"succeeded\".\n\n#### Structured MCP result\n\n```typescript\ndocker_db({ \"action\": \"query\", \"service\": \"postgres\", \"project\": \"shop\",\n            \"query\": \"select status, count(*) from orders group by status\" })\n```\n\n```json\n{\n  \"action\": \"query\",\n  \"service\": \"postgres\",\n  \"project\": \"shop\",\n  \"engine\": \"postgresql\",\n  \"query\": \"select status, count(*) from orders group by status\",\n  \"output\": \"  status   | count\\n-----------+-------\\n paid      |  1284\\n pending   |    17\\n(2 rows)\",\n  \"stderr\": \"\",\n  \"exit_code\": 0,\n  \"clipped_bytes\": 0,\n  \"warnings\": [],\n  \"reason\": null,\n  \"legend\": {}\n}\n```\n\n#### What changes for the agent\n\n| Raw docker CLI | Structured MCP | Your gain |\n|---|---|---|\n| A different client, flag set and user for every engine | One tool for PostgreSQL, MySQL/MariaDB, Redis, MongoDB and SQLite | One thing to learn, five databases |\n| A refused statement still looks like a successful command | `exit_code` and `stderr` stay apart from `output` | A failure reads as a failure |\n| Credentials get retyped into the command line | The container's own user and database are the default | Fewer secrets in the transcript |\n\n`status` asks the database about itself instead — version, size, uptime, connections — and\n`format: \"csv\"` lays PostgreSQL rows out for parsing. A statement that would destroy a database\nor a whole keyspace does not run until it carries the confirmation marker; see\n[Destructive command protection for AI agents](#destructive-command-protection-for-ai-agents).\n\n### Back up a database before a risky migration\n\n> **Situation:** A migration is about to rewrite a table. You want a dump first — and you want\n> to know the dump is real before the migration touches anything.\n>\n> **Question:** \"Do I actually have a backup?\"\n\n#### Raw docker CLI\n\n```console\n$ docker compose exec -T postgres pg_dump -U app shop | gzip > backup.sql.gz\n$ echo $?\n0\n$ ls -lh backup.sql.gz\n-rw-r--r--  1 you  staff    20B 24 Aug 11:02 backup.sql.gz\n```\n\nExit code zero came from `gzip`, the last command in the pipe — not from `pg_dump`. Twenty\nbytes is an empty archive: the dump failed on a wrong user, the error went to the terminal, and\nthe shell reported success. A migration now runs on the strength of a backup that does not exist.\n\n#### Structured MCP result\n\n```typescript\ndocker_db_admin({ \"action\": \"backup\", \"service\": \"postgres\", \"project\": \"shop\" })\n```\n\n```json\n{\n  \"action\": \"backup\",\n  \"service\": \"postgres\",\n  \"project\": \"shop\",\n  \"engine\": \"postgresql\",\n  \"file\": \"/backups/shop-2026-08-24T11-02-17.dump.gz\",\n  \"bytes\": 48219553,\n  \"verified\": true,\n  \"message\": null,\n  \"confirmed\": false,\n  \"restarted\": false,\n  \"reason\": null,\n  \"legend\": {\n    \"verified\": \"The file was read back around the call, and bytes is its size on disk. This is the difference between a dump that exists and a command that did not fail.\"\n  }\n}\n```\n\n#### What changes for the agent\n\n| Raw docker CLI | Structured MCP | Your gain |\n|---|---|---|\n| A pipe reports the exit code of its last command | The dump is read back and its size named | An empty backup cannot pass for a good one |\n| Each engine needs its own dump command and flags | One call for PostgreSQL, MySQL, MongoDB, SQLite and Redis | Same workflow whatever the stack runs |\n| \"Done\" is a word | `verified` is a field, and `bytes: null` says why it is unknown | The agent knows what it does not know |\n\nWhen the file cannot be read back, the answer says so instead of claiming a size: `verified`\nstays false, `bytes` is `null`, and `message` names what stopped it — an unknown size is never\nreported as an empty file.\n\nFilling the database back is the same tool: `action: \"restore\"`. It overwrites what is there,\nso it is refused until the call carries the confirmation marker in its `confirm` field — and the\nrefusal tells you to take a backup here first.\n\n### Understand Docker disk usage before cleaning up\n\n> **Situation:** The server is at 90% disk. Docker is the obvious suspect, but you do not know\n> whether it is images, volumes, stopped containers or build cache — and pruning the wrong one\n> destroys a database.\n>\n> **Question:** \"What is safe to reclaim?\"\n\n#### Raw docker CLI\n\n```console\n$ docker system df\nTYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE\nImages          48        9         31.2GB    22.4GB (71%)\nContainers      21        7         1.8GB     412MB (22%)\nLocal Volumes   19        6         64.9GB    38.1GB (58%)\nBuild Cache     264       0         9.7GB     9.7GB\n$ docker system df -v | head -40\n... several screens of per-image, per-container and per-volume tables ...\n```\n\nThe summary is readable by a person and expensive for an agent: percentages in parentheses,\nsizes as text with mixed units, and the detail view is several screens long. To compare\n\"reclaimable volumes\" against \"reclaimable images\" the agent has to parse `64.9GB` and\n`38.1GB (58%)` back into numbers.\n\n#### Structured MCP result\n\n```typescript\ndocker_resource({ \"action\": \"disk\" })\n```\n\n```json\n{\n  \"action\": \"disk\",\n  \"count\": 4,\n  \"total_bytes\": 115534368358,\n  \"disk\": [\n    { \"group\": \"images\", \"count\": 48, \"active\": 9,\n      \"size_bytes\": 33500985344, \"unused_bytes\": 24051816448 },\n    { \"group\": \"containers\", \"count\": 21, \"active\": 7,\n      \"size_bytes\": 1932735283, \"unused_bytes\": 431994470 },\n    { \"group\": \"volumes\", \"count\": 19, \"active\": 6,\n      \"size_bytes\": 69686362112, \"unused_bytes\": 40908324864 },\n    { \"group\": \"build_cache\", \"count\": 264, \"active\": 0,\n      \"size_bytes\": 10414285619, \"unused_bytes\": 10414285619 }\n  ],\n  \"reason\": null,\n  \"legend\": {\n    \"disk[].active\": \"How many of them something uses right now: a running container, or an image behind one.\",\n    \"disk[].unused_bytes\": \"Size of what nothing uses at the moment. Removing it frees less than this: what is shared stays until its last holder is gone.\",\n    \"disk[].group=images\": \"Images are counted by their layers on disk, so this is what they really take; the sizes of separate images add up to more, because a shared layer belongs to each of them.\",\n    \"disk[].group=containers\": \"Only what a container wrote on top of its image. What the image itself takes is counted with the images.\"\n  }\n}\n```\n\n#### What changes for the agent\n\n| Raw docker CLI | Structured MCP | Your gain |\n|---|---|---|\n| Sizes as text with mixed units and percentages | Bytes as numbers, in four named groups | The agent compares instead of parsing |\n| The detail view costs several screens | One call, one number per group | Fewer tokens for the same answer |\n| \"Reclaimable\" hides what is holding a volume | `active` counts what is in use, `unused_bytes` what is not | Build cache goes, the database volume stays |\n\n`images`, `volumes` and `networks` list what the host holds — volumes with their size and how\nmany containers hold them. An empty list means asked and found nothing, which is not the same\nas never asked.\n\n## Built-in destructive command protection for AI agents\n\nAn AI agent can generate a valid command that is still the wrong command to run. This server adds a\nlocal guard before anything reaches Docker or a database client, so irreversible operations need\nan explicit confirmation in the call that requests them.\n\nThe guard distinguishes between deleting the thing that holds data — a database, volume, or\ntop-level directory — and changing or deleting data inside it. The first category is refused\nuntil confirmed. The second can run, but returns a warning that states what changed.\n\n### Block irreversible loss, warn about destructive changes\n\n| Refused — the vessel itself | Only warned about — its contents |\n|---|---|\n| `DROP DATABASE`, `DROP SCHEMA`, `dropdb` | `DROP TABLE`, `TRUNCATE` |\n| `FLUSHALL`, `FLUSHDB` (Redis) | `DELETE FROM` with no `WHERE` |\n| `db.dropDatabase()` (MongoDB) | `deleteMany({})`, `updateMany({})` with an empty filter |\n| `docker volume rm`, `docker volume prune` | `docker image prune`, `docker container prune` |\n| `docker system prune` | `docker network prune` |\n| `rm -rf /`, `rm -rf ~`, `rm -rf /srv` | `rm -rf /srv/app/cache` |\n| `mkfs`, `dd of=/dev/...` | |\n\nA warning is not a refusal: the operation runs, and what it destroyed is stated as a fact in\nthe answer's `warnings`. `DELETE FROM orders` with a `WHERE` is not remarked on at all — that is\na normal statement.\n\nRemoval reads by where it points. A directory named directly under the root holds all of\nsomething — the application, the data, the system — so `rm -rf /srv` is refused, while\n`rm -rf /srv/app/cache` is what deleting files is for. A command wrapped in `sh -c` is unwrapped\nand read however many shells it hides behind. The command runs as one argv, so a separator at the\ntop level is an argument: `echo \"hi\"; rm -rf /data` prints a line and removes nothing.\n\n### Confirm a deliberate destructive operation\n\nNothing is forbidden permanently. Three tools take the confirmation, each where the danger is:\n\n```typescript\ndocker_exec({ \"service\": \"api\",\n              \"command\": \"rm -rf /srv/legacy # CONFIRMED-DESTRUCTIVE\" })\n\ndocker_db({ \"action\": \"query\", \"service\": \"redis\",\n            \"query\": \"FLUSHDB # CONFIRMED-DESTRUCTIVE\" })\n\ndocker_compose_control({ \"action\": \"down\", \"volumes\": true,\n                         \"confirm\": \"# CONFIRMED-DESTRUCTIVE\" })\n```\n\nThe marker is carried inside the call, so it lifts the refusal for that one call and no other.\nThe marker itself never reaches the database or the shell — it is taken out of the statement\nbefore it runs.\n\nEvery refusal names what would have gone, so it can be read before the call is repeated rather\nthan stepped over: `DROP DATABASE shop destroys the database itself. Add # CONFIRMED-DESTRUCTIVE\nto the command to run it.`\n\nThe guard reads one call at a time. It cannot connect a delete in one call with a read in the\nnext, and it knows the tools it knows — a custom binary that wipes a directory is not something\nit recognizes. It is a seatbelt, not a policy engine: recoverable operations remain your call.\n\n## Docker MCP tools for containers, compose and databases\n\nA focused Docker MCP toolkit for the work AI agents do most often: inspect a Compose stack,\ndiagnose a service, control its lifecycle, work with its database, and understand host resources.\nFull parameters and examples live in **[docs/tools.md](docs/tools.md)**.\n\nEvery tool speaks the same Docker Compose vocabulary. `profile` selects the Docker host;\n`project` selects the Compose project; `service` selects the service from its Compose file, not\nthe generated container name. Leave `profile` out and the default host answers. Leave `project`\nout and the project in the working directory answers. Use `compose_path` only when Docker cannot\nresolve a project because it has not run on that host yet.\n\n### Inspect a Docker Compose stack\n\n| Tool | What it does |\n|---|---|\n| `docker_container` | List a project with state, health and ports, or measure what one service consumes |\n| `docker_logs` | Read what a container printed, line by line, in a window bounded by time |\n| `docker_compose` | Read what the project declares: the resolved compose file, or its variables |\n| `docker_health` | Healthchecks of the services, health of this server, the hosts it was given |\n| `docker_resource` | Images, volumes and networks of a host, and where the disk went |\n\n### Control services and stack lifecycle\n\n| Tool | What it does |\n|---|---|\n| `docker_container_control` | Start, stop or restart one service, reporting where it stood before |\n| `docker_compose_control` | Bring the whole stack up or take it down |\n\n### Work with databases running in Docker\n\n| Tool | What it does |\n|---|---|\n| `docker_db` | Run a statement, or ask the database about itself |\n| `docker_db_admin` | Take a dump, or fill a database from one |\n\nPostgreSQL, MySQL/MariaDB, Redis, MongoDB and SQLite are spoken to through the client the image\nalready carries — nothing is installed into your containers.\n\n### Secrets stay hidden by default\n\nReading a project's variables with `docker_compose` hides the values whose key names a secret —\nanything containing `PASSWORD`, `TOKEN`, `KEY`, `SECRET`, `PRIVATE` or `CREDENTIALS`. Each\nvariable says whether it was hidden, so a masked value is never mistaken for the real one, and\n`mask: false` returns it when you actually need it.\n\n### Run a command when no dedicated tool fits\n\n| Tool | What it does |\n|---|---|\n| `docker_exec` | Run a command inside a service container, stdout and stderr apart |\n\n### Safe defaults in MCP clients\n\nStandard MCP annotations tell your client which tools are safe to run without asking. The five\nreading tools declare `readOnlyHint`. The two control tools declare `destructiveHint` with\n`idempotentHint` — the same call twice leaves the same state. `docker_db`, `docker_db_admin` and\n`docker_exec` declare `destructiveHint` alone: they carry something the server did not write, so\nwhat happens is decided by what you handed in.\n\n## Set up the Docker MCP server\n\nFor local Docker, add the server to your MCP client and start working. No profile file or Docker\nendpoint configuration is required: the server uses the Docker socket on this machine.\n\nConfigure profiles only when an AI agent needs to reach remote Docker hosts over SSH.\n\n### Configure remote Docker hosts\n\nStore the profiles file wherever your MCP client keeps its configuration. Each profile gives the\nagent a name for a Docker host and the SSH connection details it needs:\n\n```json\n{\n  \"default\": \"local\",\n  \"profiles\": {\n    \"local\": { \"mode\": \"local\" },\n    \"production\": {\n      \"host\": \"prod.example.com\",\n      \"username\": \"deployer\",\n      \"port\": 22,\n      \"privateKeyPath\": \"~/.ssh/id_ed25519\"\n    },\n    \"staging\": {\n      \"host\": \"staging.example.com\",\n      \"username\": \"deployer\",\n      \"port\": 2222,\n      \"privateKeyPath\": \"~/.ssh/id_ed25519_staging\"\n    }\n  }\n}\n```\n\nA profile with `mode: \"local\"` is the Docker on this machine. A remote profile needs `host` and\n`username`; `port` defaults to 22. Where the compose projects live on that server is not asked\nfor: a project is found by the labels its containers carry. `default` names the profile used\nwhen a call leaves `profile` out.\n\nPrefer keys. A profile without `privateKeyPath` uses your SSH agent, which is the better answer\nwhen the key is encrypted.\n\nThe host key of the machine is checked: the first connection remembers it in\n`~/.ssh/known_hosts`, and a machine that later answers with a different key is refused instead of\nbeing talked to. `knownHostsPath` in a profile points at another file when you keep those keys\napart.\n\n### Configure Claude Code, Codex, Cursor and other MCP clients\n\n**Claude Code** — one command; `-s user` makes the server available in every project:\n\n```bash\nclaude mcp add docker -s user \\\n  -e DOCKER_MCP_PROFILES_FILE=\"$HOME/.claude/docker-profiles.json\" \\\n  -- npx -y @hypnosis/docker-mcp-server\n```\n\n**Codex CLI**\n\n```bash\ncodex mcp add docker \\\n  --env DOCKER_MCP_PROFILES_FILE=\"$HOME/.codex/docker-profiles.json\" \\\n  -- npx -y @hypnosis/docker-mcp-server\n```\n\n**Cursor** — in `~/.cursor/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"docker\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@hypnosis/docker-mcp-server\"],\n      \"env\": {\n        \"DOCKER_MCP_PROFILES_FILE\": \"~/.cursor/docker-profiles.json\"\n      }\n    }\n  }\n}\n```\n\n**opencode** — in `~/.config/opencode/opencode.json`:\n\n```json\n{\n  \"$schema\": \"https://opencode.ai/config.json\",\n  \"mcp\": {\n    \"docker\": {\n      \"type\": \"local\",\n      \"command\": [\"npx\", \"-y\", \"@hypnosis/docker-mcp-server\"],\n      \"enabled\": true,\n      \"environment\": {\n        \"DOCKER_MCP_PROFILES_FILE\": \"~/.config/opencode/docker-profiles.json\"\n      }\n    }\n  }\n}\n```\n\n**Other MCP clients** — Gemini CLI, Qwen Code, Cline, an editor plugin or your own agent work\nthe same way. All they need is a command to run and, for remote hosts, one environment variable.\n\n### Verify the connection\n\nRestart your MCP client, then run `docker_health({ action: \"profiles\" })`. It lists the Docker\nhosts the server received from configuration and identifies any profile it could not read, along\nwith the field that needs attention.\n\n## Docker MCP server configuration\n\n| Variable | What it does | Default |\n|---|---|---|\n| `DOCKER_MCP_PROFILES_FILE` | Path to the profiles JSON. Without it, only local Docker | — |\n| `DOCKER_PROFILES` | The same JSON inline, used when no profiles file is set | — |\n| `DOCKER_MCP_ALLOW_SSH_FALLBACK` | Keep going when a profile's key is missing, instead of refusing | `false` |\n| `LOG_LEVEL` | `debug`, `info`, `warn`, `error` | `info` |\n\n`DOCKER_PROFILES_FILE` is the deprecated spelling of the first variable. It still works and logs\na line asking you to rename it.\n\nA profile pointing at a key that does not exist is refused by default, with the path and the\nways out named — a missing key otherwise turns into a connection attempt that fails much later,\nsomewhere less obvious.\n\n## Docker MCP server limitations\n\nThe server favours bounded, explicit tool calls over open-ended or implicit behaviour. These are\nthe current limits to keep in mind:\n\n- **Log following is bounded.** `follow: true` returns what arrived within 10 seconds or 1 MB,\n  whichever comes first. It is a look at a live stream, not a subscription.\n- **The guard reads one call at a time.** It cannot connect a delete in one call with a read in\n  the next, and it recognizes the tools it knows — a custom binary that wipes a directory is not\n  one of them.\n- **Host resources are per host, not per project.** `docker_resource` answers about everything\n  the daemon holds; narrowing to one project is `docker_container`.\n- **Windows is not verified.** Nothing in the server is written against one platform, but it has\n  not been run end to end on Windows.\n\n## Develop and test the Docker MCP server\n\n```bash\nnpm install\nnpm run build            # tsc\nnpx tsc --noEmit         # types\nnpm run test:unit        # unit tests\nnpm run docker:test:up   # start the test containers\nnpm run test:e2e         # end-to-end suite against those containers\n```\n\nThe end-to-end suite runs against real PostgreSQL, MySQL, MariaDB, MongoDB, Redis, and web\nservice containers. It verifies Docker integration behaviour that unit tests and mocks cannot\nprove alone. See [docs/architecture.md](docs/architecture.md) for the project layout.\n\n## Support Docker MCP Server\n\nIf the tool helps your team, [star the project on GitHub](https://github.com/hypnosis/docker-mcp-server).\nIt helps other developers find it too.\n\n## Contribute to the Docker MCP server\n\nIssues and pull requests are welcome at\n[github.com/hypnosis/docker-mcp-server](https://github.com/hypnosis/docker-mcp-server).\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n",
  "bytes": 36015,
  "sha": "13cf2e944d951f1e3db42470627775ca1266d1b69fc24367de8dd527f8338feb",
  "repo_slug": "hypnosis/docker-mcp-server",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_hypnosis_docker_mcp_server_6fd89a81/readme"
}