@@ -644,6 +644,176 @@ func GetPullRequestComments(getClient GetClientFn, t translations.TranslationHel
644
644
}
645
645
}
646
646
647
+ // AddPullRequestReviewComment creates a tool to add a review comment to a pull request.
648
+ func AddPullRequestReviewComment (getClient GetClientFn , t translations.TranslationHelperFunc ) (tool mcp.Tool , handler server.ToolHandlerFunc ) {
649
+ return mcp .NewTool ("add_pull_request_review_comment" ,
650
+ mcp .WithDescription (t ("TOOL_ADD_PULL_REQUEST_COMMENT_DESCRIPTION" , "Add a review comment to a pull request" )),
651
+ mcp .WithString ("owner" ,
652
+ mcp .Required (),
653
+ mcp .Description ("Repository owner" ),
654
+ ),
655
+ mcp .WithString ("repo" ,
656
+ mcp .Required (),
657
+ mcp .Description ("Repository name" ),
658
+ ),
659
+ mcp .WithNumber ("pull_number" ,
660
+ mcp .Required (),
661
+ mcp .Description ("Pull request number" ),
662
+ ),
663
+ mcp .WithString ("body" ,
664
+ mcp .Required (),
665
+ mcp .Description ("The text of the review comment" ),
666
+ ),
667
+ mcp .WithString ("commit_id" ,
668
+ mcp .Description ("The SHA of the commit to comment on. Required unless in_reply_to is specified." ),
669
+ ),
670
+ mcp .WithString ("path" ,
671
+ mcp .Description ("The relative path to the file that necessitates a comment. Required unless in_reply_to is specified." ),
672
+ ),
673
+ mcp .WithString ("subject_type" ,
674
+ mcp .Description ("The level at which the comment is targeted, 'line' or 'file'" ),
675
+ mcp .Enum ("line" , "file" ),
676
+ ),
677
+ mcp .WithNumber ("line" ,
678
+ mcp .Description ("The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range" ),
679
+ ),
680
+ mcp .WithString ("side" ,
681
+ mcp .Description ("The side of the diff to comment on. Can be LEFT or RIGHT" ),
682
+ mcp .Enum ("LEFT" , "RIGHT" ),
683
+ ),
684
+ mcp .WithNumber ("start_line" ,
685
+ mcp .Description ("For multi-line comments, the first line of the range that the comment applies to" ),
686
+ ),
687
+ mcp .WithString ("start_side" ,
688
+ mcp .Description ("For multi-line comments, the starting side of the diff that the comment applies to. Can be LEFT or RIGHT" ),
689
+ mcp .Enum ("LEFT" , "RIGHT" ),
690
+ ),
691
+ mcp .WithNumber ("in_reply_to" ,
692
+ mcp .Description ("The ID of the review comment to reply to. When specified, only body is required and all other parameters are ignored" ),
693
+ ),
694
+ ),
695
+ func (ctx context.Context , request mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
696
+ owner , err := requiredParam [string ](request , "owner" )
697
+ if err != nil {
698
+ return mcp .NewToolResultError (err .Error ()), nil
699
+ }
700
+ repo , err := requiredParam [string ](request , "repo" )
701
+ if err != nil {
702
+ return mcp .NewToolResultError (err .Error ()), nil
703
+ }
704
+ pullNumber , err := RequiredInt (request , "pull_number" )
705
+ if err != nil {
706
+ return mcp .NewToolResultError (err .Error ()), nil
707
+ }
708
+ body , err := requiredParam [string ](request , "body" )
709
+ if err != nil {
710
+ return mcp .NewToolResultError (err .Error ()), nil
711
+ }
712
+
713
+ client , err := getClient (ctx )
714
+ if err != nil {
715
+ return nil , fmt .Errorf ("failed to get GitHub client: %w" , err )
716
+ }
717
+
718
+ // Check if this is a reply to an existing comment
719
+ if replyToFloat , ok := request .Params .Arguments ["in_reply_to" ].(float64 ); ok {
720
+ // Use the specialized method for reply comments due to inconsistency in underlying go-github library: https://github.com/google/go-github/pull/950
721
+ commentID := int64 (replyToFloat )
722
+ createdReply , resp , err := client .PullRequests .CreateCommentInReplyTo (ctx , owner , repo , pullNumber , body , commentID )
723
+ if err != nil {
724
+ return nil , fmt .Errorf ("failed to reply to pull request comment: %w" , err )
725
+ }
726
+ defer func () { _ = resp .Body .Close () }()
727
+
728
+ if resp .StatusCode != http .StatusCreated {
729
+ respBody , err := io .ReadAll (resp .Body )
730
+ if err != nil {
731
+ return nil , fmt .Errorf ("failed to read response body: %w" , err )
732
+ }
733
+ return mcp .NewToolResultError (fmt .Sprintf ("failed to reply to pull request comment: %s" , string (respBody ))), nil
734
+ }
735
+
736
+ r , err := json .Marshal (createdReply )
737
+ if err != nil {
738
+ return nil , fmt .Errorf ("failed to marshal response: %w" , err )
739
+ }
740
+
741
+ return mcp .NewToolResultText (string (r )), nil
742
+ }
743
+
744
+ // This is a new comment, not a reply
745
+ // Verify required parameters for a new comment
746
+ commitID , err := requiredParam [string ](request , "commit_id" )
747
+ if err != nil {
748
+ return mcp .NewToolResultError (err .Error ()), nil
749
+ }
750
+ path , err := requiredParam [string ](request , "path" )
751
+ if err != nil {
752
+ return mcp .NewToolResultError (err .Error ()), nil
753
+ }
754
+
755
+ comment := & github.PullRequestComment {
756
+ Body : github .Ptr (body ),
757
+ CommitID : github .Ptr (commitID ),
758
+ Path : github .Ptr (path ),
759
+ }
760
+
761
+ subjectType , err := OptionalParam [string ](request , "subject_type" )
762
+ if err != nil {
763
+ return mcp .NewToolResultError (err .Error ()), nil
764
+ }
765
+ if subjectType != "file" {
766
+ line , lineExists := request .Params .Arguments ["line" ].(float64 )
767
+ startLine , startLineExists := request .Params .Arguments ["start_line" ].(float64 )
768
+ side , sideExists := request .Params .Arguments ["side" ].(string )
769
+ startSide , startSideExists := request .Params .Arguments ["start_side" ].(string )
770
+
771
+ if ! lineExists {
772
+ return mcp .NewToolResultError ("line parameter is required unless using subject_type:file" ), nil
773
+ }
774
+
775
+ comment .Line = github .Ptr (int (line ))
776
+ if sideExists {
777
+ comment .Side = github .Ptr (side )
778
+ }
779
+ if startLineExists {
780
+ comment .StartLine = github .Ptr (int (startLine ))
781
+ }
782
+ if startSideExists {
783
+ comment .StartSide = github .Ptr (startSide )
784
+ }
785
+
786
+ if startLineExists && ! lineExists {
787
+ return mcp .NewToolResultError ("if start_line is provided, line must also be provided" ), nil
788
+ }
789
+ if startSideExists && ! sideExists {
790
+ return mcp .NewToolResultError ("if start_side is provided, side must also be provided" ), nil
791
+ }
792
+ }
793
+
794
+ createdComment , resp , err := client .PullRequests .CreateComment (ctx , owner , repo , pullNumber , comment )
795
+ if err != nil {
796
+ return nil , fmt .Errorf ("failed to create pull request comment: %w" , err )
797
+ }
798
+ defer func () { _ = resp .Body .Close () }()
799
+
800
+ if resp .StatusCode != http .StatusCreated {
801
+ respBody , err := io .ReadAll (resp .Body )
802
+ if err != nil {
803
+ return nil , fmt .Errorf ("failed to read response body: %w" , err )
804
+ }
805
+ return mcp .NewToolResultError (fmt .Sprintf ("failed to create pull request comment: %s" , string (respBody ))), nil
806
+ }
807
+
808
+ r , err := json .Marshal (createdComment )
809
+ if err != nil {
810
+ return nil , fmt .Errorf ("failed to marshal response: %w" , err )
811
+ }
812
+
813
+ return mcp .NewToolResultText (string (r )), nil
814
+ }
815
+ }
816
+
647
817
// GetPullRequestReviews creates a tool to get the reviews on a pull request.
648
818
func GetPullRequestReviews (getClient GetClientFn , t translations.TranslationHelperFunc ) (tool mcp.Tool , handler server.ToolHandlerFunc ) {
649
819
return mcp .NewTool ("get_pull_request_reviews" ,
0 commit comments