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