Skip to main content
Glama
kg912

shell-mcp

by kg912
README.md
# shell-mcp

A super-secure MCP (Model Context Protocol) server for running shell commands safely with a local LLM client.

## Security Model

Commands pass through **8 independent security layers** — every one must pass:

| Layer | What it does |
|-------|-------------|
| 1 | **Zod schema validation** — strict type checks before any processing |
| 2 | **No-shell spawn** — `execFile` with `shell: false`, no `/bin/sh -c "..."` ever |
| 3 | **Hardcoded blocklist** — `rm`, `sudo`, `dd`, `curl`, `brew`, `docker`, etc. permanently blocked |
| 4 | **Argument inspection** — rejects metacharacters, null bytes, suspicious patterns, per-command dangerous flags |
| 5 | **Directory allowlist** — every path must be inside `--allow-dir` |
| 6 | **Symlink resolution** — symlinks resolved before path check (no escape via symlink) |
| 7 | **Resource limits** — timeout, output size cap, concurrency limit |
| 8 | **Sanitized environment** — strips `AWS_*`, `*_TOKEN`, `*_SECRET`, all cloud provider secrets |

## Quick Start

```bash
# Clone and build
git clone https://github.com/your-username/shell-mcp.git
cd shell-mcp
npm install
npm run build

# Run (must specify at least one --allow-dir)
node dist/index.js --allow-dir ~/projects --allow-dir ~/data
```

## LLM Client Configuration

```json
{
  "shell-mcp": {
    "command": "node",
    "args": [
      "/path/to/shell-mcp/dist/index.js",
      "--allow-dir", "/home/user/projects",
      "--allow-dir", "/home/user/data",
      "--allow-cmd", "ls",
      "--allow-cmd", "cat",
      "--allow-cmd", "grep",
      "--allow-cmd", "find",
      "--allow-cmd", "python3",
      "--timeout", "30",
      "--log-file", "/home/user/.shell-mcp/audit.log"
    ]
  }
}
```

## Flags

| Flag | Description | Default |
|------|-------------|---------|
| `--allow-dir <path>` | Permitted directory (required, repeatable) | *none — server won't start* |
| `--allow-cmd <cmd>` | Whitelist specific commands (repeatable) | *all non-blocked* |
| `--block-cmd <cmd>` | Extra commands to block | — |
| `--timeout <secs>` | Max execution time (1–300) | 30 |
| `--max-output <bytes>` | Max output size | 1048576 (1MB) |
| `--max-concurrent <n>` | Max parallel commands (1–10) | 3 |
| `--log-file <path>` | Audit log file path | stderr only |
| `--enable-write` | Enable `write_file` tool | off |
| `--allow-overwrite` | Permit overwriting files | off |
| `--read-only-mode` | Disables all writes (overrides `--enable-write`) | off |
| `--allow-network-cmds` | Unblock curl, wget, nc | off |
| `--allow-env-passthrough` | Forward full env to child processes | off (sanitized) |

## Tools Exposed to the LLM

### `run_command`
Run a command with explicit arguments array. **No shell** — pipes and redirects won't work.

```json
{
  "command": "grep",
  "args": ["-r", "TODO", "/home/user/projects/myapp"],
  "cwd": "/home/user/projects/myapp",
  "timeout": 10
}
```

### `read_file`
Read a file's contents. Supports line ranges and binary (base64) output.

```json
{
  "path": "/home/user/projects/myapp/src/main.py",
  "startLine": 1,
  "endLine": 50
}
```

### `list_directory`
List a directory with tree view, sizes, and permissions.

```json
{
  "path": "/home/user/projects/myapp",
  "recursive": true
}
```

### `write_file` *(requires `--enable-write`)*
Write a file. Won't overwrite without `--allow-overwrite`.

```json
{
  "path": "/home/user/projects/myapp/notes.txt",
  "content": "Hello world"
}
```

### `get_server_info`
Returns current server config and restrictions. Useful for the LLM to understand its sandbox before acting.

## Permanently Blocked Commands

These **cannot** be enabled by any flag:

```
rm, rmdir, shred, dd, mkfs, fdisk, diskutil, sudo, su, doas,
osascript, defaults, open, launchctl, crontab,
curl, wget, nc, ssh, scp, rsync,
brew, npm, yarn, pip, gem, cargo, docker,
gcc, clang, kill, killall, shutdown, reboot,
gpg, security (macOS keychain), ...and more
```

## Audit Log

Every command attempt (allowed or denied) is logged in JSON:

```json
{"timestamp":"2024-01-01T12:00:00.000Z","sessionId":"A3F9B2C1","event":"COMMAND_DENY","command":"rm","args":["-rf","/"],"denyLayer":3,"denyReason":"Command \"rm\" is on the hardcoded blocklist"}
{"timestamp":"2024-01-01T12:00:01.000Z","sessionId":"A3F9B2C1","event":"COMMAND_ALLOW","command":"ls","args":["-la"],"cwd":"/home/user/projects","exitCode":0,"durationMs":12,"outputBytes":1024}
```

## Inspect with MCP Inspector

```bash
npx @modelcontextprotocol/inspector node dist/index.js --allow-dir /tmp/test
```