shelpa-mcp virtual pipelines and CWD management
Command routing, pipelines, session CWD, and audit mirrors in shelpa-mcp. An AI system development record of implemented restrictions and an interface that models did not consistently adopt.
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:
- Whitelist Control: Only permitted commands can execute
- Pipeline Chaining: UNIX pipe (
|) command concatenation - Workspace Restriction: Access denial outside designated directories
- 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
Navigation Commands
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:
- Real File: Writes to specified workspace path (overwrite or append)
- 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.
