use crate::output::sanitize_display_text;
use crate::{
config::{McPaths, McpServerConfig, read_settings, validate_mcp_server_name},
mcp::{
McpClient,
manager::{McpManager, McpServerStatus},
oauth,
},
};
use anyhow::Result;
use std::{
io::Write,
sync::{Arc, atomic::AtomicBool},
thread,
time::Duration,
};
pub(crate) fn run_list(stdout: &mut impl Write, stderr: &mut impl Write) -> Result<()> {
run_list_with_paths(&McPaths::resolve()?, stdout, stderr)
}
fn run_list_with_paths(
paths: &McPaths,
stdout: &mut impl Write,
stderr: &mut impl Write,
) -> Result<()> {
let settings = read_settings(paths)?;
writeln!(stdout, "MCP servers")?;
writeln!(
stdout,
" Definitions: CONFIG_DIR/.mcp.json or cwd/.mcp.json"
)?;
writeln!(
stdout,
" Disabled by default; enable with /mcp in Mission Control, then restart."
)?;
if settings.mcp_servers.is_empty() {
writeln!(stdout, " none configured")?;
return Ok(());
}
let manager = McpManager::from_settings_with_paths(&settings.mcp_servers, Some(&paths.root));
for (name, config) in &settings.mcp_servers {
let server_type = match config {
McpServerConfig::Stdio(_) => "stdio".to_string(),
McpServerConfig::Http(http) => format!("http {}", sanitized_url(&http.url)),
};
let auth = oauth_status_label(paths, name, config, &mut *stderr);
if !config.enabled() {
writeln!(stdout, " {name} ({server_type}{auth}): disabled")?;
continue;
}
match manager.statuses().get(name) {
Some(McpServerStatus::Connected { tool_count, .. }) => {
writeln!(
stdout,
" {name} ({server_type}{auth}): connected ({tool_count} tools)"
)?;
}
Some(McpServerStatus::Failed { phase, error }) => {
writeln!(
stdout,
" {name} ({server_type}{auth}): failed during {phase}"
)?;
writeln!(
stderr,
"mcp {name}: {phase} failed: {}",
sanitize_display_text(error)
)?;
}
None => {
writeln!(
stdout,
" {name} ({server_type}{auth}): failed during startup"
)?;
writeln!(stderr, "mcp {name}: startup failed without status")?;
}
}
}
Ok(())
}
pub(crate) fn run_login(
server: &str,
stdout: &mut impl Write,
stderr: &mut impl Write,
) -> Result<()> {
validate_mcp_server_name(server)?;
let paths = McPaths::resolve()?;
let settings = read_settings(&paths)?;
let config = settings
.mcp_servers
.get(server)
.ok_or_else(|| anyhow::anyhow!("MCP server '{server}' is not configured"))?;
let McpServerConfig::Http(http) = config else {
anyhow::bail!("MCP server '{server}' is not an HTTP server")
};
let oauth_config = http
.oauth
.as_ref()
.ok_or_else(|| anyhow::anyhow!("server does not have OAuth configured"))?;
let client = reqwest::blocking::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.timeout(Duration::from_secs(
http.timeout
.unwrap_or(crate::config::DEFAULT_MCP_TIMEOUT_SECONDS),
))
.build()?;
let (listener, redirect_uri) = oauth::bind_callback_listener().map_err(sanitized_error)?;
let login = oauth::prepare_login(&http.url, oauth_config, &redirect_uri, &client)
.map_err(sanitized_error)?;
writeln!(
stderr,
"Open this URL in your browser:\n{}",
login.authorization_url
)?;
let _ = crate::browser::open_url_in_browser(&login.authorization_url);
let cancel = Arc::new(AtomicBool::new(false));
let (tx, rx) = std::sync::mpsc::sync_channel(1);
thread::spawn(move || {
let mut line = String::new();
if std::io::stdin().read_line(&mut line).is_ok() && !line.trim().is_empty() {
let _ = tx.send(line);
}
});
let code = oauth::capture_loopback_or_manual_code(
Some(listener),
&login.state,
oauth::LOGIN_WAIT_TIMEOUT,
&cancel,
Some(&rx),
)
.map_err(sanitized_error)?;
let response = oauth::exchange_code(
&login.metadata.token_endpoint,
&code,
&login.redirect_uri,
&login.client_id,
&login.verifier,
&login.resource,
login.client_secret.as_deref(),
&client,
)
.map_err(sanitized_error)?;
let token = oauth::stored_token_from_response(
response,
&login.client_id,
login.client_secret,
&http.url,
Some(login.authorization_server),
Some(login.metadata.issuer),
Some(login.metadata.token_endpoint),
Some(login.resource),
)
.map_err(sanitized_error)?;
oauth::write_token(&paths.root, server, &token).map_err(sanitized_error)?;
writeln!(stdout, "Authenticated with {server}. Token stored.")?;
Ok(())
}
pub(crate) fn run_logout(
server: &str,
stdout: &mut impl Write,
_stderr: &mut impl Write,
) -> Result<()> {
validate_mcp_server_name(server)?;
let paths = McPaths::resolve()?;
oauth::delete_token(&paths.root, server).map_err(sanitized_error)?;
writeln!(stdout, "Logged out from {server}")?;
Ok(())
}
pub(crate) fn run_test(
server: &str,
stdout: &mut impl Write,
_stderr: &mut impl Write,
) -> Result<()> {
validate_mcp_server_name(server)?;
let paths = McPaths::resolve()?;
let settings = read_settings(&paths)?;
let config = settings
.mcp_servers
.get(server)
.ok_or_else(|| anyhow::anyhow!("MCP server '{server}' is not configured"))?;
if !config.enabled() {
anyhow::bail!("MCP server '{server}' is disabled; enable it with /mcp in Mission Control");
}
writeln!(stdout, "Testing MCP server: {server}")?;
if let McpServerConfig::Http(http) = config {
writeln!(stdout, " endpoint: {}", sanitized_url(&http.url))?;
}
let connect_phase = match config {
McpServerConfig::Stdio(_) => "spawn",
McpServerConfig::Http(_) => "connect",
};
let mut client = McpClient::connect_named(Some(server), config, Some(&paths.root))
.map_err(|error| server_phase_error(server, connect_phase, &error))?;
let init = client
.initialize()
.map_err(|error| server_phase_error(server, "initialize", &error))?;
let tools = client
.list_tools()
.map_err(|error| server_phase_error(server, "tools/list", &error))?;
let result = write_connected_diagnostics(stdout, &init, &tools);
client.shutdown();
result
}
fn server_phase_error(server: &str, phase: &str, error: &impl std::fmt::Display) -> anyhow::Error {
anyhow::anyhow!(
"mcp {server}: {phase} failed: {}",
sanitize_display_text(&error.to_string())
)
}
fn sanitized_error(error: impl std::fmt::Display) -> anyhow::Error {
anyhow::anyhow!(sanitize_display_text(&error.to_string()))
}
fn write_connected_diagnostics(
stdout: &mut impl Write,
init: &crate::mcp::protocol::InitializeResult,
tools: &[crate::mcp::protocol::Tool],
) -> Result<()> {
writeln!(stdout, "Connected")?;
writeln!(
stdout,
" server: {}",
sanitize_display_text(&init.server_info.name)
)?;
writeln!(
stdout,
" version: {}",
sanitize_display_text(&init.server_info.version)
)?;
writeln!(
stdout,
" protocol: {}",
sanitize_display_text(&init.protocol_version)
)?;
writeln!(stdout, " tools: {}", tools.len())?;
for tool in tools {
let description = sanitize_display_text(tool.description.as_deref().unwrap_or(""));
let name = sanitize_display_text(&tool.name);
if description.is_empty() {
writeln!(stdout, " - {name}")?;
} else {
writeln!(stdout, " - {name}: {description}")?;
}
}
Ok(())
}
fn oauth_status_label(
paths: &McPaths,
name: &str,
config: &McpServerConfig,
stderr: &mut impl Write,
) -> String {
let McpServerConfig::Http(http) = config else {
return String::new();
};
if http.oauth.is_none() {
return String::new();
}
match oauth::auth_status(&paths.root, name, &http.url) {
Ok(status) => format!(", auth: {status}"),
Err(error) => {
let _ = writeln!(
stderr,
"mcp {name}: auth status failed: {}",
sanitize_display_text(&error.to_string())
);
", auth: invalid (needs login)".to_string()
}
}
}
fn sanitized_url(url: &str) -> String {
match reqwest::Url::parse(url) {
Ok(parsed) => {
let host = parsed.host_str().unwrap_or("<unknown>");
let port = parsed
.port()
.map(|port| format!(":{port}"))
.unwrap_or_default();
format!("{}://{}{}{}", parsed.scheme(), host, port, parsed.path())
}
Err(_) => "<invalid-url>".to_string(),
}
}