Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8bb523f
Add planning artifacts for reply-to-review-comments feature
lossyrob Nov 19, 2025
bbab98d
Merge pull request #1 from lossyrob/feature/reply-to-review-comments_…
lossyrob Nov 19, 2025
f5140d4
Add ReplyToReviewComment tool for replying to PR review comments
lossyrob Nov 19, 2025
50910fa
Merge pull request #2 from lossyrob/feature/reply-to-review-comments_…
lossyrob Nov 20, 2025
31c8768
Register ReplyToReviewComment tool in pull_requests toolset
lossyrob Nov 20, 2025
d55854c
Update ImplementationPlan.md with Phase 2 commit hash
lossyrob Nov 20, 2025
d2784b0
docs: clarify intentional variable shadowing in error handling
lossyrob Nov 20, 2025
087774e
refactor: use responseBody to avoid variable shadowing
lossyrob Nov 20, 2025
7289421
Merge pull request #3 from lossyrob/feature/reply-to-review-comments_…
lossyrob Nov 20, 2025
8d6c3a9
Add comprehensive tests for ReplyToReviewComment tool
lossyrob Nov 20, 2025
558d07d
Update ImplementationPlan.md - Phase 3 complete
lossyrob Nov 20, 2025
dbfa30b
docs: add Phase 3 PR link to ImplementationPlan.md
lossyrob Nov 20, 2025
01f9323
test: remove E2E test - unit tests provide sufficient coverage
lossyrob Nov 20, 2025
540f87f
Merge pull request #4 from lossyrob/feature/reply-to-review-comments_…
lossyrob Nov 20, 2025
ef35315
Generate documentation and complete Phase 4 validation
lossyrob Nov 20, 2025
bc5e8a7
Merge pull request #5 from lossyrob/feature/reply-to-review-comments_…
lossyrob Nov 20, 2025
7248261
Add comprehensive documentation for reply_to_review_comment tool
lossyrob Nov 20, 2025
44e7f0f
Merge pull request #6 from lossyrob/feature/reply-to-review-comments_…
lossyrob Nov 20, 2025
80dca50
Add manual testing guide.
lossyrob Nov 20, 2025
b15f134
Remove .paw directory, as project isn't using PAW directly.
lossyrob Nov 20, 2025
68546f8
Merge branch 'main' into feature/reply-to-review-comments
lossyrob Nov 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,13 @@ Possible options:
- `pullNumber`: Pull request number (number, required)
- `repo`: Repository name (string, required)

- **reply_to_review_comment** - Reply to a review comment
- `body`: Reply text supporting GitHub-flavored Markdown (string, required)
- `comment_id`: Review comment ID from pull_request_read (number, required)
- `owner`: Repository owner (string, required)
- `pull_number`: Pull request number (number, required)
- `repo`: Repository name (string, required)

- **request_copilot_review** - Request Copilot review
- `owner`: Repository owner (string, required)
- `pullNumber`: Pull request number (number, required)
Expand Down
40 changes: 40 additions & 0 deletions pkg/github/__toolsnaps__/reply_to_review_comment.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"annotations": {
"title": "Reply to a review comment",
"readOnlyHint": false
},
"description": "Reply to a review comment on a pull request. Use this to respond directly within pull request review comment threads, maintaining conversation context at specific code locations.",
"inputSchema": {
"properties": {
"body": {
"description": "Reply text supporting GitHub-flavored Markdown",
"type": "string"
},
"comment_id": {
"description": "Review comment ID from pull_request_read",
"type": "number"
},
"owner": {
"description": "Repository owner",
"type": "string"
},
"pull_number": {
"description": "Pull request number",
"type": "number"
},
"repo": {
"description": "Repository name",
"type": "string"
}
},
"required": [
"owner",
"repo",
"pull_number",
"comment_id",
"body"
],
"type": "object"
},
"name": "reply_to_review_comment"
}
92 changes: 92 additions & 0 deletions pkg/github/pullrequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -1609,6 +1609,98 @@ func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelpe
}
}

// ReplyToReviewComment creates a tool to reply to a review comment on a pull request.
func ReplyToReviewComment(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
return mcp.NewTool("reply_to_review_comment",
mcp.WithDescription(t("TOOL_REPLY_TO_REVIEW_COMMENT_DESCRIPTION", "Reply to a review comment on a pull request. Use this to respond directly within pull request review comment threads, maintaining conversation context at specific code locations.")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_REPLY_TO_REVIEW_COMMENT_USER_TITLE", "Reply to a review comment"),
ReadOnlyHint: ToBoolPtr(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.WithNumber("comment_id",
mcp.Required(),
mcp.Description("Review comment ID from pull_request_read"),
),
mcp.WithString("body",
mcp.Required(),
mcp.Description("Reply text supporting GitHub-flavored Markdown"),
),
),
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
}

commentID, err := RequiredBigInt(request, "comment_id")
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, commentID)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to create reply to review comment",
resp,
err,
), nil
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusCreated {
responseBody, 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 reply to review comment: %s", string(responseBody))), nil
}
// Return minimal response with just essential information
minimalResponse := MinimalResponse{
ID: fmt.Sprintf("%d", comment.GetID()),
URL: comment.GetHTMLURL(),
}

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

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

// newGQLString like takes something that approximates a string (of which there are many types in shurcooL/githubv4)
// and constructs a pointer to it, or nil if the string is empty. This is extremely useful because when we parse
// params from the MCP request, we need to convert them to types that are pointers of type def strings and it's
Expand Down
Loading