Skip to content

Commit 919a10c

Browse files
authored
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
1 parent 651a3aa commit 919a10c

File tree

4 files changed

+206
-1
lines changed

4 files changed

+206
-1
lines changed

Diff for: README.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -354,14 +354,21 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
354354
- `branch`: New branch name (string, required)
355355
- `sha`: SHA to create branch from (string, required)
356356

357-
- **list_commits** - Gets commits of a branch in a repository
357+
- **list_commits** - Get a list of commits of a branch in a repository
358358
- `owner`: Repository owner (string, required)
359359
- `repo`: Repository name (string, required)
360360
- `sha`: Branch name, tag, or commit SHA (string, optional)
361361
- `path`: Only commits containing this file path (string, optional)
362362
- `page`: Page number (number, optional)
363363
- `perPage`: Results per page (number, optional)
364364

365+
- **get_commit** - Get details for a commit from a repository
366+
- `owner`: Repository owner (string, required)
367+
- `repo`: Repository name (string, required)
368+
- `sha`: Commit SHA, branch name, or tag name (string, required)
369+
- `page`: Page number, for files in the commit (number, optional)
370+
- `perPage`: Results per page, for files in the commit (number, optional)
371+
365372
### Search
366373

367374
- **search_code** - Search for code across GitHub repositories

Diff for: pkg/github/repositories.go

+67
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,73 @@ import (
1313
"github.com/mark3labs/mcp-go/server"
1414
)
1515

16+
func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
17+
return mcp.NewTool("get_commit",
18+
mcp.WithDescription(t("TOOL_GET_COMMITS_DESCRIPTION", "Get details for a commit from a GitHub repository")),
19+
mcp.WithString("owner",
20+
mcp.Required(),
21+
mcp.Description("Repository owner"),
22+
),
23+
mcp.WithString("repo",
24+
mcp.Required(),
25+
mcp.Description("Repository name"),
26+
),
27+
mcp.WithString("sha",
28+
mcp.Required(),
29+
mcp.Description("Commit SHA, branch name, or tag name"),
30+
),
31+
WithPagination(),
32+
),
33+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
34+
owner, err := requiredParam[string](request, "owner")
35+
if err != nil {
36+
return mcp.NewToolResultError(err.Error()), nil
37+
}
38+
repo, err := requiredParam[string](request, "repo")
39+
if err != nil {
40+
return mcp.NewToolResultError(err.Error()), nil
41+
}
42+
sha, err := requiredParam[string](request, "sha")
43+
if err != nil {
44+
return mcp.NewToolResultError(err.Error()), nil
45+
}
46+
pagination, err := OptionalPaginationParams(request)
47+
if err != nil {
48+
return mcp.NewToolResultError(err.Error()), nil
49+
}
50+
51+
opts := &github.ListOptions{
52+
Page: pagination.page,
53+
PerPage: pagination.perPage,
54+
}
55+
56+
client, err := getClient(ctx)
57+
if err != nil {
58+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
59+
}
60+
commit, resp, err := client.Repositories.GetCommit(ctx, owner, repo, sha, opts)
61+
if err != nil {
62+
return nil, fmt.Errorf("failed to get commit: %w", err)
63+
}
64+
defer func() { _ = resp.Body.Close() }()
65+
66+
if resp.StatusCode != 200 {
67+
body, err := io.ReadAll(resp.Body)
68+
if err != nil {
69+
return nil, fmt.Errorf("failed to read response body: %w", err)
70+
}
71+
return mcp.NewToolResultError(fmt.Sprintf("failed to get commit: %s", string(body))), nil
72+
}
73+
74+
r, err := json.Marshal(commit)
75+
if err != nil {
76+
return nil, fmt.Errorf("failed to marshal response: %w", err)
77+
}
78+
79+
return mcp.NewToolResultText(string(r)), nil
80+
}
81+
}
82+
1683
// ListCommits creates a tool to get commits of a branch in a repository.
1784
func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
1885
return mcp.NewTool("list_commits",

Diff for: pkg/github/repositories_test.go

+130
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,136 @@ func Test_CreateBranch(t *testing.T) {
475475
}
476476
}
477477

