@@ -644,6 +644,176 @@ func GetPullRequestComments(getClient GetClientFn, t translations.TranslationHel
644644 }
645645}
646646
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+
647817// GetPullRequestReviews creates a tool to get the reviews on a pull request.
648818func GetPullRequestReviews (getClient GetClientFn , t translations.TranslationHelperFunc ) (tool mcp.Tool , handler server.ToolHandlerFunc ) {
649819 return mcp .NewTool ("get_pull_request_reviews" ,
0 commit comments