git_iris/mcp/tools/
mod.rs1pub 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
32pub use self::changelog::ChangelogTool;
34pub use self::codereview::CodeReviewTool;
35pub use self::commit::CommitTool;
36pub use self::releasenotes::ReleaseNotesTool;
37
38#[derive(Debug)]
40pub enum GitIrisTools {
41 ReleaseNotesTool(ReleaseNotesTool),
42 ChangelogTool(ChangelogTool),
43 CommitTool(CommitTool),
44 CodeReviewTool(CodeReviewTool),
45}
46
47impl GitIrisTools {
48 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 pub fn try_from(params: Map<String, Value>) -> Result<Self, Error> {
60 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 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 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 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 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
99pub fn handle_tool_error(e: &anyhow::Error) -> Error {
101 Error::invalid_params(format!("Tool execution failed: {e}"), None)
102}
103
104#[derive(Clone)]
106pub struct GitIrisHandler {
107 pub git_repo: Arc<GitRepo>,
109 pub config: GitIrisConfig,
111 pub workspace_roots: Arc<Mutex<Vec<PathBuf>>>,
113}
114
115impl GitIrisHandler {
116 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 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 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 fn on_roots_list_changed(&self) -> impl Future<Output = ()> + Send + '_ {
149 log_debug!("Client workspace roots changed");
150 async move {
151 let roots = self
153 .workspace_roots
154 .lock()
155 .expect("Failed to lock workspace roots mutex");
156
157 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 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 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 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 let mut params = args.clone();
205 params.insert("name".to_string(), Value::String(request.name.to_string()));
206
207 let tool_params = GitIrisTools::try_from(params)?;
209
210 let git_repo_path = self.git_repo.repo_path().clone();
213
214 let config = self.config.clone();
216
217 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 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}