diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index ecd36d7e0..6a90787a9 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "strings" + "time" ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/raw" @@ -96,7 +97,7 @@ func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (too // ListCommits creates a tool to get commits of a branch in a repository. func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_commits", - mcp.WithDescription(t("TOOL_LIST_COMMITS_DESCRIPTION", "Get list of commits of a branch in a GitHub repository. Returns at least 30 results per page by default, but can return more if specified using the perPage parameter (up to 100).")), + mcp.WithDescription(t("TOOL_LIST_COMMITS_DESCRIPTION", "Get list of commits of a branch in a GitHub repository. Can be filtered by author, date range (since/until), or file path. Returns at least 30 results per page by default, but can return more if specified using the perPage parameter (up to 100).")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_LIST_COMMITS_USER_TITLE", "List commits"), ReadOnlyHint: ToBoolPtr(true), @@ -115,6 +116,15 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t mcp.WithString("author", mcp.Description("Author username or email address to filter commits by"), ), + mcp.WithString("since", + mcp.Description("Only return commits after this date (ISO 8601 format, e.g., '2024-01-01T00:00:00Z')"), + ), + mcp.WithString("until", + mcp.Description("Only return commits before this date (ISO 8601 format, e.g., '2024-12-31T23:59:59Z')"), + ), + mcp.WithString("path", + mcp.Description("Only return commits that touch the specified file path"), + ), WithPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -134,6 +144,18 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t if err != nil { return mcp.NewToolResultError(err.Error()), nil } + since, err := OptionalParam[string](request, "since") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + until, err := OptionalParam[string](request, "until") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + path, err := OptionalParam[string](request, "path") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } pagination, err := OptionalPaginationParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil @@ -143,15 +165,35 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t if perPage == 0 { perPage = 30 } + opts := &github.CommitsListOptions{ SHA: sha, Author: author, + Path: path, ListOptions: github.ListOptions{ Page: pagination.Page, PerPage: perPage, }, } + // Parse since time if provided + if since != "" { + sinceTime, err := time.Parse(time.RFC3339, since) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("invalid since date format (use ISO 8601/RFC3339): %s", err.Error())), nil + } + opts.Since = sinceTime + } + + // Parse until time if provided + if until != "" { + untilTime, err := time.Parse(time.RFC3339, until) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("invalid until date format (use ISO 8601/RFC3339): %s", err.Error())), nil + } + opts.Until = untilTime + } + client, err := getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index 1572a12f4..cd7098cc2 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -719,6 +719,9 @@ func Test_ListCommits(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "repo") assert.Contains(t, tool.InputSchema.Properties, "sha") assert.Contains(t, tool.InputSchema.Properties, "author") + assert.Contains(t, tool.InputSchema.Properties, "since") + assert.Contains(t, tool.InputSchema.Properties, "until") + assert.Contains(t, tool.InputSchema.Properties, "path") assert.Contains(t, tool.InputSchema.Properties, "page") assert.Contains(t, tool.InputSchema.Properties, "perPage") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) @@ -826,6 +829,150 @@ func Test_ListCommits(t *testing.T) { expectError: false, expectedCommits: mockCommits, }, + { + name: "successful commits fetch with time filtering", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposCommitsByOwnerByRepo, + expectQueryParams(t, map[string]string{ + "since": "2024-01-01T00:00:00Z", + "until": "2024-12-31T23:59:59Z", + "page": "1", + "per_page": "30", + }).andThen( + mockResponse(t, http.StatusOK, mockCommits), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "since": "2024-01-01T00:00:00Z", + "until": "2024-12-31T23:59:59Z", + }, + expectError: false, + expectedCommits: mockCommits, + }, + { + name: "successful commits fetch with only since parameter", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposCommitsByOwnerByRepo, + expectQueryParams(t, map[string]string{ + "since": "2024-06-01T00:00:00Z", + "page": "1", + "per_page": "30", + }).andThen( + mockResponse(t, http.StatusOK, mockCommits), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "since": "2024-06-01T00:00:00Z", + }, + expectError: false, + expectedCommits: mockCommits, + }, + { + name: "successful commits fetch with only until parameter", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposCommitsByOwnerByRepo, + expectQueryParams(t, map[string]string{ + "until": "2024-06-30T23:59:59Z", + "page": "1", + "per_page": "30", + }).andThen( + mockResponse(t, http.StatusOK, mockCommits), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "until": "2024-06-30T23:59:59Z", + }, + expectError: false, + expectedCommits: mockCommits, + }, + { + name: "invalid since date format", + mockedClient: mock.NewMockedHTTPClient( + // No HTTP requests expected due to early validation error + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "since": "invalid-date-format", + }, + expectError: false, // This returns a tool error, not a Go error + expectedErrMsg: "invalid since date format", + }, + { + name: "invalid until date format", + mockedClient: mock.NewMockedHTTPClient( + // No HTTP requests expected due to early validation error + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "until": "2024/12/31", // Wrong format + }, + expectError: false, // This returns a tool error, not a Go error + expectedErrMsg: "invalid until date format", + }, + { + name: "successful commits fetch with path filtering", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposCommitsByOwnerByRepo, + expectQueryParams(t, map[string]string{ + "path": "src/main.go", + "page": "1", + "per_page": "30", + }).andThen( + mockResponse(t, http.StatusOK, mockCommits), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "path": "src/main.go", + }, + expectError: false, + expectedCommits: mockCommits, + }, + { + name: "successful commits fetch with path and time filtering", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposCommitsByOwnerByRepo, + expectQueryParams(t, map[string]string{ + "path": "docs/README.md", + "since": "2024-01-01T00:00:00Z", + "until": "2024-12-31T23:59:59Z", + "author": "testuser", + "page": "1", + "per_page": "30", + }).andThen( + mockResponse(t, http.StatusOK, mockCommits), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "path": "docs/README.md", + "since": "2024-01-01T00:00:00Z", + "until": "2024-12-31T23:59:59Z", + "author": "testuser", + }, + expectError: false, + expectedCommits: mockCommits, + }, { name: "commits fetch fails", mockedClient: mock.NewMockedHTTPClient( @@ -867,6 +1014,14 @@ func Test_ListCommits(t *testing.T) { return } + if tc.expectedErrMsg != "" { + require.NoError(t, err) + require.True(t, result.IsError) + errorContent := getErrorResult(t, result) + assert.Contains(t, errorContent.Text, tc.expectedErrMsg) + return + } + require.NoError(t, err) require.False(t, result.IsError)
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: