From shelpa to filesystem: file operations and recovery
A Rust MCP server redesigned as filesystem with 14 tools. Trash, undo/redo, and tracking records provide recovery from worker mistakes during AI system development.
filesystem: file operations and recovery
I rebuilt the local-LLM MCP server shelpa as filesystem. I removed the pipeline engine and added undo/redo, trash, and file tracking.
The previous design is in Virtual Pipeline Architecture and Sandbox Design and Lessons Learned.
Why I removed pipeline execution
shelpa offered shelpa_pipe for pipelines and shelpa_write for direct writes. Its grammar allowed only approved commands, and writes left mirror copies in .shelpa/.
Pipeline execution had limited compatibility with LLMs. I redesigned filesystem around file operations and recovery from mistakes.
Reused implementation
I kept path validation and workspace restrictions, added missing features, and assembled a prototype.
I removed parser.rs, executor.rs, types.rs, and their tests, along with the regex, os_pipe, shell-words, and thiserror dependencies.
A Rust MCP filesystem API
I replaced the pipeline engine with standard file operations based on src/filesystem in the official modelcontextprotocol/servers repository.
I reimplemented the TypeScript reference in Rust. The old mirror-recording approach became a .filesystem/ history with trash, version records, and file tracking.
8 basic, 4 delete/restore, and 2 history tools
There are 14 tools in total.
Basic File Operations
| Tool | Function |
|---|---|
read_text_file | Read files with optional head/tail for partial reads |
create_file | Create new file; errors if file already exists |
write_file | Create or overwrite |
edit_file | Search-and-replace via oldText/newText; diff output; dryRun support |
create_directory | Recursive directory creation |
list_directory | Listing with [FILE]/[DIR] prefixes |
directory_tree | Recursive JSON tree with exclude pattern support |
move_file | Move/rename; errors if destination exists |
Safe Delete and Restore
| Tool | Function |
|---|---|
delete_file | Moves file to .filesystem/.trash/ (never permanently deletes) |
list_trash | List trash contents |
restore_file | Restore from trash to original path |
search_file | Search for file status; tracks moves and deletes |
Version History (--backup mode only)
| Tool | Function |
|---|---|
undo_file | Restore to n versions back |
redo_file | Advance n versions forward after undo |
create_file, write_file, edit_file, and move_file record to .filesystem/. Moves append to .filesystem/.moves; deletions log to .filesystem/.deletes.
Two startup modes
filesystem supports --mcp for a single project and --server for a multi-project daemon. The CLI follows the same pattern as ctree and pathfinder.
filesystem --mcp [--root <ROOT>] # single project
filesystem --server # global daemon
| Aspect | --mcp | --server |
|---|---|---|
--root at startup | Optional (default: cwd) | Not allowed |
root in tool calls | Ignored | Required |
| Multi-project | No | Yes |
| Use case | CLI / standalone | agent-gateway daemon |
In --server mode, agent-gateway is expected to launch the persistent process and inject root into every call.
ServerConfig{
Name: "filesystem",
Command: "filesystem",
Args: []string{"--server"},
Scope: "global",
}
The first message determines the protocol. A leading { selects Line JSON; Content-Length: selects HTTP-style framing compatible with LSP.
.filesystem/ history
The simple mirror became a history system with recovery operations and tracking logs.
Structure in --backup Mode
.filesystem/
src/
main.rs/ # history for src/main.rs
00001 # version 1 (full content snapshot)
00002 # version 2
.head # current version pointer
.moves # append-only rename log (tab-separated)
.trash/
00001 # deleted file content
00001.history/ # deleted file's version history
.deletes # append-only delete/restore log
Each write appends a full snapshot. This is simpler and faster than storing diffs. .head tracks the current version: undo decrements it and redo increments it. A write after undo clears the redo stack.
move_file relocates the history directory and logs to .moves. delete_file moves the file and history to .trash/ and logs to .deletes. Trash IDs increase monotonically and are never reused.
Without --backup
Only .moves, .trash/, and .deletes are active. Version snapshots, .head, and undo/redo are disabled.
Path Safety and Sandboxing
I retained and strengthened shelpa’s workspace restrictions.
- Paths are normalized without filesystem access (collapsing
..and.). - Absolute paths must remain inside the workspace root or return
PathEscape. - Workspace-root symlinks resolve at startup, including macOS
/tmp→/private/tmp. - No shell execution, removing that command-injection path.
list_directory and directory_tree exclude .filesystem, .git, node_modules, target, __pycache__, and common build/cache directories by default. They also apply the root .gitignore.
Error Model
Errors return as MCP tool results with isError: true.
| Error Kind | Condition |
|---|---|
PathEscape | Path escapes the workspace root |
NotFound | File/directory not found; oldText not found in edit_file |
AlreadyExists | create_file target exists; move_file/restore_file destination exists |
IoError | Filesystem I/O failure |
InvalidArgument | Invalid parameter (e.g., both head and tail specified, undo exceeds history) |
Recovery for agent-gateway workers
The redesign followed the move of agent-gateway toward a custom agent system. AI system development needed recovery and verification when local-LLM workers deleted or damaged files. I reused shelpa’s workspace boundaries and mirror records in the trash and version-history design.
