devcontainer-mcp
This MCP server provides tools for running shell commands and managing the full lifecycle of a devcontainer:
bash: Run arbitrary shell commands inside the devcontainer viadevcontainer exec ... bash -lc, capturing combined stdout/stderr and exit status. Supports pipes, redirects, globs, custom working directory, extra environment variables, and configurable timeouts (default 120s, max 600s).bash_output: Retrieve full output logs from previousbash,up, orrebuildcommands when the initial response was truncated.up: Start a devcontainer for a worktree, automatically handling git-related bind mounts.rebuild: Recreate the devcontainer, optionally forcing a cache-less image build after config or Dockerfile changes.stop: Gracefully halt a devcontainer while preserving its state and named volumes.down: Fully tear down a devcontainer, its networks, and optionally its named volumes.list_containers: Discover all devcontainers for the current repository, including detection of orphaned containers whose worktree no longer exists on disk.
Additionally, the server supports SSH agent forwarding into containers, automatic Compose project naming, git decoupling for linked worktrees, and a CLI hook for Claude Code's WorktreeRemove to automatically clean up containers when a worktree is deleted.
Allows executing bash commands inside a devcontainer using Docker, providing access to the container's environment for command execution with stdout/stderr and exit code.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@devcontainer-mcprun 'npm test' in the devcontainer"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
devcontainer-mcp
A Model Context Protocol server that runs
shell commands inside a devcontainer (mirroring Claude Code's built-in
Bash tool) and manages the container's lifecycle — start, rebuild, stop,
tear down — plus a container lister with orphan detection. It also ships a
small CLI used by the Claude Code WorktreeRemove hook to tear a container
down when its worktree is deleted.
Tools are addressed as mcp__devcontainer__<tool> (server name devcontainer):
bash, up, rebuild, stop, down, list_containers.
Why
This is a Python rewrite of the TypeScript
mcp-devcontainers
server. The original bash-equivalent returned a fixed string and tee'd output
to a file, so the model never saw what actually happened. Here, real combined
stdout/stderr and the exit status flow back in the tool result, and a non-zero
exit (or a timeout) flags the result as an error.
Container lifecycle is explicit and agent-driven: bash never brings the
container up. The agent calls up once for a fresh worktree, rebuild after
changing the devcontainer config, stop to pause, and down to tear down.
There are no SessionStart / SessionEnd hooks — only a WorktreeRemove hook
(see below) to clean up after a deleted worktree.
Related MCP server: Shell Server
Requirements
Python ≥ 3.10
The
@devcontainers/cliavailable asdevcontaineronPATH(or override viaDEVCONTAINER_CLI, e.g.npx @devcontainers/cli) — used bybash,up,rebuild.Docker with the Compose v2 plugin (
docker compose); falls back to the legacydocker-composebinary — used bystop,down,list_containers.
Install / run
Run straight from the repo with uvx — no install
step:
uvx --from git+https://github.com/vovayartsev-dice/devcontainer-mcp devcontainer-mcpThe server speaks MCP over stdio.
.mcp.json
Add it to your project's .mcp.json (the server key is what produces the
mcp__devcontainer__* tool prefix):
{
"mcpServers": {
"devcontainer": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/vovayartsev-dice/devcontainer-mcp",
"devcontainer-mcp"
],
"env": {
"DEVCONTAINER_WORKSPACE_FOLDER": "${workspaceFolder}"
}
}
}
}Configuration (environment variables)
Variable | Default | Purpose |
|
| devcontainer CLI invocation; shell-split, so |
|
| Path to the |
| current dir | Default host workspace folder when the |
| (from origin) | Compose project base name. Takes priority over the git |
| (from | In-container workspace folder for the linked-worktree |
Project naming
The Compose project name is derived automatically, with no hardcoded project name. First a base shared by all worktrees of one repo:
base = slug($DEVCONTAINER_PROJECT_PREFIX) if set (explicit config wins)
| slug("<org>/<repo>") from `git -C <wf> remote get-url origin`
| error if neither is available → the tool fails
slug(s) = lowercase(s), "/" → "-", keep only [a-z0-9_-]Then the worktree:
project(wf) = base # main checkout
= base + "-" + slug(basename(wf)) # linked worktreeorigin git@github.com:acme/widgets.git → base acme-widgets, so the main
checkout is acme-widgets and a linked worktree feature-x is
acme-widgets-feature-x. Main vs linked is detected from git itself
(--git-dir vs --git-common-dir), not from the folder name.
Git decoupling (auto-derived, no global GIT_* env)
Lifecycle tools/commands that start or recreate a container (up, rebuild,
stop, down — tool and the WorktreeRemove CLI) inject a small environment,
computed purely from git in the workspace folder (argv-only, no shell). The
user and the compose file never set these by hand; they work identically for a
main checkout and a linked worktree. The MCP merges them into a copy of
os.environ for the child devcontainer / docker compose process.
COMPOSE_PROJECT_NAME=project(wf)(above).HOST_GIT_COMMON_DIR=realpath(git -C <wf> rev-parse --git-common-dir)— host abs path of the shared.git(the bind-mount source for/home/app/git/common).
No GIT_DIR / GIT_COMMON_DIR / GIT_WORK_TREE is ever exported. A global
env var can't be scoped to "only the project worktree", so it leaks into git
invocations for dependency repos — e.g. mix deps.get clones a git dep and
runs git --git-dir=.git config …; a global GIT_COMMON_DIR redirects that
write to the shared, read-only common config →
error: could not write config file … Device or resource busy. Instead the
project's git is decoupled by overlaying its .git via bind mounts (no
env), done by up / rebuild:
Main checkout —
.gitis a real directory arriving via the workspace mount; plain git already works. Nothing git-specific is added.Linked worktree —
.gitis a file containinggitdir: <HOST abs path>(absent in the container).up/rebuildstage two tiny host files under<wf>/.devcontainer/.gitwiring/(gitignored; created beforedevcontainer upso the mount sources exist) and pass them as container-onlydevcontainer up --mount type=bind,…flags — never rewriting the host's shared git files:dotgit=gitdir: /home/app/git/common/<rel>→ mounted over<container-workspace>/.git. git resolves the gitdir under the shared common mount, whosecommondir(../..) lands at/home/app/git/common, and infers the work-tree as the dir holding.git.reverse_gitdir=<container-workspace>/.git→ mounted over/home/app/git/common/<rel>/gitdirso git's worktree↔gitdir consistency check passes in the container.
<rel> is the gitdir's path relative to the common dir (. for main,
worktrees/<name> for a worktree). The in-container workspace folder
(<container-workspace>, e.g. /home/app/kimelixir) is resolved from
$DEVCONTAINER_CONTAINER_WORKSPACE_FOLDER if set, else from devcontainer read-configuration --workspace-folder <wf> (its workspaceFolder). For a
linked worktree that can't be resolved, up / rebuild fail with a clear
message rather than silently starting a container with broken project git.
Consuming repos: add
.devcontainer/.gitwiring/to.gitignore.
If wf isn't a git repo (or git is missing) HOST_GIT_COMMON_DIR is omitted
and compose falls back to its default mount source.
The compose file mounts the shared common dir and sets no GIT_* env:
volumes:
- ${HOST_GIT_COMMON_DIR:-../.git}:/home/app/git/common
- ${HOST_GIT_COMMON_DIR:-../.git}/hooks:/home/app/git/common/hooks:ro
- ${HOST_GIT_COMMON_DIR:-../.git}/config:/home/app/git/common/config:roSSH agent forwarding (Colima)
Symptom — git-over-SSH fails inside the container even though it works in a
plain devcontainer exec bash on the host:
Could not open a connection to your authentication agent.
git@github.com: Permission denied (publickey).mix deps.get, bundle, go get, etc. that pull a private dep over
git@github.com: then fail with Could not read from remote repository.
Why. The MCP bash tool runs devcontainer exec … bash -lc from the
long-lived MCP server process, whose environment is a snapshot taken when
Claude Code launched it — detached from your interactive login shell. So unlike
your terminal, it usually has no SSH_AUTH_SOCK, and the devcontainer CLI
has no agent to forward. Note the bash tool only forwards env vars you pass
explicitly via env (as --remote-env); it never forwards SSH_AUTH_SOCK from
the host, and forwarding one variable is useless unless the agent socket is also
mounted into the container.
With Colima this is worse: the docker daemon runs inside a Lima VM, and the
macOS agent socket (/private/tmp/com.apple.launchd.*/Listeners) is not a
path that exists inside the VM, so neither the CLI's auto-forward nor a bind
mount of that path can reach it.
Fix — forward the agent at the Colima layer and mount the forwarded socket in
compose (this sidesteps the MCP-env problem entirely, since the mount is wired
at container-create time, not via the MCP's SSH_AUTH_SOCK):
Start Colima with agent forwarding (documented flag is
--ssh-agent; there is no-sshort form). This exposes the agent in the VM at the Docker-Desktop-compatible path/run/host-services/ssh-auth.sock:colima stop colima start --ssh-agentOn the Mac, make sure the key is loaded:
ssh-add -l(add it withssh-add ~/.ssh/<key>if empty), thenssh -T git@github.comshould greet you rather than deny publickey.In the project's
.devcontainer/docker-compose.yml, mount the forwarded socket and pointSSH_AUTH_SOCKat it (service name varies per project):environment: SSH_AUTH_SOCK: /run/host-services/ssh-auth.sock volumes: - /run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sockRecreate the container so the mount applies —
rebuild(ordownthenup). A plainbash/execis not enough: mounts are established only at container creation.
Verify inside the container with ssh -T git@github.com (not ssh-add -l —
ssh-add may be absent, and the agent itself lives on the Mac).
Caveats:
Restart ordering (a known Lima/virtiofs issue): if you
colima stopwhile containers using the agent are still up, the forward can break after the nextcolima start. Stop those containers first, thencolima stop→colima start --ssh-agent.Headless / CI alternative — skip SSH entirely by rewriting the remote to an HTTPS token URL inside the container:
git config --global url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf "git@github.com:"Cleaner for CI; for local dev the agent forward above keeps the key off disk.
Tools
All tools accept an optional workspaceFolder (host path to the worktree
holding the .devcontainer config); it defaults to
$DEVCONTAINER_WORKSPACE_FOLDER or the server's working directory.
Every tool returns the underlying command's combined stdout/stderr (the
bash/lifecycle tools also append the exit code on failure), with isError
set on a non-zero exit or timeout.
Output is returned lazily: by default bash, up and rebuild send back
only a head+tail preview of the first/last maxLines lines (default
200); the full output is retained and any line range can be re-read with the
bash_output tool, or the whole thing fetched in one call with
maxLines: 0. Whatever is returned is also char-capped to ~30 000 chars as a
final safety net. A preview is not the full output — see
Guidance for consuming agents.
bash
Runs a command inside the container via
devcontainer exec --workspace-folder <wf> [--remote-env K=V ...] bash -lc "<command>".
It does not start the container — call up first.
Field | Type | Required | Default | Notes |
| string | ✅ | — | Run via |
| string | workspace root | Working dir inside the container: | |
| object | — | Forwarded as repeated | |
| integer (ms) |
| On timeout the process group is killed; result flagged as error. | |
| integer |
| Max lines in the returned head+tail preview. | |
| string | — | Logged for observability; otherwise unused. | |
| string | cwd / env var | Host path holding the |
bash_output
Reads any line range from the full output of an earlier bash/up/rebuild
run that was truncated to a preview. Outputs are retained per session (most
recent runs kept); an evicted/unknown id returns an error (just re-run).
Field | Type | Required | Default | Notes |
| string | ✅ | — | The id reported by a truncated run (e.g. |
| integer |
| 1-based start line. | |
| integer |
| Number of lines to return. |
up
devcontainer up --workspace-folder <wf> (plus the COMPOSE_PROJECT_NAME /
HOST_GIT_COMMON_DIR env above, and — for a linked worktree — the .git
overlay --mount flags from Git decoupling).
Starts and, if needed, creates the container. Call once for a fresh worktree
before bash.
Field | Type | Required | Default | Notes |
| boolean |
|
| |
| integer |
| Max lines in the returned preview. |
rebuild
devcontainer up --workspace-folder <wf> --remove-existing-container [--build-no-cache].
Recreates the container (which triggers an image build); use after changing the
Dockerfile / devcontainer config. Two strategies:
Field | Type | Required | Default | Notes |
| boolean |
|
| |
| boolean |
|
| |
| integer |
| Max lines in the returned preview. |
Note:
--buildis adocker composeflag, not adevcontainer upflag — passing it errorsUnknown argument: build.
stop
docker compose --project-directory <wf>/.devcontainer -p <project> stop. Stops
the container but keeps it and its named volumes for a fast resume (up).
down
docker compose -p <project> down [-v]. Full teardown of containers and
networks. Label-based: <project> is read from the container whose
working_dir label points at workspaceFolder (not recomputed from the path),
so it also tears down orphans whose worktree dir is already gone. A workspace
with no matching container is a no-op.
Field | Type | Required | Default | Notes |
| boolean |
| Also remove named volumes ( |
list_containers
Lists this repo's devcontainer containers — those whose compose project is the
repo base or base-<worktree> (see Project naming). Takes no arguments and
returns a JSON array of:
{
"name": "acme-widgets-feature-x-app-1",
"project": "acme-widgets-feature-x",
"status": "Up 2 hours",
"workspaceFolder": "/home/me/work/feature-x", // working_dir label, "/.devcontainer" stripped
"orphan": true // worktree root no longer exists on disk
}Deleting orphan devcontainers composes from the primitives — no separate
prune tool: call list_containers, then down (by workspaceFolder, with
removeVolumes: true to also drop volumes) on every entry with
"orphan": true. Teardown is label-based — it reads the container's real
compose project rather than recomputing it from the path — so down works on
orphans whose worktree dir is already gone, same as the WorktreeRemove CLI
below.
Guidance for consuming agents
The tool descriptions already teach this, but if a project wants to reinforce it
for the agents working in that repo, drop a section like the following into the
consuming project's CLAUDE.md (this is the repo reached through the
bash tool — e.g. your app, not this server). Tool descriptions reach every
consumer automatically; a CLAUDE.md only helps the one repo that has it, so
treat this as belt-and-suspenders.
## Running commands in the devcontainer (MCP `devcontainer` tools)
All shell work runs **inside the devcontainer** via `mcp__devcontainer__bash`,
not on the host. Call `mcp__devcontainer__up` once per fresh worktree before the
first `bash` call.
**`bash` returns a PREVIEW for large output, not the full thing.** When output
exceeds `maxLines` (default 200) only a head+tail slice comes back; the middle
is omitted and the result carries a `bash_execution_id`. Do not act on a
truncated preview as if it were the complete result.
To get the full output:
- **Need everything** (reading a file, parsing a command's full result,
inspecting complete test/migration/build logs) → pass `maxLines: 0` on the
`bash` call.
- **Need only part** of a long result → make a separate
`mcp__devcontainer__bash_output` call with the reported `bash_execution_id`,
paging via `offset`/`limit`.
Setting up test databases is full-output work — run it with `maxLines: 0`:
bash({ command: "mix ecto.create && mix ecto.migrate", maxLines: 0 })WorktreeRemove hook (CLI mode)
devcontainer-mcp down (with the Claude Code hook JSON on stdin) tears down
the container that belonged to the removed worktree. Cleanup is label-based:
it asks docker for the container(s) whose compose working_dir points at
worktree_path, reads their real com.docker.compose.project label, and runs
docker compose -p <project> down. It never recomputes the project name from
the path, so it keeps working when invoked from the uvx cache after the
worktree directory (and its compose file) has been deleted — and regardless of
the naming scheme. No matching container, bad input, or docker being
unavailable are no-ops (exit 0); only a real docker compose failure exits
non-zero.
Pass -v (--volumes / --remove-volumes) to tear down with docker compose down -v, removing the named volumes as well. Use it in the hook — once a
worktree is removed there is no path left to reach its volumes, so without -v
they leak. This makes the hook a complete teardown (containers + networks +
volumes); the matching git cleanup (git worktree remove, git branch -D) is
already handled by Claude Code's own worktree removal that fires this hook.
Register it in your Claude Code settings:
{
"hooks": {
"WorktreeRemove": [
{
"hooks": [
{
"type": "command",
"command": "uvx --from git+https://github.com/vovayartsev-dice/devcontainer-mcp devcontainer-mcp down -v"
}
]
}
]
}
}Security / scope
All subprocesses are launched argv-only (never
shell=True, never a hostsh -c). The only shell that interprets a command string isbashinside the container, so host-side shell injection isn't possible.The container is the blast radius — this server mounts no Docker socket into the container and runs nothing
--privileged.No background execution / streaming, and no auto-
upfrombash.
Development
uv run devcontainer-mcp # run the server from a checkout
uv run devcontainer-mcp down # run the WorktreeRemove CLI (reads JSON on stdin)
uv run devcontainer-mcp down -v # ...and also remove the named volumes
uv build # build wheel + sdistMaintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Tools
- bashA
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/vovayartsev-dice/devcontainer-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server