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

ToolFunction
read_text_fileRead files with optional head/tail for partial reads
create_fileCreate new file; errors if file already exists
write_fileCreate or overwrite
edit_fileSearch-and-replace via oldText/newText; diff output; dryRun support
create_directoryRecursive directory creation
list_directoryListing with [FILE]/[DIR] prefixes
directory_treeRecursive JSON tree with exclude pattern support
move_fileMove/rename; errors if destination exists

Safe Delete and Restore

ToolFunction
delete_fileMoves file to .filesystem/.trash/ (never permanently deletes)
list_trashList trash contents
restore_fileRestore from trash to original path
search_fileSearch for file status; tracks moves and deletes

Version History (--backup mode only)

ToolFunction
undo_fileRestore to n versions back
redo_fileAdvance 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 startupOptional (default: cwd)Not allowed
root in tool callsIgnoredRequired
Multi-projectNoYes
Use caseCLI / standaloneagent-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 PathEscape error 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 KindCondition
PathEscapePath escapes the workspace root
NotFoundFile/directory not found; oldText not found in edit_file
AlreadyExistscreate_file target exists; move_file/restore_file destination exists
IoErrorFilesystem I/O failure
InvalidArgumentInvalid parameter (e.g., both head and tail specified, undo exceeds history)

Summary

The key outcomes of this redesign:

  1. From shelpa to filesystem — Completely removed the pipeline execution engine and moved to a standard filesystem API
  2. 14-tool MCP server — 8 basic file operations, 4 safe delete/restore tools, 2 version history tools
  3. Dual mode--mcp (single project) and --server (multi-project daemon), unifying the CLI pattern with ctree and pathfinder
  4. .filesystem/ history model — Undo/redo via full-content snapshots, safe delete via trash, tracking logs for renames and deletes
  5. 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.