use crate::output::sanitize_display_text;
use crate::{
config::{
McPaths, McpServerConfig, McpServersSettings, read_settings, validate_mcp_server_name,
},
mcp::{
McpClient,
manager::{McpManager, McpServerStatus},
oauth,
},
};
use anyhow::Result;
#[cfg(not(windows))]
use std::process::Command;
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: {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(|e| anyhow::anyhow!(e))?;
let login = oauth::prepare_login(&http.url, oauth_config, &redirect_uri, &client)
.map_err(|e| anyhow::anyhow!(e))?;
writeln!(
stderr,
"Open this URL in your browser:\n{}",
login.authorization_url
)?;
let _ = open_browser(&login.authorization_url);
let cancel = Arc::new(AtomicBool::new(false));
let (tx, rx) = std::sync::mpsc::channel();
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(|e| anyhow::anyhow!(e))?;
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(|e| anyhow::anyhow!(e))?;
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(|e| anyhow::anyhow!(e))?;
oauth::write_token(&paths.root, server, &token).map_err(|e| anyhow::anyhow!(e))?;
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(|e| anyhow::anyhow!(e))?;
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 mut client = connect_selected(server, &settings.mcp_servers, &paths, stderr)?;
let init = client.initialize().map_err(|error| {
let _ = writeln!(stderr, "mcp {server}: initialize failed: {error}");
anyhow::anyhow!(error)
})?;
let tools = client.list_tools().map_err(|error| {
let _ = writeln!(stderr, "mcp {server}: tools/list failed: {error}");
anyhow::anyhow!(error)
})?;
let result = write_connected_diagnostics(stdout, &init, &tools);
client.shutdown();
result
}
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 connect_selected(
server: &str,
settings: &McpServersSettings,
paths: &McPaths,
stderr: &mut impl Write,
) -> Result<McpClient> {
validate_mcp_server_name(server)?;
let config = settings
.get(server)
.expect("server presence validated before connect");
McpClient::connect_named(Some(server), config, Some(&paths.root)).map_err(|error| {
let phase = match config {
McpServerConfig::Stdio(_) => "spawn",
McpServerConfig::Http(_) => "connect",
};
let _ = writeln!(stderr, "mcp {server}: {phase} failed: {error}");
anyhow::anyhow!(error)
})
}
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: {error}");
", auth: invalid (needs login)".to_string()
}
}
}
fn open_browser(url: &str) -> std::io::Result<()> {
let parsed = reqwest::Url::parse(url).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid browser URL")
})?;
if !matches!(parsed.scheme(), "http" | "https") || url.contains('\0') {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"browser URL must use HTTP or HTTPS",
));
}
#[cfg(target_os = "macos")]
{
Command::new("open").arg(url).spawn().map(|_| ())
}
#[cfg(target_os = "windows")]
{
use windows_sys::Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOWNORMAL};
let url: Vec<u16> = url.encode_utf16().chain(Some(0)).collect();
let operation: Vec<u16> = "open".encode_utf16().chain(Some(0)).collect();
let result = unsafe {
ShellExecuteW(
std::ptr::null_mut(),
operation.as_ptr(),
url.as_ptr(),
std::ptr::null(),
std::ptr::null(),
SW_SHOWNORMAL,
)
};
if result as isize <= 32 {
Err(std::io::Error::other("could not open browser"))
} else {
Ok(())
}
}
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
{
Command::new("xdg-open").arg(url).spawn().map(|_| ())
}
}
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(),
}
}