Skip to content

Commit 80cc568

Browse files
authored
Merge branch 'main' into feat/remove-reaction-tools
2 parents 5135adf + c71961c commit 80cc568

7 files changed

Lines changed: 329 additions & 0 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,13 @@ The following sets of tools are available:
10571057
- `repo`: Repository name (string, required)
10581058
- `sub_issue_id`: The ID of the sub-issue to add. ID is not the same as issue number (number, required)
10591059

1060+
- **update_issue_comment** - Update issue comment
1061+
- **OAuth Challenge Scopes**: `repo`
1062+
- `body`: New comment content (string, required)
1063+
- `comment_id`: The numeric ID of the issue or pull request conversation comment to update. Do not use a pull request review comment ID. (integer, required)
1064+
- `owner`: Repository owner (string, required)
1065+
- `repo`: Repository name (string, required)
1066+
10601067
</details>
10611068

10621069
<details>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"annotations": {
3+
"idempotentHint": false,
4+
"readOnlyHint": false,
5+
"title": "Update issue comment"
6+
},
7+
"description": "Update the body of an existing issue or pull request conversation comment. This tool cannot update pull request review comments.",
8+
"inputSchema": {
9+
"properties": {
10+
"body": {
11+
"description": "New comment content",
12+
"minLength": 1,
13+
"type": "string"
14+
},
15+
"comment_id": {
16+
"description": "The numeric ID of the issue or pull request conversation comment to update. Do not use a pull request review comment ID.",
17+
"minimum": 1,
18+
"type": "integer"
19+
},
20+
"owner": {
21+
"description": "Repository owner",
22+
"type": "string"
23+
},
24+
"repo": {
25+
"description": "Repository name",
26+
"type": "string"
27+
}
28+
},
29+
"required": [
30+
"owner",
31+
"repo",
32+
"comment_id",
33+
"body"
34+
],
35+
"type": "object"
36+
},
37+
"name": "update_issue_comment"
38+
}

pkg/github/helper_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const (
6464
GetReposIssuesCommentsByOwnerByRepoByIssueNumber = "GET /repos/{owner}/{repo}/issues/{issue_number}/comments"
6565
PostReposIssuesByOwnerByRepo = "POST /repos/{owner}/{repo}/issues"
6666
PostReposIssuesCommentsByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/comments"
67+
PatchReposIssuesCommentByOwnerByRepoByCommentID = "PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}"
6768
PostReposIssuesReactionsByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/reactions"
6869
DeleteReposIssuesReactionsByOwnerByRepoByIssueNumber = "DELETE /repos/{owner}/{repo}/issues/{issue_number}/reactions/{reaction_id}"
6970
PatchReposIssuesByOwnerByRepoByIssueNumber = "PATCH /repos/{owner}/{repo}/issues/{issue_number}"

pkg/github/issues.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,6 +1552,97 @@ func AddIssueComment(t translations.TranslationHelperFunc) inventory.ServerTool
15521552
})
15531553
}
15541554

