Skip to main content

systemprompt_cli/commands/build/
mcp.rs

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