func AddPullRequestReviewComment()

in pkg/github/pullrequests.go [684:855]


func AddPullRequestReviewComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
	return mcp.NewTool("add_pull_request_review_comment",
			mcp.WithDescription(t("TOOL_ADD_PULL_REQUEST_REVIEW_COMMENT_DESCRIPTION", "Add a review comment to a pull request.")),
			mcp.WithToolAnnotation(mcp.ToolAnnotation{
				Title:        t("TOOL_ADD_PULL_REQUEST_REVIEW_COMMENT_USER_TITLE", "Add review comment to pull request"),
				ReadOnlyHint: false,
			}),
			mcp.WithString("owner",
				mcp.Required(),
				mcp.Description("Repository owner"),
			),
			mcp.WithString("repo",
				mcp.Required(),
				mcp.Description("Repository name"),
			),
			mcp.WithNumber("pull_number",
				mcp.Required(),
				mcp.Description("Pull request number"),
			),
			mcp.WithString("body",
				mcp.Required(),
				mcp.Description("The text of the review comment"),
			),
			mcp.WithString("commit_id",
				mcp.Description("The SHA of the commit to comment on. Required unless in_reply_to is specified."),
			),
			mcp.WithString("path",
				mcp.Description("The relative path to the file that necessitates a comment. Required unless in_reply_to is specified."),
			),
			mcp.WithString("subject_type",
				mcp.Description("The level at which the comment is targeted"),
				mcp.Enum("line", "file"),
			),
			mcp.WithNumber("line",
				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"),
			),
			mcp.WithString("side",
				mcp.Description("The side of the diff to comment on"),
				mcp.Enum("LEFT", "RIGHT"),
			),
			mcp.WithNumber("start_line",
				mcp.Description("For multi-line comments, the first line of the range that the comment applies to"),
			),
			mcp.WithString("start_side",
				mcp.Description("For multi-line comments, the starting side of the diff that the comment applies to"),
				mcp.Enum("LEFT", "RIGHT"),
			),
			mcp.WithNumber("in_reply_to",
				mcp.Description("The ID of the review comment to reply to. When specified, only body is required and all other parameters are ignored"),
			),
		),
		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			owner, err := requiredParam[string](request, "owner")
			if err != nil {
				return mcp.NewToolResultError(err.Error()), nil
			}
			repo, err := requiredParam[string](request, "repo")
			if err != nil {
				return mcp.NewToolResultError(err.Error()), nil
			}
			pullNumber, err := RequiredInt(request, "pull_number")
			if err != nil {
				return mcp.NewToolResultError(err.Error()), nil
			}
			body, err := requiredParam[string](request, "body")
			if err != nil {
				return mcp.NewToolResultError(err.Error()), nil
			}

			client, err := getClient(ctx)
			if err != nil {
				return nil, fmt.Errorf("failed to get GitHub client: %w", err)
			}

			// Check if this is a reply to an existing comment
			if replyToFloat, ok := request.Params.Arguments["in_reply_to"].(float64); ok {
				// Use the specialized method for reply comments due to inconsistency in underlying go-github library: https://github.com/google/go-github/pull/950
				commentID := int64(replyToFloat)
				createdReply, resp, err := client.PullRequests.CreateCommentInReplyTo(ctx, owner, repo, pullNumber, body, commentID)
				if err != nil {
					return nil, fmt.Errorf("failed to reply to pull request comment: %w", err)
				}
				defer func() { _ = resp.Body.Close() }()

				if resp.StatusCode != http.StatusCreated {
					respBody, err := io.ReadAll(resp.Body)
					if err != nil {
						return nil, fmt.Errorf("failed to read response body: %w", err)
					}
					return mcp.NewToolResultError(fmt.Sprintf("failed to reply to pull request comment: %s", string(respBody))), nil
				}

				r, err := json.Marshal(createdReply)
				if err != nil {
					return nil, fmt.Errorf("failed to marshal response: %w", err)
				}

				return mcp.NewToolResultText(string(r)), nil
			}

			// This is a new comment, not a reply
			// Verify required parameters for a new comment
			commitID, err := requiredParam[string](request, "commit_id")
			if err != nil {
				return mcp.NewToolResultError(err.Error()), nil
			}
			path, err := requiredParam[string](request, "path")
			if err != nil {
				return mcp.NewToolResultError(err.Error()), nil
			}

			comment := &github.PullRequestComment{
				Body:     github.Ptr(body),
				CommitID: github.Ptr(commitID),
				Path:     github.Ptr(path),
			}

			subjectType, err := OptionalParam[string](request, "subject_type")
			if err != nil {
				return mcp.NewToolResultError(err.Error()), nil
			}
			if subjectType != "file" {
				line, lineExists := request.Params.Arguments["line"].(float64)
				startLine, startLineExists := request.Params.Arguments["start_line"].(float64)
				side, sideExists := request.Params.Arguments["side"].(string)
				startSide, startSideExists := request.Params.Arguments["start_side"].(string)

				if !lineExists {
					return mcp.NewToolResultError("line parameter is required unless using subject_type:file"), nil
				}

				comment.Line = github.Ptr(int(line))
				if sideExists {
					comment.Side = github.Ptr(side)
				}
				if startLineExists {
					comment.StartLine = github.Ptr(int(startLine))
				}
				if startSideExists {
					comment.StartSide = github.Ptr(startSide)
				}

				if startLineExists && !lineExists {
					return mcp.NewToolResultError("if start_line is provided, line must also be provided"), nil
				}
				if startSideExists && !sideExists {
					return mcp.NewToolResultError("if start_side is provided, side must also be provided"), nil
				}
			}

			createdComment, resp, err := client.PullRequests.CreateComment(ctx, owner, repo, pullNumber, comment)
			if err != nil {
				return nil, fmt.Errorf("failed to create pull request comment: %w", err)
			}
			defer func() { _ = resp.Body.Close() }()

			if resp.StatusCode != http.StatusCreated {
				respBody, err := io.ReadAll(resp.Body)
				if err != nil {
					return nil, fmt.Errorf("failed to read response body: %w", err)
				}
				return mcp.NewToolResultError(fmt.Sprintf("failed to create pull request comment: %s", string(respBody))), nil
			}

			r, err := json.Marshal(createdComment)
			if err != nil {
				return nil, fmt.Errorf("failed to marshal response: %w", err)
			}

			return mcp.NewToolResultText(string(r)), nil
		}
}