Skip to main content

mcp_execution_cli/commands/
generate.rs

1//! Generate command implementation.
2//!
3//! Generates progressive loading TypeScript files from MCP server tool definitions.
4//! This command:
5//! 1. Introspects the server to discover tools and schemas
6//! 2. Generates TypeScript files for progressive loading (one file per tool)
7//! 3. Saves files to `~/.claude/servers/{server-id}/` directory
8
9use 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/// Result of progressive loading code generation.
20#[derive(Debug, Serialize)]
21struct GenerationResult {
22    /// Server ID
23    server_id: String,
24    /// Server name
25    server_name: String,
26    /// Number of tools generated
27    tool_count: usize,
28    /// Path where files were saved
29    output_path: String,
30}
31
32/// Runs the generate command.
33///
34/// Generates progressive loading TypeScript files from an MCP server.
35///
36/// This command performs the following steps:
37/// 1. Builds `ServerConfig` from CLI arguments or loads from ~/.claude/mcp.json
38/// 2. Introspects the MCP server to discover tools
39/// 3. Generates TypeScript files (one per tool) using progressive loading pattern
40/// 4. Exports VFS to `~/.claude/servers/{server-id}/` directory
41///
42/// # Arguments
43///
44/// * `from_config` - Load server config from ~/.claude/mcp.json by name
45/// * `server` - Server command (binary name or path), None for HTTP/SSE
46/// * `args` - Arguments to pass to the server command
47/// * `env` - Environment variables in KEY=VALUE format
48/// * `cwd` - Working directory for the server process
49/// * `http` - HTTP transport URL
50/// * `sse` - SSE transport URL
51/// * `headers` - HTTP headers in KEY=VALUE format
52/// * `name` - Custom server name for directory (default: `server_id`)
53/// * `output_dir` - Custom output directory (default: ~/.claude/servers/)
54/// * `output_format` - Output format (json, text, pretty)
55///
56/// # Errors
57///
58/// Returns an error if:
59/// - Server configuration is invalid
60/// - Server not found in mcp.json (when using --from-config)
61/// - Server connection fails
62/// - Tool introspection fails
63/// - Code generation fails
64/// - File export fails
65#[allow(clippy::too_many_arguments)]
66pub async fn run(
67    from_config: Option<String>,
68    server: Option<String>,
69    args: Vec<String>,
70    env: Vec<String>,
71    cwd: Option<String>,
72    http: Option<String>,
73    sse: Option<String>,
74    headers: Vec<String>,
75    name: Option<String>,
76    output_dir: Option<PathBuf>,
77    output_format: OutputFormat,
78) -> Result<ExitCode> {
79    // Build server config: either from mcp.json or from CLI arguments
80    let (server_id, server_config) = if let Some(config_name) = from_config {
81        debug!(
82            "Loading server configuration from ~/.claude/mcp.json: {}",
83            config_name
84        );
85        load_server_from_config(&config_name)?
86    } else {
87        build_server_config(server, args, env, cwd, http, sse, headers)?
88    };
89
90    info!("Connecting to MCP server: {}", server_id);
91
92    // Introspect server
93    let mut introspector = Introspector::new();
94    let server_info = introspector
95        .discover_server(server_id, &server_config)
96        .await
97        .context("failed to introspect MCP server")?;
98
99    info!(
100        "Discovered {} tools from server '{}'",
101        server_info.tools.len(),
102        server_info.name
103    );
104
105    if server_info.tools.is_empty() {
106        warn!("Server has no tools to generate code for");
107        return Ok(ExitCode::SUCCESS);
108    }
109
110    // Override server_info.id with custom name if provided
111    // This ensures generated code uses the correct server_id that matches mcp.json
112    let mut server_info = server_info;
113    if let Some(ref custom_name) = name {
114        server_info.id = mcp_execution_core::ServerId::new(custom_name);
115    }
116
117    // Determine server directory name (use custom name if provided, otherwise server_id)
118    let server_dir_name = server_info.id.to_string();
119
120    // Generate progressive loading files
121    let generator = ProgressiveGenerator::new().context("failed to create code generator")?;
122
123    let generated_code = generator
124        .generate(&server_info)
125        .context("failed to generate TypeScript code")?;
126
127    info!(
128        "Generated {} files for progressive loading",
129        generated_code.file_count()
130    );
131
132    // Build VFS with generated code
133    // Note: base_path should be "/" because generated files already have flat structure
134    // The server_dir_name will be used when exporting to filesystem
135    let vfs = FilesBuilder::from_generated_code(generated_code, "/")
136        .build()
137        .context("failed to build VFS")?;
138
139    // Determine output directory
140    // Always append server_dir_name to ensure proper structure
141    let base_dir = if let Some(custom_dir) = output_dir {
142        custom_dir
143    } else {
144        dirs::home_dir()
145            .context("failed to get home directory")?
146            .join(".claude")
147            .join("servers")
148    };
149
150    let output_path = base_dir.join(&server_dir_name);
151
152    info!("Exporting files to: {}", output_path.display());
153
154    // Create output directory if it doesn't exist
155    std::fs::create_dir_all(&output_path).context("failed to create output directory")?;
156
157    // Export VFS to filesystem
158    vfs.export_to_filesystem(&output_path)
159        .context("failed to export files to filesystem")?;
160
161    // Prepare result
162    let result = GenerationResult {
163        server_id: server_info.id.to_string(),
164        server_name: server_info.name.clone(),
165        tool_count: server_info.tools.len(),
166        output_path: output_path.display().to_string(),
167    };
168
169    // Output result
170    match output_format {
171        OutputFormat::Json => {
172            println!("{}", serde_json::to_string_pretty(&result)?);
173        }
174        OutputFormat::Text => {
175            println!("Server: {} ({})", result.server_name, result.server_id);
176            println!("Generated {} tool files", result.tool_count);
177            println!("Output: {}", result.output_path);
178        }
179        OutputFormat::Pretty => {
180            println!("✓ Successfully generated progressive loading files");
181            println!("  Server: {} ({})", result.server_name, result.server_id);
182            println!("  Tools: {}", result.tool_count);
183            println!("  Location: {}", result.output_path);
184        }
185    }
186
187    Ok(ExitCode::SUCCESS)
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use mcp_execution_core::ServerId;
194    use mcp_execution_introspector::{ServerCapabilities, ServerInfo, ToolInfo};
195    use serde_json::json;
196
197    fn create_mock_server_info() -> ServerInfo {
198        ServerInfo {
199            id: ServerId::new("test-server"),
200            name: "Test Server".to_string(),
201            version: "1.0.0".to_string(),
202            tools: vec![ToolInfo {
203                name: mcp_execution_core::ToolName::new("test_tool"),
204                description: "A test tool".to_string(),
205                input_schema: json!({
206                    "type": "object",
207                    "properties": {
208                        "param": {"type": "string"}
209                    }
210                }),
211                output_schema: None,
212            }],
213            capabilities: ServerCapabilities {
214                supports_tools: true,
215                supports_resources: false,
216                supports_prompts: false,
217            },
218        }
219    }
220
221    #[test]
222    fn test_generation_result_serialization() {
223        let result = GenerationResult {
224            server_id: "test".to_string(),
225            server_name: "Test Server".to_string(),
226            tool_count: 5,
227            output_path: "/path/to/output".to_string(),
228        };
229
230        let json = serde_json::to_string(&result).unwrap();
231        assert!(json.contains("\"server_id\":\"test\""));
232        assert!(json.contains("\"tool_count\":5"));
233    }
234
235    #[test]
236    fn test_progressive_generator_creation() {
237        let generator = ProgressiveGenerator::new();
238        assert!(generator.is_ok());
239    }
240
241    #[test]
242    fn test_progressive_code_generation() {
243        let generator = ProgressiveGenerator::new().unwrap();
244        let server_info = create_mock_server_info();
245
246        let result = generator.generate(&server_info);
247        assert!(result.is_ok());
248
249        let code = result.unwrap();
250        assert!(code.file_count() > 0);
251    }
252}