1use 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)]
61fn format_size(bytes: usize) -> String {
62 if bytes < 1024 {
63 format!("{bytes} B")
64 } else if bytes < 1024 * 1024 {
65 format!("{:.1} KB", bytes as f64 / 1024.0)
66 } else {
67 format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
68 }
69}
70
71#[allow(clippy::too_many_arguments)]
114pub async fn run(
115 from_config: Option<String>,
116 server: Option<String>,
117 args: Vec<String>,
118 env: Vec<String>,
119 cwd: Option<String>,
120 http: Option<String>,
121 sse: Option<String>,
122 headers: Vec<String>,
123 name: Option<String>,
124 output_dir: Option<PathBuf>,
125 dry_run: bool,
126 connect_timeout_secs: Option<u64>,
127 discover_timeout_secs: Option<u64>,
128 output_format: OutputFormat,
129) -> Result<ExitCode> {
130 let (server_id, server_config) = if let Some(config_name) = from_config {
131 debug!(
132 "Loading server configuration from ~/.claude/mcp.json: {}",
133 config_name
134 );
135 load_server_from_config(&config_name)?
136 } else {
137 build_server_config(
138 server,
139 args,
140 env,
141 cwd,
142 http,
143 sse,
144 headers,
145 connect_timeout_secs,
146 discover_timeout_secs,
147 )?
148 };
149
150 info!("Connecting to MCP server: {}", server_id);
151
152 let mut introspector = Introspector::new();
153 let server_info = introspector
154 .discover_server(server_id, &server_config)
155 .await
156 .context("failed to introspect MCP server")?;
157
158 info!(
159 "Discovered {} tools from server '{}'",
160 server_info.tools.len(),
161 server_info.name
162 );
163
164 if server_info.tools.is_empty() {
165 warn!("Server has no tools to generate code for");
166 return Ok(ExitCode::SUCCESS);
167 }
168
169 let mut server_info = server_info;
172 if let Some(ref custom_name) = name {
173 server_info.id = mcp_execution_core::ServerId::new(custom_name);
174 }
175
176 let server_dir_name = server_info.id.to_string();
177
178 let generator = ProgressiveGenerator::new().context("failed to create code generator")?;
179 let generated_code = generator
180 .generate(&server_info)
181 .context("failed to generate TypeScript code")?;
182
183 info!(
184 "Generated {} files for progressive loading",
185 generated_code.file_count()
186 );
187
188 let base_dir = if let Some(custom_dir) = output_dir {
189 custom_dir
190 } else {
191 dirs::home_dir()
192 .context("failed to get home directory")?
193 .join(".claude")
194 .join("servers")
195 };
196 let output_path = base_dir.join(&server_dir_name);
197
198 if dry_run {
199 let files: Vec<FilePreview> = generated_code
200 .files
201 .iter()
202 .map(|f| FilePreview {
203 path: format!("{}/{}", server_dir_name, f.path),
204 size: f.content.len(),
205 })
206 .collect();
207 let total_size: usize = files.iter().map(|f| f.size).sum();
208 let total_files = files.len();
209
210 let result = DryRunResult {
211 server_id: server_info.id.to_string(),
212 server_name: server_info.name,
213 output_path: output_path.display().to_string(),
214 files,
215 total_files,
216 total_size,
217 };
218
219 match output_format {
220 OutputFormat::Json => {
221 println!("{}", serde_json::to_string_pretty(&result)?);
222 }
223 OutputFormat::Text => {
224 println!("Server: {} ({})", result.server_name, result.server_id);
225 println!(
226 "Would generate {} files ({}) to {}/",
227 result.total_files,
228 format_size(result.total_size),
229 result.output_path
230 );
231 }
232 OutputFormat::Pretty => {
233 println!(
234 "Would generate {} files to {}/:",
235 result.total_files, result.output_path
236 );
237 println!();
238 for f in &result.files {
239 println!(" - {} ({})", f.path, format_size(f.size));
240 }
241 println!();
242 println!(
243 "Total: {} files, ~{}",
244 result.total_files,
245 format_size(result.total_size)
246 );
247 }
248 }
249
250 return Ok(ExitCode::SUCCESS);
251 }
252
253 let vfs = FilesBuilder::from_generated_code(generated_code, "/")
256 .build()
257 .context("failed to build VFS")?;
258
259 info!("Exporting files to: {}", output_path.display());
260
261 std::fs::create_dir_all(&base_dir).context("failed to create output directory")?;
266 vfs.export_to_filesystem(&output_path)
267 .context("failed to export files to filesystem")?;
268
269 let result = GenerationResult {
270 server_id: server_info.id.to_string(),
271 server_name: server_info.name.clone(),
272 tool_count: server_info.tools.len(),
273 output_path: output_path.display().to_string(),
274 };
275
276 match output_format {
277 OutputFormat::Json => {
278 println!("{}", serde_json::to_string_pretty(&result)?);
279 }
280 OutputFormat::Text => {
281 println!("Server: {} ({})", result.server_name, result.server_id);
282 println!("Generated {} tool files", result.tool_count);
283 println!("Output: {}", result.output_path);
284 }
285 OutputFormat::Pretty => {
286 println!("✓ Successfully generated progressive loading files");
287 println!(" Server: {} ({})", result.server_name, result.server_id);
288 println!(" Tools: {}", result.tool_count);
289 println!(" Location: {}", result.output_path);
290 }
291 }
292
293 Ok(ExitCode::SUCCESS)
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use mcp_execution_core::ServerId;
300 use mcp_execution_introspector::{ServerCapabilities, ServerInfo, ToolInfo};
301 use serde_json::json;
302
303 fn create_mock_server_info() -> ServerInfo {
304 ServerInfo {
305 id: ServerId::new("test-server"),
306 name: "Test Server".to_string(),
307 version: "1.0.0".to_string(),
308 tools: vec![ToolInfo {
309 name: mcp_execution_core::ToolName::new("test_tool"),
310 description: "A test tool".to_string(),
311 input_schema: json!({
312 "type": "object",
313 "properties": {
314 "param": {"type": "string"}
315 }
316 }),
317 output_schema: None,
318 }],
319 capabilities: ServerCapabilities {
320 supports_tools: true,
321 supports_resources: false,
322 supports_prompts: false,
323 },
324 }
325 }
326
327 #[test]
328 fn test_generation_result_serialization() {
329 let result = GenerationResult {
330 server_id: "test".to_string(),
331 server_name: "Test Server".to_string(),
332 tool_count: 5,
333 output_path: "/path/to/output".to_string(),
334 };
335
336 let json = serde_json::to_string(&result).unwrap();
337 assert!(json.contains("\"server_id\":\"test\""));
338 assert!(json.contains("\"tool_count\":5"));
339 }
340
341 #[test]
342 fn test_progressive_generator_creation() {
343 let generator = ProgressiveGenerator::new();
344 assert!(generator.is_ok());
345 }
346
347 #[test]
348 fn test_progressive_code_generation() {
349 let generator = ProgressiveGenerator::new().unwrap();
350 let server_info = create_mock_server_info();
351
352 let result = generator.generate(&server_info);
353 assert!(result.is_ok());
354
355 let code = result.unwrap();
356 assert!(code.file_count() > 0);
357 }
358
359 #[test]
360 fn test_format_size_bytes() {
361 assert_eq!(format_size(0), "0 B");
362 assert_eq!(format_size(512), "512 B");
363 assert_eq!(format_size(1023), "1023 B");
364 }
365
366 #[test]
367 fn test_format_size_kilobytes() {
368 assert_eq!(format_size(1024), "1.0 KB");
369 assert_eq!(format_size(2048), "2.0 KB");
370 assert_eq!(format_size(1536), "1.5 KB");
371 }
372
373 #[test]
374 fn test_format_size_megabytes() {
375 assert_eq!(format_size(1024 * 1024), "1.0 MB");
376 assert_eq!(format_size(2 * 1024 * 1024), "2.0 MB");
377 }
378
379 #[test]
380 fn test_dry_run_result_serialization() {
381 let result = DryRunResult {
382 server_id: "github".to_string(),
383 server_name: "GitHub MCP Server".to_string(),
384 output_path: "/home/user/.claude/servers/github".to_string(),
385 files: vec![
386 FilePreview {
387 path: "github/createIssue.ts".to_string(),
388 size: 2450,
389 },
390 FilePreview {
391 path: "github/listRepos.ts".to_string(),
392 size: 1200,
393 },
394 ],
395 total_files: 2,
396 total_size: 3650,
397 };
398
399 let json = serde_json::to_string_pretty(&result).unwrap();
400 assert!(json.contains("\"server_id\": \"github\""));
401 assert!(json.contains("\"total_files\": 2"));
402 assert!(json.contains("\"total_size\": 3650"));
403 assert!(json.contains("\"path\": \"github/createIssue.ts\""));
404 assert!(json.contains("\"size\": 2450"));
405 }
406
407 #[test]
408 fn test_dry_run_collects_file_metadata() {
409 let generator = ProgressiveGenerator::new().unwrap();
410 let server_info = create_mock_server_info();
411 let generated_code = generator.generate(&server_info).unwrap();
412
413 let server_dir_name = server_info.id.to_string();
414 let files: Vec<FilePreview> = generated_code
415 .files
416 .iter()
417 .map(|f| FilePreview {
418 path: format!("{}/{}", server_dir_name, f.path),
419 size: f.content.len(),
420 })
421 .collect();
422
423 assert!(!files.is_empty());
424 for file in &files {
425 assert!(file.path.starts_with("test-server/"));
426 assert!(file.size > 0);
427 }
428
429 let total_size: usize = files.iter().map(|f| f.size).sum();
430 assert_eq!(
431 total_size,
432 generated_code
433 .files
434 .iter()
435 .map(|f| f.content.len())
436 .sum::<usize>()
437 );
438 }
439
440 #[test]
441 fn test_dry_run_does_not_write_files() {
442 use std::path::Path;
443
444 let generator = ProgressiveGenerator::new().unwrap();
445 let server_info = create_mock_server_info();
446 let generated_code = generator.generate(&server_info).unwrap();
447
448 let server_dir_name = server_info.id.to_string();
450 let fake_output_path = Path::new("/tmp/dry-run-test-should-not-exist-abc123");
451 let output_path = fake_output_path.join(&server_dir_name);
452
453 let files: Vec<FilePreview> = generated_code
454 .files
455 .iter()
456 .map(|f| FilePreview {
457 path: format!("{}/{}", server_dir_name, f.path),
458 size: f.content.len(),
459 })
460 .collect();
461
462 assert!(!files.is_empty());
464
465 assert!(
467 !output_path.exists(),
468 "dry-run must not write files to disk"
469 );
470 }
471
472 #[tokio::test]
473 async fn test_run_zero_connect_timeout_override_rejected_by_validation() {
474 let result = run(
477 None,
478 Some("nonexistent-server-timeout-test".to_string()),
479 vec![],
480 vec![],
481 None,
482 None,
483 None,
484 vec![],
485 None,
486 None,
487 false,
488 Some(0),
489 None,
490 OutputFormat::Json,
491 )
492 .await;
493
494 assert!(result.is_err());
495 let err = result.unwrap_err();
496 let chain_msg = err
497 .chain()
498 .map(ToString::to_string)
499 .collect::<Vec<_>>()
500 .join(" | ");
501 assert!(
502 chain_msg.contains("greater than zero"),
503 "expected connect_timeout validation error in the error chain, got: {chain_msg}"
504 );
505 }
506
507 #[tokio::test]
508 async fn test_run_with_valid_timeout_overrides_reaches_connection_attempt() {
509 let result = run(
511 None,
512 Some("nonexistent-server-timeout-test-2".to_string()),
513 vec![],
514 vec![],
515 None,
516 None,
517 None,
518 vec![],
519 None,
520 None,
521 false,
522 Some(5),
523 Some(90),
524 OutputFormat::Json,
525 )
526 .await;
527
528 assert!(result.is_err());
529 let err_msg = result.unwrap_err().to_string();
530 assert!(err_msg.contains("failed to introspect MCP server"));
531 }
532}