Skip to main content

lean_ctx/tools/registered/
shell_alias.rs

1use rmcp::model::Tool;
2use rmcp::ErrorData;
3use serde_json::{json, Map, Value};
4
5use crate::server::tool_trait::{get_str, McpTool, ToolContext, ToolOutput};
6use crate::tool_defs::tool_def;
7
8/// A `shell` tool alias that transparently delegates to `ctx_shell`'s compression
9/// logic. Registered for all MCP clients (see `server::registry`); it exists for
10/// clients (like Codex Desktop) whose agent model prefers a tool named `shell` /
11/// `bash` over `ctx_shell` and would otherwise fall back to a native, uncompressed
12/// shell tool.
13///
14/// This solves the "Codex Desktop doesn't compress" issue (#337): the Desktop app
15/// loads the MCP server but the agent ignores `ctx_shell` and uses its native
16/// `Bash` tool instead. By providing a `shell` tool with a familiar interface,
17/// the model naturally routes commands through our compression pipeline.
18pub struct ShellAliasTool;
19
20impl McpTool for ShellAliasTool {
21    fn name(&self) -> &'static str {
22        "shell"
23    }
24
25    fn tool_def(&self) -> Tool {
26        tool_def(
27            "shell",
28            "Execute a shell command. Returns token-optimized compressed output (95+ patterns for git, npm, cargo, docker, tsc, etc). Equivalent to running the command in a terminal but with automatic output compression for efficiency.",
29            json!({
30                "type": "object",
31                "properties": {
32                    "command": {
33                        "type": "string",
34                        "description": "The shell command to execute"
35                    },
36                    "cwd": {
37                        "type": "string",
38                        "description": "Working directory (optional, defaults to project root)"
39                    }
40                },
41                "required": ["command"]
42            }),
43        )
44    }
45
46    fn handle(
47        &self,
48        args: &Map<String, Value>,
49        ctx: &ToolContext,
50    ) -> Result<ToolOutput, ErrorData> {
51        let command = get_str(args, "command")
52            .ok_or_else(|| ErrorData::invalid_params("command is required", None))?;
53
54        if let Some(rejection) = crate::tools::ctx_shell::validate_command(&command) {
55            return Ok(ToolOutput::simple(rejection));
56        }
57
58        if let Err(msg) = crate::core::shell_allowlist::check_shell_allowlist(&command) {
59            return Ok(ToolOutput::simple(msg));
60        }
61
62        tokio::task::block_in_place(|| {
63            let cwd = get_str(args, "cwd");
64            let mut shell_args = Map::new();
65            shell_args.insert("command".to_string(), Value::String(command));
66            if let Some(dir) = cwd {
67                shell_args.insert("cwd".to_string(), Value::String(dir));
68            }
69            // raw=false → always compress (the whole point of this alias)
70            shell_args.insert("raw".to_string(), Value::Bool(false));
71
72            crate::tools::registered::ctx_shell::CtxShellTool.handle(&shell_args, ctx)
73        })
74    }
75}