Skip to main content

foundry_mcp/core/ops/
list_specs.rs

1//! Core op for listing specs in a project (tool-agnostic)
2
3use anyhow::{Context, Result};
4
5use crate::core::foundry;
6use crate::types::responses::{FoundryResponse, ListSpecsResponse, SpecInfo};
7use crate::utils::response::{build_incomplete_response, build_success_response};
8
9#[derive(Debug, Clone)]
10pub struct Input {
11    pub project_name: String,
12}
13
14pub async fn run(input: Input) -> Result<FoundryResponse<ListSpecsResponse>> {
15    let foundry = foundry::get_default_foundry()?;
16
17    validate_project_exists(&foundry, &input.project_name).await?;
18
19    let specs = foundry
20        .list_specs(&input.project_name)
21        .await
22        .with_context(|| format!("Failed to list specs for project '{}'", input.project_name))?;
23
24    let spec_infos: Vec<SpecInfo> = specs
25        .into_iter()
26        .map(|spec_meta| SpecInfo {
27            name: spec_meta.name,
28            feature_name: spec_meta.feature_name,
29            created_at: spec_meta.created_at,
30        })
31        .collect();
32
33    let response_data = ListSpecsResponse {
34        project_name: input.project_name.clone(),
35        specs: spec_infos.clone(),
36        total_count: spec_infos.len(),
37    };
38
39    if response_data.specs.is_empty() {
40        let next_steps = vec![
41            "No specifications found for this project - ready for specification creation"
42                .to_string(),
43            format!(
44                "You can create your first specification: mcp_foundry_create_spec {} <feature_name>",
45                input.project_name
46            ),
47            "You can use 'mcp_foundry_load_project' to see full project context".to_string(),
48        ];
49
50        let workflow_hints = vec![
51            "You can start by creating specifications to track development features".to_string(),
52            "Each spec includes implementation notes and task lists for comprehensive planning"
53                .to_string(),
54        ];
55
56        Ok(build_incomplete_response(
57            response_data,
58            next_steps,
59            workflow_hints,
60        ))
61    } else {
62        let spec_count = response_data.specs.len();
63        let mut next_steps = vec![
64            format!("Found {} specification(s) in project", spec_count),
65            format!(
66                "You can load a specific spec: mcp_foundry_load_spec {} <spec_name>",
67                input.project_name
68            ),
69        ];
70
71        if spec_count <= 5 {
72            next_steps.push("Available specs:".to_string());
73            for spec in &response_data.specs {
74                next_steps.push(format!("  - {} ({})", spec.name, spec.feature_name));
75            }
76        }
77
78        next_steps.push(format!(
79            "You can create a new spec: mcp_foundry_create_spec {} <feature_name>",
80            input.project_name
81        ));
82
83        let workflow_hints = vec![
84            "Specifications are timestamped and organized by feature for easy navigation"
85                .to_string(),
86            format!("Total specs: {}", spec_count),
87            "You can load individual specs to see detailed implementation plans".to_string(),
88            "Specs include specification content, notes, and task lists for complete context"
89                .to_string(),
90        ];
91
92        Ok(build_success_response(
93            response_data,
94            next_steps,
95            workflow_hints,
96        ))
97    }
98}
99
100async fn validate_project_exists(
101    foundry: &foundry::Foundry<crate::core::backends::filesystem::FilesystemBackend>,
102    project_name: &str,
103) -> Result<()> {
104    if !foundry.project_exists(project_name).await? {
105        return Err(anyhow::anyhow!(
106            "Project '{}' not found. Use 'mcp_foundry_list_projects' to see available projects.",
107            project_name
108        ));
109    }
110    Ok(())
111}