8000 Merge branch 'main' into pullrequestreviewline · github/github-mcp-server@21424ec · GitHub
[go: up one dir, main page]

Skip to content

Commit 21424ec

Browse files
authored
Merge branch 'main' into pullrequestreviewline
2 parents 5700af1 + c77ea94 commit 21424ec

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
< 7802 /code>
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)
0