Skip to content

Commit c77ea94

Browse files
Add support for retrieving GitHub Issue Comments (#106)
* Add support for retrieving GitHub Issue Comments Add new tool 'get_issue_comments' that allows fetching comments associated with GitHub issues. This complements the existing issue retrieval functionality and follows the same patterns as the pull request comments implementation. The implementation includes: - New getIssueComments function in pkg/github/issues.go - Tool registration in server.go - Comprehensive test coverage in issues_test.go * Support pagination for get_issue_comments --------- Co-authored-by: Javier Uruen Val <juruen@github.com>
1 parent e6b73f7 commit c77ea94

File tree

5 files changed

+217
-0
lines changed

5 files changed

+217
-0
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
153153
- `repo`: Repository name (string, required)
154154
- `issue_number`: Issue number (number, required)
155155

156+
- **get_issue_comments** - Get comments for a GitHub issue
157+
158+
- `owner`: Repository owner (string, required)
159+
- `repo`: Repository name (string, required)
160+
- `issue_number`: Issue number (number, required)
161+
156162
- **create_issue** - Create a new issue in a GitHub repository
157163

158164
- `owner`: Repository owner (string, required)

cmd/mcpcurl/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Available Commands:
5050
fork_repository Fork a GitHub repository to your account or specified organization
5151
get_file_contents Get the contents of a file or directory from a GitHub repository
5252
get_issue Get details of a specific issue in a GitHub repository.
53+
get_issue_comments Get comments for a GitHub issue
5354
list_commits Get list of commits of a branch in a GitHub repository
5455
list_issues List issues in a GitHub repository with filtering options
5556
push_files Push multiple files to a GitHub repository in a single commit

pkg/github/issues.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,81 @@ func updateIssue(client *github.Client, t translations.TranslationHelperFunc) (t
597597
}
598598
}
599599

600+
// getIssueComments creates a tool to get comments for a GitHub issue.
601+
func getIssueComments(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
602+
return mcp.NewTool("get_issue_comments",
603+
mcp.WithDescription(t("TOOL_GET_ISSUE_COMMENTS_DESCRIPTION", "Get comments for a GitHub issue")),
604+
mcp.WithString("owner",
605+
mcp.Required(),
606+
mcp.Description("Repository owner"),
607+
),
608+
mcp.WithString("repo",
609+
mcp.Required(),
610+
mcp.Description("Repository name"),
611+
),
612+
mcp.WithNumber("issue_number",
613+
mcp.Required(),
614+
mcp.Description("Issue number"),
615+
),
616+
mcp.WithNumber("page",
617+
mcp.Description("Page number"),
618+
),
619+
mcp.WithNumber("per_page",
620+
mcp.Description("Number of records per page"),
621+
),
622+
),
623+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
624+
owner, err := requiredParam[string](request, "owner")
625+
if err != nil {
626+
return mcp.NewToolResultError(err.Error()), nil
627+
}
628+
repo, err := requiredParam[string](request, "repo")
629+
if err != nil {
630+
return mcp.NewToolResultError(err.Error()), nil
631+
}
632+
issueNumber, err := requiredInt(request, "issue_number")
633+
if err != nil {
634+
return mcp.NewToolResultError(err.Error()), nil
635+
}
636+
page, err := optionalIntParamWithDefault(request, "page", 1)
637+
if err != nil {
638+
return mcp.NewToolResultError(err.Error()), nil
639+
}
640+
perPage, err := optionalIntParamWithDefault(request, "per_page", 30)
641+
if err != nil {
642+
return mcp.NewToolResultError(err.Error()), nil
643+
}
644+
645+
opts := &github.IssueListCommentsOptions{
646+
ListOptions: github.ListOptions{
647+
Page: page,
648+
PerPage: perPage,
649+
},
650+
}
651+
652+
comments, resp, err := client.Issues.ListComments(ctx, owner, repo, issueNumber, opts)
653+
if err != nil {
654+
return nil, fmt.Errorf("failed to get issue comments: %w", err)
655+
}
656+
defer func() { _ = resp.Body.Close() }()
657+
658+
if resp.StatusCode != http.StatusOK {
659+
body, err := io.ReadAll(resp.Body)
660+
if err != nil {
661+
return nil, fmt.Errorf("failed to read response body: %w", err)
662+
}
663+
return mcp.NewToolResultError(fmt.Sprintf("failed to get issue comments: %s", string(body))), nil
664+
}
665+
666+
r, err := json.Marshal(comments)
667+
if err != nil {
668+
return nil, fmt.Errorf("failed to marshal response: %w", err)
669+
}
670+
671+
return mcp.NewToolResultText(string(r)), nil
672+
}
673+
}
674+
600675
// parseISOTimestamp parses an ISO 8601 timestamp string into a time.Time object.
601676
// Returns the parsed time or an error if parsing fails.
602677
// Example formats supported: "2023-01-15T14:30:00Z", "2023-01-15"

