foundry_mcp/core/ops/
create_spec.rs1use anyhow::{Context, Result};
4
5use crate::core::{foundry, validation};
6use crate::types::responses::{CreateSpecResponse, FoundryResponse, ValidationStatus};
7use crate::types::spec::{SpecConfig, SpecContentData};
8use crate::utils::paths;
9
10#[derive(Debug, Clone)]
12pub struct Input {
13 pub project_name: String,
14 pub feature_name: String,
15 pub spec: String,
16 pub notes: String,
17 pub tasks: String,
18}
19
20pub async fn run(input: Input) -> Result<FoundryResponse<CreateSpecResponse>> {
22 let foundry = foundry::get_default_foundry()?;
23
24 validate_project_exists(&foundry, &input.project_name).await?;
26
27 validate_feature_name(&input.feature_name)?;
29
30 let content_validation = validate_content(&input)?;
32 let has_validation_warnings = content_validation
33 .iter()
34 .any(|(_, result)| !result.is_valid);
35
36 let spec_config = build_spec_config(input);
38 let created_spec = foundry
39 .create_spec(spec_config)
40 .await
41 .context("Failed to create specification")?;
42
43 let response_data = CreateSpecResponse {
45 project_name: created_spec.project_name.clone(),
46 spec_name: created_spec.name.clone(),
47 created_at: created_spec.created_at.clone(),
48 spec_path: created_spec.path.to_string_lossy().to_string(),
49 files_created: vec![
50 format!("{}/spec.md", created_spec.name),
51 format!("{}/notes.md", created_spec.name),
52 format!("{}/task-list.md", created_spec.name),
53 ],
54 };
55
56 let validation_status = if has_validation_warnings {
57 ValidationStatus::Incomplete
58 } else {
59 ValidationStatus::Complete
60 };
61
62 let next_steps = generate_next_steps(&created_spec.project_name, &created_spec.name);
63 let workflow_hints = generate_workflow_hints(&content_validation);
64
65 Ok(FoundryResponse {
66 data: response_data,
67 next_steps,
68 validation_status,
69 workflow_hints,
70 })
71}
72
73async fn validate_project_exists(
75 foundry: &foundry::Foundry<crate::core::backends::filesystem::FilesystemBackend>,
76 project_name: &str,
77) -> Result<()> {
78 if !foundry.project_exists(project_name).await? {
79 return Err(anyhow::anyhow!(
80 "Project '{}' not found. Use list_projects via MCP to see available projects: {{\"name\": \"list_projects\", \"arguments\": {{}}}}",
81 project_name
82 ));
83 }
84 Ok(())
85}
86
87fn validate_feature_name(feature_name: &str) -> Result<()> {
89 paths::validate_feature_name(feature_name).context("Feature name validation failed")
90}
91
92fn validate_content(input: &Input) -> Result<Vec<(&'static str, validation::ValidationResult)>> {
94 let validations = vec![
95 (
96 "Spec Content",
97 validation::validate_content(validation::ContentType::Spec, &input.spec),
98 ),
99 (
100 "Implementation Notes",
101 validation::validate_content(validation::ContentType::Notes, &input.notes),
102 ),
103 (
104 "Task List",
105 validation::validate_content(validation::ContentType::Tasks, &input.tasks),
106 ),
107 ];
108
109 Ok(validations)
110}
111
112fn build_spec_config(input: Input) -> SpecConfig {
114 SpecConfig {
115 project_name: input.project_name,
116 feature_name: input.feature_name,
117 content: SpecContentData {
118 spec: input.spec,
119 notes: input.notes,
120 tasks: input.tasks,
121 },
122 }
123}
124
125fn generate_next_steps(project_name: &str, spec_name: &str) -> Vec<String> {
127 vec![
128 format!(
129 "Specification '{}' created successfully from your provided content",
130 spec_name
131 ),
132 "Your specification content has been structured and is ready for implementation work"
133 .to_string(),
134 format!(
135 "Load spec: {{\"name\": \"load_spec\", \"arguments\": {{\"project_name\": \"{}\", \"spec_name\": \"{}\"}}}}; Load project: {{\"name\": \"load_project\", \"arguments\": {{\"project_name\": \"{}\"}}}}",
136 project_name, spec_name, project_name
137 ),
138 ]
139}
140
141fn generate_workflow_hints(
143 validation_results: &[(&'static str, validation::ValidationResult)],
144) -> Vec<String> {
145 let mut hints = vec![
146 "📋 DOCUMENT PURPOSE: Your spec content serves as COMPLETE CONTEXT for future implementation".to_string(),
147 "🎯 CONTEXT TEST: Could someone with no prior knowledge implement this feature using only your spec documents?".to_string(),
148 "Your specification content has been structured with task-list.md for implementation tracking".to_string(),
149 "Review spec: {\"name\": \"load_spec\", \"arguments\": {\"project_name\": \"<project>\", \"spec_name\": \"<spec>\"}}".to_string(),
150 "Load project: {\"name\": \"load_project\", \"arguments\": {\"project_name\": \"<project>\"}}".to_string(),
151 ];
152
153 let invalid_content: Vec<&str> = validation_results
155 .iter()
156 .filter_map(|(name, result)| if !result.is_valid { Some(*name) } else { None })
157 .collect();
158
159 if !invalid_content.is_empty() {
160 hints.push(format!(
161 "You might consider reviewing content quality for: {}",
162 invalid_content.join(", ")
163 ));
164 }
165
166 hints.push("Tool selection guidance: {\"name\": \"get_foundry_help\", \"arguments\": {\"topic\": \"decision-points\"}}".to_string());
167
168 hints
169}