Skip to main content

lean_ctx/tools/registered/
ctx_execute.rs

1use rmcp::ErrorData;
2use rmcp::model::Tool;
3use serde_json::{Map, Value, json};
4
5use crate::server::tool_trait::{
6    McpTool, ToolContext, ToolOutput, get_int, get_str, require_resolved_path,
7};
8use crate::tool_defs::tool_def;
9
10pub struct CtxExecuteTool;
11
12impl McpTool for CtxExecuteTool {
13    fn name(&self) -> &'static str {
14        "ctx_execute"
15    }
16
17    fn tool_def(&self) -> Tool {
18        tool_def(
19            "ctx_execute",
20            "Run code in sandbox (11 languages) — use when conditionals, multi-line or cross-language transforms.\n\
21             ANTIPATTERN: for simple one-liners, prefer ctx_shell (lower overhead, auto-compressed).\n\
22             language=shell is the trusted script path: no allowlist, by design (not an escape hatch) —\n\
23             use for multi-line scripts, pipelines, or commands ctx_shell blocks.\n\
24             action=code (default) for one-shot; action=batch for parallel multi-language;\n\
25             action=file to process a project file (extension auto-detects).\n\
26             Pass intent to focus large output and save tokens. Languages: javascript,\n\
27             typescript, python, shell, ruby, go, rust, php, perl, r, elixir.",
28            json!({
29                "type": "object",
30                "properties": {
31                    "language": {
32                        "type": "string",
33                        "description": "javascript|typescript|python|shell|ruby|go|rust|php|perl|r|elixir (for action=code)"
34                    },
35                    "code": {
36                        "type": "string",
37                        "description": "Source code for action=code. Set intent to filter large output."
38                    },
39                    "intent": {
40                        "type": "string",
41                        "description": "Focus intent; triggers filtering when output is large."
42                    },
43                    "timeout": {
44                        "type": "integer",
45                        "description": "Timeout in seconds (default: 30)"
46                    },
47                    "action": {
48                        "type": "string",
49                        "description": "code (default, run script) | batch (parallel) | file (project file)"
50                    },
51                    "items": {
52                        "type": "string",
53                        "description": "JSON array of [{language, code}] for batch action."
54                    },
55                    "path": {
56                        "type": "string",
57                        "description": "File path for action=file (language auto-detected)."
58                    }
59                }
60            }),
61        )
62    }
63
64    fn handle(
65        &self,
66        args: &Map<String, Value>,
67        ctx: &ToolContext,
68    ) -> Result<ToolOutput, ErrorData> {
69        let action = get_str(args, "action").unwrap_or_default();
70
71        let (result, outcome) = if action == "batch" {
72            let items_str = get_str(args, "items")
73                .ok_or_else(|| ErrorData::invalid_params("items is required for batch", None))?;
74            let items: Vec<serde_json::Value> = serde_json::from_str(&items_str)
75                .map_err(|e| ErrorData::invalid_params(format!("Invalid items JSON: {e}"), None))?;
76            let batch: Vec<(String, String)> = items
77                .iter()
78                .filter_map(|item| {
79                    let lang = item.get("language")?.as_str()?.to_string();
80                    let code = item.get("code")?.as_str()?.to_string();
81                    Some((lang, code))
82                })
83                .collect();
84            crate::tools::ctx_execute::handle_batch(&batch)
85        } else if action == "file" {
86            let path = require_resolved_path(ctx, args, "path")?;
87            let project_root = if ctx.project_root.is_empty() {
88                None
89            } else {
90                Some(ctx.project_root.as_str())
91            };
92            let intent = get_str(args, "intent");
93            crate::tools::ctx_execute::handle_file(&path, intent.as_deref(), project_root)
94        } else {
95            let language = get_str(args, "language")
96                .ok_or_else(|| ErrorData::invalid_params("language is required", None))?;
97            let code = get_str(args, "code")
98                .ok_or_else(|| ErrorData::invalid_params("code is required", None))?;
99            let intent = get_str(args, "intent");
100            let timeout = get_int(args, "timeout").and_then(|t| u64::try_from(t).ok());
101            crate::tools::ctx_execute::handle(&language, &code, intent.as_deref(), timeout)
102        };
103
104        let result = crate::core::redaction::redact_text_if_enabled(&result);
105        Ok(ToolOutput {
106            text: result,
107            original_tokens: 0,
108            saved_tokens: 0,
109            mode: Some(action),
110            path: None,
111            changed: false,
112            shell_outcome: Some(outcome),
113            content_blocks: None,
114        })
115    }
116}