use crate::cli::commands;
use crate::cli::error::{CliResult, ErrorFactory};
use crate::cli::ui;
use crate::commands::install_mcp_service;
use crate::logging::{log_error, log_info, log_success};
use colored::Colorize;
use serde_json::{json, Value};
use std::fs;
use std::net::{IpAddr, Ipv4Addr};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use xbp_mcp::McpConfig;
pub async fn handle_mcp(cmd: commands::McpCmd, debug: bool) -> CliResult<()> {
match cmd.command {
commands::McpSubCommand::Serve(serve_cmd) => {
if serve_cmd.detach {
return spawn_detached_mcp_server(serve_cmd, debug).await;
}
let config = build_mcp_config(&serve_cmd);
let _ = log_info(
"mcp",
&format!(
"Starting MCP server on {} (SSE: {}/sse)",
config.socket_addr(),
config.base_url()
),
None,
)
.await;
if serve_cmd.stdio {
if let Err(error) = xbp_mcp::serve_stdio(&config.xbp_executable).await {
return Err(ErrorFactory::operation(
"mcp",
"run MCP stdio server",
error.to_string(),
None,
));
}
} else if let Err(error) = xbp_mcp::serve(config).await {
return Err(ErrorFactory::operation(
"mcp",
"run MCP HTTP server",
error.to_string(),
None,
));
}
}
commands::McpSubCommand::Install(install_cmd) => {
if let Err(error) = ui::with_loader(
&format!(
"Installing xbp-mcp.service on {}:{}",
install_cmd.bind, install_cmd.port
),
install_mcp_service(install_cmd.port, &install_cmd.bind, debug),
)
.await
{
let _ = log_error("mcp", "MCP systemd install failed", Some(&error)).await;
return Err(ErrorFactory::operation(
"mcp",
"install xbp-mcp.service",
error,
Some("Check systemd availability and permissions, then retry."),
));
}
print_cursor_config(&build_mcp_config_from_install(&install_cmd));
let _ = log_success(
"mcp",
&format!(
"xbp-mcp.service installed on {}:{}",
install_cmd.bind, install_cmd.port
),
None,
)
.await;
}
commands::McpSubCommand::Status(status_cmd) => {
print_mcp_status(&build_mcp_config_from_status(&status_cmd)).await;
}
commands::McpSubCommand::Config(config_cmd) => {
print_cursor_config(&build_mcp_config_from_config(&config_cmd));
}
commands::McpSubCommand::Inspector(inspector_cmd) => {
run_mcp_inspector(&inspector_cmd).await?;
}
}
Ok(())
}
fn build_mcp_config(cmd: &commands::McpServeCmd) -> McpConfig {
McpConfig {
bind: cmd.bind.parse().unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)),
port: cmd.port,
xbp_executable: std::env::current_exe()
.ok()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "xbp".to_string()),
}
}
fn build_mcp_config_from_install(cmd: &commands::McpInstallCmd) -> McpConfig {
McpConfig {
bind: cmd.bind.parse().unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)),
port: cmd.port,
xbp_executable: "xbp".to_string(),
}
}
fn build_mcp_config_from_status(cmd: &commands::McpStatusCmd) -> McpConfig {
McpConfig {
bind: cmd.bind.parse().unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)),
port: cmd.port,
xbp_executable: "xbp".to_string(),
}
}
fn build_mcp_config_from_config(cmd: &commands::McpConfigCmd) -> McpConfig {
McpConfig {
bind: cmd.bind.parse().unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)),
port: cmd.port,
xbp_executable: "xbp".to_string(),
}
}
fn print_cursor_config(config: &McpConfig) {
ui::configure_color_output();
println!();
println!("{}", "Cursor MCP configuration".bright_magenta().bold());
ui::divider(32);
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"mcpServers": {
"xbp": {
"url": config.cursor_sse_url()
}
}
}))
.unwrap_or_default()
.dimmed()
);
println!();
println!(
"{} {}",
"Health:".bright_cyan(),
format!("{}/health", config.base_url()).bright_white()
);
println!(
"{} {}",
"Tip:".bright_yellow(),
"Ask the agent to run xbp_workers_logs_build with failed=true for worker CI errors."
.bright_black()
);
}
async fn print_mcp_status(config: &McpConfig) {
ui::configure_color_output();
let health_url = format!("{}/health", config.base_url());
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(3))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
println!();
println!("{}", "xbp mcp status".bright_magenta().bold());
ui::divider(20);
println!(
" {} {}",
"URL".bright_cyan(),
config.cursor_sse_url().bright_white()
);
match client.get(&health_url).send().await {
Ok(response) if response.status().is_success() => {
let body = response.text().await.unwrap_or_default();
ui::status_line("MCP server", &body, true);
}
Ok(response) => {
ui::status_line("MCP server", &format!("HTTP {}", response.status()), false);
}
Err(error) => {
ui::status_line("MCP server", &format!("not reachable ({error})"), false);
ui::tip("Start with `xbp mcp serve` or `xbp mcp install` on Linux.");
}
}
println!();
}
async fn run_mcp_inspector(cmd: &commands::McpInspectorCmd) -> CliResult<()> {
let config = build_mcp_config_from_inspector(cmd);
let cwd = std::env::current_dir().map_err(|error| {
ErrorFactory::operation("mcp", "resolve current directory", error.to_string(), None)
})?;
let playbook = build_inspector_playbook(&config, &cwd, cmd.inspector_port);
if cmd.write_config || cmd.launch || !cmd.json {
write_inspector_config(&playbook.config_path, &config)?;
write_inspector_presets(&playbook.config_path, &playbook.json)?;
}
if cmd.json {
println!(
"{}",
serde_json::to_string_pretty(&playbook.json).unwrap_or_default()
);
} else {
print_inspector_playbook(&playbook);
}
if cmd.launch {
launch_inspector(&playbook)?;
}
Ok(())
}
fn build_mcp_config_from_inspector(cmd: &commands::McpInspectorCmd) -> McpConfig {
McpConfig {
bind: cmd.bind.parse().unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)),
port: cmd.port,
xbp_executable: std::env::current_exe()
.ok()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "xbp".to_string()),
}
}
struct InspectorPlaybook {
json: Value,
config_path: PathBuf,
launch_command: String,
}
fn build_inspector_playbook(
config: &McpConfig,
cwd: &PathBuf,
inspector_port: u16,
) -> InspectorPlaybook {
let sse_url = config.cursor_sse_url();
let encoded_sse_url = percent_encode_query_component(&sse_url);
let inspector_base = format!("http://127.0.0.1:{inspector_port}");
let inspector_connect_url = format!(
"{inspector_base}/?transport=sse&serverUrl={encoded_sse_url}&MCP_SERVER_REQUEST_TIMEOUT=600000"
);
let config_path = cwd.join(".xbp").join("mcp-inspector.json");
let config_relative = ".xbp/mcp-inspector.json";
let launch_command =
format!("npx @modelcontextprotocol/inspector --config {config_relative} --server xbp");
let cwd_display = cwd.display().to_string();
let tool_presets = inspector_tool_presets(&cwd_display);
let json = json!({
"mcp_sse_url": sse_url,
"health_url": format!("{}/health", config.base_url()),
"inspector_connect_url": inspector_connect_url,
"inspector_connect_note": "Append &MCP_PROXY_AUTH_TOKEN=<token> from the inspector console when auth is enabled.",
"config_path": config_relative,
"launch_command": launch_command,
"serve_command": "xbp mcp serve",
"credential_resolution": {
"cloudflare_account_id": {
"order": [
"process environment (CLOUDFLARE_ACCOUNT_ID, XBP_CLOUDFLARE_ACCOUNT_ID)",
"project .env / .env.local / .env.development / .env.production",
"global XBP config (xbp config cloudflare set-account-id)"
],
"project_env_keys": ["CLOUDFLARE_ACCOUNT_ID", "XBP_CLOUDFLARE_ACCOUNT_ID"]
}
},
"cli_examples": [
format!("npx @modelcontextprotocol/inspector --cli {sse_url} --method tools/list"),
format!(
"npx @modelcontextprotocol/inspector --cli {sse_url} --method tools/call --tool-name xbp_raw --tool-arg args=[\"build\",\"-p\",\"xbp\"]"
),
],
"tool_presets": tool_presets,
});
InspectorPlaybook {
json,
config_path,
launch_command,
}
}
fn inspector_tool_presets(cwd: &str) -> Value {
let common = json!({
"cwd": cwd,
"timeout_seconds": 600
});
let mut presets = Vec::new();
presets.extend(inspector_build_presets(&common));
presets.extend(inspector_config_presets(&common));
presets.extend(inspector_version_presets(&common));
presets.extend(inspector_secrets_presets(&common));
presets.extend(inspector_ops_presets(&common));
Value::Array(presets)
}
fn inspector_preset(category: &str, label: &str, tool: &str, arguments: Value) -> Value {
json!({
"category": category,
"label": label,
"tool": tool,
"arguments": arguments
})
}
fn inspector_raw_preset(category: &str, label: &str, common: &Value, args: &[&str]) -> Value {
let args_json: Vec<Value> = args.iter().map(|arg| json!(arg)).collect();
inspector_preset(
category,
label,
"xbp_raw",
merge_objects(common, json!({ "args": args_json })),
)
}
fn inspector_build_presets(common: &Value) -> Vec<Value> {
vec![
inspector_raw_preset(
"build",
"Build CLI (cargo build -p xbp)",
common,
&["build", "-p", "xbp"],
),
inspector_raw_preset(
"build",
"Test workspace (cargo test --locked)",
common,
&["test", "--locked"],
),
]
}
fn inspector_config_presets(common: &Value) -> Vec<Value> {
vec![
inspector_preset(
"config",
"Project config (.xbp/xbp.yaml, no editor)",
"xbp_config",
merge_objects(common, json!({ "project": true, "no_open": true })),
),
inspector_preset(
"config",
"Global XBP config paths (no editor)",
"xbp_config",
merge_objects(common, json!({ "no_open": true })),
),
inspector_raw_preset(
"config",
"Cloudflare setup wizard",
common,
&["config", "cloudflare", "setup"],
),
inspector_raw_preset(
"config",
"Cloudflare show API token (masked)",
common,
&["config", "cloudflare", "show-key"],
),
inspector_raw_preset(
"config",
"Cloudflare credential sources (env, project .env, global)",
common,
&["config", "cloudflare", "status"],
),
inspector_raw_preset(
"config",
"Cloudflare show account ID (resolved: env → project .env → global)",
common,
&["config", "cloudflare", "show-account-id"],
),
inspector_raw_preset(
"config",
"GitHub token configured (masked)",
common,
&["config", "github", "show"],
),
inspector_raw_preset(
"config",
"OpenRouter key configured (masked)",
common,
&["config", "openrouter", "show"],
),
inspector_raw_preset(
"config",
"Linear key configured (masked)",
common,
&["config", "linear", "show"],
),
inspector_raw_preset(
"config",
"Linear select initiative for repo",
common,
&["config", "linear", "select-initiative"],
),
inspector_raw_preset(
"config",
"npm registry token configured (masked)",
common,
&["config", "npm", "show"],
),
inspector_raw_preset(
"config",
"npm publish settings wizard (.xbp/xbp.yaml)",
common,
&["config", "npm", "setup-release"],
),
inspector_raw_preset(
"config",
"crates.io token configured (masked)",
common,
&["config", "crates", "show"],
),
inspector_raw_preset(
"config",
"crates publish settings wizard (.xbp/xbp.yaml)",
common,
&["config", "crates", "setup-release"],
),
inspector_raw_preset(
"config",
"crates.io cargo login (sync local credentials)",
common,
&["config", "crates", "login"],
),
inspector_raw_preset(
"config",
"MCP Cursor config JSON",
common,
&["mcp", "config"],
),
]
}
fn inspector_version_presets(common: &Value) -> Vec<Value> {
vec![
inspector_raw_preset(
"version",
"Show tracked package versions",
common,
&["version"],
),
inspector_raw_preset(
"version",
"Show versions with git tags",
common,
&["version", "--git"],
),
inspector_raw_preset(
"version",
"Discover nested services (dry-run)",
common,
&["version", "discover", "--dry-run"],
),
inspector_raw_preset(
"version",
"Bump mutated packages (patch, dry-run)",
common,
&["version", "bump", "--patch", "--dry-run"],
),
inspector_raw_preset(
"version",
"Workspace drift check (JSON)",
common,
&["version", "workspace", "check", "--json"],
),
inspector_raw_preset(
"version",
"Workspace sync preview (JSON)",
common,
&["version", "workspace", "sync", "--json"],
),
inspector_raw_preset(
"version",
"Workspace sync apply (--write)",
common,
&["version", "workspace", "sync", "--write", "--json"],
),
inspector_raw_preset(
"version",
"Workspace validate (package dry-run)",
common,
&["version", "workspace", "validate", "--package-dry-run"],
),
inspector_raw_preset(
"version",
"Workspace validate (cargo check + package dry-run)",
common,
&[
"version",
"workspace",
"validate",
"--cargo-check",
"--package-dry-run",
],
),
inspector_raw_preset(
"version",
"Workspace publish plan (JSON)",
common,
&["version", "workspace", "publish", "plan", "--json"],
),
inspector_raw_preset(
"version",
"Workspace publish run (dry-run)",
common,
&["version", "workspace", "publish", "run", "--dry-run"],
),
inspector_raw_preset(
"version",
"Workspace publish run (single crate, with prereqs)",
common,
&[
"version",
"workspace",
"publish",
"run",
"--only",
"xbp",
"--include-prereqs",
"--dry-run",
],
),
inspector_raw_preset(
"version",
"GitHub release (draft, no publish)",
common,
&["version", "release", "--draft"],
),
inspector_raw_preset(
"version",
"Publish workflows dry-run (npm/crates)",
common,
&["publish", "--dry-run"],
),
inspector_raw_preset(
"version",
"Publish crates target dry-run",
common,
&["publish", "--target", "crates", "--dry-run"],
),
]
}
fn inspector_secrets_presets(common: &Value) -> Vec<Value> {
vec![
inspector_raw_preset(
"secrets",
"List secrets providers",
common,
&["secrets", "providers"],
),
inspector_raw_preset(
"secrets",
"List local env keys",
common,
&["secrets", "list"],
),
inspector_raw_preset(
"secrets",
"List local env keys (JSON)",
common,
&["secrets", "list", "--format", "json"],
),
inspector_raw_preset(
"secrets",
"Push local env to GitHub (dry-run)",
common,
&["secrets", "push", "--dry-run"],
),
inspector_raw_preset(
"secrets",
"Push local env to GitHub (force overwrite)",
common,
&["secrets", "push", "--force"],
),
inspector_raw_preset(
"secrets",
"Pull GitHub secrets to .env.local",
common,
&["secrets", "pull"],
),
inspector_raw_preset(
"secrets",
"Generate .env.default from code scan",
common,
&["secrets", "generate-default"],
),
inspector_raw_preset(
"secrets",
"Generate .env.example with categories",
common,
&["secrets", "generate-example"],
),
inspector_raw_preset(
"secrets",
"Diff local vs remote GitHub variables",
common,
&["secrets", "diff"],
),
inspector_raw_preset(
"secrets",
"Verify required env vars locally",
common,
&["secrets", "verify"],
),
inspector_raw_preset(
"secrets",
"Secrets connectivity diag (token, scope, repo)",
common,
&["secrets", "diag"],
),
inspector_raw_preset(
"secrets",
"Secrets command usage",
common,
&["secrets", "usage"],
),
inspector_raw_preset(
"secrets",
"Cloudflare secrets stores list",
common,
&["secrets", "--provider", "cloudflare", "stores", "list"],
),
inspector_raw_preset(
"secrets",
"Cloudflare secrets store get (set --store-id)",
common,
&[
"secrets",
"--provider",
"cloudflare",
"stores",
"get",
"--store-id",
"STORE_ID",
],
),
inspector_raw_preset(
"secrets",
"Cloudflare secrets in store list (set --store-id)",
common,
&[
"secrets",
"--provider",
"cloudflare",
"secrets",
"list",
"--store-id",
"STORE_ID",
],
),
inspector_raw_preset(
"secrets",
"Cloudflare secret get (set store-id + secret-id)",
common,
&[
"secrets",
"--provider",
"cloudflare",
"secrets",
"get",
"--store-id",
"STORE_ID",
"--secret-id",
"SECRET_ID",
],
),
inspector_raw_preset(
"secrets",
"Cloudflare quota usage",
common,
&["secrets", "--provider", "cloudflare", "quota", "get"],
),
inspector_raw_preset(
"secrets",
"Worker secret bindings list",
common,
&["workers", "secrets", "list", "--json"],
),
]
}
fn inspector_ops_presets(common: &Value) -> Vec<Value> {
vec![
inspector_raw_preset("ops", "MCP server status", common, &["mcp", "status"]),
inspector_preset(
"ops",
"List project services",
"xbp_services",
common.clone(),
),
inspector_preset(
"ops",
"List Workers (project scope, JSON)",
"xbp_workers_list",
merge_objects(common, json!({ "json": true })),
),
inspector_preset(
"ops",
"Workers deployment logs (JSON)",
"xbp_workers_logs",
merge_objects(common, json!({ "json": true })),
),
inspector_preset(
"ops",
"Workers build logs — latest failure (JSON)",
"xbp_workers_logs_build",
merge_objects(common, json!({ "failed": true, "json": true })),
),
inspector_preset("ops", "System diagnostics", "xbp_diag", common.clone()),
inspector_preset(
"ops",
"Listening ports (full)",
"xbp_ports",
merge_objects(common, json!({ "full": true })),
),
inspector_preset(
"ops",
"Control-plane API health",
"xbp_api_health",
common.clone(),
),
inspector_preset(
"ops",
"API GET /health",
"xbp_api_request",
merge_objects(common, json!({ "path": "/health", "method": "GET" })),
),
inspector_preset(
"ops",
"Redeploy entire project",
"xbp_redeploy",
common.clone(),
),
]
}
fn merge_objects(base: &Value, extra: Value) -> Value {
let mut merged = base.as_object().cloned().unwrap_or_default();
if let Some(extra_object) = extra.as_object() {
merged.extend(extra_object.clone());
}
Value::Object(merged)
}
fn write_inspector_config(config_path: &PathBuf, config: &McpConfig) -> CliResult<()> {
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent).map_err(|error| {
ErrorFactory::operation("mcp", "create .xbp directory", error.to_string(), None)
})?;
}
let payload = json!({
"mcpServers": {
"xbp": {
"type": "sse",
"url": config.cursor_sse_url()
}
}
});
fs::write(
config_path,
format!(
"{}\n",
serde_json::to_string_pretty(&payload).unwrap_or_default()
),
)
.map_err(|error| {
ErrorFactory::operation("mcp", "write MCP Inspector config", error.to_string(), None)
})?;
Ok(())
}
fn write_inspector_presets(config_path: &PathBuf, playbook: &Value) -> CliResult<()> {
let presets_path = config_path
.parent()
.map(|dir| dir.join("mcp-inspector-presets.json"))
.unwrap_or_else(|| PathBuf::from("mcp-inspector-presets.json"));
let payload = json!({
"mcp_sse_url": playbook.get("mcp_sse_url"),
"inspector_connect_url": playbook.get("inspector_connect_url"),
"tool_presets": playbook.get("tool_presets"),
});
fs::write(
&presets_path,
format!(
"{}\n",
serde_json::to_string_pretty(&payload).unwrap_or_default()
),
)
.map_err(|error| {
ErrorFactory::operation(
"mcp",
"write MCP Inspector presets",
error.to_string(),
None,
)
})?;
Ok(())
}
fn print_inspector_playbook(playbook: &InspectorPlaybook) {
ui::configure_color_output();
let sse_url = playbook
.json
.get("mcp_sse_url")
.and_then(Value::as_str)
.unwrap_or_default();
let connect_url = playbook
.json
.get("inspector_connect_url")
.and_then(Value::as_str)
.unwrap_or_default();
println!();
println!("{}", "MCP Inspector playbook".bright_magenta().bold());
ui::divider(28);
println!(
"{} {}",
"1. Start server:".bright_cyan(),
"xbp mcp serve".bright_white()
);
println!(
"{} {}",
"2. Launch UI:".bright_cyan(),
playbook.launch_command.bright_white()
);
println!(
"{} {}",
" (or)".bright_black(),
format!("xbp mcp inspector --launch").bright_black()
);
println!();
println!(
"{}",
"Prefilled connect URL (paste in browser)"
.bright_yellow()
.bold()
);
println!("{}", connect_url.dimmed());
println!(
"{}",
"Append &MCP_PROXY_AUTH_TOKEN=<token> from the inspector console if auth is enabled."
.bright_black()
);
println!();
println!(
"{} {}",
"SSE endpoint:".bright_cyan(),
sse_url.bright_white()
);
println!(
"{} {}",
"Config file:".bright_cyan(),
playbook.config_path.display().to_string().bright_white()
);
println!();
println!(
"{}",
"Tool presets (Tools tab → pick tool → paste arguments)"
.bright_yellow()
.bold()
);
println!(
"{}",
"Saved to .xbp/mcp-inspector-presets.json — secrets presets need `cargo build --features secrets`."
.bright_black()
);
if let Some(presets) = playbook.json.get("tool_presets").and_then(Value::as_array) {
let mut current_category = "";
for preset in presets {
let category = preset
.get("category")
.and_then(Value::as_str)
.unwrap_or("other");
if category != current_category {
current_category = category;
println!();
println!(" {}", category.to_uppercase().bright_cyan().bold());
}
let label = preset
.get("label")
.and_then(Value::as_str)
.unwrap_or("preset");
let tool = preset.get("tool").and_then(Value::as_str).unwrap_or("tool");
let arguments = preset
.get("arguments")
.cloned()
.unwrap_or_else(|| json!({}));
println!();
println!(" {} {}", "•".bright_magenta(), label.bright_white());
println!(" {} {}", "tool".bright_cyan(), tool.dimmed());
println!(
" {}",
serde_json::to_string_pretty(&arguments)
.unwrap_or_default()
.lines()
.map(|line| format!(" {line}"))
.collect::<Vec<_>>()
.join("\n")
.trim_end()
.to_string()
.dimmed()
);
}
}
println!();
println!("{}", "CLI one-liners".bright_yellow().bold());
if let Some(examples) = playbook.json.get("cli_examples").and_then(Value::as_array) {
for example in examples {
if let Some(line) = example.as_str() {
println!(" {}", line.dimmed());
}
}
}
println!();
}
fn launch_inspector(playbook: &InspectorPlaybook) -> CliResult<()> {
let _ = log_info(
"mcp",
"Launching MCP Inspector (Ctrl+C to stop)",
Some(&playbook.launch_command),
);
let status = if cfg!(windows) {
Command::new("cmd")
.arg("/c")
.arg(&playbook.launch_command)
.status()
} else {
Command::new("sh")
.arg("-lc")
.arg(&playbook.launch_command)
.status()
}
.map_err(|error| {
ErrorFactory::operation(
"mcp",
"launch MCP Inspector",
error.to_string(),
Some("Install Node.js 22+ and ensure npx is on PATH."),
)
})?;
if !status.success() {
return Err(ErrorFactory::operation(
"mcp",
"launch MCP Inspector",
format!("exit code {}", status.code().unwrap_or(-1)),
None,
));
}
Ok(())
}
fn percent_encode_query_component(input: &str) -> String {
input
.bytes()
.map(|byte| match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
(byte as char).to_string()
}
_ => format!("%{byte:02X}"),
})
.collect()
}
async fn spawn_detached_mcp_server(serve_cmd: commands::McpServeCmd, debug: bool) -> CliResult<()> {
let exe = std::env::current_exe().map_err(|error| {
ErrorFactory::operation("mcp", "resolve current executable", error.to_string(), None)
})?;
let mut command = Command::new(exe);
command
.arg("mcp")
.arg("serve")
.arg("--bind")
.arg(&serve_cmd.bind)
.arg("--port")
.arg(serve_cmd.port.to_string())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
if debug {
command.env("RUST_LOG", "info");
}
#[cfg(windows)]
{
const DETACHED_PROCESS: u32 = 0x00000008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
std::os::windows::process::CommandExt::creation_flags(
&mut command,
DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP,
);
}
command.spawn().map_err(|error| {
ErrorFactory::operation("mcp", "spawn detached MCP server", error.to_string(), None)
})?;
let config = build_mcp_config(&serve_cmd);
print_cursor_config(&config);
let _ = log_success(
"mcp",
&format!(
"Detached MCP server started on {}:{}",
serve_cmd.bind, serve_cmd.port
),
None,
)
.await;
Ok(())
}