shelpa-mcpの実装と廃止

shelpa-mcpは、LLMエージェントのファイル操作とテキスト処理を制限する、Model Context Protocol (MCP)準拠の仮想パイプラインサーバーです。実装しましたが、モデルに使用を定着させられず廃止しました。理由はセキュリティ設計と教訓にあります。

ここではコマンドルーティング、ステージ管理、セッションCWDを記録します。AIシステム開発で、ツールの制限とモデルの使いやすさを両立させるための試みでした。

仮想パイプラインの制限

許可コマンドとワークスペース

任意のシェルコマンドを渡す代わりに、次の制限を設けました。

  1. ホワイトリスト制御:許可されたコマンドのみ実行可能
  2. パイプラインチェーン:UNIXパイプ(|)によるコマンド連結
  3. ワークスペース制限:指定されたディレクトリ外へのアクセスを遮断
  4. 監査証跡:全ての書き込み操作を.shelpa/にミラー保持

MCPサーバーとしての位置づけ

stdio MCPサーバーとして、単一ツールshelpa_pipeを公開しました。

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

コマンドルーティング設計

コマンド分類

コマンドを2種類に分けました。

パイプラインコマンド

バイトストリームを生成・消費し、パイプチェーンに参加します。

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

使用例です。

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

ナビゲーションコマンド

単独ステージでのみ実行します。パイプチェーンには参加できません。

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

lsコマンドの再分類

初期のlsはナビゲーション扱いでした。ls src | rg fnが頻繁に使われたため、パイプラインコマンドへ移しました。

変更前:

  let nav_cmds = ["pwd", "cd", "ls"];  // lsが単独使用に制限
  

変更後:

  let pipeline_cmds = ["tail", "rg", ..., "ls"];  // lsがパイプラインに参加可能
let nav_cmds = ["pwd", "cd"];  // 純粋なナビゲーションのみ
  

単独のlsは、ワークスペース制限を持つbuiltin_lsへ引き続き渡します。

パイプラインステージ管理

パース処理

  pub fn parse_pipeline(command_str: &str) -> Result<Vec<PipelineStage>, GuardViolation> {
    // 1. シェルクオート解析
    let tokens = shell_words::split(command_str)?;
    
    // 2. パイプ(|)で分割
    let stages: Vec<PipelineStage> = split_by_pipe(&tokens);
    
    // 3. 各ステージのコマンド検証
    for stage in &stages {
        validate_command(&stage.command, &stage.args)?;
    }
    
    // 4. リダイレクト検出(禁止)
    check_no_redirects(&stages)?;
    
    Ok(stages)
}
  

ステージ間データフロー

  Stage 1 (tail)     Stage 2 (rg)      Stage 3 (tee)
  stdout ──pipe──→  stdin              stdin
                    stdout ──pipe──→    stdin
                                       stdout → 返却
                                       file   → ワークスペース
                                       mirror → .shelpa/
  

各ステージを別のサブプロセスで実行し、OSのパイプでstdin/stdoutをつなぎます。

実行メタデータ

ステージごとの実行結果をメタデータに残します。

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

セッションCWD管理

呼び出し間でCWDを保持する

JSON-RPCの各呼び出しをまたいで、ファイル操作の基準ディレクトリを保持する必要がありました。

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")? {
        // 明示的な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) => {
            // cdコマンドの場合、セッションCWDを更新
            if let Some(new_cwd) = result.new_cwd.clone() {
                *CWD_MUTEX.lock().unwrap() = Some(new_cwd);
            }
            // ...
        }
    }
}
  

cdが成功したときに、Mutexで保護したCWDを更新します。以後のコマンドは新しいCWDを基準に実行します。

デュアルライトtee実装

設計

teeは2か所へ書き込みます。

  1. 実ファイル:ワークスペース内の指定パスに書き込み(上書き or 追加)
  2. 監査ミラー:.shelpa/{cwd_rel}/{target}に常に追記
  tee output.txt
  ↓
  ├── workspace/output.txt    (上書きモード)
  └── .shelpa/output.txt      (追記モード、セパレータ付き)

tee -a output.txt
  ↓
  ├── workspace/output.txt    (追記モード)
  └── .shelpa/output.txt      (追記モード)
  

上書き時のセパレータ

上書き時は、.shelpa/のミラーへ境界を示すセパレータを挿入します。

  --- shelpa:overwrite ts=2026-02-25T13:17:30Z record_id=1772025450395572000 ---
(新しい内容がここに追記される)
  

監査ログで上書きの境界を追跡できます。

MCPインターフェース設計

CLIヘルプ出力

シェルに近いツール名とヘルプを使い、学習済みのshell知識を利用させる狙いでした。この方法では使用を定着させられませんでした。セキュリティ設計と教訓で詳しく扱っています。

  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.
  

ツール定義

  {
  "name": "shelpa_pipe",
  "description": "Execute a virtual pipeline string or navigation command",
  "inputSchema": {
    "type": "object",
    "properties": {
      "command": {
        "type": "string",
        "description": "Pipeline command string (e.g., 'tail -n 100 file | rg pattern')"
      },
      "cwd": {
        "type": "string",
        "description": "Optional working directory within workspace"
      },
      "confirm_oversize": {
        "type": "boolean",
        "description": "Confirm large writes exceeding approval threshold"
      }
    },
    "required": ["command"]
  }
}
  

レスポンス形式

成功時:

  {
  "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": {
    "code": "GUARD_VIOLATION",
    "reason": "DISALLOWED_CMD",
    "detail": "'rm' is not allowed.",
    "suggestion": "Use tee to write files instead."
  }
}
  

統合テスト

パイプライン動作確認

  # ファイルリスト → フィルタ
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"shelpa_pipe","arguments":{"command":"ls src | rg \\.rs$"}}}' | shelpa-mcp --root /workspace

# 結果
{"stdout": "error.rs\nexecutor.rs\nhistory.rs\nlib.rs\nparser.rs\ntypes.rs\n"}
  

tee動作確認

  # パイプライン → ファイル書き込み
echo '{"...","arguments":{"command":"rg --version | tee out/ver.txt"}}' | shelpa-mcp --root /workspace

# 実ファイルに書き込まれ、.shelpa/にもミラーコピーされる
  

設計で確認できたこと

1. 単一ツールの利点と限界

単一ツールにまとめると、LLMが選ぶツール数は減ります。ただし、今回の実装ではパイプラインの利用定着にはつながりませんでした。

2. CWDの共有範囲

CWDはサーバー側のMutexで管理しました。単純な方式ですが、マルチクライアントでは状態を共有する範囲に注意が必要です。

3. lsの2つの実行経路

lsはパイプラインに参加させ、単独時はビルトインへ渡します。用途ごとに経路を分けられました。

4. 書き込みと監査を同時に残す

実ファイルへの書き込みと監査ミラーを同時に行い、セパレータで上書き境界を残しました。事後に編集経緯を確認できます。

廃止後に残した設計

許可コマンド、多層ガード、UNIXパイプの処理、セッションCWD、デュアルライトteeを実装しました。ただし、LLMに利用を定着させられなかったため、ツールは廃止しました。セキュリティ設計と教訓に運用時の問題を残しています。