1555+
// UpdateIssueComment creates a tool to update an issue or pull request conversation comment.
1556+
func UpdateIssueComment(t translations.TranslationHelperFunc) inventory.ServerTool {
1557+
return NewTool(
1558+
ToolsetMetadataIssues,
1559+
mcp.Tool{
1560+
Name: "update_issue_comment",
1561+
Description: t("TOOL_UPDATE_ISSUE_COMMENT_DESCRIPTION", "Update the body of an existing issue or pull request conversation comment. This tool cannot update pull request review comments."),
1562+
Annotations: &mcp.ToolAnnotations{
1563+
Title: t("TOOL_UPDATE_ISSUE_COMMENT_USER_TITLE", "Update issue comment"),
1564+
ReadOnlyHint: false,
1565+
},
1566+
InputSchema: &jsonschema.Schema{
1567+
Type: "object",
1568+
Properties: map[string]*jsonschema.Schema{
1569+
"owner": {
1570+
Type: "string",
1571+
Description: "Repository owner",
1572+
},
1573+
"repo": {
1574+
Type: "string",
1575+
Description: "Repository name",
1576+
},
1577+
"comment_id": {
1578+
Type: "integer",
1579+
Description: "The numeric ID of the issue or pull request conversation comment to update. Do not use a pull request review comment ID.",
1580+
Minimum: jsonschema.Ptr(1.0),
1581+
},
1582+
"body": {
1583+
Type: "string",
1584+
Description: "New comment content",
1585+
MinLength: jsonschema.Ptr(1),
1586+
},
1587+
},
1588+
Required: []string{"owner", "repo", "comment_id", "body"},
1589+
},
1590+
},
1591+
publicRepositoryWriteScopeAccess(),
1592+
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
1593+
owner, err := RequiredParam[string](args, "owner")
1594+
if err != nil {
1595+
return utils.NewToolResultError(err.Error()), nil, nil
1596+
}
1597+
repo, err := RequiredParam[string](args, "repo")
1598+
if err != nil {
1599+
return utils.NewToolResultError(err.Error()), nil, nil
1600+
}
1601+
commentID, err := RequiredBigInt(args, "comment_id")
1602+
if err != nil {
1603+
return utils.NewToolResultError(err.Error()), nil, nil
1604+
}
1605+
if commentID < 1 {
1606+
return utils.NewToolResultError("comment_id must be greater than 0"), nil, nil
1607+
}
1608+
body, hasBody, err := OptionalParamOK[string](args, "body")
1609+
if err != nil {
1610+
return utils.NewToolResultError(err.Error()), nil, nil
1611+
}
1612+
if !hasBody {
1613+
return utils.NewToolResultError("missing required parameter: body"), nil, nil
1614+
}
1615+
if body == "" {
1616+
return utils.NewToolResultError("body cannot be empty when provided"), nil, nil
1617+
}
1618+
1619+
client, err := deps.GetClient(ctx)
1620+
if err != nil {
1621+
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
1622+
}
1623+
1624+
updatedComment, resp, err := client.Issues.EditComment(ctx, owner, repo, commentID, &github.IssueComment{
1625+
Body: github.Ptr(body),
1626+
})
1627+
if resp != nil && resp.Body != nil {
1628+
defer func() { _ = resp.Body.Close() }()
1629+
}
1630+
if err != nil {
1631+
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to update issue comment", resp, err), nil, nil
1632+
}
1633+
1634+
r, err := json.Marshal(MinimalResponse{
1635+
ID: fmt.Sprintf("%d", updatedComment.GetID()),
1636+
URL: updatedComment.GetHTMLURL(),
1637+
})
1638+
if err != nil {
1639+
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
1640+
}
1641+
1642+
return utils.NewToolResultText(string(r)), nil, nil
1643+
})
1644+
}
1645+
15551646
func isValidIssueReaction(reaction string) bool {
15561647
switch reaction {
15571648
case "+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes":

pkg/github/issues_test.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6639,6 +6639,195 @@ func TestAddIssueCommentHandler(t *testing.T) {
66396639
}
66406640
}
66416641

6642+
func TestUpdateIssueCommentSchema(t *testing.T) {
6643+
t.Parallel()
6644+
6645+
tool := UpdateIssueComment(translations.NullTranslationHelper).Tool
6646+
require.NoError(t, toolsnaps.Test(tool.Name, tool))
6647+
6648+
assert.Equal(t, "update_issue_comment", tool.Name)
6649+
assert.NotEmpty(t, tool.Description)
6650+
schema := tool.InputSchema.(*jsonschema.Schema)
6651+
assert.Contains(t, schema.Properties, "owner")
6652+
assert.Contains(t, schema.Properties, "repo")
6653+
assert.Contains(t, schema.Properties, "comment_id")
6654+
assert.Contains(t, schema.Properties, "body")
6655+
assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "comment_id", "body"})
6656+
6657+
resolved, err := schema.Resolve(nil)
6658+
require.NoError(t, err)
6659+
6660+
baseArgs := map[string]any{
6661+
"owner": "owner",
6662+
"repo": "repo",
6663+
"comment_id": 456,
6664+
"body": "Updated comment",
6665+
}
6666+
tests := []struct {
6667+
name string
6668+
args map[string]any
6669+
isValid bool
6670+
}{
6671+
{
6672+
name: "valid arguments",
6673+
args: map[string]any{},
6674+
isValid: true,
6675+
},
6676+
{
6677+
name: "missing required body",
6678+
args: map[string]any{"body": nil},
6679+
isValid: false,
6680+
},
6681+
{
6682+
name: "empty body",
6683+
args: map[string]any{"body": ""},
6684+
isValid: false,
6685+
},
6686+
{
6687+
name: "zero comment ID",
6688+
args: map[string]any{"comment_id": 0},
6689+
isValid: false,
6690+
},
6691+
{
6692+
name: "fractional comment ID",
6693+
args: map[string]any{"comment_id": 1.5},
6694+
isValid: false,
6695+
},
6696+
}
6697+
6698+
for _, tc := range tests {
6699+
t.Run(tc.name, func(t *testing.T) {
6700+
t.Parallel()
6701+
6702+
args := maps.Clone(baseArgs)
6703+
maps.Copy(args, tc.args)
6704+
err := resolved.Validate(args)
6705+
if tc.isValid {
6706+
require.NoError(t, err)
6707+
return
6708+
}
6709+
require.Error(t, err)
6710+
})
6711+
}
6712+
}
6713+
6714+
func TestUpdateIssueCommentHandler(t *testing.T) {
6715+
t.Parallel()
6716+
6717+
updatedComment := &github.IssueComment{
6718+
ID: github.Ptr(int64(456)),
6719+
Body: github.Ptr("Updated comment"),
6720+
HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42#issuecomment-456"),
6721+
}
6722+
6723+
tests := []struct {
6724+
name string
6725+
mockedClient *http.Client
6726+
requestArgs map[string]any
6727+
expectToolError bool
6728+
expectedToolErrMsg string
6729+
}{
6730+
{
6731+
name: "successful update",
6732+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
6733+
PatchReposIssuesCommentByOwnerByRepoByCommentID: expectRequestBody(t, map[string]any{
6734+
"body": "Updated comment",
6735+
}).andThen(mockResponse(t, http.StatusOK, updatedComment)),
6736+
}),
6737+
requestArgs: map[string]any{
6738+
"owner": "owner",
6739+
"repo": "repo",
6740+
"comment_id": float64(456),
6741+
"body": "Updated comment",
6742+
},
6743+
},
6744+
{
6745+
name: "missing body",
6746+
requestArgs: map[string]any{
6747+
"owner": "owner",
6748+
"repo": "repo",
6749+
"comment_id": float64(456),
6750+
},
6751+
expectToolError: true,
6752+
expectedToolErrMsg: "missing required parameter: body",
6753+
},
6754+
{
6755+
name: "empty body",
6756+
requestArgs: map[string]any{
6757+
"owner": "owner",
6758+
"repo": "repo",
6759+
"comment_id": float64(456),
6760+
"body": "",
6761+
},
6762+
expectToolError: true,
6763+
expectedToolErrMsg: "body cannot be empty when provided",
6764+
},
6765+
{
6766+
name: "negative comment ID",
6767+
requestArgs: map[string]any{
6768+
"owner": "owner",
6769+
"repo": "repo",
6770+
"comment_id": float64(-1),
6771+
"body": "Updated comment",
6772+
},
6773+
expectToolError: true,
6774+
expectedToolErrMsg: "comment_id must be greater than 0",
6775+
},
6776+
{
6777+
name: "fractional comment ID",
6778+
requestArgs: map[string]any{
6779+
"owner": "owner",
6780+
"repo": "repo",
6781+
"comment_id": float64(1.5),
6782+
"body": "Updated comment",
6783+
},
6784+
expectToolError: true,
6785+
expectedToolErrMsg: "parameter comment_id is not a valid number",
6786+
},
6787+
{
6788+
name: "API error",
6789+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
6790+
PatchReposIssuesCommentByOwnerByRepoByCommentID: mockResponse(t, http.StatusNotFound, `{"message": "Not Found"}`),
6791+
}),
6792+
requestArgs: map[string]any{
6793+
"owner": "owner",
6794+
"repo": "repo",
6795+
"comment_id": float64(456),
6796+
"body": "Updated comment",
6797+
},
6798+
expectToolError: true,
6799+
expectedToolErrMsg: "failed to update issue comment",
6800+
},
6801+
}
6802+
6803+
for _, tc := range tests {
6804+
t.Run(tc.name, func(t *testing.T) {
6805+
t.Parallel()
6806+
6807+
client := mustNewGHClient(t, tc.mockedClient)
6808+
deps := BaseDeps{Client: client}
6809+
serverTool := UpdateIssueComment(translations.NullTranslationHelper)
6810+
handler := serverTool.Handler(deps)
6811+
6812+
request := createMCPRequest(tc.requestArgs)
6813+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
6814+
require.NoError(t, err)
6815+
6816+
if tc.expectToolError {
6817+
require.True(t, result.IsError)
6818+
assert.Contains(t, getErrorResult(t, result).Text, tc.expectedToolErrMsg)
6819+
return
6820+
}
6821+
6822+
require.False(t, result.IsError)
6823+
var response MinimalResponse
6824+
require.NoError(t, json.Unmarshal([]byte(getTextResult(t, result).Text), &response))
6825+
assert.Equal(t, "456", response.ID)
6826+
assert.Equal(t, "https://github.com/owner/repo/issues/42#issuecomment-456", response.URL)
6827+
})
6828+
}
6829+
}
6830+
66426831
func Test_RemoveSubIssue(t *testing.T) {
66436832
// Verify tool definition once
66446833
serverTool := SubIssueWrite(translations.NullTranslationHelper)

pkg/github/public_repo_scopes_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ func TestPublicRepoContributionToolScopeAccess(t *testing.T) {
2222
{name: "create_pull_request", tool: CreatePullRequest(translations.NullTranslationHelper)},
2323
{name: "issue_write", tool: IssueWrite(translations.NullTranslationHelper)},
2424
{name: "add_issue_comment", tool: AddIssueComment(translations.NullTranslationHelper)},
25+
{name: "update_issue_comment", tool: UpdateIssueComment(translations.NullTranslationHelper)},
2526
}
2627

2728
for _, tt := range tools {
@@ -50,6 +51,7 @@ func TestPublicRepoContributionToolsVisibleToPATs(t *testing.T) {
5051
CreatePullRequest(translations.NullTranslationHelper),
5152
IssueWrite(translations.NullTranslationHelper),
5253
AddIssueComment(translations.NullTranslationHelper),
54+
UpdateIssueComment(translations.NullTranslationHelper),
5355
}
5456

5557
tests := []struct {

pkg/github/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ func AllTools(t translations.TranslationHelperFunc, opts ...ToolOption) []invent
259259
ListIssueFields(t),
260260
IssueWrite(t),
261261
AddIssueComment(t),
262+
UpdateIssueComment(t),
262263
SubIssueWrite(t),
263264
IssueDependencyRead(t),
264265
IssueDependencyWrite(t),

0 commit comments

Comments
 (0)