Skip to content

Commit 06246a7

Browse files
committed
Add tool for getting a commit
1 parent 7ab5d96 commit 06246a7

File tree

4 files changed

+205
-33
lines changed

4 files changed

+205
-33
lines changed

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,14 +342,21 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
342342
- `branch`: New branch name (string, required)
343343
- `sha`: SHA to create branch from (string, required)
344344

345-
- **list_commits** - Gets commits of a branch in a repository
345+
- **list_commits** - Get a list of commits of a branch in a repository
346346
- `owner`: Repository owner (string, required)
347347
- `repo`: Repository name (string, required)
348348
- `sha`: Branch name, tag, or commit SHA (string, optional)
349349
- `path`: Only commits containing this file path (string, optional)
350350
- `page`: Page number (number, optional)
351351
- `perPage`: Results per page (number, optional)
352352

353+
- **get_commit** - Get details for a commit from a repository
354+
- `owner`: Repository owner (string, required)
355+
- `repo`: Repository name (string, required)
356+
- `sha`: Commit SHA, branch name, or tag name (string, required)
357+
- `page`: Page number, for files in the commit (number, optional)
358+
- `perPage`: Results per page, for files in the commit (number, optional)
359+
353360
### Search
354361

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

pkg/github/repositories.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,70 @@ import (
1313
"github.com/mark3labs/mcp-go/server"
1414
)
1515

