Skip to main content

foundry_mcp/core/ops/
analyze_project.rs

1//! Core op for analyze_project (tool-agnostic)
2
3use anyhow::{Context, Result};
4
5use crate::core::foundry;
6use crate::core::validation::{ContentType, validate_content};
7use crate::types::project::ProjectConfig;
8use crate::types::responses::{AnalyzeProjectResponse, FoundryResponse, ValidationStatus};
9
10#[derive(Debug, Clone)]
11pub struct Input {
12    pub project_name: String,
13    pub vision: String,
14    pub tech_stack: String,
15    pub summary: String,
16}
17
18pub async fn run(input: Input) -> Result<FoundryResponse<AnalyzeProjectResponse>> {
19    let foundry = foundry::get_default_foundry()?;
20
21    validate_project_name(&input.project_name).with_context(|| "Project name validation failed")?;
22
23    validate_content_sizes(&input.vision, &input.tech_stack, &input.summary)
24        .with_context(|| "Content size validation failed")?;
25
26    if foundry
27        .project_exists(&input.project_name)
28        .await
29        .with_context(|| format!("Failed to check if project '{}' exists", input.project_name))?
30    {
31        return Err(anyhow::anyhow!(
32            "Project '{}' already exists. Use MCP to discover existing projects: {{\"name\": \"list_projects\", \"arguments\": {{}}}} or choose a different name.",
33            input.project_name
34        ));
35    }
36
37    let vision_validation = validate_content(ContentType::Vision, &input.vision);
38    let tech_stack_validation = validate_content(ContentType::TechStack, &input.tech_stack);
39    let summary_validation = validate_content(ContentType::Summary, &input.summary);
40
41    let mut validation_errors = Vec::new();
42    if !vision_validation.is_valid {
43        validation_errors.extend(vision_validation.errors);
44    }
45    if !tech_stack_validation.is_valid {
46        validation_errors.extend(tech_stack_validation.errors);
47    }
48    if !summary_validation.is_valid {
49        validation_errors.extend(summary_validation.errors);
50    }
51
52    if !validation_errors.is_empty() {
53        let error_count = validation_errors.len();
54        return Err(anyhow::anyhow!(
55            "Content validation failed with {} error(s):\n{}",
56            error_count,
57            validation_errors.join("\n")
58        ));
59    }
60
61    // Create project using the foundry façade
62    let project_config = ProjectConfig {
63        name: input.project_name.clone(),
64        vision: input.vision,
65        tech_stack: input.tech_stack,
66        summary: input.summary,
67    };
68
69    foundry
70        .create_project(project_config)
71        .await
72        .with_context(|| format!("Failed to create project '{}'", input.project_name))?;
73
74    let files_created = vec![
75        "vision.md".to_string(),
76        "tech-stack.md".to_string(),
77        "summary.md".to_string(),
78        "specs/".to_string(),
79    ];
80
81    let response_data = AnalyzeProjectResponse {
82        project_name: input.project_name.clone(),
83        files_created,
84    };
85
86    let next_steps = vec![
87        format!(
88            "Project '{}' analyzed and structure created from your codebase analysis",
89            input.project_name
90        ),
91        "Your analyzed content has been structured and is ready for development work".to_string(),
92        format!(
93            "Create a spec: {{\"name\": \"create_spec\", \"arguments\": {{\"project_name\": \"{}\", \"feature_name\": \"<feature>\", \"spec\": \"...\", \"tasks\": \"...\", \"notes\": \"...\"}}}}; Load project: {{\"name\": \"load_project\", \"arguments\": {{\"project_name\": \"{}\"}}}}; Or continue codebase analysis",
94            input.project_name, input.project_name
95        ),
96    ];
97
98    let mut workflow_hints: Vec<String> = vec![
99        "Project structure created based on your codebase analysis".to_string(),
100        format!("Create a spec when you identify a feature: {{\"name\": \"create_spec\", \"arguments\": {{\"project_name\": \"{}\", \"feature_name\": \"<feature>\", \"spec\": \"...\", \"tasks\": \"...\", \"notes\": \"...\"}}}}", input.project_name),
101        format!("Load full project context: {{\"name\": \"load_project\", \"arguments\": {{\"project_name\": \"{}\"}}}}", input.project_name),
102        "You can continue using codebase_search, grep_search, read_file for deeper analysis".to_string(),
103        "Tool selection guidance: {\"name\": \"get_foundry_help\", \"arguments\": {\"topic\": \"decision-points\"}}".to_string(),
104    ];
105
106    workflow_hints.extend(vision_validation.suggestions);
107    workflow_hints.extend(tech_stack_validation.suggestions);
108    workflow_hints.extend(summary_validation.suggestions);
109
110    Ok(FoundryResponse {
111        data: response_data,
112        next_steps,
113        validation_status: ValidationStatus::Complete,
114        workflow_hints,
115    })
116}
117
118fn validate_project_name(name: &str) -> Result<()> {
119    if name.trim().is_empty() {
120        return Err(anyhow::anyhow!(
121            "Project name cannot be empty. Please provide a descriptive project name."
122        ));
123    }
124    if name.len() > 100 {
125        return Err(anyhow::anyhow!(
126            "Project name too long ({} characters). Please keep it under 100 characters for filesystem compatibility.",
127            name.len()
128        ));
129    }
130    let invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
131    if let Some(invalid_char) = name.chars().find(|c| invalid_chars.contains(c)) {
132        return Err(anyhow::anyhow!(
133            "Project name contains invalid character '{}'. Please use only letters, numbers, hyphens, and underscores.",
134            invalid_char
135        ));
136    }
137    let reserved_names = ["CON", "PRN", "AUX", "NUL"];
138    let upper_name = name.to_uppercase();
139    if reserved_names.contains(&upper_name.as_str()) {
140        return Err(anyhow::anyhow!(
141            "Project name '{}' is reserved by the operating system. Please choose a different name.",
142            name
143        ));
144    }
145    Ok(())
146}
147
148fn validate_content_sizes(vision: &str, tech_stack: &str, summary: &str) -> Result<()> {
149    const MAX_CONTENT_SIZE: usize = 50_000;
150    if vision.len() > MAX_CONTENT_SIZE {
151        return Err(anyhow::anyhow!(
152            "Vision content too large ({} characters). Please keep it under {} characters for optimal performance.",
153            vision.len(),
154            MAX_CONTENT_SIZE
155        ));
156    }
157    if tech_stack.len() > MAX_CONTENT_SIZE {
158        return Err(anyhow::anyhow!(
159            "Tech stack content too large ({} characters). Please keep it under {} characters for optimal performance.",
160            tech_stack.len(),
161            MAX_CONTENT_SIZE
162        ));
163    }
164    if summary.len() > MAX_CONTENT_SIZE {
165        return Err(anyhow::anyhow!(
166            "Summary content too large ({} characters). Please keep it under {} characters for optimal performance.",
167            summary.len(),
168            MAX_CONTENT_SIZE
169        ));
170    }
171    Ok(())
172}