Описание
mcp-shell
MCP server that runs shell commands. Your LLM gets a tool; you get control over what runs and how.
Built on mark3labs/mcp-go. Written in Go.
Run it
Docker (easiest):
docker run -it --rm -v /tmp/mcp-workspace:/tmp/mcp-workspace sonirico/mcp-shell:latest
From source:
git clone https://github.com/sonirico/mcp-shell && cd mcp-shell
make install
mcp-shell
Configure it
Secure mode is the default. With no config file, mcp-shell boots in secure
mode and registers only typed tools: file reads, grep/glob, git inspection,
and (opt-in) file/git writes and operator-defined scripts. There is no raw
shell command. You only need a config file to change the defaults below. To
run fully unrestricted you must opt in explicitly:
MCP_SHELL_ALLOW_UNSAFE=1 mcp-shell # disables secure mode; the only tool is shell_exec
To customize the policy, point to a YAML config:
export MCP_SHELL_SEC_CONFIG_FILE=/path/to/security.yaml
mcp-shell
Secure mode (default) — typed tools only, every path confined to working_directory:
security:
enabled: true
working_directory: /tmp/mcp-workspace
max_execution_time: 30s
max_output_size: 1048576
run_as_user: ""
audit_log: true
# Expose file and git write tools (write_file, edit_file, mkdir, move,
# delete, git_add, git_commit, git_switch, git_restore, git_stash). Off by
# default.
writes_enabled: false
# Operator-defined scripts exposed through the run_script tool. The client
# picks a name; the argv is yours and cannot be altered.
# scripts:
# test: ["go", "test", "./..."]
# lint: ["golangci-lint", "run"]
Wire it up
Claude Desktop — add to your MCP config:
{
"mcpServers": {
"shell": {
"command": "docker",
"args": ["run", "--rm", "-i", "sonirico/mcp-shell:latest"],
"env": { "MCP_SHELL_LOG_LEVEL": "info" }
}
}
}
For custom config, mount the file and set the env:
{
"command": "docker",
"args": ["run", "--rm", "-i", "-v", "/path/to/security.yaml:/etc/mcp-shell/security.yaml", "-e", "MCP_SHELL_SEC_CONFIG_FILE=/etc/mcp-shell/security.yaml", "sonirico/mcp-shell:latest"]
}
Tools
Secure mode (the default) registers these typed tools. * marks a required
parameter.
| Tool | Parameters | Available |
|---|---|---|
read_file | path*, offset, limit, tail | always |
list_dir | path, depth, include_hidden | always |
glob | pattern*, path, newer_than, max_results | always |
grep | pattern*, path, glob, ignore_case, context, files_only, count, max_results | always |
stat | path* | always |
diff_files | path_a*, path_b* | always |
system_info | always | |
git_status | always | |
git_log | max_count, ref, path, author, grep, since, until, oneline, follow | always |
git_diff | ref, ref_to, staged, path, stat_only, name_only | always |
git_show | ref, path, stat_only | always |
git_blame | path*, ref, line_start, line_end | always |
git_branches | all, merged | always |
git_tags | pattern | always |
git_rev_parse | ref* | always |
git_ls_files | path, untracked | always |
git_stash_list | always | |
git_remotes | always | |
write_file | path*, content*, append | writes_enabled |
edit_file | path*, old_string*, new_string*, replace_all | writes_enabled |
mkdir | path* | writes_enabled |
move | from*, to* | writes_enabled |
delete | path*, recursive | writes_enabled |
git_add | paths, all | writes_enabled |
git_commit | message*, all | writes_enabled |
git_switch | branch*, create | writes_enabled |
git_restore | paths*, staged | writes_enabled |
git_stash | action*, message | writes_enabled |
run_script | name* | scripts |
Every path parameter is resolved against working_directory (symlinks
followed); anything outside it is rejected. Git paths and refs are passed
positionally and validated: a ref starting with - is rejected. There are no
network tools; push, fetch and clone are not offered.
Unrestricted mode: shell_exec exists only with MCP_SHELL_ALLOW_UNSAFE=1, runs the command through bash -c with no validation, by design, and it is the only tool registered in that mode.
Environment variables
| Variable | Description |
|---|---|
MCP_SHELL_SEC_CONFIG_FILE | Path to security YAML (overrides built-in secure defaults) |
MCP_SHELL_ALLOW_UNSAFE | Set 1 (or true) to disable secure mode and expose shell_exec instead of the typed tools (opt-in) |
MCP_SHELL_SERVER_NAME | Server name (default: "mcp-shell 🐚") |
MCP_SHELL_LOG_LEVEL | debug, info, warn, error, fatal |
MCP_SHELL_LOG_FORMAT | json, console |
MCP_SHELL_LOG_OUTPUT | stdout, stderr, file |
Development
make install dev-tools # deps + goimports, golines
make fmt test lint
make docker-build # build image locally
make release # binary + docker image
Security
- Default: Secure mode. The server builds every command's argv itself; the client never supplies a shell string. Only typed tools are registered.
- Path confinement: every path parameter is resolved against
working_directory, symlinks followed, and anything that resolves outside it is rejected. - Git hardening: paths are passed after
--, refs after--end-of-options, and a ref starting with-is rejected. Git runs withGIT_CONFIG_NOSYSTEM=1,GIT_CONFIG_GLOBAL=/dev/null,core.fsmonitor,core.pagerandcore.hooksPathneutralised, and--no-ext-diff --no-textconvon log/diff/show/blame. - Minimal environment: child processes get only
PATH,HOMEandLANG, never the server's own environment or.envsecrets. - Writes and scripts are opt-in:
writes_enabled: trueexposes the file/git write tools; a non-emptyscriptsmap exposesrun_script. Both are off by default. - Unrestricted: only via
MCP_SHELL_ALLOW_UNSAFE=1. The only tool registered isshell_exec, which runsbash -cwith no validation. Fine for local dev, dangerous otherwise. - Docker: Runs as non-root, Alpine-based. Use it in production. Best paired with an OS sandbox (read-only FS, dropped caps) as defense-in-depth.
Threat model, guarantees, and the scope for vulnerability reports live in SECURITY.md. Read it before opening an advisory.
Migrating from 0.x
Secure mode no longer validates a shell_exec command string; it exposes
typed tools instead. A config file's security: block no longer accepts:
| Removed key | Replacement |
|---|---|
use_shell_execution | not needed; typed tools never shell out |
allowed_executables | not needed; each tool runs a fixed, server-built argv |
allowed_commands | not needed; same as above |
blocked_commands | not needed; same as above |
blocked_patterns | not needed; same as above |
Loading a config file that still sets one of these fails at startup with an
error naming the key. There is no more "legacy mode" and no
security-legacy.yaml example. If you need raw shell access, set
MCP_SHELL_ALLOW_UNSAFE=1 to get shell_exec back; it is no longer
constrained by the security: block at all.
Contributing
Fork, branch, make fmt test, open a PR.