shelpa-mcp implementation and retirement

shelpa-mcp was a Model Context Protocol (MCP) virtual pipeline server that restricted file operations and text processing for LLM agents. I implemented it but retired it when the models did not consistently use it. Security Design and Lessons explains the outcome.

This record covers command routing, pipeline stages, and session CWD. The goal was to balance tool restrictions and model usability in AI system development.

Virtual pipeline restrictions

Allowed commands and workspace boundaries

Instead of arbitrary shell access, shelpa applied these restrictions:

  1. Whitelist Control: Only permitted commands can execute
  2. Pipeline Chaining: UNIX pipe (|) command concatenation
  3. Workspace Restriction: Access denial outside designated directories
  4. Audit Trail: All write operations mirrored in .shelpa/

MCP Server Role

The stdio MCP server exposed one shelpa_pipe tool:

  LLM Agent (Claude, etc.)
    ↓ (JSON-RPC over stdio)
shelpa-mcp Server
    ↓
shelpa Library (parse → validate → execute)
    ↓
Workspace Files + .shelpa/ Audit Trail
  

Command Routing Design

Command Classification

Commands were divided into two categories:

Pipeline Commands

Participate in pipe chains, producing and consuming byte streams:

  let pipeline_cmds = [
    "tail", "rg", "awk", "sed", "tr", "jq", "wc", 
    "tee", "fd", "ls", "head", "sort",
    "ctree_check", "ctree_generate", "serena_find_symbol"
];
  

Usage examples:

  tail -n 100 app.log | rg "ERROR" | awk '{print $3}' | sort -u
ls src | rg "\.rs$"
fd "\.toml$" | head -5
  

Execute only as sole stages; cannot participate in pipe chains:

  let nav_cmds = ["pwd", "cd"];
  

Reclassifying ls

ls initially belonged to navigation commands. Frequent use of ls src | rg fn led me to move it into the pipeline category:

Before:

  let nav_cmds = ["pwd", "cd", "ls"];  // ls restricted to standalone
  

After:

  let pipeline_cmds = ["tail", "rg", ..., "ls"];  // ls joins pipelines
let nav_cmds = ["pwd", "cd"];  // Pure navigation only
  

Standalone ls still used the workspace-restricted builtin_ls.

Pipeline Stage Management

Parsing

  pub fn parse_pipeline(command_str: &str) -> Result<Vec<PipelineStage>, GuardViolation> {
    // 1. Shell quote analysis
    let tokens = shell_words::split(command_str)?;
    
    // 2. Split by pipe (|)
    let stages: Vec<PipelineStage> = split_by_pipe(&tokens);
    
    // 3. Validate each stage's command
    for stage in &stages {
        validate_command(&stage.command, &stage.args)?;
    }
    
    // 4. Detect redirections (prohibited)
    check_no_redirects(&stages)?;
    
    Ok(stages)
}
  

Inter-Stage Data Flow

  Stage 1 (tail)     Stage 2 (rg)      Stage 3 (tee)
  stdout ──pipe──→  stdin              stdin
                    stdout ──pipe──→    stdin
                                       stdout → returned
                                       file   → workspace
                                       mirror → .shelpa/
  

Each stage ran in a separate subprocess, with stdin/stdout connected through OS pipes.

Execution Metadata

Each stage recorded execution metadata:

  pub struct StepMeta {
    pub command: String,
    pub output_size: usize,
    pub truncated: bool,
    pub execution_time_ms: u64,
}
  

Session CWD Management

Keeping CWD between calls

File operations needed a current directory that persisted across JSON-RPC calls.

Updating CWD through a Mutex

  static CWD_MUTEX: LazyLock<Mutex<Option<PathBuf>>> = LazyLock::new(|| Mutex::new(None));

fn current_cwd() -> PathBuf {
    CWD_MUTEX.lock().unwrap()
        .clone()
        .unwrap_or_else(|| workspace_root())
}

