Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,13 @@ The following sets of tools are available:
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)

- **update_issue_comment** - Update an existing issue or pull request comment
- **Required OAuth Scopes**: `repo`
- `body`: New comment content (string, required)
- `comment_id`: ID of the comment to update, from issue_read get_comments or add_issue_comment response (number, required)
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)

- **get_label** - Get a specific label from a repository.
- **Required OAuth Scopes**: `repo`
- `name`: Label name. (string, required)
Expand Down Expand Up @@ -1093,8 +1100,8 @@ The following sets of tools are available:

- **pull_request_read** - Get details for a single pull request
- **Required OAuth Scopes**: `repo`
- `method`: Action to specify what pull request data needs to be retrieved from GitHub.
Possible options:
- `method`: Action to specify what pull request data needs to be retrieved from GitHub.
Possible options:
1. get - Get details of a specific pull request.
2. get_diff - Get the diff of a pull request.
3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.
Expand Down
34 changes: 34 additions & 0 deletions pkg/github/__toolsnaps__/update_issue_comment.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"annotations": {
"title": "Update issue comment"
},
"description": "Update an existing comment on an issue or pull request in a GitHub repository. Use the comment ID from issue_read with method get_comments, or from the comment object when adding a comment.",
"inputSchema": {
"properties": {
"body": {
"description": "New comment content",
"type": "string"
},
"comment_id": {
"description": "ID of the comment to update (from issue_read get_comments or add_issue_comment response)",
"type": "number"
},
"owner": {
"description": "Repository owner",
"type": "string"
},
"repo": {
"description": "Repository name",
"type": "string"
}
},
"required": [
"owner",
"repo",
"comment_id",
"body"
],
"type": "object"
},
"name": "update_issue_comment"
}
1 change: 1 addition & 0 deletions pkg/github/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const (
GetReposIssuesCommentsByOwnerByRepoByIssueNumber = "GET /repos/{owner}/{repo}/issues/{issue_number}/comments"
PostReposIssuesByOwnerByRepo = "POST /repos/{owner}/{repo}/issues"
PostReposIssuesCommentsByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/comments"
PatchReposIssuesCommentsByOwnerByRepoByCommentID = "PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}"
PatchReposIssuesByOwnerByRepoByIssueNumber = "PATCH /repos/{owner}/{repo}/issues/{issue_number}"
GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber = "GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues"
PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues"
Expand Down
84 changes: 84 additions & 0 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,90 @@ func AddIssueComment(t translations.TranslationHelperFunc) inventory.ServerTool
})
}

// UpdateIssueComment creates a tool to update an existing comment on an issue (or pull request).
func UpdateIssueComment(t translations.TranslationHelperFunc) inventory.ServerTool {
return NewTool(
ToolsetMetadataIssues,
mcp.Tool{
Name: "update_issue_comment",
Description: t("TOOL_UPDATE_ISSUE_COMMENT_DESCRIPTION", "Update an existing comment on an issue or pull request in a GitHub repository. Use the comment ID from issue_read with method get_comments, or from the comment object when adding a comment."),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_UPDATE_ISSUE_COMMENT_USER_TITLE", "Update issue comment"),
ReadOnlyHint: false,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"comment_id": {
Type: "number",
Description: "ID of the comment to update (from issue_read get_comments or add_issue_comment response)",
},
"body": {
Type: "string",
Description: "New comment content",
},
},
Required: []string{"owner", "repo", "comment_id", "body"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
owner, err := RequiredParam[string](args, "owner")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
repo, err := RequiredParam[string](args, "repo")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
commentID, err := RequiredBigInt(args, "comment_id")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
body, err := RequiredParam[string](args, "body")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

comment := &github.IssueComment{
Body: github.Ptr(body),
}

client, err := deps.GetClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
}
updatedComment, resp, err := client.Issues.EditComment(ctx, owner, repo, commentID, comment)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to update comment", err), nil, nil
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to update comment", resp, body), nil, nil
}

r, err := json.Marshal(updatedComment)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
}

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

// SubIssueWrite creates a tool to add a sub-issue to a parent issue.
func SubIssueWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
return NewTool(
Expand Down
103 changes: 103 additions & 0 deletions pkg/github/issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,109 @@ func Test_AddIssueComment(t *testing.T) {
}
}

func Test_UpdateIssueComment(t *testing.T) {
// Verify tool definition once
serverTool := UpdateIssueComment(translations.NullTranslationHelper)
tool := serverTool.Tool
require.NoError(t, toolsnaps.Test(tool.Name, tool))

assert.Equal(t, "update_issue_comment", tool.Name)
assert.NotEmpty(t, tool.Description)

assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "comment_id")
assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "body")
assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo", "comment_id", "body"})

mockComment := &github.IssueComment{
ID: github.Ptr(int64(456)),
Body: github.Ptr("Updated comment body"),
User: &github.User{
Login: github.Ptr("testuser"),
},
HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42#issuecomment-456"),
}

tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedComment *github.IssueComment
expectedErrMsg string
}{
{
name: "successful comment update",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesCommentsByOwnerByRepoByCommentID: mockResponse(t, http.StatusOK, mockComment),
}),
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"comment_id": float64(456),
"body": "Updated comment body",
},
expectError: false,
expectedComment: mockComment,
},
{
name: "comment update fails - missing body",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesCommentsByOwnerByRepoByCommentID: mockResponse(t, http.StatusOK, mockComment),
}),
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"comment_id": float64(456),
"body": "",
},
expectError: false,
expectedErrMsg: "missing required parameter: body",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := github.NewClient(tc.mockedClient)
deps := BaseDeps{
Client: client,
}
handler := serverTool.Handler(deps)

request := createMCPRequest(tc.requestArgs)

result, err := handler(ContextWithDeps(context.Background(), deps), &request)

if tc.expectError {
require.Error(t, err)
if tc.expectedErrMsg != "" {
assert.Contains(t, err.Error(), tc.expectedErrMsg)
}
return
}

if tc.expectedErrMsg != "" {
require.NotNil(t, result)
textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, tc.expectedErrMsg)
return
}

require.NoError(t, err)

textContent := getTextResult(t, result)

var returnedComment github.IssueComment
err = json.Unmarshal([]byte(textContent.Text), &returnedComment)
require.NoError(t, err)
assert.Equal(t, *tc.expectedComment.ID, *returnedComment.ID)
assert.Equal(t, *tc.expectedComment.Body, *returnedComment.Body)
assert.Equal(t, *tc.expectedComment.User.Login, *returnedComment.User.Login)
})
}
}

func Test_SearchIssues(t *testing.T) {
// Verify tool definition once
serverTool := SearchIssues(translations.NullTranslationHelper)
Expand Down
1 change: 1 addition & 0 deletions pkg/github/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ func AllTools(t translations.TranslationHelperFunc) []inventory.ServerTool {
ListIssueTypes(t),
IssueWrite(t),
AddIssueComment(t),
UpdateIssueComment(t),
SubIssueWrite(t),

// User tools
Expand Down