Skip to main content

turbomcp_cli/
executor.rs

1//! Command execution using turbomcp-client
2
3use crate::cli::*;
4use crate::error::{CliError, CliResult};
5use crate::formatter::Formatter;
6use crate::path_security;
7use crate::transport::create_client;
8use std::collections::HashMap;
9
10/// Execute CLI commands
11pub struct CommandExecutor {
12    pub formatter: Formatter,
13    verbose: bool,
14}
15
16impl CommandExecutor {
17    #[must_use]
18    pub fn new(format: OutputFormat, colored: bool, verbose: bool) -> Self {
19        Self {
20            formatter: Formatter::new(format, colored),
21            verbose,
22        }
23    }
24
25    /// Display an error with rich formatting
26    pub fn display_error(&self, error: &CliError) {
27        self.formatter.display_error(error);
28    }
29
30    /// Execute a command
31    pub async fn execute(&self, command: Commands) -> CliResult<()> {
32        match command {
33            Commands::Tools(cmd) => self.execute_tool_command(cmd).await,
34            Commands::Resources(cmd) => self.execute_resource_command(cmd).await,
35            Commands::Prompts(cmd) => self.execute_prompt_command(cmd).await,
36            Commands::Complete(cmd) => self.execute_completion_command(cmd).await,
37            Commands::Server(cmd) => self.execute_server_command(cmd).await,
38            Commands::Sample(cmd) => self.execute_sampling_command(cmd).await,
39            Commands::Connect(conn) => self.execute_connect(conn).await,
40            Commands::Status(conn) => self.execute_status(conn).await,
41            Commands::Dev(args) => self.execute_dev(args),
42            Commands::Install(args) => self.execute_install(args),
43            Commands::Build(args) => self.execute_build(args),
44            Commands::Deploy(args) => self.execute_deploy(args),
45            Commands::New(args) => self.execute_new(args),
46        }
47    }
48
49    // Tool commands
50
51    async fn execute_tool_command(&self, command: ToolCommands) -> CliResult<()> {
52        match command {
53            ToolCommands::List { conn } => {
54                let client = create_client(&conn).await?;
55                client.initialize().await?;
56                let tools = client.list_tools().await?;
57                self.formatter.display_tools(&tools)
58            }
59
60            ToolCommands::Call {
61                conn,
62                name,
63                arguments,
64            } => {
65                let args: HashMap<String, serde_json::Value> =
66                    if arguments.trim().is_empty() || arguments == "{}" {
67                        HashMap::new()
68                    } else {
69                        serde_json::from_str(&arguments).map_err(|e| {
70                            let location = format!("line {}, column {}", e.line(), e.column());
71                            CliError::InvalidArguments(format!(
72                                "Invalid JSON arguments at {}: {}",
73                                location, e
74                            ))
75                        })?
76                    };
77
78                let client = create_client(&conn).await?;
79                client.initialize().await?;
80                let result = client.call_tool(&name, Some(args)).await?;
81                self.formatter.display(&result)
82            }
83
84            ToolCommands::Schema { conn, name } => {
85                let client = create_client(&conn).await?;
86                client.initialize().await?;
87                let tools = client.list_tools().await?;
88
89                if let Some(tool_name) = name {
90                    let tool = tools.iter().find(|t| t.name == tool_name).ok_or_else(|| {
91                        CliError::Other(format!("Tool '{}' not found", tool_name))
92                    })?;
93
94                    self.formatter.display(&tool.input_schema)
95                } else {
96                    let schemas: Vec<_> = tools
97                        .iter()
98                        .map(|t| {
99                            serde_json::json!({
100                                "name": t.name,
101                                "schema": t.input_schema
102                            })
103                        })
104                        .collect();
105
106                    self.formatter.display(&schemas)
107                }
108            }
109
110            ToolCommands::Export { conn, output } => {
111                let client = create_client(&conn).await?;
112                client.initialize().await?;
113                let tools = client.list_tools().await?;
114
115                // Validate output directory path before creation
116                if !output.is_absolute() {
117                    return Err(CliError::InvalidArguments(
118                        "Output directory must be an absolute path".to_string(),
119                    ));
120                }
121
122                // Create output directory after validation
123                std::fs::create_dir_all(&output)?;
124
125                let mut exported_count = 0;
126                let mut skipped_count = 0;
127
128                for tool in tools {
129                    // Sanitize tool name and construct safe output path
130                    // This prevents path traversal attacks from malicious servers
131                    match path_security::safe_output_path(&output, &tool.name, "json") {
132                        Ok(filepath) => {
133                            let schema = serde_json::to_string_pretty(&tool.input_schema)?;
134                            std::fs::write(&filepath, schema)?;
135
136                            if self.verbose {
137                                println!("Exported: {}", filepath.display());
138                            }
139                            exported_count += 1;
140                        }
141                        Err(e) => {
142                            // Log security violation but continue processing other tools
143                            eprintln!("Warning: Skipped tool '{}': {}", tool.name, e);
144                            skipped_count += 1;
145                        }
146                    }
147                }
148
149                if exported_count > 0 {
150                    println!(
151                        "✓ Exported {} schema{} to: {}",
152                        exported_count,
153                        if exported_count == 1 { "" } else { "s" },
154                        output.display()
155                    );
156                }
157
158                if skipped_count > 0 {
159                    println!(
160                        "⚠ Skipped {} tool{} due to invalid names",
161                        skipped_count,
162                        if skipped_count == 1 { "" } else { "s" }
163                    );
164                }
165
166                Ok(())
167            }
168        }
169    }
170
171    // Resource commands
172
173    async fn execute_resource_command(&self, command: ResourceCommands) -> CliResult<()> {
174        match command {
175            ResourceCommands::List { conn } => {
176                let client = create_client(&conn).await?;
177                client.initialize().await?;
178                let resources = client.list_resources().await?;
179                self.formatter.display(&resources)
180            }
181
182            ResourceCommands::Read { conn, uri } => {
183                let client = create_client(&conn).await?;
184                client.initialize().await?;
185                let result = client.read_resource(&uri).await?;
186                self.formatter.display(&result)
187            }
188
189            ResourceCommands::Templates { conn } => {
190                let client = create_client(&conn).await?;
191                client.initialize().await?;
192                let templates = client.list_resource_templates().await?;
193                self.formatter.display(&templates)
194            }
195
196            ResourceCommands::Subscribe { conn, uri } => {
197                let client = create_client(&conn).await?;
198                client.initialize().await?;
199                client.subscribe(&uri).await?;
200                println!("✓ Subscribed to: {uri}");
201                Ok(())
202            }
203
204            ResourceCommands::Unsubscribe { conn, uri } => {
205                let client = create_client(&conn).await?;
206                client.initialize().await?;
207                client.unsubscribe(&uri).await?;
208                println!("✓ Unsubscribed from: {uri}");
209                Ok(())
210            }
211        }
212    }
213
214    // Prompt commands
215
216    async fn execute_prompt_command(&self, command: PromptCommands) -> CliResult<()> {
217        match command {
218            PromptCommands::List { conn } => {
219                let client = create_client(&conn).await?;
220                client.initialize().await?;
221                let prompts = client.list_prompts().await?;
222                self.formatter.display_prompts(&prompts)
223            }
224
225            PromptCommands::Get {
226                conn,
227                name,
228                arguments,
229            } => {
230                // Parse arguments as HashMap<String, Value>
231                let args: HashMap<String, serde_json::Value> =
232                    if arguments.trim().is_empty() || arguments == "{}" {
233                        HashMap::new()
234                    } else {
235                        serde_json::from_str(&arguments).map_err(|e| {
236                            let location = format!("line {}, column {}", e.line(), e.column());
237                            CliError::InvalidArguments(format!(
238                                "Invalid JSON arguments at {}: {}",
239                                location, e
240                            ))
241                        })?
242                    };
243
244                let args_option = if args.is_empty() { None } else { Some(args) };
245
246                let client = create_client(&conn).await?;
247                client.initialize().await?;
248                let result = client.get_prompt(&name, args_option).await?;
249                self.formatter.display(&result)
250            }
251
252            PromptCommands::Schema { conn, name } => {
253                let client = create_client(&conn).await?;
254                client.initialize().await?;
255                let prompts = client.list_prompts().await?;
256
257                let prompt = prompts
258                    .iter()
259                    .find(|p| p.name == name)
260                    .ok_or_else(|| CliError::Other(format!("Prompt '{}' not found", name)))?;
261
262                self.formatter.display(&prompt.arguments)
263            }
264        }
265    }
266
267    // Completion commands
268
269    async fn execute_completion_command(&self, command: CompletionCommands) -> CliResult<()> {
270        match command {
271            CompletionCommands::Get {
272                conn,
273                ref_type,
274                ref_value,
275                argument,
276            } => {
277                let client = create_client(&conn).await?;
278                client.initialize().await?;
279
280                // Use the appropriate completion method based on reference type
281                let result = match ref_type {
282                    RefType::Prompt => {
283                        let arg_name = argument.as_deref().unwrap_or("value");
284                        client
285                            .complete_prompt(&ref_value, arg_name, "", None)
286                            .await?
287                    }
288                    RefType::Resource => {
289                        let arg_name = argument.as_deref().unwrap_or("uri");
290                        client
291                            .complete_resource(&ref_value, arg_name, "", None)
292                            .await?
293                    }
294                };
295
296                self.formatter.display(&result)
297            }
298        }
299    }
300
301    // Server commands
302
303    async fn execute_server_command(&self, command: ServerCommands) -> CliResult<()> {
304        match command {
305            ServerCommands::Info { conn } => {
306                let client = create_client(&conn).await?;
307                let result = client.initialize().await?;
308                self.formatter.display_server_info(&result.server_info)
309            }
310
311            ServerCommands::Ping { conn } => {
312                let client = create_client(&conn).await?;
313                let start = std::time::Instant::now();
314
315                client.initialize().await?;
316                client.ping().await?;
317
318                let elapsed = start.elapsed();
319                println!("✓ Pong! ({:.2}ms)", elapsed.as_secs_f64() * 1000.0);
320                Ok(())
321            }
322
323            ServerCommands::LogLevel { conn, level } => {
324                // Convert level once before using
325                let protocol_level: turbomcp_protocol::types::LogLevel = level.clone().into();
326
327                let client = create_client(&conn).await?;
328                client.initialize().await?;
329                client.set_log_level(protocol_level).await?;
330                println!("✓ Log level set to: {:?}", level);
331                Ok(())
332            }
333
334            ServerCommands::Roots { conn } => {
335                // Roots are part of server capabilities returned during initialization
336                let client = create_client(&conn).await?;
337                let result = client.initialize().await?;
338
339                // Display server capabilities which includes roots info
340                self.formatter.display(&result.server_capabilities)
341            }
342        }
343    }
344
345    // Sampling commands
346
347    async fn execute_sampling_command(&self, _command: SamplingCommands) -> CliResult<()> {
348        Err(CliError::NotSupported(
349            "Sampling commands require LLM handler implementation".to_string(),
350        ))
351    }
352
353    // Connection commands
354
355    async fn execute_connect(&self, conn: Connection) -> CliResult<()> {
356        println!("Connecting to server...");
357        let client = create_client(&conn).await?;
358
359        let result = client.initialize().await?;
360
361        println!("✓ Connected successfully!");
362        self.formatter.display_server_info(&result.server_info)
363    }
364
365    async fn execute_status(&self, conn: Connection) -> CliResult<()> {
366        let client = create_client(&conn).await?;
367
368        let result = client.initialize().await?;
369
370        println!("Status: Connected");
371        self.formatter.display_server_info(&result.server_info)
372    }
373
374    // Development commands
375
376    fn execute_dev(&self, args: DevArgs) -> CliResult<()> {
377        crate::dev::execute(&args).map_err(|e| CliError::Other(e.to_string()))
378    }
379
380    fn execute_install(&self, args: InstallArgs) -> CliResult<()> {
381        crate::install::execute(&args).map_err(|e| CliError::Other(e.to_string()))
382    }
383
384    // WASM build and deploy commands
385
386    fn execute_build(&self, args: crate::cli::BuildArgs) -> CliResult<()> {
387        crate::build::execute(&args)
388    }
389
390    fn execute_deploy(&self, args: crate::cli::DeployArgs) -> CliResult<()> {
391        crate::deploy::execute(&args)
392    }
393
394    fn execute_new(&self, args: crate::cli::NewArgs) -> CliResult<()> {
395        crate::new::execute(&args)
396    }
397}