use anyhow::Context;
use clap::Parser;
use pathfinder_common::config::PathfinderConfig;
use pathfinder_common::types::WorkspaceRoot;
use rmcp::ServiceExt;
use std::path::PathBuf;
use tracing_subscriber::EnvFilter;
mod server;
use server::PathfinderServer;
#[derive(Parser, Debug)]
#[command(name = "pathfinder-mcp", version, about)]
struct Cli {
#[arg(value_name = "WORKSPACE_PATH")]
workspace_path: PathBuf,
#[arg(long, default_value_t = false)]
lsp_trace: bool,
}
pub(crate) async fn run(workspace_path: PathBuf, lsp_trace: bool) -> anyhow::Result<()> {
let mut filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
if lsp_trace {
if let Ok(dir) = "pathfinder_lsp::client::transport=debug".parse() {
filter = filter.add_directive(dir);
}
}
tracing_subscriber::fmt()
.json()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.with_target(false)
.init();
tracing::info!(
workspace = %workspace_path.display(),
version = env!("CARGO_PKG_VERSION"),
"Pathfinder starting"
);
let workspace_root = WorkspaceRoot::new(&workspace_path)
.with_context(|| format!("Invalid workspace path: {}", workspace_path.display()))?;
let config = PathfinderConfig::load(workspace_root.path())
.await
.with_context(|| "Failed to load configuration")?;
let server = PathfinderServer::new(workspace_root, config).await;
tracing::info!("Starting MCP stdio transport");
let service = server
.serve(rmcp::transport::io::stdio())
.await
.context("Failed to start MCP server")?;
service.waiting().await?;
tracing::info!("Pathfinder shutting down");
Ok(())
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
run(cli.workspace_path, cli.lsp_trace).await
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn test_cli_parse_workspace_path() {
let test_path = std::env::temp_dir().join("workspace");
let cli = Cli::parse_from(["pathfinder", test_path.to_str().unwrap()]);
assert_eq!(cli.workspace_path, test_path);
assert!(!cli.lsp_trace);
}
#[test]
fn test_cli_parse_lsp_trace_flag() {
let test_path = std::env::temp_dir().join("ws");
let cli = Cli::parse_from(["pathfinder", test_path.to_str().unwrap(), "--lsp-trace"]);
assert!(cli.lsp_trace);
}
#[test]
fn test_cli_parse_missing_workspace_fails() {
let result = Cli::try_parse_from(["pathfinder"]);
assert!(result.is_err(), "should require workspace path");
}
#[tokio::test]
async fn test_run_invalid_workspace_path() {
let result = run(
PathBuf::from("/nonexistent/path/that/does/not/exist"),
false,
)
.await;
if let Err(e) = result {
let msg = format!("{e:#}");
assert!(msg.contains("path") || msg.contains("Invalid"));
}
}
}