Skip to main content
Glama
README.md
# shell-mcp

Remote shell + file operations MCP server. Core is pure stdlib (fully unit-testable); only `server.py` depends on `mcp` / `starlette` / `uvicorn`.

## Features

- **Streamable HTTP** at `/mcp` (POST). No long-lived connections, safe behind buffered reverse proxies.
- **Bearer token auth** (constant-time compare) + optional IP allowlist + `/health` endpoint.
- **Two execution modes**: restricted (command allowlist, no shell operators) / unrestricted (full shell). Windows: unrestricted mode uses `cmd.exe` or PowerShell; POSIX: `$SHELL -lc`.
- **Reliable process execution**: stdout+stderr merged in real order; output streamed to temp file (bounded memory, no pipe deadlock); children in their own process group; timeout kills the **entire process tree** (POSIX `killpg` / Windows `taskkill /T`).
- **Background tasks**: `run_command(background="always")` for dev servers / builds; `task(action="wait"|"output"|"cancel"|"list")` to manage.
- **Sync-first by default**: `run_command` waits up to `SHELL_MCP_WAIT_SECONDS` (30s) then auto-backgrounds. Short commands return results inline.
- **Safe editing**: `edit_file` exact replacement + atomic write + unified diff; `apply_patch` multi-file atomic; `expected_sha256` precondition prevents stale-read conflicts (returns `FILE_CHANGED` without touching files).
- **Code search**: `grep` (ripgrep preferred, pure-Python fallback) + `glob` (sort by path or modified time).
- **Encoding consistency**: auto-detect (UTF-8 / Windows code pages / GB18030), preserve original encoding on write.
- **Output governance**: line+byte caps, tail truncation, full output spilled to OS temp dir (`shell-mcp-spool/`), auto-pruned.
- **Machine-readable errors**: all failures return `code` (e.g. `OLD_TEXT_NOT_FOUND`, `FILE_NOT_FOUND`) + structured fields.
- **Audit log**: JSON lines, auto-rotated.
- **Git inspection**: `git_log`/`diff`/`blame`/`show`/`status` — all read-only, gated behind `SHELL_MCP_EXPOSE_GIT=true`.
- **Pi bridge**: dispatch prompts to the [pi coding agent](https://github.com/earendil-works/pi) with session continuity. Gated behind `SHELL_MCP_PI_ENABLED=true`.
- **Cross-platform**: Windows / macOS / Linux. Platform-specific code gated behind `os.name == "nt"`.

## Quick Start

```bash
pip install -r requirements.txt
cp .env.example .env   # fill SHELL_MCP_TOKEN (openssl rand -hex 32)
# macOS / Linux:
./run.sh
# Windows:
run.bat
```

Client config (Streamable HTTP):

```json
{
  "url": "http://127.0.0.1:8000/mcp",
  "headers": { "Authorization": "Bearer <SHELL_MCP_TOKEN>" }
}
```

> Print a ready-to-paste client snippet (with real token + auto-detected Tailscale URL):
>
> ```bash
> python -m shell_mcp.server --print-client
> ```

## Real-World Stack: Notion + shell-mcp + pi + Tailscale

This project is commonly used as the backend of a **four-layer AI coding stack**:

```
┌─────────────────────────────────────┐
│  Notion                             │  ← You write prompts here
│  (MCP connector → AI agent)         │
├─────────────────────────────────────┤
│  shell-mcp (this server)            │  ← Executes commands, reads/writes files
├─────────────────────────────────────┤
│  pi (coding agent, optional)        │  ← Handles complex multi-step tasks
├─────────────────────────────────────┤
│  Tailscale (secure tunnel)          │  ← Connects everything remotely
└─────────────────────────────────────┘
```

### How it works

1. **Start shell-mcp** on your development machine (Windows/macOS/Linux).
2. **Expose via Tailscale** (`tailscale serve --bg 8000`). The server stays on `127.0.0.1` — never exposed to the public internet. Tailscale handles TLS and authentication.
3. **Configure Notion's MCP connector** to point at your Tailscale URL (`https://<machine>.<tailnet>.ts.net/mcp`). Notion passes your prompts to an AI model (e.g. Claude), which calls shell-mcp's tools to read files, run commands, search code, and make edits — all on your machine.
4. **Enable pi bridge** (optional, `SHELL_MCP_PI_ENABLED=true`) for complex coding tasks. When the AI at the Notion layer decides a task is too large for one-shot tools, it dispatches the prompt to pi via `pi_run`. Pi handles multi-step reasoning, file editing, and debugging autonomously, and returns the result.

### Why this works

| Layer | Role | Why it matters |
|-------|------|----------------|
| Notion | Prompt interface | Your existing Notion workspace becomes an AI coding environment. No separate chat app. |
| shell-mcp | Execution runtime | File read/write, shell commands, code search, git inspection — all the tools an AI needs to work on a real codebase. |
| pi | Autonomous agent | Handles complex multi-step tasks that require planning, iteration, and debugging. Session continuity across calls. |
| Tailscale | Secure transport | Zero-config VPN. No open ports, no public IPs, no firewall rules. Works across NAT, on any network. |

### Quick setup

```bash
# 1. Server (your dev machine)
git clone https://github.com/takereshui/shell-mcp.git
cd shell-mcp
pip install -r requirements.txt
cp .env.example .env
# Edit .env: set SHELL_MCP_TOKEN, optionally SHELL_MCP_PI_ENABLED=true

# 2. Start
./run.sh  # or run.bat on Windows

# 3. Expose via Tailscale
tailscale serve --bg 8000

# 4. Print client config (includes Tailscale URL + token)
python -m shell_mcp.server --print-client

# 5. Copy the JSON into Notion's MCP connector settings
```

That's it. Your Notion workspace can now read, edit, test, and debug code on your machine — remotely, securely, with AI assistance at every step.

## Tools

Default surface (13 tools):

| Tool | Description |
|------|-------------|
| `run_command` | Execute a shell command. `background="auto"` (default): sync-first, auto-backgrounds after `wait_seconds`. Optional `cwd`, `timeout`, `detail` level. |
| `task` | Unified background task lifecycle. `action`: `wait` / `output` / `cancel` / `list`. |
| `grep` | Regex search. Structured results: `[{path, line, text}]`. Ripgrep preferred, pure-Python fallback. |
| `glob` | File pattern matching. Sort by `path` (stable) or `modified` (newest first). |
| `list_dir` | Directory listing with optional `depth` recursion. |
| `read_file` | Read a text file with paging. Returns `sha256` + `next_start_line`. Images (png/jpeg/gif/webp) returned as image content. |
| `read_files` | Batch read up to 20 files. Partial failures don't block other files. |
| `write_file` | Atomic write. Optional `expected_sha256` precondition. `append` mode supported. |
| `edit_file` | Exact string replacement with `{old, new, replace_all?}`. Returns unified diff. |
| `apply_patch` | Multi-file patch (`*** Begin Patch` envelope): add / update / delete / move. All-or-nothing. |
| `set_workdir` | Change working directory (convenience; prefer per-call `cwd`). |
| `outline` | Extract code symbols per language (regex-based, shebang-aware). |
| `review_file` | One-shot audit: read file + recent commits + diff (parallel via anyio). |

Low-level tools (`task_output`, `kill_task`, `list_tasks`, `delete_file`, `make_dir`, `rename_file`) hidden by default; enable with `SHELL_MCP_EXPOSE_LOWLEVEL=true`.

**Gated tools:**

| Gate | Tools |
|------|-------|
| `SHELL_MCP_EXPOSE_GIT=true` | `git_log` — commit log with path filter, `git_diff` — working tree/staged diff, `git_blame` — line annotations, `git_show` — commit details, `git_status` — working tree status |
| `SHELL_MCP_PI_ENABLED=true` | `pi_run` — dispatch prompt to pi (sync-first, auto-session), `pi_status` — check running task, `pi_cancel` — kill running task |

## Configuration

All via environment variables (see `.env.example` for full list):

| Variable | Default | Description |
|----------|---------|-------------|
| `SHELL_MCP_TOKEN` | *(required)* | Bearer token for auth |
| `SHELL_MCP_HOST` | `127.0.0.1` | Bind address |
| `SHELL_MCP_PORT` | `8000` | Bind port |
| `SHELL_MCP_WORKDIR` | `./default` | Working directory |
| `SHELL_MCP_UNRESTRICTED` | `false` | Full shell vs. allowlist |
| `SHELL_MCP_READONLY` | `true` | Disable write/edit/patch tools |
| `SHELL_MCP_TIMEOUT` | `120` | Default command timeout (seconds) |
| `SHELL_MCP_WAIT_SECONDS` | `30` | Sync-first wait before auto-background |
| `SHELL_MCP_ALLOWLIST` | *(built-in)* | Comma-separated command list (restricted mode) |
| `SHELL_MCP_ALLOWED_IPS` | *(empty)* | Comma-separated IP allowlist |
| `SHELL_MCP_ENCODING` | `auto` | Output/file encoding |
| `SHELL_MCP_LOG` | `shell-mcp-audit.log` | Audit log path |
| `SHELL_MCP_MAX_OUTPUT` | `51200` | Max bytes returned per call |
| `SHELL_MCP_MAX_LINES` | `2000` | Max lines returned per call |
| `SHELL_MCP_MAX_CAPTURE` | `5242880` | Hard cap on in-memory output |
| `SHELL_MCP_SPOOL_MAX_FILES` | `50` | Max spilled output files kept |
| `SHELL_MCP_EXPOSE_LOWLEVEL` | `false` | Expose low-level task/file tools |
| `SHELL_MCP_EXPOSE_GIT` | `false` | Expose git inspection tools |
| `SHELL_MCP_PI_ENABLED` | `false` | Enable pi coding agent bridge |
| `SHELL_MCP_PI_COMMAND` | `pi` | Pi binary/command path |
| `SHELL_MCP_PI_ARGS` | `""` | Extra arguments to pi |
| `SHELL_MCP_PI_WAIT_SECONDS` | `120` | Default wait for pi_run |

## Exposing via Tailscale

Keep `SHELL_MCP_HOST=127.0.0.1` (never bind to public interfaces). Let tailscaled handle TLS:

```bash
# Tailnet-only (auto-HTTPS):
tailscale serve --bg 8000

# Public internet (Funnel; requires Funnel enabled in admin console):
tailscale funnel --bg 8000

# Check status / stop:
tailscale serve status
tailscale funnel --bg off 8000
```

Client URL: `https://<machine>.<tailnet>.ts.net/mcp`

**Security notes:**
- Funnel = anyone on the internet can reach this port. Use a long random token (`openssl rand -hex 32`). Keep `SHELL_MCP_UNRESTRICTED=false`.
- Tailscale serve forwards real client IPs (`100.x.y.z`). Pin them with `SHELL_MCP_ALLOWED_IPS` for token+IP dual defense (tailnet only; don't use with Funnel).
- Funnel only supports ports 443/8443/10000 for HTTPS. TLS terminated by Tailscale.
- Audit log records commands and paths. Restrict file permissions on the log.

## Architecture

```
shell_mcp/
├── config.py          # env → Config; SessionState
├── errors.py          # ToolFailure → MCP isError
├── paths.py           # Path resolution + sandbox enforcement
├── encoding.py        # Auto-detect encoding (UTF-8 / Windows / GB18030)
├── output.py          # Line+byte truncation, spool spill, pruning
├── process.py         # Command parse/execute: merged output, file capture, tree kill
├── tasks.py           # Background task registry
├── mutation_queue.py  # Per-path serialized write queue
├── editing.py         # Transactional edits: validate → atomic write → diff
├── audit.py           # JSON lines audit log
├── tools/
│   ├── shell.py       # run_command / task
│   ├── files.py       # File tools (read/write/edit/patch)
│   ├── search.py      # grep / glob
│   ├── audit.py       # outline / review_file / git_* tools
│   └── pi.py          # pi_run/status/cancel (gated)
└── server.py          # FastMCP + auth middleware (only third-party deps)
```

## spool:// Resources

Truncated command output and background task logs are served as `spool://<name>` MCP resources. Tool results include `full_output_resource` / `log_resource` fields. Files live in the OS temp directory (`shell-mcp-spool/`) and are auto-pruned.

## Design: Stale-Read Protection

```
read_file(path) → {text, sha256}
write_file(path, content, expected_sha256=<from read>)
→ server re-reads file, computes sha256(disk)
→ match? write. mismatch? FILE_CHANGED (file untouched)
```

No server-side cache. The SHA256 is computed from disk at read time and re-verified at write time. This is optimistic concurrency (ETag/If-Match pattern), not a cache.

## Tests

Core is dependency-free; run directly:

```bash
python3 -m unittest discover -s tests -v
```

## License

MIT — see [LICENSE](./LICENSE).