8000 Merge branch 'main' into feat/259/assign-reviewers · github/github-mcp-server@9ac7250 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9ac7250

Browse files
authored
Merge branch 'main' into feat/259/assign-reviewers
2 parents b9d2e28 + 92d95e4 commit 9ac7250

File tree

6 files changed

+576
-8
lines changed

6 files changed

+576
-8
lines changed

e2e/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,6 @@ FAIL
8181

8282
The current test suite is intentionally very limited in scope. This is because the maintenance costs on e2e tests tend to increase significantly over time. To read about some challenges with GitHub integration tests, see [go-github integration tests README](https://github.com/google/go-github/blob/5b75aa86dba5cf4af2923afa0938774f37fa0a67/test/README.md). We will expand this suite circumspectly!
8383

84-
Currently, visibility into failures is not particularly good.
84+
The tests are quite repetitive and verbose. This is intentional as we want to see them develop more before committing to abstractions.
85+
86+
Currently, visibility into failures is not particularly good. We're hoping that we can pull apart the mcp-go client and have it hook into streams representing stdio without requiring an exec. This way we can get breakpoints in the debugger easily.

e2e/e2e_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,139 @@ func TestToolsets(t *testing.T) {
206206
require.True(t, toolsContains("list_branches"), "expected to find 'list_branches' tool")
207207
require.False(t, toolsContains("get_pull_request"), "expected not to find 'get_pull_request' tool")
208208
}
209+
210+
func TestTags(t *testing.T) {
211+
mcpClient := setupMCPClient(t)
212+
213+
ctx := context.Background()
214+
215+
// First, who am I
216+
getMeRequest := mcp.CallToolRequest{}
217+
getMeRequest.Params.Name = "get_me"
218+
219+
t.Log("Getting current user...")
220+
resp, err := mcpClient.CallTool(ctx, getMeRequest)
221+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
222+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
223+
224+
require.False(t, resp.IsError, "expected result not to be an error")
225+
require.Len(t, resp.Content, 1, "expected content to have one item")
226+
227+
textContent, ok := resp.Content[0].(mcp.TextContent)
228+
require.True(t, ok, "expected content to be of type TextContent")
229+
230+
var trimmedGetMeText struct {
231+
Login string `json:"login"`
232+
}
233+
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
234+
require.NoError(t, err, "expected to unmarshal text content successfully")
235+
236+
currentOwner := trimmedGetMeText.Login
237+
238+
// Then create a repository with a README (via autoInit)
239+
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
240+
createRepoRequest := mcp.CallToolRequest{}
241+
createRepoRequest.Params.Name = "create_repository"
242+
createRepoRequest.Params.Arguments = map[string]any{
243+
"name": repoName,
244+
"private": true< B41A /span>,
245+
"autoInit": true,
246+
}
247+
248+
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
249+
_, err = mcpClient.CallTool(ctx, createRepoRequest)
250+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
251+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
252+
253+
// Cleanup the repository after the test
254+
t.Cleanup(func() {
255+
// MCP Server doesn't support deletions, but we can use the GitHub Client
256+
ghClient := github.NewClient(nil).WithAuthToken(getE2EToken(t))
257+
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
258+
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
259+
require.NoError(t, err, "expected to delete repository successfully")
260+
})
261+
262+
// Then create a tag
263+
// MCP Server doesn't support tag creation, but we can use the GitHub Client
264+
ghClient := github.NewClient(nil).WithAuthToken(getE2EToken(t))
265+
t.Logf("Creating tag %s/%s:%s...", currentOwner, repoName, "v0.0.1")
266+
ref, _, err := ghClient.Git.GetRef(context.Background(), currentOwner, repoName, "refs/heads/main")
267+
require.NoError(t, err, "expected to get ref successfully")
268+
269+
tagObj, _, err := ghClient.Git.CreateTag(context.Background(), currentOwner, repoName, &github.Tag{
270+
Tag: github.Ptr("v0.0.1"),
271+
Message: github.Ptr("v0.0.1"),
272+
Object: &github.GitObject{
273+
SHA: ref.Object.SHA,
274< F438 span class="diff-text-marker">+
Type: github.Ptr("commit"),
275+
},
276+
})
277+
require.NoError(t, err, "expected to create tag object successfully")
278+
279+
_, _, err = ghClient.Git.CreateRef(context.Background(), currentOwner, repoName, &github.Reference{
280+
Ref: github.Ptr("refs/tags/v0.0.1"),
281+
Object: &github.GitObject{
282+
SHA: tagObj.SHA,
283+
},
284+
})
285+
require.NoError(t, err, "expected to create tag ref successfully")
286+
287+
// List the tags
288+
listTagsRequest := mcp.CallToolRequest{}
289+
listTagsRequest.Params.Name = "list_tags"
290+
listTagsRequest.Params.Arguments = map[string]any{
291+
"owner": currentOwner,
292+
"repo": repoName,
293+
}
294+
295+
t.Logf("Listing tags for %s/%s...", currentOwner, repoName)
296+
resp, err = mcpClient.CallTool(ctx, listTagsRequest)
297+
require.NoError(t, err, "expected to call 'list_tags' tool successfully")
298+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
299+
300+
require.False(t, resp.IsError, "expected result not to be an error")
301+
require.Len(t, resp.Content, 1, "expected content to have one item")
302+
303+
textContent, ok = resp.Content[0].(mcp.TextContent)
304+
require.True(t, ok, "expected content to be of type TextContent")
305+
306+
var trimmedTags []struct {
307+
Name string `json:"name"`
308+
Commit struct {
309+
SHA string `json:"sha"`
310+
} `json:"commit"`
311+
}
312+
err = json.Unmarshal([]byte(textContent.Text), &trimmedTags)
313+
require.NoError(t, err, "expected to unmarshal text content successfully")
314+
315+
require.Len(t, trimmedTags, 1, "expected to find one tag")
316+
require.Equal(t, "v0.0.1", trimmedTags[0].Name, "expected tag name to match")
317+
require.Equal(t, *ref.Object.SHA, trimmedTags[0].Commit.SHA, "expected tag SHA to match")
318+
319+
// And fetch an individual tag
320+
getTagRequest := mcp.CallToolRequest{}
321+
getTagRequest.Params.Name = "get_tag"
322+
getTagRequest.Params.Arguments = map[string]any{
323+
"owner": currentOwner,
324+
"repo": repoName,
325+
"tag": "v0.0.1",
326+
}
327+
328+
t.Logf("Getting tag %s/%s:%s...", currentOwner, repoName, "v0.0.1")
329+
resp, err = mcpClient.CallTool(ctx, getTagRequest)
330+
require.NoError(t, err, "expected to call 'get_tag' tool successfully")
331+
require.False(t, resp.IsError, "expected result not to be an error")
332+
333+
var trimmedTag []struct { // don't understand why this is an array
334+
Name string `json:"name"`
335+
Commit struct {
336+
SHA string `json:"sha"`
337+
} `json:"commit"`
338+
}
339+
err = json.Unmarshal([]byte(textContent.Text), &trimmedTag)
340+
require.NoError(t, err, "expected to unmarshal text content successfully")
341+
require.Len(t, trimmedTag, 1, "expected to find one tag")
342+
require.Equal(t, "v0.0.1", trimmedTag[0].Name, "expected tag name to match")
343+
require.Equal(t, *ref.Object.SHA, trimmedTag[0].Commit.SHA, "expected tag SHA to match")
344+
}

pkg/github/helper_test.go

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ import (
1010
"github.com/stretchr/testify/require"
1111
)
1212

13+
// expectPath is a helper function to create a partial mock that expects a
14+
// request with the given path, with the ability to chain a response handler.
15+
func expectPath(t *testing.T, expectedPath string) *partialMock {
16+
return &partialMock{
17+
t: t,
18+
expectedPath: expectedPath,
19+
}
20+
}
21+
1322
// expectQueryParams is a helper function to create a partial mock that expects a
1423
// request with the given query parameters, with the ability to chain a response handler.
1524
func expectQueryParams(t *testing.T, expectedQueryParams map[string]string) *partialMock {
@@ -29,20 +38,18 @@ func expectRequestBody(t *testing.T, expectedRequestBody any) *partialMock {
2938
}
3039

3140
type partialMock struct {
32-
t *testing.T
41+
t *testing.T
42+
43+
expectedPath string
3344
expectedQueryParams map[string]string
3445
expectedRequestBody any
3546
}
3647

3748
func (p *partialMock) andThen(responseHandler http.HandlerFunc) http.HandlerFunc {
3849
p.t.Helper()
3950
return func(w http.ResponseWriter, r *http.Request) {
40-
if p.expectedRequestBody != nil {
41-
var unmarshaledRequestBody any
42-
err := json.NewDecoder(r.Body).Decode(&unmarshaledRequestBody)
43-
require.NoError(p.t, err)
44-
45-
require.Equal(p.t, p.expectedRequestBody, unmarshaledRequestBody)
51+
if p.expectedPath != "" {
52+
require.Equal(p.t, p.expectedPath, r.URL.Path)
4653
}
4754

4855
if p.expectedQueryParams != nil {
@@ -52,6 +59,14 @@ func (p *partialMock) andThen(responseHandler http.HandlerFunc) http.HandlerFunc
5259
}
5360
}
5461

62+
if p.expectedRequestBody != nil {
63+
var unmarshaledRequestBody any
64+
err := json.NewDecoder(r.Body).Decode(&unmarshaledRequestBody)
65+
require.NoError(p.t, err)
66+
67+
require.Equal(p.t, p.expectedRequestBody, unmarshaledRequestBody)
68+
}
69+
5570
responseHandler(w, r)
5671
}
5772
}

pkg/github/repositories.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,3 +796,147 @@ func PushFiles(getClient GetClientFn, t translations.TranslationHelperFunc) (too
796796
return mcp.NewToolResultText(string(r)), nil
797797
}
798798
}
799+
800+
// ListTags creates a tool to list tags in a GitHub repository.
801+
func ListTags(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
802+
return mcp.NewTool("list_tags",
803+
mcp.WithDescription(t("TOOL_LIST_TAGS_DESCRIPTION", "List git tags in a GitHub repository")),
804+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
805+
Title: t("TOOL_LIST_TAGS_USER_TITLE", "List tags"),
806+
ReadOnlyHint: true,
807+
}),
808+
mcp.WithString("owner",
809+
mcp.Required(),
810+
mcp.Description("Repository owner"),
811+
),
812+
mcp.WithString("repo",
813+
mcp.Required(),
814+
mcp.Description("Repository name"),
815+
),
816+
WithPagination(),
817+
),
818+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
819+
owner, err := requiredParam[string](request, "owner")
820+
if err != nil {
821+
return mcp.NewToolResultError(err.Error()), nil
822+
}
823+
repo, err := requiredParam[string](request, "repo")
824+
if err != nil {
825+
return mcp.NewToolResultError(err.Error()), nil
826+
}
827+
pagination, err := OptionalPaginationParams(request)
828+
if err != nil {
829+
return mcp.NewToolResultError(err.Error()), nil
830+
}
831+
832+
opts := &github.ListOptions{
833+
Page: pagination.page,
834+
PerPage: pagination.perPage,
835+
}
836+
837+
client, err := getClient(ctx)
838+
if err != nil {
839+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
840+
}
841+
842+
tags, resp, err := client.Repositories.ListTags(ctx, owner, repo, opts)
843+
if err != nil {
844+
return nil, fmt.Errorf("failed to list tags: %w", err)
845+
}
846+
defer func() { _ = resp.Body.Close() }()
847+
848+
if resp.StatusCode != http.StatusOK {
849+
body, err := io.ReadAll(resp.Body)
850+
if err != nil {
851+
return nil, fmt.Errorf("failed to read response body: %w", err)
852+
}
853+
return mcp.NewToolResultError(fmt.Sprintf("failed to list tags: %s", string(body))), nil
854+
}
855+
856+
r, err := json.Marshal(tags)
857+
if err != nil {
858+
return nil, fmt.Errorf("failed to marshal response: %w", err)
859+
}
860+
861+
return mcp.NewToolResultText(string(r)), nil
862+
}
863+
}
864+
865+
// GetTag creates a tool to get details about a specific tag in a GitHub repository.
866+
func GetTag(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
867+
return mcp.NewTool("get_tag",
868+
mcp.WithDescription(t("TOOL_GET_TAG_DESCRIPTION", "Get details about a specific git tag in a GitHub repository")),
869+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
870+
Title: t("TOOL_GET_TAG_USER_TITLE", "Get tag details"),
871+
ReadOnlyHint: true,
872+
}),
873+
mcp.WithString("owner",
874+
mcp.Required(),
875+
mcp.Description("Repository owner"),
876+
),
877+
mcp.WithString("repo",
878+
mcp.Required(),
879+
mcp.Description("Repository name"),
880+
),
881+
mcp.WithString("tag",
882+
mcp.Required(),
883+
mcp.Description("Tag name"),
884+
),
885+
),
886+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
887+
owner, err := requiredParam[string](request, "owner")
888+
if err != nil {
889+
return mcp.NewToolResultError(err.Error()), nil
890+
}
891+
repo, err := requiredParam[string](request, "repo")
892+
if err != nil {
893+
return mcp.NewToolResultError(err.Error()), nil
894+
}
895+
tag, err := requiredParam[string](request, "tag")
896+
if err != nil {
897+
return mcp.NewToolResultError(err.Error()), nil
898+
}
899+
900+
client, err := getClient(ctx)
901+
if err != nil {
902+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
903+
}
904+
905+
// First get the tag reference
906+
ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/tags/"+tag)
907+
if err != nil {
908+
return nil, fmt.Errorf("failed to get tag reference: %w", err)
909+
}
910+
defer func() { _ = resp.Body.Close() }()
911+
912+
if resp.StatusCode != http.StatusOK {
913+
body, err := io.ReadAll(resp.Body)
914+
if err != nil {
915+
return nil, fmt.Errorf("failed to read response body: %w", err)
916+
}
917+
return mcp.NewToolResultError(fmt.Sprintf("failed to get tag reference: %s", string(body))), nil
918+
}
919+
920+
// Then get the tag object
921+
tagObj, resp, err := client.Git.GetTag(ctx, owner, repo, *ref.Object.SHA)
922+
if err != nil {
923+
return nil, fmt.Errorf("failed to get tag object: %w", err)
924+
}
925+
defer func() { _ = resp.Body.Close() }()
926+
927+
if resp.StatusCode != http.StatusOK {
928+
body, err := io.ReadAll(resp.Body)
929+
if err != nil {
930+
return nil, fmt.Errorf("failed to read response body: %w", err)
931+
}
932+
return mcp.NewToolResultError(fmt.Sprintf("failed to get tag object: %s", string(body))), nil
933+
}
934+
935+
r, err := json.Marshal(tagObj)
936+
if err != nil {
937+
return nil, fmt.Errorf("failed to marshal response: %w", err)
938+
}
939+
940+
return mcp.NewToolResultText(string(r)), nil
941+
}
942+
}

0 commit comments

Comments
 (0)
0