Content-Length: 12607 | pFad | http://github.com/github/github-mcp-server/pull/723.patch
thub.com
From a6059e14c8329c17381c8b395780b050e780a618 Mon Sep 17 00:00:00 2001
From: Piotr Laskowski
Date: Mon, 21 Jul 2025 20:55:55 +0200
Subject: [PATCH] feat: add add_reply_to_pull_request_comment tool
Add a new tool that allows AI agents to reply to existing pull request comments.
This tool uses GitHub's CreateCommentInReplyTo REST API to create threaded
conversations on pull requests.
Features:
- Reply to any existing PR comment using its ID
- Proper error handling for missing parameters and API failures
- Comprehensive test coverage (8 test cases)
- Follows project patterns and conventions
- Registered in pull_requests toolset as a write operation
Parameters:
- owner: Repository owner (required)
- repo: Repository name (required)
- pullNumber: Pull request number (required)
- commentId: ID of comment to reply to (required)
- body: Reply text content (required)
This tool complements the existing add_comment_to_pending_review tool by
enabling responses to already-posted comments, enhancing AI-powered
code review workflows.
Closes: #N/A
---
.../add_reply_to_pull_request_comment.snap | 40 +++++
pkg/github/pullrequests.go | 79 +++++++++
pkg/github/pullrequests_test.go | 165 ++++++++++++++++++
pkg/github/tools.go | 1 +
4 files changed, 285 insertions(+)
create mode 100644 pkg/github/__toolsnaps__/add_reply_to_pull_request_comment.snap
diff --git a/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment.snap b/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment.snap
new file mode 100644
index 000000000..4f0d86e3c
--- /dev/null
+++ b/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment.snap
@@ -0,0 +1,40 @@
+{
+ "annotations": {
+ "title": "Add reply to pull request comment",
+ "readOnlyHint": false
+ },
+ "description": "Add a reply to an existing pull request comment. This creates a new comment that is linked as a reply to the specified comment.",
+ "inputSchema": {
+ "properties": {
+ "body": {
+ "description": "The text of the reply comment",
+ "type": "string"
+ },
+ "commentId": {
+ "description": "The ID of the comment to reply to",
+ "type": "number"
+ },
+ "owner": {
+ "description": "Repository owner",
+ "type": "string"
+ },
+ "pullNumber": {
+ "description": "Pull request number",
+ "type": "number"
+ },
+ "repo": {
+ "description": "Repository name",
+ "type": "string"
+ }
+ },
+ "required": [
+ "owner",
+ "repo",
+ "pullNumber",
+ "commentId",
+ "body"
+ ],
+ "type": "object"
+ },
+ "name": "add_reply_to_pull_request_comment"
+}
\ No newline at end of file
diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go
index 47b7c6bd2..fef461e15 100644
--- a/pkg/github/pullrequests.go
+++ b/pkg/github/pullrequests.go
@@ -891,6 +891,85 @@ func GetPullRequestComments(getClient GetClientFn, t translations.TranslationHel
}
}
+// AddReplyToPullRequestComment creates a tool to add a reply to an existing pull request comment.
+func AddReplyToPullRequestComment(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
+ return mcp.NewTool("add_reply_to_pull_request_comment",
+ mcp.WithDescription(t("TOOL_ADD_REPLY_TO_PULL_REQUEST_COMMENT_DESCRIPTION", "Add a reply to an existing pull request comment. This creates a new comment that is linked as a reply to the specified comment.")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_ADD_REPLY_TO_PULL_REQUEST_COMMENT_USER_TITLE", "Add reply to pull request comment"),
+ ReadOnlyHint: ToBoolPtr(false),
+ }),
+ mcp.WithString("owner",
+ mcp.Required(),
+ mcp.Description("Repository owner"),
+ ),
+ mcp.WithString("repo",
+ mcp.Required(),
+ mcp.Description("Repository name"),
+ ),
+ mcp.WithNumber("pullNumber",
+ mcp.Required(),
+ mcp.Description("Pull request number"),
+ ),
+ mcp.WithNumber("commentId",
+ mcp.Required(),
+ mcp.Description("The ID of the comment to reply to"),
+ ),
+ mcp.WithString("body",
+ mcp.Required(),
+ mcp.Description("The text of the reply comment"),
+ ),
+ ),
+ 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, "pullNumber")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ commentID, err := RequiredInt(request, "commentId")
+ 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)
+ }
+
+ comment, resp, err := client.PullRequests.CreateCommentInReplyTo(ctx, owner, repo, pullNumber, body, int64(commentID))
+ if err != nil {
+ return nil, fmt.Errorf("failed to add reply to pull request comment: %w", err)
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusCreated {
+ body, 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 add reply to pull request comment: %s", string(body))), nil
+ }
+
+ r, err := json.Marshal(comment)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+ }
+}
+
// GetPullRequestReviews creates a tool to get the reviews on a pull request.
func GetPullRequestReviews(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
return mcp.NewTool("get_pull_request_reviews",
diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go
index 42fd5bf03..f460b4001 100644
--- a/pkg/github/pullrequests_test.go
+++ b/pkg/github/pullrequests_test.go
@@ -2590,3 +2590,168 @@ func getLatestPendingReviewQuery(p getLatestPendingReviewQueryParams) githubv4mo
),
)
}
+
+func TestAddReplyToPullRequestComment(t *testing.T) {
+ t.Parallel()
+
+ // Verify tool definition once
+ mockClient := github.NewClient(nil)
+ tool, _ := AddReplyToPullRequestComment(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+ require.NoError(t, toolsnaps.Test(tool.Name, tool))
+
+ assert.Equal(t, "add_reply_to_pull_request_comment", tool.Name)
+ assert.NotEmpty(t, tool.Description)
+ assert.Contains(t, tool.InputSchema.Properties, "owner")
+ assert.Contains(t, tool.InputSchema.Properties, "repo")
+ assert.Contains(t, tool.InputSchema.Properties, "pullNumber")
+ assert.Contains(t, tool.InputSchema.Properties, "commentId")
+ assert.Contains(t, tool.InputSchema.Properties, "body")
+ assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber", "commentId", "body"})
+
+ // Setup mock reply comment for success case
+ mockReplyComment := &github.PullRequestComment{
+ ID: github.Ptr(int64(456)),
+ Body: github.Ptr("This is a reply to the comment"),
+ InReplyTo: github.Ptr(int64(123)),
+ HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42#discussion_r456"),
+ User: &github.User{
+ Login: github.Ptr("responder"),
+ },
+ CreatedAt: &github.Timestamp{Time: time.Now()},
+ UpdatedAt: &github.Timestamp{Time: time.Now()},
+ }
+
+ tests := []struct {
+ name string
+ mockedClient *http.Client
+ requestArgs map[string]interface{}
+ expectToolError bool
+ expectedToolErrMsg string
+ }{
+ {
+ name: "successful reply to pull request comment",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.PostReposPullsCommentsByOwnerByRepoByPullNumber,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusCreated)
+ responseData, _ := json.Marshal(mockReplyComment)
+ _, _ = w.Write(responseData)
+ }),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "owner": "owner",
+ "repo": "repo",
+ "pullNumber": float64(42),
+ "commentId": float64(123),
+ "body": "This is a reply to the comment",
+ },
+ },
+ {
+ name: "missing required parameter owner",
+ requestArgs: map[string]interface{}{
+ "repo": "repo",
+ "pullNumber": float64(42),
+ "commentId": float64(123),
+ "body": "This is a reply to the comment",
+ },
+ expectToolError: true,
+ expectedToolErrMsg: "missing required parameter: owner",
+ },
+ {
+ name: "missing required parameter repo",
+ requestArgs: map[string]interface{}{
+ "owner": "owner",
+ "pullNumber": float64(42),
+ "commentId": float64(123),
+ "body": "This is a reply to the comment",
+ },
+ expectToolError: true,
+ expectedToolErrMsg: "missing required parameter: repo",
+ },
+ {
+ name: "missing required parameter pullNumber",
+ requestArgs: map[string]interface{}{
+ "owner": "owner",
+ "repo": "repo",
+ "commentId": float64(123),
+ "body": "This is a reply to the comment",
+ },
+ expectToolError: true,
+ expectedToolErrMsg: "missing required parameter: pullNumber",
+ },
+ {
+ name: "missing required parameter commentId",
+ requestArgs: map[string]interface{}{
+ "owner": "owner",
+ "repo": "repo",
+ "pullNumber": float64(42),
+ "body": "This is a reply to the comment",
+ },
+ expectToolError: true,
+ expectedToolErrMsg: "missing required parameter: commentId",
+ },
+ {
+ name: "missing required parameter body",
+ requestArgs: map[string]interface{}{
+ "owner": "owner",
+ "repo": "repo",
+ "pullNumber": float64(42),
+ "commentId": float64(123),
+ },
+ expectToolError: true,
+ expectedToolErrMsg: "missing required parameter: body",
+ },
+ {
+ name: "API error when adding reply",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.PostReposPullsCommentsByOwnerByRepoByPullNumber,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+ _, _ = w.Write([]byte(`{"message": "Not Found"}`))
+ }),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "owner": "owner",
+ "repo": "repo",
+ "pullNumber": float64(42),
+ "commentId": float64(123),
+ "body": "This is a reply to the comment",
+ },
+ expectToolError: true,
+ expectedToolErrMsg: "failed to add reply to pull request comment",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // Setup client with mock
+ client := github.NewClient(tc.mockedClient)
+ _, handler := AddReplyToPullRequestComment(stubGetClientFn(client), translations.NullTranslationHelper)
+
+ // Create call request
+ request := createMCPRequest(tc.requestArgs)
+
+ // Call handler
+ result, err := handler(context.Background(), request)
+ require.NoError(t, err)
+
+ if tc.expectToolError {
+ require.True(t, result.IsError)
+ errorContent := getErrorResult(t, result)
+ assert.Contains(t, errorContent.Text, tc.expectedToolErrMsg)
+ return
+ }
+
+ // Parse the result and verify it's not an error
+ require.False(t, result.IsError)
+ textContent := getTextResult(t, result)
+ assert.Contains(t, textContent.Text, "This is a reply to the comment")
+ })
+ }
+}
diff --git a/pkg/github/tools.go b/pkg/github/tools.go
index bd349171d..47a178d8d 100644
--- a/pkg/github/tools.go
+++ b/pkg/github/tools.go
@@ -85,6 +85,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
toolsets.NewServerTool(CreatePullRequest(getClient, t)),
toolsets.NewServerTool(UpdatePullRequest(getClient, t)),
toolsets.NewServerTool(RequestCopilotReview(getClient, t)),
+ toolsets.NewServerTool(AddReplyToPullRequestComment(getClient, t)),
// Reviews
toolsets.NewServerTool(CreateAndSubmitPullRequestReview(getGQLClient, t)),
--- a PPN by Garber Painting Akron. With Image Size Reduction included!Fetched URL: http://github.com/github/github-mcp-server/pull/723.patch
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy