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)),








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- 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