Skip to content

Commit 919a10c

Browse files
authored
Add tool for getting a single commit (includes stats, files) (github#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

README.md

Lines changed: 8 additions & 1 deletion
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

pkg/github/repositories.go

Lines changed: 67 additions & 0 deletions
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",

pkg/github/repositories_test.go

Lines changed: 130 additions & 0 deletions
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)

pkg/github/server.go

Lines changed: 1 addition & 0 deletions
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)
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