use anyhow::{Context, Result};
use std::path::Path;
use tracing::{debug, info};
use crate::cli::McpArgs;
use crate::config::TurboPropConfig;
use crate::mcp::McpServer;
use crate::types::parse_filesize;
#[derive(Debug, Clone)]
pub struct McpLoggingConfig {
pub verbose: bool,
pub debug: bool,
}
impl McpLoggingConfig {
pub fn new(verbose: bool, debug: bool) -> Self {
Self { verbose, debug }
}
}
impl From<&McpArgs> for McpLoggingConfig {
fn from(args: &McpArgs) -> Self {
Self::new(args.verbose, args.debug)
}
}
fn validate_args(args: &McpArgs) -> Result<()> {
if !args.repo.exists() {
return Err(anyhow::anyhow!(
"Repository path does not exist: {}",
args.repo.display()
));
}
if !args.repo.is_dir() {
return Err(anyhow::anyhow!(
"Repository path is not a directory: {}",
args.repo.display()
));
}
if let Some(max_filesize) = &args.max_filesize {
if let Err(e) = parse_filesize(max_filesize) {
return Err(anyhow::anyhow!(
"Invalid file size format '{}': {}",
max_filesize,
e
));
}
}
if let Some(filter) = &args.filter {
if let Err(e) = glob::Pattern::new(filter) {
return Err(anyhow::anyhow!("Invalid glob pattern '{}': {}", filter, e));
}
}
Ok(())
}
pub async fn execute_mcp_command(args: McpArgs) -> Result<()> {
setup_logging(McpLoggingConfig::from(&args))
.context("Failed to initialize logging for MCP server")?;
info!("Starting TurboProp MCP server");
debug!("MCP arguments: {:?}", args);
validate_args(&args).context("MCP command argument validation failed")?;
print_setup_info(&args.repo);
let mut config =
if let Ok(config_path) = std::fs::canonicalize(args.repo.join(".turboprop.yml")) {
TurboPropConfig::load_from_file(&config_path).with_context(|| {
format!(
"Failed to load configuration from {}",
config_path.display()
)
})?
} else {
TurboPropConfig::load().with_context(|| "Failed to load default configuration")?
};
apply_config_overrides(&mut config, &args)
.context("Failed to apply CLI argument overrides to configuration")?;
log_config_summary(&config, &args);
let server = McpServer::new(&args.repo, &config)
.await
.context("Failed to create MCP server")?;
info!(
"MCP server starting for repository: {}",
args.repo.display()
);
info!("Ready to accept connections from coding agents");
info!("Use Ctrl+C or close stdin to shutdown");
server.run().await.context("MCP server execution failed")?;
info!("MCP server shutdown complete");
Ok(())
}
fn apply_config_overrides(config: &mut TurboPropConfig, args: &McpArgs) -> Result<()> {
if let Some(model) = &args.model {
config.embedding.model_name = model.clone();
}
if let Some(max_filesize_str) = &args.max_filesize {
let max_filesize_bytes = parse_filesize(max_filesize_str)
.map_err(|e| anyhow::anyhow!("Invalid file size '{}': {}", max_filesize_str, e))?;
config.file_discovery.max_filesize_bytes = Some(max_filesize_bytes);
}
if let Some(filter) = &args.filter {
return Err(anyhow::anyhow!(
"Filter patterns (--filter '{}') are not yet supported in MCP mode; \
use .turboprop.yml configuration file to specify filtering options",
filter
));
}
if let Some(filetype) = &args.filetype {
return Err(anyhow::anyhow!(
"File type filtering (--filetype '{}') is not yet supported in MCP mode; \
use .turboprop.yml configuration file to specify filtering options",
filetype
));
}
if args.force_rebuild {
return Err(anyhow::anyhow!(
"Force rebuild (--force-rebuild) is not yet supported in MCP mode; \
remove or recreate the index directory to force a rebuild"
));
}
Ok(())
}
fn setup_logging(config: McpLoggingConfig) -> Result<()> {
debug!(
"MCP server using global logging configuration (verbose: {}, debug: {})",
config.verbose, config.debug
);
Ok(())
}
fn log_config_summary(config: &TurboPropConfig, args: &McpArgs) {
info!("Configuration summary:");
info!(" Repository: {}", args.repo.display());
info!(" Model: {}", config.embedding.model_name);
info!(
" Max file size: {:?}",
config.file_discovery.max_filesize_bytes
);
info!(" Batch size: {}", config.embedding.batch_size);
if let Some(filter) = &args.filter {
info!(" Filter pattern: {}", filter);
}
if let Some(filetype) = &args.filetype {
info!(" File type: {}", filetype);
}
if args.force_rebuild {
info!(" Force rebuild: enabled");
}
debug!("Full configuration: {:#?}", config);
}
pub fn print_setup_info(repo_path: &Path) {
eprintln!();
eprintln!("🚀 TurboProp Semantic Search MCP Server Started");
eprintln!("──────────────────────────────────────────────");
eprintln!("Repository: {}", repo_path.display());
eprintln!();
eprintln!("To integrate with coding agents, add this to your MCP configuration:");
eprintln!();
eprintln!("Claude Code (.mcp.json):");
eprintln!(
r#"{{
"mcpServers": {{
"turboprop": {{
"command": "tp",
"args": ["mcp", "--repo", "{}"]
}}
}}
}}"#,
repo_path.display()
);
eprintln!();
eprintln!("Cursor (.cursor/mcp.json):");
eprintln!(
r#"{{
"mcpServers": {{
"turboprop": {{
"command": "tp",
"args": ["mcp", "--repo", "{}"]
}}
}}
}}"#,
repo_path.display()
);
eprintln!();
eprintln!("Semantic search tool parameters:");
eprintln!(" • query (required): Natural language search query");
eprintln!(" • limit: Maximum results (default: 10)");
eprintln!(" • threshold: Similarity threshold (0.0-1.0)");
eprintln!(" • filetype: File extension filter (e.g., '.rs', '.js')");
eprintln!(" • filter: Glob pattern filter (e.g., 'src/**/*.rs')");
eprintln!();
eprintln!("Press Ctrl+C to stop the server");
eprintln!();
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_mcp_args_validation() {
let temp_dir = TempDir::new().unwrap();
let valid_args = McpArgs {
repo: temp_dir.path().to_path_buf(),
model: None,
max_filesize: Some("1mb".to_string()),
filter: Some("*.rs".to_string()),
filetype: None,
force_rebuild: false,
verbose: false,
debug: false,
};
assert!(validate_args(&valid_args).is_ok());
let invalid_repo = McpArgs {
repo: "/nonexistent/path".into(),
model: None,
max_filesize: None,
filter: None,
filetype: None,
force_rebuild: false,
verbose: false,
debug: false,
};
assert!(validate_args(&invalid_repo).is_err());
let invalid_size = McpArgs {
repo: temp_dir.path().to_path_buf(),
model: None,
max_filesize: Some("invalid_size".to_string()),
filter: None,
filetype: None,
force_rebuild: false,
verbose: false,
debug: false,
};
assert!(validate_args(&invalid_size).is_err());
let invalid_glob = McpArgs {
repo: temp_dir.path().to_path_buf(),
model: None,
max_filesize: None,
filter: Some("[invalid".to_string()),
filetype: None,
force_rebuild: false,
verbose: false,
debug: false,
};
assert!(validate_args(&invalid_glob).is_err());
}
#[test]
fn test_config_overrides() {
let temp_dir = TempDir::new().unwrap();
let mut config = TurboPropConfig::default();
let supported_args = McpArgs {
repo: temp_dir.path().to_path_buf(),
model: Some("custom-model".to_string()),
max_filesize: Some("5mb".to_string()),
filter: None,
filetype: None,
force_rebuild: false,
verbose: false,
debug: false,
};
apply_config_overrides(&mut config, &supported_args).unwrap();
assert_eq!(config.embedding.model_name, "custom-model");
assert_eq!(
config.file_discovery.max_filesize_bytes,
Some(5 * 1024 * 1024)
);
let filter_args = McpArgs {
repo: temp_dir.path().to_path_buf(),
model: None,
max_filesize: None,
filter: Some("src/**/*.rs".to_string()),
filetype: None,
force_rebuild: false,
verbose: false,
debug: false,
};
assert!(apply_config_overrides(&mut config, &filter_args).is_err());
let filetype_args = McpArgs {
repo: temp_dir.path().to_path_buf(),
model: None,
max_filesize: None,
filter: None,
filetype: Some("rust".to_string()),
force_rebuild: false,
verbose: false,
debug: false,
};
assert!(apply_config_overrides(&mut config, &filetype_args).is_err());
let force_rebuild_args = McpArgs {
repo: temp_dir.path().to_path_buf(),
model: None,
max_filesize: None,
filter: None,
filetype: None,
force_rebuild: true,
verbose: false,
debug: false,
};
assert!(apply_config_overrides(&mut config, &force_rebuild_args).is_err());
}
}