Skip to main content
Glama
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.

Maintenance

ActivitySlowing
ResponsivenessNo issues