git_iris/mcp/
server.rs

1//! MCP server implementation for Git-Iris
2//!
3//! This module contains the implementation of the MCP server
4//! that allows Git-Iris to be used directly from compatible tools.
5
6use crate::config::Config as GitIrisConfig;
7use crate::git::GitRepo;
8use crate::log_debug;
9use crate::mcp::config::{MCPServerConfig, MCPTransportType};
10use crate::mcp::tools::GitIrisHandler;
11
12use anyhow::{Context, Result};
13use rmcp::ServiceExt;
14use rmcp::transport::sse_server::SseServer;
15use std::net::SocketAddr;
16use std::sync::Arc;
17use tokio::io::{stdin, stdout};
18
19/// Serve the MCP server with the provided configuration
20pub async fn serve(config: MCPServerConfig) -> Result<()> {
21    // Configure logging based on transport type and dev mode
22    if config.dev_mode {
23        // In dev mode, set up appropriate logging
24        let log_path = format!("git-iris-mcp-{}.log", std::process::id());
25        if let Err(e) = crate::logger::set_log_file(&log_path) {
26            // For non-stdio transports, we can print this error
27            if config.transport != MCPTransportType::StdIO {
28                eprintln!("Failed to set up log file: {e}");
29            }
30            // Continue without file logging
31        }
32
33        // For stdio transport, we must NEVER log to stdout
34        if config.transport == MCPTransportType::StdIO {
35            crate::logger::set_log_to_stdout(false);
36        } else {
37            crate::logger::set_log_to_stdout(true);
38        }
39
40        crate::logger::enable_logging();
41    }
42
43    log_debug!("Starting MCP server with config: {:?}", config);
44
45    // Display configuration info if not using stdio transport
46    if config.transport != MCPTransportType::StdIO {
47        use crate::ui;
48        ui::print_info(&format!(
49            "Starting Git-Iris MCP server with {:?} transport",
50            config.transport
51        ));
52        if let Some(port) = config.port {
53            ui::print_info(&format!("Port: {port}"));
54        }
55        if let Some(addr) = &config.listen_address {
56            ui::print_info(&format!("Listening on: {addr}"));
57        }
58        ui::print_info(&format!(
59            "Development mode: {}",
60            if config.dev_mode {
61                "Enabled"
62            } else {
63                "Disabled"
64            }
65        ));
66    }
67
68    // Initialize GitRepo for use with tools
69    let git_repo = Arc::new(GitRepo::new_from_url(None)?);
70    log_debug!(
71        "Initialized Git repository at: {}",
72        git_repo.repo_path().display()
73    );
74
75    // Load Git-Iris configuration
76    let git_iris_config = GitIrisConfig::load()?;
77    log_debug!("Loaded Git-Iris configuration");
78
79    // Create the handler with necessary dependencies
80    let handler = GitIrisHandler::new(git_repo, git_iris_config);
81
82    // Start the appropriate transport
83    match config.transport {
84        MCPTransportType::StdIO => serve_stdio(handler, config.dev_mode).await,
85        MCPTransportType::SSE => {
86            // Get socket address for the server
87            let socket_addr = get_socket_addr(&config)?;
88            serve_sse(handler, socket_addr).await
89        }
90    }
91}
92
93/// Start the MCP server using `StdIO` transport
94async fn serve_stdio(handler: GitIrisHandler, _dev_mode: bool) -> Result<()> {
95    log_debug!("Starting MCP server with StdIO transport");
96
97    let transport = (stdin(), stdout());
98
99    let server = handler.serve(transport).await?;
100
101    // Wait for the server to finish
102    log_debug!("MCP server initialized, waiting for completion");
103    let quit_reason = server.waiting().await?;
104    log_debug!("MCP server finished: {:?}", quit_reason);
105
106    Ok(())
107}
108
109/// Start the MCP server using SSE transport
110async fn serve_sse(handler: GitIrisHandler, socket_addr: SocketAddr) -> Result<()> {
111    log_debug!("Starting MCP server with SSE transport on {}", socket_addr);
112
113    // Create and start the SSE server
114    let server = SseServer::serve(socket_addr).await?;
115
116    // Set up the service with our handler
117    let control = server.with_service(move || {
118        // Return a clone of the handler directly as it implements ServerHandler
119        handler.clone()
120    });
121
122    // Wait for Ctrl+C signal
123    log_debug!("SSE server initialized, waiting for interrupt signal");
124    tokio::signal::ctrl_c()
125        .await
126        .context("Failed to listen for ctrl+c signal")?;
127
128    // Cancel the server gracefully
129    log_debug!("Interrupt signal received, shutting down SSE server");
130    control.cancel();
131
132    Ok(())
133}
134
135/// Helper function to get a socket address from the configuration
136fn get_socket_addr(config: &MCPServerConfig) -> Result<SocketAddr> {
137    // Get listen address, or use default
138    let listen_address = config.listen_address.as_deref().unwrap_or("127.0.0.1");
139    let port = config.port.context("Port is required for SSE transport")?;
140
141    // Parse the socket address
142    let socket_addr: SocketAddr = format!("{listen_address}:{port}")
143        .parse()
144        .context("Failed to parse socket address")?;
145
146    Ok(socket_addr)
147}