CodeServer MCP
by yangkerrrr
README.md
# CodeServer MCP
A production-oriented Model Context Protocol server for code-server, built around **stateful development sessions** rather than stateless file operations.
## Architecture
### Core Modules
**`workspace/`** — Sandboxed file operations scoped to `WORKSPACE_ROOT`
- `read.py` — Read files with optional line-range slicing
- `write.py` — Atomic file writes (create/overwrite)
- `patch.py` — Targeted edits via unified diff or find/replace (never resend the whole file)
- `search.py` — Ripgrep-based search with structured results
- `tree.py` — Recursive directory listing (respects `.gitignore`-style ignore rules)
- `watch.py` — Async file watching; detect changes made by the editor, git, build tools
**`terminal/`** — Persistent shells & background processes
- `pty.py` — Real pseudo-terminal wrapper (ptyprocess) for authentic terminal behavior (colors, pagers, history)
- `shell.py` — Persistent shell sessions that survive crashes/restarts
- `process.py` — Background process manager with log capture
**`util/`**
- `paths.py` — Workspace sandboxing: every file operation goes through `resolve_path()` which proves the result still lives inside `WORKSPACE_ROOT`
- `diff.py` — Unified diff helpers for generating and applying patches
### Key Design Decisions
1. **Real PTYs, not subprocesses**
- Shells are real pseudo-terminals (ptyprocess), so programs that check `isatty()` behave naturally.
- Output includes ANSI colors, spinner sequences, and pager control codes.
- Shell history and readline state persist across calls.
2. **Stateful vs. Stateless**
- Unlike generic filesystem MCP servers, shells and processes are first-class, long-lived entities.
- A shell can run `npm run dev`, and the dev server keeps running. Later calls can read its output, resize its terminal, or send it signals.
- Session recovery on restart: PTY metadata is stored in SQLite so crashed dev servers can be reconnected.
3. **Sandboxing**
- Every workspace operation (read/write/patch/search/tree/watch) goes through `util.paths.resolve_path()`.
- All paths are canonicalized and checked to be within `WORKSPACE_ROOT` — symlinks cannot escape.
4. **Patching, not Overwriting**
- `replace_text()` and `apply_patch()` let Claude make surgical edits without resending entire files.
- Diffs are generated and returned so clients (Claude) can see what changed.
5. **Async-first**
- All I/O is async (asyncio, aiofiles, aiosqlite).
- Long-lived watches and process log tailing don't block.
## Installation
### Docker (Recommended)
```bash
docker-compose up -d
# MCP server now listens on localhost:8080
```
### Local (Python 3.12+)
```bash
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
export WORKSPACE_ROOT=/path/to/code
export MCP_PORT=8080
python app.py
```
## Environment Variables
- `MCP_HOST` (default: `0.0.0.0`) — Listen address
- `MCP_PORT` (default: `8080`) — Listen port
- `WORKSPACE_ROOT` (default: `/workspace`) — Sandbox root; all file operations must stay within this
- `RG_BIN` (default: `rg`) — Path to ripgrep binary if not on PATH
- `LOG_LEVEL` (default: `info`) — Uvicorn log level
## MCP Tools
### Workspace
- `workspace_read_file(path, start_line, end_line)` — Read a file (with optional line range)
- `workspace_write_file(path, content, mode, create_dirs)` — Write or create a file
- `workspace_replace_text(path, old, new, expected_count)` — Find and replace (safe, requires uniqueness)
- `workspace_apply_patch(path, diff_text)` — Apply a unified diff
- `workspace_search(pattern, path, glob, case_sensitive, fixed_string, max_results)` — Ripgrep-based search
- `workspace_tree(path, max_depth, max_entries)` — Directory listing
- `workspace_watch_start(path)` — Start watching a directory for changes
- `workspace_watch_poll(watch_id, timeout)` — Poll a watch for events
- `workspace_watch_stop(watch_id)` — Stop a watch
- `workspace_watch_list()` — List active watches
### Terminal
- `terminal_shell_create(cwd)` — Create a new persistent PTY shell
- `terminal_shell_execute(shell_id, command, timeout)` — Run a command in a shell
- `terminal_shell_read(shell_id)` — Read pending output (non-blocking)
- `terminal_shell_resize(shell_id, rows, cols)` — Resize the terminal
- `terminal_shell_list()` — List active shells
- `terminal_shell_terminate(shell_id)` — Kill a shell
- `terminal_process_start(command, cwd, proc_id)` — Start a background process
- `terminal_process_logs(proc_id, lines)` — Read process logs
- `terminal_process_stop(proc_id, timeout)` — Stop a process
- `terminal_process_list()` — List active processes
## Usage Examples
### Create a Persistent Dev Server Shell
```python
# Create a shell
shell_resp = await terminal_shell_create(cwd=".")
shell_id = shell_resp["id"] # "shell-abc123"
# Start a dev server (runs in background)
await terminal_shell_execute(shell_id, "npm run dev")
# Read output later
output = await terminal_shell_read(shell_id)
print(output["output"]) # "VITE ready in 314ms..."
# Even if the MCP server crashes, the shell survives.
# On restart, terminal_shell_list() will still see it.
```
### Edit a File Without Resending It
```python
# Read a file
file_resp = workspace_read_file("src/app.py")
before = file_resp["content"]
# Client (or Claude) modifies it locally
after = before.replace("const x = 1", "const x = 2")
# Send only the diff
diff = generate_unified_diff(before, after, "src/app.py")
patch_resp = await workspace_apply_patch("src/app.py", diff)
print(patch_resp["diff"]) # Shows what changed
```
### Watch for Changes
```python
# Start watching the src/ directory
watch_resp = await workspace_watch_start("src")
watch_id = watch_resp["id"]
# Do work (edit files, run git pull, etc.)
await asyncio.sleep(5)
# Poll for changes
poll_resp = await workspace_watch_poll(watch_id, timeout=1)
for event in poll_resp["events"]:
print(event["change"], event["path"]) # "modified src/main.py"
```
## Reconnection & Session Recovery
When the MCP server restarts:
1. **Shells**: Their PTY metadata is loaded from the database and re-spawned. Background processes (like `npm run dev`) will still be running on the system; reconnecting to the shell picks up where you left off.
2. **Processes**: Background processes are re-attached to if they're still alive (by PID lookup).
3. **Watches**: Not persisted (ephemeral); will need to be recreated.
This design assumes you're running this in a long-lived container (Docker or systemd) and not losing the PID space.
## Security
- **Workspace sandboxing**: `resolve_path()` ensures all operations stay within `WORKSPACE_ROOT`. Symlinks are resolved and validated.
- **No command injection**: Process commands are passed as strings to `subprocess.Popen(..., shell=True)`, so be careful with user input. Consider restricting this tool in production.
- **No authentication**: This server is designed for a trusted network (your local machine, or behind a VPN/Tailscale). Run it behind a reverse proxy with auth in production.
## Performance
- **First-call startup**: ~50ms (database init, shell spawn)
- **Shell execute**: 5–100ms depending on command
- **File operations**: <5ms (mostly I/O latency, not CPU)
- **Search**: 50–500ms depending on repo size and pattern complexity
- **Watch poll**: 0ms if no changes, else <50ms to report changes
## Testing
Run the workspace module smoke test:
```bash
export WORKSPACE_ROOT=/tmp/fake_workspace
python test_workspace_manual.py
```
This tests sandboxing, file I/O, patching, searching, tree walking, and watching.
## TODO / Future
- [ ] LSP diagnostics integration (pull errors from code-server's language servers)
- [ ] VS Code Tasks runner
- [ ] Port detection (surface forwarded ports from code-server)
- [ ] Editor state (which files are open, cursor position)
- [ ] Git wrappers (optional; can be used via shells)
- [ ] Docker wrappers (optional; can be used via shells)
- [ ] More comprehensive logging and telemetry
- [ ] Pytest suite (currently only manual smoke tests)
## Contributing
This is a single-developer project. If you'd like to extend it:
1. Add new tools in the appropriate module (`workspace/`, `terminal/`, or a new one).
2. Register them in `app.py` with `@mcp.tool()`.
3. Update this README.
---
Built for Claude on code-server. Not affiliated with Anthropic or Coder.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues