gitai/server/tools/
pr.rs

1//! MCP PR description tool implementation
2//!
3//! This module provides the MCP tool for generating pull request descriptions.
4
5use crate::config::Config as PilotConfig;
6use crate::debug;
7use crate::features::commit::{service::CommitService, types::format_pull_request};
8use crate::git::GitRepo;
9use crate::server::tools::utils::{
10    PilotTool, apply_custom_instructions, create_text_result, resolve_git_repo,
11    validate_repository_parameter,
12};
13
14use rmcp::handler::server::tool::cached_schema_for_type;
15use rmcp::model::{CallToolResult, Tool};
16use rmcp::schemars;
17
18use serde::{Deserialize, Serialize};
19use std::borrow::Cow;
20use std::sync::Arc;
21
22/// PR description tool for generating comprehensive pull request descriptions
23#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
24pub struct PrTool {
25    /// Starting reference (commit hash, tag, or branch name)
26    pub from: String,
27
28    /// Ending reference (commit hash, tag, or branch name). Defaults to HEAD if not specified.
29    #[serde(default)]
30    pub to: String,
31
32    /// Preset instruction set to use for the PR description
33    #[serde(default)]
34    pub preset: String,
35
36    /// Custom instructions for the AI
37    #[serde(default)]
38    pub custom_instructions: String,
39
40    /// Repository path (local) or URL (remote). Required.
41    pub repository: String,
42}
43
44impl PrTool {
45    /// Returns the tool definition for the PR description tool
46    pub fn get_tool_definition() -> Tool {
47        Tool {
48            name: Cow::Borrowed("gitai_pr"),
49            description: Some(Cow::Borrowed(
50                "Generate comprehensive pull request descriptions for changesets spanning multiple commits. Analyzes commits and changes as an atomic unit.",
51            )),
52            input_schema: cached_schema_for_type::<Self>(),
53            annotations: None,
54            icons: None,
55            output_schema: None,
56            title: None,
57        }
58    }
59}
60
61#[async_trait::async_trait]
62impl PilotTool for PrTool {
63    /// Execute the PR description tool with the provided repository and configuration
64    async fn execute(
65        &self,
66        git_repo: Arc<GitRepo>,
67        config: PilotConfig,
68    ) -> Result<CallToolResult, anyhow::Error> {
69        debug!("Generating PR description with: {:?}", self);
70
71        // Validate repository parameter
72        validate_repository_parameter(&self.repository)?;
73        let git_repo = resolve_git_repo(Some(self.repository.as_str()), git_repo)?;
74        debug!("Using repository: {}", git_repo.repo_path().display());
75
76        // Validate that we have both from and to parameters
77        if self.from.trim().is_empty() {
78            return Err(anyhow::anyhow!(
79                "The 'from' parameter is required for PR description generation"
80            ));
81        }
82
83        // Default to HEAD if to is empty
84        let to = if self.to.trim().is_empty() {
85            "HEAD".to_string()
86        } else {
87            self.to.clone()
88        };
89
90        // Check if this is a remote repository (read-only mode)
91        if git_repo.is_remote() {
92            debug!("Operating on remote repository in read-only mode");
93        }
94
95        // Create a commit service for processing
96        let repo_path = git_repo.repo_path().clone();
97        let provider_name = &config.default_provider;
98
99        let service = CommitService::new(
100            config.clone(),
101            &repo_path,
102            provider_name,
103            false, // emoji not needed for PR
104            false, // verification not needed for PR
105            GitRepo::new(&repo_path)?,
106        )?;
107
108        // Set up config with custom instructions if provided
109        let mut config_clone = config.clone();
110        apply_custom_instructions(&mut config_clone, &self.custom_instructions);
111
112        // Process the preset
113        let preset = if self.preset.trim().is_empty() {
114            "default"
115        } else {
116            &self.preset
117        };
118
119        // Generate the PR description using the commit range
120        let pr_description = service
121            .generate_pr_for_commit_range(preset, &self.custom_instructions, &self.from, &to)
122            .await?;
123
124        // Format and return the PR description
125        let formatted_pr = format_pull_request(&pr_description);
126        Ok(create_text_result(formatted_pr))
127    }
128}