Add tool for getting a single commit (includes stats, files) (#216)
* Add tool for getting a commit * Split mock back out, use RepositoryCommit with Files/Stats
This commit is contained in:
@@ -354,7 +354,7 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
|
||||
- `branch`: New branch name (string, required)
|
||||
- `sha`: SHA to create branch from (string, required)
|
||||
|
||||
- **list_commits** - Gets commits of a branch in a repository
|
||||
- **list_commits** - Get a list of commits of a branch in a repository
|
||||
- `owner`: Repository owner (string, required)
|
||||
- `repo`: Repository name (string, required)
|
||||
- `sha`: Branch name, tag, or commit SHA (string, optional)
|
||||
@@ -362,6 +362,13 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
|
||||
- `page`: Page number (number, optional)
|
||||
- `perPage`: Results per page (number, optional)
|
||||
|
||||
- **get_commit** - Get details for a commit from a repository
|
||||
- `owner`: Repository owner (string, required)
|
||||
- `repo`: Repository name (string, required)
|
||||
- `sha`: Commit SHA, branch name, or tag name (string, required)
|
||||
- `page`: Page number, for files in the commit (number, optional)
|
||||
- `perPage`: Results per page, for files in the commit (number, optional)
|
||||
|
||||
### Search
|
||||
|
||||
- **search_code** - Search for code across GitHub repositories
|
||||
|
||||
@@ -13,6 +13,73 @@ import (
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
|
||||
return mcp.NewTool("get_commit",
|
||||
mcp.WithDescription(t("TOOL_GET_COMMITS_DESCRIPTION", "Get details for a commit from a GitHub repository")),
|
||||
mcp.WithString("owner",
|
||||
mcp.Required(),
|
||||
mcp.Description("Repository owner"),
|
||||
),
|
||||
mcp.WithString("repo",
|
||||
mcp.Required(),
|
||||
mcp.Description("Repository name"),
|
||||
),
|
||||
mcp.WithString("sha",
|
||||
mcp.Required(),
|
||||
mcp.Description("Commit SHA, branch name, or tag name"),
|
||||
),
|
||||
WithPagination(),
|
||||
),
|
||||
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
|
||||
}
|
||||
sha, err := requiredParam[string](request, "sha")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
pagination, err := OptionalPaginationParams(request)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
opts := &github.ListOptions{
|
||||
Page: pagination.page,
|
||||
PerPage: pagination.perPage,
|
||||
}
|
||||
|
||||
client, err := getClient(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
|
||||
}
|
||||
commit, resp, err := client.Repositories.GetCommit(ctx, owner, repo, sha, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get commit: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
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 commit: %s", string(body))), nil
|
||||
}
|
||||
|
||||
r, err := json.Marshal(commit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal response: %w", err)
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(string(r)), nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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",
|
||||
|
||||
@@ -475,6 +475,136 @@ func Test_CreateBranch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GetCommit(t *testing.T) {
|
||||
// Verify tool definition once
|
||||
mockClient := github.NewClient(nil)
|
||||
tool, _ := GetCommit(stubGetClientFn(mockClient), translations.NullTranslationHelper)
|
||||
|
||||
assert.Equal(t, "get_commit", 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, "sha")
|
||||
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "sha"})
|
||||
|
||||
mockCommit := &github.RepositoryCommit{
|
||||
SHA: github.Ptr("abc123def456"),
|
||||
Commit: &github.Commit{
|
||||
Message: github.Ptr("First commit"),
|
||||
Author: &github.CommitAuthor{
|
||||
Name: github.Ptr("Test User"),
|
||||
Email: github.Ptr("test@example.com"),
|
||||
Date: &github.Timestamp{Time: time.Now().Add(-48 * time.Hour)},
|
||||
},
|
||||
},
|
||||
Author: &github.User{
|
||||
Login: github.Ptr("testuser"),
|
||||
},
|
||||
HTMLURL: github.Ptr("https://github.com/owner/repo/commit/abc123def456"),
|
||||
Stats: &github.CommitStats{
|
||||
Additions: github.Ptr(10),
|
||||
Deletions: github.Ptr(2),
|
||||
Total: github.Ptr(12),
|
||||
},
|
||||
Files: []*github.CommitFile{
|
||||
{
|
||||
Filename: github.Ptr("file1.go"),
|
||||
Status: github.Ptr("modified"),
|
||||
Additions: github.Ptr(10),
|
||||
Deletions: github.Ptr(2),
|
||||
Changes: github.Ptr(12),
|
||||
Patch: github.Ptr("@@ -1,2 +1,10 @@"),
|
||||
},
|
||||
},
|
||||
}
|
||||
// This one currently isn't defined in the mock package we're using.
|
||||
var mockEndpointPattern = mock.EndpointPattern{
|
||||
Pattern: "/repos/{owner}/{repo}/commits/{sha}",
|
||||
Method: "GET",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mockedClient *http.Client
|
||||
requestArgs map[string]interface{}
|
||||
expectError bool
|
||||
expectedCommit *github.RepositoryCommit
|
||||
expectedErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "successful commit fetch",
|
||||
mockedClient: mock.NewMockedHTTPClient(
|
||||
mock.WithRequestMatchHandler(
|
||||
mockEndpointPattern,
|
||||
mockResponse(t, http.StatusOK, mockCommit),
|
||||
),
|
||||
),
|
||||
requestArgs: map[string]interface{}{
|
||||
"owner": "owner",
|
||||
"repo": "repo",
|
||||
"sha": "abc123def456",
|
||||
},
|
||||
expectError: false,
|
||||
expectedCommit: mockCommit,
|
||||
},
|
||||
{
|
||||
name: "commit fetch fails",
|
||||
mockedClient: mock.NewMockedHTTPClient(
|
||||
mock.WithRequestMatchHandler(
|
||||
mockEndpointPattern,
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
|
||||
}),
|
||||
),
|
||||
),
|
||||
requestArgs: map[string]interface{}{
|
||||
"owner": "owner",
|
||||
"repo": "repo",
|
||||
"sha": "nonexistent-sha",
|
||||
},
|
||||
expectError: true,
|
||||
expectedErrMsg: "failed to get commit",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Setup client with mock
|
||||
client := github.NewClient(tc.mockedClient)
|
||||
_, handler := GetCommit(stubGetClientFn(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)
|
||||
|
||||
// Parse the result and get the text content if no error
|
||||
textContent := getTextResult(t, result)
|
||||
|
||||
// Unmarshal and verify the result
|
||||
var returnedCommit github.RepositoryCommit
|
||||
err = json.Unmarshal([]byte(textContent.Text), &returnedCommit)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, *tc.expectedCommit.SHA, *returnedCommit.SHA)
|
||||
assert.Equal(t, *tc.expectedCommit.Commit.Message, *returnedCommit.Commit.Message)
|
||||
assert.Equal(t, *tc.expectedCommit.Author.Login, *returnedCommit.Author.Login)
|
||||
assert.Equal(t, *tc.expectedCommit.HTMLURL, *returnedCommit.HTMLURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ListCommits(t *testing.T) {
|
||||
// Verify tool definition once
|
||||
mockClient := github.NewClient(nil)
|
||||
|
||||
@@ -61,6 +61,7 @@ func NewServer(getClient GetClientFn, version string, readOnly bool, t translati
|
||||
// Add GitHub tools - Repositories
|
||||
s.AddTool(SearchRepositories(getClient, t))
|
||||
s.AddTool(GetFileContents(getClient, t))
|
||||
s.AddTool(GetCommit(getClient, t))
|
||||
s.AddTool(ListCommits(getClient, t))
|
||||
if !readOnly {
|
||||
s.AddTool(CreateOrUpdateFile(getClient, t))
|
||||
|
||||
Reference in New Issue
Block a user