478+
func Test_GetCommit(t *testing.T) {
479+
// Verify tool definition once
480+
mockClient := github.NewClient(nil)
481+
tool, _ := GetCommit(stubGetClientFn(mockClient), translations.NullTranslationHelper)
482+
483+
assert.Equal(t, "get_commit", tool.Name)
484+
assert.NotEmpty(t, tool.Description)
485+
assert.Contains(t, tool.InputSchema.Properties, "owner")
486+
assert.Contains(t, tool.InputSchema.Properties, "repo")
487+
assert.Contains(t, tool.InputSchema.Properties, "sha")
488+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "sha"})
489+
490+
mockCommit := &github.RepositoryCommit{
491+
SHA: github.Ptr("abc123def456"),
492+
Commit: &github.Commit{
493+
Message: github.Ptr("First commit"),
494+
Author: &github.CommitAuthor{
495+
Name: github.Ptr("Test User"),
496+
Email: github.Ptr("test@example.com"),
497+
Date: &github.Timestamp{Time: time.Now().Add(-48 * time.Hour)},
498+
},
499+
},
500+
Author: &github.User{
501+
Login: github.Ptr("testuser"),
502+
},
503+
HTMLURL: github.Ptr("https://github.com/owner/repo/commit/abc123def456"),
504+
Stats: &github.CommitStats{
505+
Additions: github.Ptr(10),
506+
Deletions: github.Ptr(2),
507+
Total: github.Ptr(12),
508+
},
509+
Files: []*github.CommitFile{
510+
{
511+
Filename: github.Ptr("file1.go"),
512+
Status: github.Ptr("modified"),
513+
Additions: github.Ptr(10),
514+
Deletions: github.Ptr(2),
515+
Changes: github.Ptr(12),
516+
Patch: github.Ptr("@@ -1,2 +1,10 @@"),
517+
},
518+
},
519+
}
520+
// This one currently isn't defined in the mock package we're using.
521+
var mockEndpointPattern = mock.EndpointPattern{
522+
Pattern: "/repos/{owner}/{repo}/commits/{sha}",
523+
Method: "GET",
524+
}
525+
526+
tests := []struct {
527+
name string
528+
mockedClient *http.Client
529+
requestArgs map[string]interface{}
530+
expectError bool
531+
expectedCommit *github.RepositoryCommit
532+
expectedErrMsg string
533+
}{
534+
{
535+
name: "successful commit fetch",
536+
mockedClient: mock.NewMockedHTTPClient(
537+
mock.WithRequestMatchHandler(
538+
mockEndpointPattern,
539+
mockResponse(t, http.StatusOK, mockCommit),
540+
),
541+
),
542+
requestArgs: map[string]interface{}{
543+
"owner": "owner",
544+
"repo": "repo",
545+
"sha": "abc123def456",
546+
},
547+
expectError: false,
548+
expectedCommit: mockCommit,
549+
},
550+
{
551+
name: "commit fetch fails",
552+
mockedClient: mock.NewMockedHTTPClient(
553+
mock.WithRequestMatchHandler(
554+
mockEndpointPattern,
555+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
556+
w.WriteHeader(http.StatusNotFound)
557+
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
558+
}),
559+
),
560+
),
561+
requestArgs: map[string]interface{}{
562+
"owner": "owner",
563+
"repo": "repo",
564+
"sha": "nonexistent-sha",
565+
},
566+
expectError: true,
567+
expectedErrMsg: "failed to get commit",
568+
},
569+
}
570+
571+
for _, tc := range tests {
572+
t.Run(tc.name, func(t *testing.T) {
573+
// Setup client with mock
574+
client := github.NewClient(tc.mockedClient)
575+
_, handler := GetCommit(stubGetClientFn(client), translations.NullTranslationHelper)
576+
577+
// Create call request
578+
request := createMCPRequest(tc.requestArgs)
579+
580+
// Call handler
581+
result, err := handler(context.Background(), request)
582+
583+
// Verify results
584+
if tc.expectError {
585+
require.Error(t, err)
586+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
587+
return
588+
}
589+
590+
require.NoError(t, err)
591+
592+
// Parse the result and get the text content if no error
593+
textContent := getTextResult(t, result)
594+
595+
// Unmarshal and verify the result
596+
var returnedCommit github.RepositoryCommit
597+
err = json.Unmarshal([]byte(textContent.Text), &returnedCommit)
598+
require.NoError(t, err)
599+
600+
assert.Equal(t, *tc.expectedCommit.SHA, *returnedCommit.SHA)
601+
assert.Equal(t, *tc.expectedCommit.Commit.Message, *returnedCommit.Commit.Message)
602+
assert.Equal(t, *tc.expectedCommit.Author.Login, *returnedCommit.Author.Login)
603+
assert.Equal(t, *tc.expectedCommit.HTMLURL, *returnedCommit.HTMLURL)
604+
})
605+
}
606+
}
607+
478608
func Test_ListCommits(t *testing.T) {
479609
// Verify tool definition once
480610
mockClient := github.NewClient(nil)

Diff for: pkg/github/server.go

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ func NewServer(getClient GetClientFn, version string, readOnly bool, t translati
6161
// Add GitHub tools - Repositories
6262
s.AddTool(SearchRepositories(getClient, t))
6363
s.AddTool(GetFileContents(getClient, t))
64+
s.AddTool(GetCommit(getClient, t))
6465
s.AddTool(ListCommits(getClient, t))
6566
if !readOnly {
6667
s.AddTool(CreateOrUpdateFile(getClient, t))

0 commit comments

Comments
 (0)