lean_ctx/tools/registered/
ctx_execute.rs1use rmcp::model::Tool;
2use rmcp::ErrorData;
3use serde_json::{json, Map, Value};
4
5use crate::server::tool_trait::{get_int, get_str, McpTool, ToolContext, ToolOutput};
6use crate::tool_defs::tool_def;
7
8pub struct CtxExecuteTool;
9
10impl McpTool for CtxExecuteTool {
11 fn name(&self) -> &'static str {
12 "ctx_execute"
13 }
14
15 fn tool_def(&self) -> Tool {
16 tool_def(
17 "ctx_execute",
18 "Run code in sandbox (11 languages). Only stdout enters context. Raw data never leaves subprocess. Languages: javascript, typescript, python, shell, ruby, go, rust, php, perl, r, elixir.",
19 json!({
20 "type": "object",
21 "properties": {
22 "language": {
23 "type": "string",
24 "description": "Language: javascript|typescript|python|shell|ruby|go|rust|php|perl|r|elixir"
25 },
26 "code": {
27 "type": "string",
28 "description": "Code to execute in sandbox"
29 },
30 "intent": {
31 "type": "string",
32 "description": "What you want from the output (triggers intent-driven filtering for large results)"
33 },
34 "timeout": {
35 "type": "integer",
36 "description": "Timeout in seconds (default: 30)"
37 },
38 "action": {
39 "type": "string",
40 "description": "batch — execute multiple scripts. Provide items as JSON array [{language, code}]"
41 },
42 "items": {
43 "type": "string",
44 "description": "JSON array of [{\"language\": \"...\", \"code\": \"...\"}] for batch execution"
45 },
46 "path": {
47 "type": "string",
48 "description": "File path for action=file"
49 }
50 }
51 }),
52 )
53 }
54
55 fn handle(
56 &self,
57 args: &Map<String, Value>,
58 ctx: &ToolContext,
59 ) -> Result<ToolOutput, ErrorData> {
60 let action = get_str(args, "action").unwrap_or_default();
61
62 let result = if action == "batch" {
63 let items_str = get_str(args, "items")
64 .ok_or_else(|| ErrorData::invalid_params("items is required for batch", None))?;
65 let items: Vec<serde_json::Value> = serde_json::from_str(&items_str)
66 .map_err(|e| ErrorData::invalid_params(format!("Invalid items JSON: {e}"), None))?;
67 let batch: Vec<(String, String)> = items
68 .iter()
69 .filter_map(|item| {
70 let lang = item.get("language")?.as_str()?.to_string();
71 let code = item.get("code")?.as_str()?.to_string();
72 Some((lang, code))
73 })
74 .collect();
75 crate::tools::ctx_execute::handle_batch(&batch)
76 } else if action == "file" {
77 let path = ctx
78 .resolved_path("path")
79 .ok_or_else(|| ErrorData::invalid_params("path is required for action=file", None))?
80 .to_string();
81 let project_root = if ctx.project_root.is_empty() {
82 None
83 } else {
84 Some(ctx.project_root.as_str())
85 };
86 let intent = get_str(args, "intent");
87 crate::tools::ctx_execute::handle_file(&path, intent.as_deref(), project_root)
88 } else {
89 let language = get_str(args, "language")
90 .ok_or_else(|| ErrorData::invalid_params("language is required", None))?;
91 let code = get_str(args, "code")
92 .ok_or_else(|| ErrorData::invalid_params("code is required", None))?;
93 let intent = get_str(args, "intent");
94 let timeout = get_int(args, "timeout").map(|t| t as u64);
95 crate::tools::ctx_execute::handle(&language, &code, intent.as_deref(), timeout)
96 };
97
98 let result = crate::core::redaction::redact_text_if_enabled(&result);
99 Ok(ToolOutput {
100 text: result,
101 original_tokens: 0,
102 saved_tokens: 0,
103 mode: Some(action),
104 path: None,
105 changed: false,
106 })
107 }
108}