From shelpa to filesystem — Complete Redesign of a Rust MCP Filesystem Server
A record of renaming the local LLM agent MCP server shelpa to filesystem, removing the pipeline execution engine, and redesigning it into a full MCP filesystem server with undo/redo, trash, and file tracking
Overview
I rebuilt the MCP server “shelpa,” originally built for local LLM agents, into “filesystem.” Beyond just renaming, this was a complete overhaul: removing the pipeline execution engine entirely and redesigning it as an MCP filesystem server with undo/redo, trash, and file tracking capabilities.
For shelpa’s original design, see Virtual Pipeline Architecture and Sandbox Design and Lessons Learned. This article documents the design and implementation of filesystem, its successor.
The Problems with shelpa
shelpa provided two MCP tools — shelpa_pipe (pipeline execution) and shelpa_write (direct write) — enforcing a minimal grammar that only allowed permitted commands. All write operations saved mirror copies to the .shelpa/ directory for traceability.
However, the pipeline execution concept itself had limited compatibility with LLMs. In rebuilding this library as “filesystem,” I decided to rethink the architecture from the ground up, not just the name.
Extracting Reusable Code from shelpa into a Prototype
The first step was to keep only the reusable implementations from shelpa, fill in the missing pieces, assemble the whole into a working shape, and build a prototype.
I removed the pipeline-related modules (parser.rs, executor.rs, types.rs and their test files), keeping only the reusable core — path safety validation and workspace-scope restrictions. Dependencies were also slimmed down by removing regex, os_pipe, shell-words, and thiserror.
Removing the Pipeline Engine and Moving to an MCP Filesystem API
The biggest design change was completely removing shelpa’s pipeline execution engine and replacing it with standard filesystem operation tools modeled after the official MCP filesystem server (modelcontextprotocol/servers’s src/filesystem).
The official reference implementation is written in TypeScript. I reimplemented it as a Rust-based MCP server while expanding shelpa’s .filesystem/ directory traceability concept into a design with trash, version history, and file tracking.
14-Tool Architecture
The final tool set consists of 14 tools.
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 |
Write operations (create_file, write_file, edit_file, move_file) automatically record to .filesystem/. move_file appends to .filesystem/.moves, and delete_file logs to .filesystem/.deletes.
Dual-Mode MCP Server
filesystem has two startup modes: --mcp and --server. This design unifies the CLI interface with other tools in the same MCP toolkit — 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, the process is expected to run as a persistent daemon launched by agent-gateway, which injects root into every tool call.
ServerConfig{
Name: "filesystem",
Command: "filesystem",
Args: []string{"--server"},
Scope: "global",
}
The wire protocol is auto-detected from the first message: if the first byte is {, it’s Line JSON; if there’s a Content-Length: header, it’s HTTP-style framing (LSP compatible).
The .filesystem/ History Model
The .filesystem/ directory evolved from shelpa’s simple mirror into a structured history system.
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 content snapshot — not a diff. Simple, fast, low risk. .head tracks the current version; undo decrements it, redo increments it. Writing after undo prunes the redo stack, the same behavior as a typical editor.
move_file relocates the history directory along with the file and appends to .moves. delete_file moves the file and its history to .trash/ and logs to .deletes. Trash IDs are monotonically increasing and never reused.
Without --backup
Only .moves, .trash/, and .deletes are used. No version snapshots, no .head, no undo/redo. Minimal footprint.
Path Safety and Sandboxing
The workspace-scope restrictions inherited from shelpa were maintained and strengthened.
- Paths are normalized without filesystem access (collapsing
..and.) - Absolute paths must still be within the workspace root; otherwise a
PathEscapeerror is returned - Symlinks in the workspace root are resolved at startup (handles macOS
/tmp→/private/tmp) - No shell execution — zero command injection surface
list_directory and directory_tree exclude .filesystem, .git, node_modules, target, __pycache__, and other common build/cache directories by default, and also apply patterns from the workspace root’s .gitignore.
Error Model
All errors are returned 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) |
Summary
The key outcomes of this redesign:
- From shelpa to filesystem — Completely removed the pipeline execution engine and moved to a standard filesystem API
- 14-tool MCP server — 8 basic file operations, 4 safe delete/restore tools, 2 version history tools
- Dual mode —
--mcp(single project) and--server(multi-project daemon), unifying the CLI pattern with ctree and pathfinder .filesystem/history model — Undo/redo via full-content snapshots, safe delete via trash, tracking logs for renames and deletes- Graduated functionality via
--backup— Runs with minimal footprint when version history isn’t needed
The direct motivation for rebuilding filesystem was adapting agent-gateway into a custom agent system. I needed a mechanism where, even if a local LLM running as a worker accidentally deletes or corrupts files, recovery and verification would be straightforward through the trash and version history. shelpa’s workspace-scoping and mirroring design philosophy was directly reusable for this requirement.