16+
func getCommit(client *github.Client, 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+
commit, resp, err := client.Repositories.GetCommit(ctx, owner, repo, sha, opts)
57+
if err != nil {
58+
return nil, fmt.Errorf("failed to get commit: %w", err)
59+
}
60+
defer func() { _ = resp.Body.Close() }()
61+
62+
if resp.StatusCode != 200 {
63+
body, err := io.ReadAll(resp.Body)
64+
if err != nil {
65+
return nil, fmt.Errorf("failed to read response body: %w", err)
66+
}
67+
return mcp.NewToolResultError(fmt.Sprintf("failed to get commit: %s", string(body))), nil
68+
}
69+
70+
r, err := json.Marshal(commit)
71+
if err != nil {
72+
return nil, fmt.Errorf("failed to marshal response: %w", err)
73+
}
74+
75+
return mcp.NewToolResultText(string(r)), nil
76+
}
77+
}
78+
79+
1680
// listCommits creates a tool to get commits of a branch in a repository.
1781
func listCommits(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
1882
return mcp.NewTool("list_commits",

pkg/github/repositories_test.go

Lines changed: 132 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,39 @@ import (
1515
"github.com/stretchr/testify/require"
1616
)
1717

18+
var mockCommits = []*github.RepositoryCommit{
19+
{
20+
SHA: github.Ptr("abc123def456"),
21+
Commit: &github.Commit{
22+
Message: github.Ptr("First commit"),
23+
Author: &github.CommitAuthor{
24+
Name: github.Ptr("Test User"),
25+
Email: github.Ptr("[email protected]"),
26+
Date: &github.Timestamp{Time: time.Now().Add(-48 * time.Hour)},
27+
},
28+
},
29+
Author: &github.User{
30+
Login: github.Ptr("testuser"),
31+
},
32+
HTMLURL: github.Ptr("https://github.com/owner/repo/commit/abc123def456"),
33+
},
34+
{
35+
SHA: github.Ptr("def456abc789"),
36+
Commit: &github.Commit{
37+
Message: github.Ptr("Second commit"),
38+
Author: &github.CommitAuthor{
39+
Name: github.Ptr("Another User"),
40+
Email: github.Ptr("[email protected]"),
41+
Date: &github.Timestamp{Time: time.Now().Add(-24 * time.Hour)},
42+
},
43+
},
44+
Author: &github.User{
45+
Login: github.Ptr("anotheruser"),
46+
},
47+
HTMLURL: github.Ptr("https://github.com/owner/repo/commit/def456abc789"),
48+
},
49+
}
50+
1851
func Test_GetFileContents(t *testing.T) {
1952
// Verify tool definition once
2053
mockClient := github.NewClient(nil)
@@ -475,54 +508,121 @@ func Test_CreateBranch(t *testing.T) {
475508
}
476509
}
477510

478-
func Test_ListCommits(t *testing.T) {
511+
func Test_GetCommit(t *testing.T) {
479512
// Verify tool definition once
480513
mockClient := github.NewClient(nil)
481-
tool, _ := listCommits(mockClient, translations.NullTranslationHelper)
514+
tool, _ := getCommit(mockClient, translations.NullTranslationHelper)
482515

483-
assert.Equal(t, "list_commits", tool.Name)
516+
assert.Equal(t, "get_commit", tool.Name)
484517
assert.NotEmpty(t, tool.Description)
485518
assert.Contains(t, tool.InputSchema.Properties, "owner")
486519
assert.Contains(t, tool.InputSchema.Properties, "repo")
487520
assert.Contains(t, tool.InputSchema.Properties, "sha")
488-
assert.Contains(t, tool.InputSchema.Properties, "page")
489-
assert.Contains(t, tool.InputSchema.Properties, "perPage")
490-
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"})
521+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "sha"})
522+
523+
mockCommit := mockCommits[0]
524+
// This one currently isn't defined in the mock package we're using.
525+
var mockEndpointPattern = mock.EndpointPattern{
526+
Pattern: "/repos/{owner}/{repo}/commits/{sha}",
527+
Method: "GET",
528+
}
491529

492-
// Setup mock commits for success case
493-
mockCommits := []*github.RepositoryCommit{
530+
tests := []struct {
531+
name string
532+
mockedClient *http.Client
533+
requestArgs map[string]interface{}
534+
expectError bool
535+
expectedCommit *github.RepositoryCommit
536+
expectedErrMsg string
537+
}{
494538
{
495-
SHA: github.Ptr("abc123def456"),
496-
Commit: &github.Commit{
497-
Message: github.Ptr("First commit"),
498-
Author: &github.CommitAuthor{
499-
Name: github.Ptr("Test User"),
500-
Email: github.Ptr("[email protected]"),
501-
Date: &github.Timestamp{Time: time.Now().Add(-48 * time.Hour)},
502-
},
503-
},
504-
Author: &github.User{
505-
Login: github.Ptr("testuser"),
539+
name: "successful commit fetch",
540+
mockedClient: mock.NewMockedHTTPClient(
541+
mock.WithRequestMatchHandler(
542+
mockEndpointPattern,
543+
mockResponse(t, http.StatusOK, mockCommit),
544+
),
545+
),
546+
requestArgs: map[string]interface{}{
547+
"owner": "owner",
548+
"repo": "repo",
549+
"sha": "abc123def456",
506550
},
507-
HTMLURL: github.Ptr("https://github.com/owner/repo/commit/abc123def456"),
551+
expectError: false,
552+
expectedCommit: mockCommit,
508553
},
509554
{
510-
SHA: github.Ptr("def456abc789"),
511-
Commit: &github.Commit{
512-
Message: github.Ptr("Second commit"),
513-
Author: &github.CommitAuthor{
514-
Name: github.Ptr("Another User"),
515-
Email: github.Ptr("[email protected]"),
516-
Date: &github.Timestamp{Time: time.Now().Add(-24 * time.Hour)},
517-
},
518-
},
519-
Author: &github.User{
520-
Login: github.Ptr("anotheruser"),
555+
name: "commit fetch fails",
556+
mockedClient: mock.NewMockedHTTPClient(
557+
mock.WithRequestMatchHandler(
558+
mockEndpointPattern,
559+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
560+
w.WriteHeader(http.StatusNotFound)
561+
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
562+
}),
563+
),
564+
),
565+
requestArgs: map[string]interface{}{
566+
"owner": "owner",
567+
"repo": "repo",
568+
"sha": "nonexistent-sha",
521569
},
522-
HTMLURL: github.Ptr("https://github.com/owner/repo/commit/def456abc789"),
570+
expectError: true,
571+
expectedErrMsg: "failed to get commit",
523572
},
524573
}
525574

575+
for _, tc := range tests {
576+
t.Run(tc.name, func(t *testing.T) {
577+
// Setup client with mock
578+
client := github.NewClient(tc.mockedClient)
579+
_, handler := getCommit(client, translations.NullTranslationHelper)
580+
581+
// Create call request
582+
request := createMCPRequest(tc.requestArgs)
583+
584+
// Call handler
585+
result, err := handler(context.Background(), request)
586+
587+
// Verify results
588+
if tc.expectError {
589+
require.Error(t, err)
590+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
591+
return
592+
}
593+
594+
require.NoError(t, err)
595+
596+
// Parse the result and get the text content if no error
597+
textContent := getTextResult(t, result)
598+
599+
// Unmarshal and verify the result
600+
var returnedCommit github.RepositoryCommit
601+
err = json.Unmarshal([]byte(textContent.Text), &returnedCommit)
602+
require.NoError(t, err)
603+
604+
assert.Equal(t, *tc.expectedCommit.SHA, *returnedCommit.SHA)
605+
assert.Equal(t, *tc.expectedCommit.Commit.Message, *returnedCommit.Commit.Message)
606+
assert.Equal(t, *tc.expectedCommit.Author.Login, *returnedCommit.Author.Login)
607+
assert.Equal(t, *tc.expectedCommit.HTMLURL, *returnedCommit.HTMLURL)
608+
})
609+
}
610+
}
611+
612+
func Test_ListCommits(t *testing.T) {
613+
// Verify tool definition once
614+
mockClient := github.NewClient(nil)
615+
tool, _ := listCommits(mockClient, translations.NullTranslationHelper)
616+
617+
assert.Equal(t, "list_commits", tool.Name)
618+
assert.NotEmpty(t, tool.Description)
619+
assert.Contains(t, tool.InputSchema.Properties, "owner")
620+
assert.Contains(t, tool.InputSchema.Properties, "repo")
621+
assert.Contains(t, tool.InputSchema.Properties, "sha")
622+
assert.Contains(t, tool.InputSchema.Properties, "page")
623+
assert.Contains(t, tool.InputSchema.Properties, "perPage")
624+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"})
625+
526626
tests := []struct {
527627
name string
528628
mockedClient *http.Client

pkg/github/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func NewServer(client *github.Client, version string, readOnly bool, t translati
5858
// Add GitHub tools - Repositories
5959
s.AddTool(searchRepositories(client, t))
6060
s.AddTool(getFileContents(client, t))
61+
s.AddTool(getCommit(client, t))
6162
s.AddTool(listCommits(client, t))
6263
if !readOnly {
6364
s.AddTool(createOrUpdateFile(client, t))

0 commit comments

Comments
 (0)