systemprompt_cli/commands/build/
mcp.rs1use 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}