fn tool_pipe(args: &Map<String, Value>) -> Result<String> {
    let cwd = if let Some(cwd_str) = get_str(args, "cwd")? {
        let canonical = fs::canonicalize(root.join(cwd_str))?;
        assert!(canonical.starts_with(&root));
        canonical
    } else {
        current_cwd()
    };
    
    match shelpa::pipe(&root, &cwd, &command) {
        Ok(result) => {
            // Update session CWD on successful cd
            if let Some(new_cwd) = result.new_cwd.clone() {
                *CWD_MUTEX.lock().unwrap() = Some(new_cwd);
            }
            // ...
        }
    }
}
  

A successful cd updated the Mutex-protected CWD. Later commands used that directory.

Dual-Write Tee Implementation

Design

tee wrote to two locations:

  1. Real File: Writes to specified workspace path (overwrite or append)
  2. Audit Mirror: Always appends to .shelpa/{cwd_rel}/{target}
  tee output.txt
  ↓
  ├── workspace/output.txt    (overwrite mode)
  └── .shelpa/output.txt      (append mode, with separator)

tee -a output.txt
  ↓
  ├── workspace/output.txt    (append mode)
  └── .shelpa/output.txt      (append mode)
  

Overwrite Separators

On overwrite, the .shelpa/ mirror inserted a boundary separator:

  --- shelpa:overwrite ts=2026-02-25T13:17:30Z record_id=1772025450395572000 ---
(new content appended here)
  

The audit log could then identify overwrite boundaries.

MCP Interface Design

CLI Help Output

The tool names and help resembled shell commands to reuse learned shell knowledge. This did not establish consistent usage. Security Design and Lessons covers the problems.

  shelpa-mcp (MCP stdio server)
Usage:
  shelpa-mcp [--root <ROOT>] [--help]
Notes:
  - This binary speaks MCP over stdio. It does not serve HTTP.
  - Use your MCP client to call tools below.
  - --root sets the workspace root directory for all tool calls.
  - cwd defaults to the workspace root if not provided.
Tools:
  - shelpa_pipe    Execute a virtual safety pipeline
  (tail  rg  awk  sed  tr  jq  wc  tee  fd  ls  sort  head)
  - shelpa_write   Execute a virtual safety tee (auto guard, save override history)
Allowed pipeline commands: tail rg awk sed tr jq wc tee fd
Navigation commands (single-stage only): pwd  cd <path>  ls [path]
Pipes only. No redirects (> >>). No sed -i. No awk file output. Save via tee only.
CRITICAL: Never use standard file editing tools (such as write_file, replace, etc.)
  - always use the specified tool exclusively.
  

Tool Definition

  {
  "name": "shelpa_pipe",
  "description": "Execute a virtual pipeline string or navigation command",
  "inputSchema": {
    "type": "object",
    "properties": {
      "command": {
        "type": "string",
        "description": "Pipeline command string"
      },
      "cwd": {
        "type": "string",
        "description": "Optional working directory within workspace"
      },
      "confirm_oversize": {
        "type": "boolean",
        "description": "Confirm large writes exceeding approval threshold"
      }
    },
    "required": ["command"]
  }
}
  

Response Format

Success:

  {
  "stdout": "matched line 1\nmatched line 2\n",
  "meta": {
    "steps": [
      {"command": "tail -n 100 file", "output_size": 5000, "truncated": false, "execution_time_ms": 12},
      {"command": "rg pattern", "output_size": 48, "truncated": false, "execution_time_ms": 8}
    ],
    "tee": null
  }
}
  

Error:

  {
  "error": {
    "code": "GUARD_VIOLATION",
    "reason": "DISALLOWED_CMD",
    "detail": "'rm' is not allowed.",
    "suggestion": "Use tee to write files instead."
  }
}
  

Design observations

1. Benefits and limits of one tool

One tool reduces the number of tool choices. It did not make models consistently adopt this pipeline interface.

2. CWD sharing scope

A server-side Mutex managed CWD. The approach was simple, but multi-client use requires attention to the scope of shared state.

3. Two execution paths for ls

ls could join pipelines and use the builtin when called alone. This supported both uses.

4. Writing and auditing together

Dual writes kept the real file and audit mirror together. Separators preserved overwrite boundaries for later inspection.

Design retained after retirement

The implementation included allowed commands, layered guards, UNIX pipes, session CWD, and dual-write tee. Models did not consistently adopt it, so I retired the tool. Security Design and Lessons records the operational problems.