shell-mcp
by takereshui
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).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing