Your agent’s tool belt is a stranger’s toolbox
You installed a filesystem MCP server last week with one npx command. You didn’t read the source. You didn’t check what it phones home to. You just wanted your coding agent to read your project files without you copy-pasting them into a chat window forty times a day.
That’s the normal workflow right now, and it’s a little unhinged when you say it out loud. Model Context Protocol servers are just Node or Python packages that an AI agent launches as a subprocess. When Claude or Cursor calls a tool, that tool runs with whatever permissions the process launching it has, which on most laptops means your full user account. Read your SSH keys, sure. Curl an internal API, why not. Nothing stops it. The npm registry has already shipped compromised packages with worse blast radius than this, and MCP servers are frequently thinner and less audited than the packages you’d install for a real project.
Docker’s answer is the MCP Toolkit: instead of running that server as a bare process on your host, you run it in a container. The idea is straightforward and good. The execution has a specific hole in it that matters more than the marketing copy admits, and that hole is the point of this article.
What Docker MCP Toolkit actually is
MCP Toolkit is a feature built into Docker Desktop, backed by a CLI plugin (docker mcp) and a component called the MCP Gateway. The Gateway sits between your AI client (Claude Desktop, Cursor, VS Code, whatever) and a set of MCP servers, and it does three jobs:
- It launches each MCP server from Docker’s MCP Catalog as its own container instead of a bare host process.
- It exposes one unified endpoint to your AI client, so the client only ever has to know about the Gateway, not about N separately-configured servers.
- It manages secrets and OAuth tokens centrally, so an API key gets injected into the one container that needs it rather than sitting in a JSON config file that every tool on your machine can read.
You get to this from Docker Desktop’s GUI (an “MCP Toolkit” tab where you browse the catalog and toggle servers on), or entirely from the terminal with the docker mcp plugin, which is the part worth actually learning because the GUI hides the moving parts you need to reason about.
The catalog is organized around profiles, which are named bundles of enabled servers plus their config. You create a profile, add servers to it, connect a client to it, and the Gateway does the routing:
# create a profile to hold your enabled serversdocker mcp profile create --name devbox
# add a server from the catalog to that profile (catalog:// references the entry by id)docker mcp profile server add devbox --server catalog://mcp/docker-mcp-catalog/github-official
# set any config the server needs (repo scope, org name, etc)docker mcp profile config devbox --set github-official.org=my-org
# see what's in the profile before you trust itdocker mcp profile show devboxOnce a profile has servers in it, you connect an AI client to that profile and the Gateway takes over from there:
docker mcp client connect claude-desktop --profile devboxThat command rewrites the client’s MCP config so it points at the Gateway instead of at N separate npx commands. Restart Claude Desktop and it now talks to one process, docker mcp gateway run, which fans requests out to whichever containers back the tools you enabled.
Secrets get their own lane, and piping the value in on stdin keeps it out of your shell history and out of the client config file:
echo "ghp_your_token_here" | docker mcp secret set GITHUB_TOKENdocker mcp secret lsThe Gateway holds that value and injects it as an environment variable into only the container that declared it needs GITHUB_TOKEN. Your Cursor config never sees the raw token. Neither does any other MCP server running alongside it. That part is a genuine improvement over the “paste your API key into six different JSON files” status quo most people are running today.
What containerizing a tool call actually buys you
This is worth setting up for reasons that go beyond Docker’s own marketing.
When an MCP server runs as a bare npx some-mcp-package process on your host, it inherits everything: your full filesystem, your network, any environment variable your shell exports, and the ability to spawn other processes with your identity. A malicious or just poorly written server can read ~/.aws/credentials because nothing tells it not to. This isn’t a hypothetical; supply chain attacks on npm packages are a known, load-bearing attack vector, and MCP servers are new enough that most of them haven’t had a security audit longer than a weekend.
Put that same server in a container and you get real defaults:
- No filesystem access unless you grant it. The container’s view of disk is whatever the image ships with, plus anything you explicitly mount.
- No inbound access from your LAN by default. Nothing can reach into the container unless you publish a port. Outbound is a different story: on the default bridge network, the container can still reach your NAS, your router, and the internet through NAT, so don’t assume egress is blocked without
--network noneor an explicit firewall rule. - No ambient host environment variables. The only secrets the container sees are the ones the Gateway explicitly injects for that server.
- Process isolation. A crashed or misbehaving MCP server can’t reach out and touch other processes on your host.
That’s a real security boundary, and it is strictly better than running the same npm package bare. If your threat model is “a compromised or buggy MCP package tries to read files it has no business reading,” containerizing it closes that door. This is the same reason you don’t run every random script you download as root: not because root access is definitely going to get abused, but because there’s no reason to hand out permissions the job doesn’t need.
Where the isolation quietly stops
Now the part that matters more than the setup instructions, because this is where people get a false sense of safety.
Container isolation protects the host from the container. It does nothing once you hand the container a path back out. And the two most common ways people hand a path back out are exactly the two things an MCP server usually needs to be useful for real work: a bind-mounted directory, and the Docker socket.
The bind mount problem. A filesystem MCP server is useless if it can’t see your project. So you mount your project directory into the container:
docker run -d \ --name mcp-filesystem \ -v /home/user/projects/myapp:/workspace \ mcp/filesystem-server:latestCongratulations, you’ve handed the “sandboxed” process read/write access to your actual project files, including your .env, your .git history, and anything else that happens to live in that directory. The container boundary is still there for everything outside /workspace, but for the one thing the agent is actually going to touch, the mount makes the container a thin curtain, not a wall. If that server has a prompt injection vulnerability (an attacker-controlled file it reads gets interpreted as instructions), the blast radius is the whole mounted tree, container or no container.
The Docker socket problem, which is worse. Some MCP servers exist specifically to let your agent manage Docker: spin up test containers, inspect running services, tail logs. To do that from inside a container, they need the Docker socket:
docker run -d \ --name mcp-docker-control \ -v /var/run/docker.sock:/var/run/docker.sock \ mcp/docker-server:latestThis is the one to actually worry about. Mounting /var/run/docker.sock into a container does not give that container “some Docker access.” It gives it root-equivalent control over the host, because the Docker daemon runs as root and anyone who can talk to its socket can ask it to do anything a root process can do, including running a new container with -v /:/host and reading or writing anywhere on your filesystem. Container escape via a mounted Docker socket isn’t a theoretical CVE. It’s the documented, expected behavior of the socket. Handing an AI agent’s tool this access is functionally the same as running that tool as root on your host, and the fact that it’s wrapped in a container doesn’t change that math even slightly. It’s a rental car with a stack of blank checks in the glovebox: the paperwork looks contained, the actual exposure isn’t.
The MCP Toolkit’s resource limits (capped CPU and memory per tool container) and its default no-filesystem-access posture are real and useful. They just don’t apply once you, the operator, grant an exception, and granting exceptions is exactly what makes half of these tools worth installing.
Setting it up without lying to yourself about the risk
If you’re going to run MCP servers through the Toolkit (and for most of them, you should), do it with the same discipline you’d apply to any container that touches your data:
Scope bind mounts to the minimum directory, and prefer read-only when the tool doesn’t need to write:
docker run -d \ --name mcp-filesystem \ -v /home/user/projects/myapp:/workspace:ro \ mcp/filesystem-server:latestThat :ro suffix means a compromised or buggy server can read your code to answer questions about it, but can’t quietly rewrite your files or drop a .git/hooks/pre-commit backdoor. Flip to read-write only for the specific project where you actually need the agent editing files, and only for the session you’re using it.
Never mount the Docker socket into a server you haven’t personally read the source of. If you need Docker control from an agent, look at whether the task can be done through a scoped API instead, or accept that you’re granting host-root-equivalent trust and treat the decision with the weight that deserves. Don’t enable a Docker-control MCP server “just to see what it does.”
Check what a server is actually configured to see before you trust it:
docker mcp profile show devboxdocker inspect mcp-filesystem --format '{{ .Mounts }}'Run that second command periodically. Mount configs drift, especially if you’re copy-pasting docker run snippets from a README you skimmed at midnight.
Use secrets, never environment files, for anything that looks like a credential:
echo "sk_test_your_key_here" | docker mcp secret set STRIPE_TEST_KEYNot a .env file sitting in the same directory the filesystem server has mounted. That’s handing the key to the same tool you’re trying to keep it away from.
Treat the MCP Catalog’s “verified” badge as a starting point, not a verdict. Docker signs and scans catalog images, which rules out a class of supply chain tampering between the publisher and your machine. It says nothing about whether the server’s own logic is well-written, and nothing about what happens once you grant it a mount it asked for.
When to actually bother with this
If you’re running one or two MCP servers that only touch scratch data (a sandboxed scratch directory, a public API with no write access), the security upside of containerizing them is real but the stakes are low enough that either approach is fine. Where this earns its setup time is the moment you’re running a filesystem server against a real project, a database server against anything with production data, or anything that touches Docker itself. At that point you have credentials and file access in play, and “the tool runs in a container” needs to be paired with “and I know exactly what I mounted into it,” or the container is just theater.
The lazy version of this that’s actually correct: use the Toolkit for the routing and secrets convenience (it’s nicer than juggling six client configs), keep every bind mount read-only until you have a concrete reason to widen it, and never let anything with “docker” in its tool description near /var/run/docker.sock unless you wrote the server yourself or trust whoever did as much as you’d trust a root shell.
Common Questions
Does Docker MCP Toolkit require Docker Desktop?
Yes, the GUI and the underlying feature ship as part of Docker Desktop, though the docker mcp CLI plugin and the Gateway can run headless on Linux without the Desktop GUI if you install the plugin and catalog data manually. Most home lab users on a server without a desktop environment will want the CLI-only path.
Can a containerized MCP server still read my SSH keys?
No, not unless you bind-mount your home directory or ~/.ssh into it, or mount the Docker socket, which effectively grants the same access indirectly. A container with no explicit mounts has no path to your host filesystem at all. The risk is entirely in what you choose to mount, not in the container mechanism itself.
Is Docker MCP Toolkit safe to use with the Docker socket mounted?
No, not in the way people mean when they ask this. Mounting /var/run/docker.sock into any container, MCP or not, hands that container root-equivalent control of your host, because the Docker daemon runs as root. Only do this for a server whose source you’ve read, and understand you’re granting host access, not sandboxed access.
Do I need MCP Toolkit if I only use one or two MCP servers?
Not strictly. The security benefit exists even for one server, but the setup and secrets management overhead pays off more once you’re juggling several servers across multiple AI clients. For a single low-risk server (a public read-only API), running it directly with npx in a scratch environment is fine.
What AI clients work with Docker MCP Gateway?
Claude Desktop, Cursor, VS Code, and other MCP-compatible clients connect through docker mcp client connect <client> --profile <name>, which rewrites that client’s MCP configuration to point at the Gateway. Any client that speaks standard MCP over stdio or Streamable HTTP works, since the Gateway just proxies the protocol.