Skip to main content

lean_ctx/tools/registered/
shell_alias.rs

1use rmcp::ErrorData;
2use rmcp::model::Tool;
3use serde_json::{Map, Value, json};
4
5use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput, get_str};
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            "Shell command with auto-compression (~95 patterns). Alias for ctx_shell.\n\
29             Output is compressed for token savings. For verbatim output pass raw=true.\n\
30             Use when your MCP client prefers shell/bash over ctx_shell — transparently\n\
31             delegates to ctx_shell internals.",
32            json!({
33                "type": "object",
34                "properties": {
35                    "command": {
36                        "type": "string",
37                        "description": "Shell command"
38                    },
39                    "cwd": {
40                        "type": "string",
41                        "description": "Working dir"
42                    },
43                    "raw": {
44                        "type": "boolean",
45                        "description": "Return verbatim output (skip compression). Default false — pass true for the exact bytes."
46                    }
47                },
48                "required": ["command"]
49            }),
50        )
51    }
52
53    fn handle(
54        &self,
55        args: &Map<String, Value>,
56        ctx: &ToolContext,
57    ) -> Result<ToolOutput, ErrorData> {
58        let command = get_str(args, "command")
59            .ok_or_else(|| ErrorData::invalid_params("command is required", None))?;
60
61        if let Some(rejection) = crate::tools::ctx_shell::validate_command(&command) {
62            return Ok(ToolOutput::simple(rejection));
63        }
64
65        if let Err(msg) = crate::core::shell_allowlist::check_shell_allowlist(&command) {
66            return Ok(ToolOutput::simple(msg));
67        }
68
69        tokio::task::block_in_place(|| {
70            let cwd = get_str(args, "cwd");
71            // Compressed by default (the point of this alias), but honor an explicit
72            // raw=true so clients restricted to "shell"/"bash" still have the verbatim
73            // escape the description advertises — no MCP-specific tool required.
74            let raw = args.get("raw").and_then(Value::as_bool).unwrap_or(false);
75            let mut shell_args = Map::new();
76            shell_args.insert("command".to_string(), Value::String(command));
77            if let Some(dir) = cwd {
78                shell_args.insert("cwd".to_string(), Value::String(dir));
79            }
80            shell_args.insert("raw".to_string(), Value::Bool(raw));
81
82            crate::tools::registered::ctx_shell::CtxShellTool.handle(&shell_args, ctx)
83        })
84    }
85}