pkg/github/issues_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,3 +984,137 @@ func Test_ParseISOTimestamp(t *testing.T) {
984984
})
985985
}
986986
}
987+
988+
func Test_GetIssueComments(t *testing.T) {
989+
// Verify tool definition once
990+
mockClient := github.NewClient(nil)
991+
tool, _ := getIssueComments(mockClient, translations.NullTranslationHelper)
992+
993+
assert.Equal(t, "get_issue_comments", tool.Name)
994+
assert.NotEmpty(t, tool.Description)
995+
assert.Contains(t, tool.InputSchema.Properties, "owner")
996+
assert.Contains(t, tool.InputSchema.Properties, "repo")
997+
assert.Contains(t, tool.InputSchema.Properties, "issue_number")
998+
assert.Contains(t, tool.InputSchema.Properties, "page")
999+
assert.Contains(t, tool.InputSchema.Properties, "per_page")
1000+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "issue_number"})
1001+
1002+
// Setup mock comments for success case
1003+
mockComments := []*github.IssueComment{
1004+
{
1005+
ID: github.Ptr(int64(123)),
1006+
Body: github.Ptr("This is the first comment"),
1007+
User: &github.User{
1008+
Login: github.Ptr("user1"),
1009+
},
1010+
CreatedAt: &github.Timestamp{Time: time.Now().Add(-time.Hour * 24)},
1011+
},
1012+
{
1013+
ID: github.Ptr(int64(456)),
1014+
Body: github.Ptr("This is the second comment"),
1015+
User: &github.User{
1016+
Login: github.Ptr("user2"),
1017+
},
1018+
CreatedAt: &github.Timestamp{Time: time.Now().Add(-time.Hour)},
1019+
},
1020+
}
1021+
1022+
tests := []struct {
1023+
name string
1024+
mockedClient *http.Client
1025+
requestArgs map[string]interface{}
1026+
expectError bool
1027+
expectedComments []*github.IssueComment
1028+
expectedErrMsg string
1029+
}{
1030+
{
1031+
name: "successful comments retrieval",
1032+
mockedClient: mock.NewMockedHTTPClient(
1033+
mock.WithRequestMatch(
1034+
mock.GetReposIssuesCommentsByOwnerByRepoByIssueNumber,
1035+
mockComments,
1036+
),
1037+
),
1038+
requestArgs: map[string]interface{}{
1039+
"owner": "owner",
1040+
"repo": "repo",
1041+
"issue_number": float64(42),
1042+
},
1043+
expectError: false,
1044+
expectedComments: mockComments,
1045+
},
1046+
{
1047+
name: "successful comments retrieval with pagination",
1048+
mockedClient: mock.NewMockedHTTPClient(
1049+
mock.WithRequestMatchHandler(
1050+
mock.GetReposIssuesCommentsByOwnerByRepoByIssueNumber,
1051+
expectQueryParams(t, map[string]string{
1052+
"page": "2",
1053+
"per_page": "10",
1054+
}).andThen(
1055+
mockResponse(t, http.StatusOK, mockComments),
1056+
),
1057+
),
1058+
),
1059+
requestArgs: map[string]interface{}{
1060+
"owner": "owner",
1061+
"repo": "repo",
1062+
"issue_number": float64(42),
1063+
"page": float64(2),
1064+
"per_page": float64(10),
1065+
},
1066+
expectError: false,
1067+
expectedComments: mockComments,
1068+
},
1069+
{
1070+
name: "issue not found",
1071+
mockedClient: mock.NewMockedHTTPClient(
1072+
mock.WithRequestMatchHandler(
1073+
mock.GetReposIssuesCommentsByOwnerByRepoByIssueNumber,
1074+
mockResponse(t, http.StatusNotFound, `{"message": "Issue not found"}`),
1075+
),
1076+
),
1077+
requestArgs: map[string]interface{}{
1078+
"owner": "owner",
1079+
"repo": "repo",
1080+
"issue_number": float64(999),
1081+
},
1082+
expectError: true,
1083+
expectedErrMsg: "failed to get issue comments",
1084+
},
1085+
}
1086+
1087+
for _, tc := range tests {
1088+
t.Run(tc.name, func(t *testing.T) {
1089+
// Setup client with mock
1090+
client := github.NewClient(tc.mockedClient)
1091+
_, handler := getIssueComments(client, translations.NullTranslationHelper)
1092+
1093+
// Create call request
1094+
request := createMCPRequest(tc.requestArgs)
1095+
1096+
// Call handler
1097+
result, err := handler(context.Background(), request)
1098+
1099+
// Verify results
1100+
if tc.expectError {
1101+
require.Error(t, err)
1102+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
1103+
return
1104+
}
1105+
1106+
require.NoError(t, err)
1107+
textContent := getTextResult(t, result)
1108+
1109+
// Unmarshal and verify the result
1110+
var returnedComments []*github.IssueComment
1111+
err = json.Unmarshal([]byte(textContent.Text), &returnedComments)
1112+
require.NoError(t, err)
1113+
assert.Equal(t, len(tc.expectedComments), len(returnedComments))
1114+
if len(returnedComments) > 0 {
1115+
assert.Equal(t, *tc.expectedComments[0].Body, *returnedComments[0].Body)
1116+
assert.Equal(t, *tc.expectedComments[0].User.Login, *returnedComments[0].User.Login)
1117+
}
1118+
})
1119+
}
1120+
}

pkg/github/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func NewServer(client *github.Client, readOnly bool, t translations.TranslationH
3434
s.AddTool(getIssue(client, t))
3535
s.AddTool(searchIssues(client, t))
3636
s.AddTool(listIssues(client, t))
37+
s.AddTool(getIssueComments(client, t))
3738
if !readOnly {
3839
s.AddTool(createIssue(client, t))
3940
s.AddTool(addIssueComment(client, t))

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy