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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,12 @@ The following sets of tools are available (all are on by default):
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)

- **update_issue_comment** - Update comment of an issue
- `body`: Comment content (string, required)
- `comment_id`: Comment ID to update (number, required)
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)

- **add_sub_issue** - Add sub-issue
- `issue_number`: The number of the parent issue (number, required)
- `owner`: Repository owner (string, required)
Expand Down
35 changes: 35 additions & 0 deletions pkg/github/__toolsnaps__/update_issue_comment.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"annotations": {
"title": "Update a previously added comment to an issue",
"readOnlyHint": false
},
"description": "Update a previously added comment to a specific issue in a GitHub repository.",
"inputSchema": {
"properties": {
"body": {
"description": "Comment content",
"type": "string"
},
"comment_id": {
"description": "ID of the comment to update",
"type": "number"
},
"owner": {
"description": "Repository owner",
"type": "string"
},
"repo": {
"description": "Repository name",
"type": "string"
}
},
"required": [
"comment_id",
"owner",
"repo",
"body"
],
"type": "object"
},
"name": "update_issue_comment"
}
74 changes: 74 additions & 0 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,80 @@ func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc
}
}

// UpdateIssueComment creates a tool to update a previously added comment to an issue.
func UpdateIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("update_issue_comment",
mcp.WithDescription(t("TOOL_UPDATE_ISSUE_COMMENT_DESCRIPTION", "Update a previously added comment to a specific issue in a GitHub repository.")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_UPDATE_ISSUE_COMMENT_USER_TITLE", "Update a previously added comment to an issue"),
ReadOnlyHint: ToBoolPtr(false),
}),
mcp.WithNumber("comment_id",
mcp.Required(),
mcp.Description("ID of the comment to update"),
),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository name"),
),
mcp.WithString("body",
mcp.Required(),
mcp.Description("Comment content"),
),
),
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
}
body, err := RequiredParam[string](request, "body")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
commentID, err := RequiredInt(request, "comment_id")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

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

client, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}
updatedComment, resp, err := client.Issues.EditComment(ctx, owner, repo, int64(commentID), comment)
if err != nil {
return nil, fmt.Errorf("failed to update comment: %w", err)
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
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 update comment: %s", string(body))), nil
}

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

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

// AddSubIssue creates a tool to add a sub-issue to a parent issue.
func AddSubIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("add_sub_issue",
Expand Down
114 changes: 114 additions & 0 deletions pkg/github/issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,120 @@ func Test_AddIssueComment(t *testing.T) {
}
}

func Test_UpdateIssueComment(t *testing.T) {
// Verify tool definition once
mockClient := github.NewClient(nil)
tool, _ := UpdateIssueComment(stubGetClientFn(mockClient), translations.NullTranslationHelper)
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.Properties, "owner")
assert.Contains(t, tool.InputSchema.Properties, "repo")
assert.Contains(t, tool.InputSchema.Properties, "comment_id")
assert.Contains(t, tool.InputSchema.Properties, "body")
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "comment_id", "body"})

// Setup mock comment for success case
mockComment := &github.IssueComment{
ID: github.Ptr(int64(123)),
Body: github.Ptr("This is a test comment"),
User: &github.User{
Login: github.Ptr("testuser"),
},
HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42#issuecomment-123"),
}

tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]interface{}
expectError bool
expectedComment *github.IssueComment
expectedErrMsg string
}{
{
name: "successful comment update",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PatchReposIssuesCommentsByOwnerByRepoByCommentId,
mockResponse(t, http.StatusOK, mockComment),
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"comment_id": float64(123),
"body": "This is a test comment",
},
expectError: false,
expectedComment: mockComment,
},
{
name: "comment update fails",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PatchReposIssuesCommentsByOwnerByRepoByCommentId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnprocessableEntity)
_, _ = w.Write([]byte(`{"message": "Invalid request"}`))
}),
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"comment_id": float64(123),
"body": "",
},
expectError: false,
expectedErrMsg: "missing required parameter: body",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := UpdateIssueComment(stubGetClientFn(client), translations.NullTranslationHelper)

// Create call request
request := createMCPRequest(tc.requestArgs)

// Call handler
result, err := handler(context.Background(), request)

// Verify results
if tc.expectError {
require.Error(t, err)
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)

// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
fmt.Println("textContent", textContent)

// Unmarshal and verify the 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
mockClient := github.NewClient(nil)
Expand Down
1 change: 1 addition & 0 deletions pkg/github/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
AddWriteTools(
toolsets.NewServerTool(CreateIssue(getClient, t)),
toolsets.NewServerTool(AddIssueComment(getClient, t)),
toolsets.NewServerTool(UpdateIssueComment(getClient, t)),
toolsets.NewServerTool(UpdateIssue(getClient, getGQLClient, t)),
toolsets.NewServerTool(AssignCopilotToIssue(getGQLClient, t)),
toolsets.NewServerTool(AddSubIssue(getClient, t)),
Expand Down