Skip to main content

foundry_mcp/core/ops/
update_spec.rs

1//! Core op for applying edit commands to a spec (tool-agnostic)
2
3use anyhow::Result;
4
5use crate::core::foundry;
6use crate::types::edit_commands::EditCommand;
7use crate::types::responses::{EditCommandsResponsePayload, FoundryResponse, ValidationStatus};
8
9#[derive(Debug, Clone)]
10pub struct Input {
11    pub project_name: String,
12    pub spec_name: String,
13    pub commands_json: String,
14}
15
16pub async fn run(input: Input) -> Result<FoundryResponse<EditCommandsResponsePayload>> {
17    let foundry = foundry::get_default_foundry()?;
18
19    validate_args(&input)?;
20    validate_project_exists(&foundry, &input.project_name).await?;
21
22    // Check if spec exists by trying to load it
23    foundry.load_spec(&input.project_name, &input.spec_name).await.map_err(|_| {
24        anyhow::anyhow!(
25            "Spec '{}' not found in project '{}'. Use load_project tool to see available specs: {{\"name\": \"load_project\", \"arguments\": {{\"project_name\": \"{}\"}}}}",
26            input.spec_name,
27            input.project_name,
28            input.project_name
29        )
30    })?;
31
32    let commands: Vec<EditCommand> = serde_json::from_str(&input.commands_json)
33        .map_err(|e| anyhow::anyhow!("Invalid commands JSON: {}", e))?;
34
35    let result = foundry
36        .apply_edit_commands(&input.project_name, &input.spec_name, &commands)
37        .await?;
38
39    let response_data = EditCommandsResponsePayload {
40        applied_count: result.applied_count,
41        skipped_idempotent_count: result.skipped_idempotent_count,
42        file_updates: result.file_updates,
43        errors: if result.errors.is_empty() {
44            None
45        } else {
46            Some(result.errors)
47        },
48        preview_diff: result.preview_diff,
49    };
50
51    Ok(FoundryResponse {
52        data: response_data,
53        next_steps: result.next_steps,
54        validation_status: ValidationStatus::Complete,
55        workflow_hints: result.workflow_hints,
56    })
57}
58
59fn validate_args(input: &Input) -> Result<()> {
60    if input.project_name.trim().is_empty() {
61        return Err(anyhow::anyhow!("Project name cannot be empty"));
62    }
63    if input.spec_name.trim().is_empty() {
64        return Err(anyhow::anyhow!("Spec name cannot be empty"));
65    }
66    if input.commands_json.trim().is_empty() {
67        return Err(anyhow::anyhow!("'commands' parameter is required"));
68    }
69    Ok(())
70}
71
72async fn validate_project_exists(
73    foundry: &foundry::Foundry<crate::core::backends::filesystem::FilesystemBackend>,
74    project_name: &str,
75) -> Result<()> {
76    if !foundry.project_exists(project_name).await? {
77        return Err(anyhow::anyhow!(
78            "Project '{}' not found. Use list_projects tool to see available projects: {{\"name\": \"list_projects\", \"arguments\": {{}}}}",
79            project_name
80        ));
81    }
82    Ok(())
83}