diff --git a/README.md b/README.md index 9330723cc..643036390 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,12 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `repo`: Repository name (string, required) - `issue_number`: Issue number (number, required) +- **get_issue_comments** - Get comments for a GitHub issue + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `issue_number`: Issue number (number, required) + - **create_issue** - Create a new issue in a GitHub repository - `owner`: Repository owner (string, required) diff --git a/cmd/mcpcurl/README.md b/cmd/mcpcurl/README.md index 06ce26ee0..95e1339a7 100644 --- a/cmd/mcpcurl/README.md +++ b/cmd/mcpcurl/README.md @@ -50,6 +50,7 @@ Available Commands: fork_repository Fork a GitHub repository to your account or specified organization get_file_contents Get the contents of a file or directory from a GitHub repository get_issue Get details of a specific issue in a GitHub repository. + get_issue_comments Get comments for a GitHub issue list_commits Get list of commits of a branch in a GitHub repository list_issues List issues in a GitHub repository with filtering options push_files Push multiple files to a GitHub repository in a single commit diff --git a/pkg/github/issues.go b/pkg/github/issues.go index df2f6f584..d5aba2e76 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -597,6 +597,81 @@ func updateIssue(client *github.Client, t translations.TranslationHelperFunc) (t } } +// getIssueComments creates a tool to get comments for a GitHub issue. +func getIssueComments(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("get_issue_comments", + mcp.WithDescription(t("TOOL_GET_ISSUE_COMMENTS_DESCRIPTION", "Get comments for a GitHub issue")), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("issue_number", + mcp.Required(), + mcp.Description("Issue number"), + ), + mcp.WithNumber("page", + mcp.Description("Page number"), + ), + mcp.WithNumber("per_page", + mcp.Description("Number of records per page"), + ), + ), + 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 + } + issueNumber, err := requiredInt(request, "issue_number") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + page, err := optionalIntParamWithDefault(request, "page", 1) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + perPage, err := optionalIntParamWithDefault(request, "per_page", 30) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + opts := &github.IssueListCommentsOptions{ + ListOptions: github.ListOptions{ + Page: page, + PerPage: perPage, + }, + } + + comments, resp, err := client.Issues.ListComments(ctx, owner, repo, issueNumber, opts) + if err != nil { + return nil, fmt.Errorf("failed to get issue comments: %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 get issue comments: %s", string(body))), nil + } + + r, err := json.Marshal(comments) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + // parseISOTimestamp parses an ISO 8601 timestamp string into a time.Time object. // Returns the parsed time or an error if parsing fails. // Example formats supported: "2023-01-15T14:30:00Z", "2023-01-15" diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 5dab16312..0326b9bee 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -984,3 +984,137 @@ func Test_ParseISOTimestamp(t *testing.T) { }) } } + +func Test_GetIssueComments(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := getIssueComments(mockClient, translations.NullTranslationHelper) + + assert.Equal(t, "get_issue_comments", 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, "issue_number") + assert.Contains(t, tool.InputSchema.Properties, "page") + assert.Contains(t, tool.InputSchema.Properties, "per_page") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "issue_number"}) + + // Setup mock comments for success case + mockComments := []*github.IssueComment{ + { + ID: github.Ptr(int64(123)), + Body: github.Ptr("This is the first comment"), + User: &github.User{ + Login: github.Ptr("user1"), + }, + CreatedAt: &github.Timestamp{Time: time.Now().Add(-time.Hour * 24)}, + }, + { + ID: github.Ptr(int64(456)), + Body: github.Ptr("This is the second comment"), + User: &github.User{ + Login: github.Ptr("user2"), + }, + CreatedAt: &github.Timestamp{Time: time.Now().Add(-time.Hour)}, + }, + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedComments []*github.IssueComment + expectedErrMsg string + }{ + { + name: "successful comments retrieval", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposIssuesCommentsByOwnerByRepoByIssueNumber, + mockComments, + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "issue_number": float64(42), + }, + expectError: false, + expectedComments: mockComments, + }, + { + name: "successful comments retrieval with pagination", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposIssuesCommentsByOwnerByRepoByIssueNumber, + expectQueryParams(t, map[string]string{ + "page": "2", + "per_page": "10", + }).andThen( + mockResponse(t, http.StatusOK, mockComments), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "issue_number": float64(42), + "page": float64(2), + "per_page": float64(10), + }, + expectError: false, + expectedComments: mockComments, + }, + { + name: "issue not found", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposIssuesCommentsByOwnerByRepoByIssueNumber, + mockResponse(t, http.StatusNotFound, `{"message": "Issue not found"}`), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "issue_number": float64(999), + }, + expectError: true, + expectedErrMsg: "failed to get issue comments", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := getIssueComments(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 + } + + require.NoError(t, err) + textContent := getTextResult(t, result) + + // Unmarshal and verify the result + var returnedComments []*github.IssueComment + err = json.Unmarshal([]byte(textContent.Text), &returnedComments) + require.NoError(t, err) + assert.Equal(t, len(tc.expectedComments), len(returnedComments)) + if len(returnedComments) > 0 { + assert.Equal(t, *tc.expectedComments[0].Body, *returnedComments[0].Body) + assert.Equal(t, *tc.expectedComments[0].User.Login, *returnedComments[0].User.Login) + } + }) + } +} diff --git a/pkg/github/server.go b/pkg/github/server.go index 66dbfd1ca..18e5da094 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -34,6 +34,7 @@ func NewServer(client *github.Client, readOnly bool, t translations.TranslationH s.AddTool(getIssue(client, t)) s.AddTool(searchIssues(client, t)) s.AddTool(listIssues(client, t)) + s.AddTool(getIssueComments(client, t)) if !readOnly { s.AddTool(createIssue(client, t)) s.AddTool(addIssueComment(client, t)) 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