Skip to main content

systemprompt_cli/commands/build/
mcp.rs

1use anyhow::{Context, Result, anyhow, bail};
2use clap::Args;
3use std::path::Path;
4use std::process::Command;
5
6use systemprompt_loader::ExtensionLoader;
7use systemprompt_logging::CliService;
8use systemprompt_models::BuildType;
9
10use super::types::{BuildExtensionRow, BuildOutput};
11use crate::CliConfig;
12use crate::shared::command_result::CommandOutput;
13use crate::shared::project::ProjectRoot;
14
15#[derive(Debug, Clone, Copy, Args)]
16pub struct McpArgs {
17    #[arg(long, default_value = "false", help = "Build in release mode")]
18    pub release: bool,
19}
20
21pub(super) fn execute(args: McpArgs, config: &CliConfig) -> Result<CommandOutput> {
22    let project_root = ProjectRoot::discover().map_err(|e| anyhow!("{}", e))?;
23    execute_in_root(args, config, project_root.as_path())
24}
25
26pub fn execute_in_root(args: McpArgs, config: &CliConfig, root: &Path) -> Result<CommandOutput> {
27    let extensions = ExtensionLoader::get_enabled_mcp_extensions(root);
28
29    if extensions.is_empty() {
30        let output = BuildOutput {
31            extensions: vec![],
32            total: 0,
33            successful: 0,
34            release_mode: args.release,
35        };
36        return Ok(CommandOutput::table_of(
37            vec!["name", "build_type", "status"],
38            &output.extensions,
39        )
40        .with_title("Build MCP Extensions"));
41    }
42
43    if !config.is_json_output() {
44        CliService::section("Building MCP Extensions");
45    }
46
47    let mut built_extensions = Vec::new();
48    let mut successful = 0;
49
50    for ext in &extensions {
51        let binary = ext.binary_name().ok_or_else(|| {
52            anyhow!(
53                "Extension {} has no binary defined",
54                ext.manifest.extension.name
55            )
56        })?;
57
58        let build_type = ext.build_type();
59        let build_type_str = match build_type {
60            BuildType::Workspace => "workspace",
61            BuildType::Submodule => "submodule",
62        };
63
64        let build_result = match build_type {
65            BuildType::Workspace => build_workspace_crate(root, binary, args.release, config),
66            BuildType::Submodule => build_submodule_crate(&ext.path, root, args.release, config),
67        };
68
69        let status = match build_result {
70            Ok(()) => {
71                successful += 1;
72                "success".to_owned()
73            },
74            Err(e) => format!("failed: {}", e),
75        };
76
77        built_extensions.push(BuildExtensionRow {
78            name: ext.manifest.extension.name.clone(),
79            build_type: build_type_str.to_owned(),
80            status,
81        });
82    }
83
84    let total = built_extensions.len();
85    let output = BuildOutput {
86        extensions: built_extensions,
87        total,
88        successful,
89        release_mode: args.release,
90    };
91
92    if !config.is_json_output() {
93        if successful == total {
94            CliService::success(&format!("All {} MCP extensions built", total));
95        } else {
96            CliService::warning(&format!(
97                "Built {}/{} MCP extensions successfully",
98                successful, total
99            ));
100        }
101    }
102
103    Ok(
104        CommandOutput::table_of(vec!["name", "build_type", "status"], &output.extensions)
105            .with_title("Build MCP Extensions"),
106    )
107}
108
109fn build_workspace_crate(
110    project_root: &Path,
111    package: &str,
112    release: bool,
113    config: &CliConfig,
114) -> Result<()> {
115    if !config.is_json_output() {
116        CliService::info(&format!("Building {} (workspace)", package));
117    }
118
119    let mut args = vec!["build", "-p", package];
120    if release {
121        args.push("--release");
122    }
123
124    let cargo_manifest = find_cargo_manifest(project_root)?;
125
126    let status = Command::new("cargo")
127        .args(&args)
128        .arg("--manifest-path")
129        .arg(&cargo_manifest)
130        .arg("--target-dir")
131        .arg(project_root.join("target"))
132        .status()
133        .context("Failed to execute cargo")?;
134
135    if !status.success() {
136        bail!("Failed to build {}", package);
137    }
138
139    if !config.is_json_output() {
140        CliService::success(&format!("  {} built", package));
141    }
142    Ok(())
143}
144
145fn build_submodule_crate(
146    extension_path: &Path,
147    project_root: &Path,
148    release: bool,
149    config: &CliConfig,
150) -> Result<()> {
151    let name = extension_path
152        .file_name()
153        .and_then(|n| n.to_str())
154        .ok_or_else(|| anyhow!("Invalid extension path: {}", extension_path.display()))?;
155
156    if !config.is_json_output() {
157        CliService::info(&format!("Building {} (submodule)", name));
158    }
159
160    let mut args = vec!["build"];
161    if release {
162        args.push("--release");
163    }
164
165    let target_dir = project_root.join("target");
166
167    let status = Command::new("cargo")
168        .args(&args)
169        .arg("--target-dir")
170        .arg(&target_dir)
171        .current_dir(extension_path)
172        .status()
173        .context("Failed to execute cargo")?;
174
175    if !status.success() {
176        bail!("Failed to build {} at {}", name, extension_path.display());
177    }
178
179    if !config.is_json_output() {
180        CliService::success(&format!("  {} built", name));
181    }
182    Ok(())
183}
184
185fn find_cargo_manifest(project_root: &Path) -> Result<std::path::PathBuf> {
186    let manifest = project_root.join("Cargo.toml");
187    if manifest.exists() {
188        return Ok(manifest);
189    }
190    bail!("Cargo.toml not found in project root")
191}