mcp_execution_cli/commands/
generate.rs1use super::common::{build_server_config, load_server_from_config};
10use anyhow::{Context, Result};
11use mcp_execution_codegen::progressive::ProgressiveGenerator;
12use mcp_execution_core::cli::{ExitCode, OutputFormat};
13use mcp_execution_files::FilesBuilder;
14use mcp_execution_introspector::Introspector;
15use serde::Serialize;
16use std::path::PathBuf;
17use tracing::{debug, info, warn};
18
19#[derive(Debug, Serialize)]
21struct GenerationResult {
22 server_id: String,
24 server_name: String,
26 tool_count: usize,
28 output_path: String,
30}
31
32#[derive(Debug, Serialize)]
34struct FilePreview {
35 path: String,
37 size: usize,
39}
40
41#[derive(Debug, Serialize)]
43struct DryRunResult {
44 server_id: String,
46 server_name: String,
48 output_path: String,
50 files: Vec<FilePreview>,
52 total_files: usize,
54 total_size: usize,
56}
57
58#[allow(clippy::cast_precision_loss)]
59fn format_size(bytes: usize) -> String {
60 if bytes < 1024 {
61 format!("{bytes} B")
62 } else if bytes < 1024 * 1024 {
63 format!("{:.1} KB", bytes as f64 / 1024.0)
64 } else {
65 format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
66 }
67}
68
69#[allow(clippy::too_many_arguments)]
104pub async fn run(
105 from_config: Option<String>,
106 server: Option<String>,
107 args: Vec<String>,
108 env: Vec<String>,
109 cwd: Option<String>,
110 http: Option<String>,
111 sse: Option<String>,
112 headers: Vec<String>,
113 name: Option<String>,
114 output_dir: Option<PathBuf>,
115 dry_run: bool,
116 output_format: OutputFormat,
117) -> Result<ExitCode> {
118 let (server_id, server_config) = if let Some(config_name) = from_config {
119 debug!(
120 "Loading server configuration from ~/.claude/mcp.json: {}",
121 config_name
122 );
123 load_server_from_config(&config_name)?
124 } else {
125 build_server_config(server, args, env, cwd, http, sse, headers)?
126 };
127
128 info!("Connecting to MCP server: {}", server_id);
129
130 let mut introspector = Introspector::new();
131 let server_info = introspector
132 .discover_server(server_id, &server_config)
133 .await
134 .context("failed to introspect MCP server")?;
135
136 info!(
137 "Discovered {} tools from server '{}'",
138 server_info.tools.len(),
139 server_info.name
140 );
141
142 if server_info.tools.is_empty() {
143 warn!("Server has no tools to generate code for");
144 return Ok(ExitCode::SUCCESS);
145 }
146
147 let mut server_info = server_info;
150 if let Some(ref custom_name) = name {
151 server_info.id = mcp_execution_core::ServerId::new(custom_name);
152 }
153
154 let server_dir_name = server_info.id.to_string();
155
156 let generator = ProgressiveGenerator::new().context("failed to create code generator")?;
157 let generated_code = generator
158 .generate(&server_info)
159 .context("failed to generate TypeScript code")?;
160
161 info!(
162 "Generated {} files for progressive loading",
163 generated_code.file_count()
164 );
165
166 let base_dir = if let Some(custom_dir) = output_dir {
167 custom_dir
168 } else {
169 dirs::home_dir()
170 .context("failed to get home directory")?
171 .join(".claude")
172 .join("servers")
173 };
174 let output_path = base_dir.join(&server_dir_name);
175
176 if dry_run {
177 let files: Vec<FilePreview> = generated_code
178 .files
179 .iter()
180 .map(|f| FilePreview {
181 path: format!("{}/{}", server_dir_name, f.path),
182 size: f.content.len(),
183 })
184 .collect();
185 let total_size: usize = files.iter().map(|f| f.size).sum();
186 let total_files = files.len();
187
188 let result = DryRunResult {
189 server_id: server_info.id.to_string(),
190 server_name: server_info.name,
191 output_path: output_path.display().to_string(),
192 files,
193 total_files,
194 total_size,
195 };
196
197 match output_format {
198 OutputFormat::Json => {
199 println!("{}", serde_json::to_string_pretty(&result)?);
200 }
201 OutputFormat::Text => {
202 println!("Server: {} ({})", result.server_name, result.server_id);
203 println!(
204 "Would generate {} files ({}) to {}/",
205 result.total_files,
206 format_size(result.total_size),
207 result.output_path
208 );
209 }
210 OutputFormat::Pretty => {
211 println!(
212 "Would generate {} files to {}/:",
213 result.total_files, result.output_path
214 );
215 println!();
216 for f in &result.files {
217 println!(" - {} ({})", f.path, format_size(f.size));
218 }
219 println!();
220 println!(
221 "Total: {} files, ~{}",
222 result.total_files,
223 format_size(result.total_size)
224 );
225 }
226 }
227
228 return Ok(ExitCode::SUCCESS);
229 }
230
231 let vfs = FilesBuilder::from_generated_code(generated_code, "/")
234 .build()
235 .context("failed to build VFS")?;
236
237 info!("Exporting files to: {}", output_path.display());
238
239 std::fs::create_dir_all(&output_path).context("failed to create output directory")?;
240 vfs.export_to_filesystem(&output_path)
241 .context("failed to export files to filesystem")?;
242
243 let result = GenerationResult {
244 server_id: server_info.id.to_string(),
245 server_name: server_info.name.clone(),
246 tool_count: server_info.tools.len(),
247 output_path: output_path.display().to_string(),
248 };
249
250 match output_format {
251 OutputFormat::Json => {
252 println!("{}", serde_json::to_string_pretty(&result)?);
253 }
254 OutputFormat::Text => {
255 println!("Server: {} ({})", result.server_name, result.server_id);
256 println!("Generated {} tool files", result.tool_count);
257 println!("Output: {}", result.output_path);
258 }
259 OutputFormat::Pretty => {
260 println!("✓ Successfully generated progressive loading files");
261 println!(" Server: {} ({})", result.server_name, result.server_id);
262 println!(" Tools: {}", result.tool_count);
263 println!(" Location: {}", result.output_path);
264 }
265 }
266
267 Ok(ExitCode::SUCCESS)
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use mcp_execution_core::ServerId;
274 use mcp_execution_introspector::{ServerCapabilities, ServerInfo, ToolInfo};
275 use serde_json::json;
276
277 fn create_mock_server_info() -> ServerInfo {
278 ServerInfo {
279 id: ServerId::new("test-server"),
280 name: "Test Server".to_string(),
281 version: "1.0.0".to_string(),
282 tools: vec![ToolInfo {
283 name: mcp_execution_core::ToolName::new("test_tool"),
284 description: "A test tool".to_string(),
285 input_schema: json!({
286 "type": "object",
287 "properties": {
288 "param": {"type": "string"}
289 }
290 }),
291 output_schema: None,
292 }],
293 capabilities: ServerCapabilities {
294 supports_tools: true,
295 supports_resources: false,
296 supports_prompts: false,
297 },
298 }
299 }
300
301 #[test]
302 fn test_generation_result_serialization() {
303 let result = GenerationResult {
304 server_id: "test".to_string(),
305 server_name: "Test Server".to_string(),
306 tool_count: 5,
307 output_path: "/path/to/output".to_string(),
308 };
309
310 let json = serde_json::to_string(&result).unwrap();
311 assert!(json.contains("\"server_id\":\"test\""));
312 assert!(json.contains("\"tool_count\":5"));
313 }
314
315 #[test]
316 fn test_progressive_generator_creation() {
317 let generator = ProgressiveGenerator::new();
318 assert!(generator.is_ok());
319 }
320
321 #[test]
322 fn test_progressive_code_generation() {
323 let generator = ProgressiveGenerator::new().unwrap();
324 let server_info = create_mock_server_info();
325
326 let result = generator.generate(&server_info);
327 assert!(result.is_ok());
328
329 let code = result.unwrap();
330 assert!(code.file_count() > 0);
331 }
332
333 #[test]
334 fn test_format_size_bytes() {
335 assert_eq!(format_size(0), "0 B");
336 assert_eq!(format_size(512), "512 B");
337 assert_eq!(format_size(1023), "1023 B");
338 }
339
340 #[test]
341 fn test_format_size_kilobytes() {
342 assert_eq!(format_size(1024), "1.0 KB");
343 assert_eq!(format_size(2048), "2.0 KB");
344 assert_eq!(format_size(1536), "1.5 KB");
345 }
346
347 #[test]
348 fn test_format_size_megabytes() {
349 assert_eq!(format_size(1024 * 1024), "1.0 MB");
350 assert_eq!(format_size(2 * 1024 * 1024), "2.0 MB");
351 }
352
353 #[test]
354 fn test_dry_run_result_serialization() {
355 let result = DryRunResult {
356 server_id: "github".to_string(),
357 server_name: "GitHub MCP Server".to_string(),
358 output_path: "/home/user/.claude/servers/github".to_string(),
359 files: vec![
360 FilePreview {
361 path: "github/createIssue.ts".to_string(),
362 size: 2450,
363 },
364 FilePreview {
365 path: "github/listRepos.ts".to_string(),
366 size: 1200,
367 },
368 ],
369 total_files: 2,
370 total_size: 3650,
371 };
372
373 let json = serde_json::to_string_pretty(&result).unwrap();
374 assert!(json.contains("\"server_id\": \"github\""));
375 assert!(json.contains("\"total_files\": 2"));
376 assert!(json.contains("\"total_size\": 3650"));
377 assert!(json.contains("\"path\": \"github/createIssue.ts\""));
378 assert!(json.contains("\"size\": 2450"));
379 }
380
381 #[test]
382 fn test_dry_run_collects_file_metadata() {
383 let generator = ProgressiveGenerator::new().unwrap();
384 let server_info = create_mock_server_info();
385 let generated_code = generator.generate(&server_info).unwrap();
386
387 let server_dir_name = server_info.id.to_string();
388 let files: Vec<FilePreview> = generated_code
389 .files
390 .iter()
391 .map(|f| FilePreview {
392 path: format!("{}/{}", server_dir_name, f.path),
393 size: f.content.len(),
394 })
395 .collect();
396
397 assert!(!files.is_empty());
398 for file in &files {
399 assert!(file.path.starts_with("test-server/"));
400 assert!(file.size > 0);
401 }
402
403 let total_size: usize = files.iter().map(|f| f.size).sum();
404 assert_eq!(
405 total_size,
406 generated_code
407 .files
408 .iter()
409 .map(|f| f.content.len())
410 .sum::<usize>()
411 );
412 }
413
414 #[test]
415 fn test_dry_run_does_not_write_files() {
416 use std::path::Path;
417
418 let generator = ProgressiveGenerator::new().unwrap();
419 let server_info = create_mock_server_info();
420 let generated_code = generator.generate(&server_info).unwrap();
421
422 let server_dir_name = server_info.id.to_string();
424 let fake_output_path = Path::new("/tmp/dry-run-test-should-not-exist-abc123");
425 let output_path = fake_output_path.join(&server_dir_name);
426
427 let files: Vec<FilePreview> = generated_code
428 .files
429 .iter()
430 .map(|f| FilePreview {
431 path: format!("{}/{}", server_dir_name, f.path),
432 size: f.content.len(),
433 })
434 .collect();
435
436 assert!(!files.is_empty());
438
439 assert!(
441 !output_path.exists(),
442 "dry-run must not write files to disk"
443 );
444 }
445}