git_iris/mcp/tools/
mod.rs

1//! MCP tools module for Git-Iris
2//!
3//! This module contains the implementation of the MCP tools
4//! that expose Git-Iris functionality to MCP clients.
5
6pub mod changelog;
7pub mod codereview;
8pub mod commit;
9pub mod releasenotes;
10pub mod utils;
11
12use crate::config::Config as GitIrisConfig;
13use crate::git::GitRepo;
14use crate::log_debug;
15use crate::mcp::tools::utils::GitIrisTool;
16
17use rmcp::Error;
18use rmcp::RoleServer;
19use rmcp::model::{
20    CallToolRequestParam, CallToolResult, ListToolsResult, PaginatedRequestParam,
21    ServerCapabilities, Tool,
22};
23use rmcp::service::RequestContext;
24use rmcp::{ServerHandler, model::ServerInfo};
25
26use serde_json::{Map, Value};
27use std::future::Future;
28use std::path::PathBuf;
29use std::sync::Arc;
30use std::sync::Mutex;
31
32// Re-export all tools for easy importing
33pub use self::changelog::ChangelogTool;
34pub use self::codereview::CodeReviewTool;
35pub use self::commit::CommitTool;
36pub use self::releasenotes::ReleaseNotesTool;
37
38// Define our tools for the Git-Iris toolbox
39#[derive(Debug)]
40pub enum GitIrisTools {
41    ReleaseNotesTool(ReleaseNotesTool),
42    ChangelogTool(ChangelogTool),
43    CommitTool(CommitTool),
44    CodeReviewTool(CodeReviewTool),
45}
46
47impl GitIrisTools {
48    /// Get all tools available in Git-Iris
49    pub fn get_tools() -> Vec<Tool> {
50        vec![
51            ReleaseNotesTool::get_tool_definition(),
52            ChangelogTool::get_tool_definition(),
53            CommitTool::get_tool_definition(),
54            CodeReviewTool::get_tool_definition(),
55        ]
56    }
57
58    /// Try to convert a parameter map into a `GitIrisTools` enum
59    pub fn try_from(params: Map<String, Value>) -> Result<Self, Error> {
60        // Check the tool name and convert to the appropriate variant
61        let tool_name = params
62            .get("name")
63            .and_then(|v| v.as_str())
64            .ok_or_else(|| Error::invalid_params("Tool name not specified", None))?;
65
66        match tool_name {
67            "git_iris_release_notes" => {
68                // Convert params to ReleaseNotesTool
69                let tool: ReleaseNotesTool = serde_json::from_value(Value::Object(params))
70                    .map_err(|e| Error::invalid_params(format!("Invalid parameters: {e}"), None))?;
71                Ok(GitIrisTools::ReleaseNotesTool(tool))
72            }
73            "git_iris_changelog" => {
74                // Convert params to ChangelogTool
75                let tool: ChangelogTool = serde_json::from_value(Value::Object(params))
76                    .map_err(|e| Error::invalid_params(format!("Invalid parameters: {e}"), None))?;
77                Ok(GitIrisTools::ChangelogTool(tool))
78            }
79            "git_iris_commit" => {
80                // Convert params to CommitTool
81                let tool: CommitTool = serde_json::from_value(Value::Object(params))
82                    .map_err(|e| Error::invalid_params(format!("Invalid parameters: {e}"), None))?;
83                Ok(GitIrisTools::CommitTool(tool))
84            }
85            "git_iris_code_review" => {
86                // Convert params to CodeReviewTool
87                let tool: CodeReviewTool = serde_json::from_value(Value::Object(params))
88                    .map_err(|e| Error::invalid_params(format!("Invalid parameters: {e}"), None))?;
89                Ok(GitIrisTools::CodeReviewTool(tool))
90            }
91            _ => Err(Error::invalid_params(
92                format!("Unknown tool: {tool_name}"),
93                None,
94            )),
95        }
96    }
97}
98
99/// Common error handling for Git-Iris tools
100pub fn handle_tool_error(e: &anyhow::Error) -> Error {
101    Error::invalid_params(format!("Tool execution failed: {e}"), None)
102}
103
104/// The main handler for Git-Iris, providing all MCP tools
105#[derive(Clone)]
106pub struct GitIrisHandler {
107    /// Git repository instance
108    pub git_repo: Arc<GitRepo>,
109    /// Git-Iris configuration
110    pub config: GitIrisConfig,
111    /// Workspace roots registered by the client
112    pub workspace_roots: Arc<Mutex<Vec<PathBuf>>>,
113}
114
115impl GitIrisHandler {
116    /// Create a new Git-Iris handler with the provided dependencies
117    pub fn new(git_repo: Arc<GitRepo>, config: GitIrisConfig) -> Self {
118        Self {
119            git_repo,
120            config,
121            workspace_roots: Arc::new(Mutex::new(Vec::new())),
122        }
123    }
124
125    /// Get the current workspace root, if available
126    pub fn get_workspace_root(&self) -> Option<PathBuf> {
127        let roots = self
128            .workspace_roots
129            .lock()
130            .expect("Failed to lock workspace roots mutex");
131        // Use the first workspace root if available
132        roots.first().cloned()
133    }
134}
135
136impl ServerHandler for GitIrisHandler {
137    fn get_info(&self) -> ServerInfo {
138        ServerInfo {
139            instructions: Some("Git-Iris is an AI-powered Git workflow assistant. You can use it to generate commit messages, review code, create changelogs and release notes.".to_string()),
140            capabilities: ServerCapabilities::builder()
141                .enable_tools()
142                .build(),
143            ..Default::default()
144        }
145    }
146
147    // Handle notification when client workspace roots change
148    fn on_roots_list_changed(&self) -> impl Future<Output = ()> + Send + '_ {
149        log_debug!("Client workspace roots changed");
150        async move {
151            // Access and update workspace roots
152            let roots = self
153                .workspace_roots
154                .lock()
155                .expect("Failed to lock workspace roots mutex");
156
157            // If we have a workspace root, log it
158            if let Some(root) = roots.first() {
159                log_debug!("Primary workspace root: {}", root.display());
160            } else {
161                log_debug!("No workspace roots provided by client");
162            }
163
164            // If this is a development log, print more information
165            if roots.len() > 1 {
166                for (i, root) in roots.iter().skip(1).enumerate() {
167                    log_debug!("Additional workspace root {}: {}", i + 1, root.display());
168                }
169            }
170        }
171    }
172
173    async fn list_tools(
174        &self,
175        _: PaginatedRequestParam,
176        _: RequestContext<RoleServer>,
177    ) -> Result<ListToolsResult, Error> {
178        // Use our custom method to get all tools
179        let tools = GitIrisTools::get_tools();
180
181        Ok(ListToolsResult {
182            next_cursor: None,
183            tools,
184        })
185    }
186
187    async fn call_tool(
188        &self,
189        request: CallToolRequestParam,
190        _: RequestContext<RoleServer>,
191    ) -> Result<CallToolResult, Error> {
192        // Get the arguments as a Map
193        let args = match &request.arguments {
194            Some(args) => args.clone(),
195            None => {
196                return Err(Error::invalid_params(
197                    String::from("Missing arguments"),
198                    None,
199                ));
200            }
201        };
202
203        // Add the tool name to the parameters
204        let mut params = args.clone();
205        params.insert("name".to_string(), Value::String(request.name.to_string()));
206
207        // Try to convert to our GitIrisTools enum
208        let tool_params = GitIrisTools::try_from(params)?;
209
210        // Make a clone of the repository path before executing the tool
211        // This prevents git2 objects from crossing async boundaries
212        let git_repo_path = self.git_repo.repo_path().clone();
213
214        // Clone config to avoid sharing it across async boundaries
215        let config = self.config.clone();
216
217        // Create a new git repo instance - handle any errors here before async code
218        let git_repo = match GitRepo::new(&git_repo_path) {
219            Ok(repo) => Arc::new(repo),
220            Err(e) => return Err(handle_tool_error(&e)),
221        };
222
223        // Use the GitIrisTool trait to execute any tool without matching on specific types
224        match tool_params {
225            GitIrisTools::ReleaseNotesTool(tool) => tool
226                .execute(git_repo.clone(), config.clone())
227                .await
228                .map_err(|e| handle_tool_error(&e)),
229            GitIrisTools::ChangelogTool(tool) => tool
230                .execute(git_repo.clone(), config.clone())
231                .await
232                .map_err(|e| handle_tool_error(&e)),
233            GitIrisTools::CommitTool(tool) => tool
234                .execute(git_repo.clone(), config.clone())
235                .await
236                .map_err(|e| handle_tool_error(&e)),
237            GitIrisTools::CodeReviewTool(tool) => tool
238                .execute(git_repo, config)
239                .await
240                .map_err(|e| handle_tool_error(&e)),
241        }
242    }
243}