lean_ctx/tools/registered/
ctx_execute.rs1use 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 supports multi-line scripts but shares ctx_shell's security policy.\n\
23 action=code (default) for one-shot; action=batch for parallel multi-language;\n\
24 action=file to process a project file (extension auto-detects).\n\
25 Pass intent to focus large output and save tokens. Languages: javascript,\n\
26 typescript, python, shell, ruby, go, rust, php, perl, r, elixir.",
27 json!({
28 "type": "object",
29 "properties": {
30 "language": {
31 "type": "string",
32 "description": "javascript|typescript|python|shell|ruby|go|rust|php|perl|r|elixir (for action=code)"
33 },
34 "code": {
35 "type": "string",
36 "description": "Source code for action=code. Set intent to filter large output."
37 },
38 "intent": {
39 "type": "string",
40 "description": "Focus intent; triggers filtering when output is large."
41 },
42 "timeout": {
43 "type": "integer",
44 "description": "Timeout in seconds (default: 30)"
45 },
46 "action": {
47 "type": "string",
48 "description": "code (default, run script) | batch (parallel) | file (project file)"
49 },
50 "items": {
51 "type": "string",
52 "description": "JSON array of [{language, code}] for batch action."
53 },
54 "path": {
55 "type": "string",
56 "description": "File path for action=file (language auto-detected)."
57 }
58 },
59 "oneOf": [
60 {
61 "properties": { "action": { "enum": ["code"] } },
62 "required": ["language", "code"]
63 },
64 {
65 "properties": { "action": { "const": "batch" } },
66 "required": ["action", "items"]
67 },
68 {
69 "properties": { "action": { "const": "file" } },
70 "required": ["action", "path"]
71 }
72 ]
73 }),
74 )
75 }
76
77 fn handle(
78 &self,
79 args: &Map<String, Value>,
80 ctx: &ToolContext,
81 ) -> Result<ToolOutput, ErrorData> {
82 let action = get_str(args, "action").unwrap_or_default();
83
84 let (result, outcome) = if action == "batch" {
85 let items_str = get_str(args, "items")
86 .ok_or_else(|| ErrorData::invalid_params("items is required for batch", None))?;
87 let items: Vec<serde_json::Value> = serde_json::from_str(&items_str)
88 .map_err(|e| ErrorData::invalid_params(format!("Invalid items JSON: {e}"), None))?;
89 let batch: Vec<(String, String)> = items
90 .iter()
91 .filter_map(|item| {
92 let lang = item.get("language")?.as_str()?.to_string();
93 let code = item.get("code")?.as_str()?.to_string();
94 Some((lang, code))
95 })
96 .collect();
97 crate::tools::ctx_execute::handle_batch(&batch)
98 } else if action == "file" {
99 let path = require_resolved_path(ctx, args, "path")?;
100 let project_root = if ctx.project_root.is_empty() {
101 None
102 } else {
103 Some(ctx.project_root.as_str())
104 };
105 let intent = get_str(args, "intent");
106 crate::tools::ctx_execute::handle_file(&path, intent.as_deref(), project_root)
107 } else {
108 let language = get_str(args, "language")
109 .ok_or_else(|| ErrorData::invalid_params("language is required", None))?;
110 let code = get_str(args, "code")
111 .ok_or_else(|| ErrorData::invalid_params("code is required", None))?;
112 let intent = get_str(args, "intent");
113 let timeout = get_int(args, "timeout").and_then(|t| u64::try_from(t).ok());
114 crate::tools::ctx_execute::handle(&language, &code, intent.as_deref(), timeout)
115 };
116
117 let result = crate::core::redaction::redact_text_if_enabled(&result);
118 Ok(ToolOutput {
119 text: result,
120 original_tokens: 0,
121 saved_tokens: 0,
122 mode: Some(action),
123 path: None,
124 changed: false,
125 shell_outcome: Some(outcome),
126 content_blocks: None,
127 })
128 }
129}