Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: Add support for Git tag operations#345

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
SamMorrowDrums merged 3 commits intogithub:mainfromeranco74:tags
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletione2e/README.md
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,4 +81,6 @@ FAIL

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!

Currently, visibility into failures is not particularly good.
The tests are quite repetitive and verbose. This is intentional as we want to see them develop more before committing to abstractions.

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.
136 changes: 136 additions & 0 deletionse2e/e2e_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -206,3 +206,139 @@ func TestToolsets(t *testing.T) {
require.True(t, toolsContains("list_branches"), "expected to find 'list_branches' tool")
require.False(t, toolsContains("get_pull_request"), "expected not to find 'get_pull_request' tool")
}

func TestTags(t *testing.T) {
mcpClient := setupMCPClient(t)

ctx := context.Background()

// First, who am I
getMeRequest := mcp.CallToolRequest{}
getMeRequest.Params.Name = "get_me"

t.Log("Getting current user...")
resp, err := mcpClient.CallTool(ctx, getMeRequest)
require.NoError(t, err, "expected to call 'get_me' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

require.False(t, resp.IsError, "expected result not to be an error")
require.Len(t, resp.Content, 1, "expected content to have one item")

textContent, ok := resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")

var trimmedGetMeText struct {
Login string `json:"login"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
require.NoError(t, err, "expected to unmarshal text content successfully")

currentOwner := trimmedGetMeText.Login

// Then create a repository with a README (via autoInit)
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
createRepoRequest := mcp.CallToolRequest{}
createRepoRequest.Params.Name = "create_repository"
createRepoRequest.Params.Arguments = map[string]any{
"name": repoName,
"private": true,
"autoInit": true,
}

t.Logf("Creating repository %s/%s...", currentOwner, repoName)
_, err = mcpClient.CallTool(ctx, createRepoRequest)
require.NoError(t, err, "expected to call 'get_me' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

// Cleanup the repository after the test
t.Cleanup(func() {
// MCP Server doesn't support deletions, but we can use the GitHub Client
ghClient := github.NewClient(nil).WithAuthToken(getE2EToken(t))
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
require.NoError(t, err, "expected to delete repository successfully")
})

// Then create a tag
// MCP Server doesn't support tag creation, but we can use the GitHub Client
ghClient := github.NewClient(nil).WithAuthToken(getE2EToken(t))
t.Logf("Creating tag %s/%s:%s...", currentOwner, repoName, "v0.0.1")
ref, _, err := ghClient.Git.GetRef(context.Background(), currentOwner, repoName, "refs/heads/main")
require.NoError(t, err, "expected to get ref successfully")

tagObj, _, err := ghClient.Git.CreateTag(context.Background(), currentOwner, repoName, &github.Tag{
Tag: github.Ptr("v0.0.1"),
Message: github.Ptr("v0.0.1"),
Object: &github.GitObject{
SHA: ref.Object.SHA,
Type: github.Ptr("commit"),
},
})
require.NoError(t, err, "expected to create tag object successfully")

_, _, err = ghClient.Git.CreateRef(context.Background(), currentOwner, repoName, &github.Reference{
Ref: github.Ptr("refs/tags/v0.0.1"),
Object: &github.GitObject{
SHA: tagObj.SHA,
},
})
require.NoError(t, err, "expected to create tag ref successfully")

// List the tags
listTagsRequest := mcp.CallToolRequest{}
listTagsRequest.Params.Name = "list_tags"
listTagsRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
}

t.Logf("Listing tags for %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, listTagsRequest)
require.NoError(t, err, "expected to call 'list_tags' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

require.False(t, resp.IsError, "expected result not to be an error")
require.Len(t, resp.Content, 1, "expected content to have one item")

textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")

var trimmedTags []struct {
Name string `json:"name"`
Commit struct {
SHA string `json:"sha"`
} `json:"commit"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedTags)
require.NoError(t, err, "expected to unmarshal text content successfully")

require.Len(t, trimmedTags, 1, "expected to find one tag")
require.Equal(t, "v0.0.1", trimmedTags[0].Name, "expected tag name to match")
require.Equal(t, *ref.Object.SHA, trimmedTags[0].Commit.SHA, "expected tag SHA to match")

// And fetch an individual tag
getTagRequest := mcp.CallToolRequest{}
getTagRequest.Params.Name = "get_tag"
getTagRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"tag": "v0.0.1",
}

t.Logf("Getting tag %s/%s:%s...", currentOwner, repoName, "v0.0.1")
resp, err = mcpClient.CallTool(ctx, getTagRequest)
require.NoError(t, err, "expected to call 'get_tag' tool successfully")
require.False(t, resp.IsError, "expected result not to be an error")

var trimmedTag []struct { // don't understand why this is an array
Name string `json:"name"`
Commit struct {
SHA string `json:"sha"`
} `json:"commit"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedTag)
require.NoError(t, err, "expected to unmarshal text content successfully")
require.Len(t, trimmedTag, 1, "expected to find one tag")
require.Equal(t, "v0.0.1", trimmedTag[0].Name, "expected tag name to match")
require.Equal(t, *ref.Object.SHA, trimmedTag[0].Commit.SHA, "expected tag SHA to match")
}
29 changes: 22 additions & 7 deletionspkg/github/helper_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,15 @@ import (
"github.com/stretchr/testify/require"
)

// expectPath is a helper function to create a partial mock that expects a
// request with the given path, with the ability to chain a response handler.
func expectPath(t *testing.T, expectedPath string) *partialMock {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I like expect path. I think it will catch both errors matching args into API requests and protect from regressions also.

GitHub API is very stable and I don't expect much maintenance will be required.

return &partialMock{
t: t,
expectedPath: expectedPath,
}
}

// expectQueryParams is a helper function to create a partial mock that expects a
// request with the given query parameters, with the ability to chain a response handler.
func expectQueryParams(t *testing.T, expectedQueryParams map[string]string) *partialMock {
Expand All@@ -29,20 +38,18 @@ func expectRequestBody(t *testing.T, expectedRequestBody any) *partialMock {
}

type partialMock struct {
t *testing.T
t *testing.T

expectedPath string
expectedQueryParams map[string]string
expectedRequestBody any
}

func (p *partialMock) andThen(responseHandler http.HandlerFunc) http.HandlerFunc {
p.t.Helper()
return func(w http.ResponseWriter, r *http.Request) {
if p.expectedRequestBody != nil {
var unmarshaledRequestBody any
err := json.NewDecoder(r.Body).Decode(&unmarshaledRequestBody)
require.NoError(p.t, err)

require.Equal(p.t, p.expectedRequestBody, unmarshaledRequestBody)
if p.expectedPath != "" {
require.Equal(p.t, p.expectedPath, r.URL.Path)
}

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

if p.expectedRequestBody != nil {
var unmarshaledRequestBody any
err := json.NewDecoder(r.Body).Decode(&unmarshaledRequestBody)
require.NoError(p.t, err)

require.Equal(p.t, p.expectedRequestBody, unmarshaledRequestBody)
}

responseHandler(w, r)
}
}
Expand Down
144 changes: 144 additions & 0 deletionspkg/github/repositories.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -796,3 +796,147 @@ func PushFiles(getClient GetClientFn, t translations.TranslationHelperFunc) (too
return mcp.NewToolResultText(string(r)), nil
}
}

// ListTags creates a tool to list tags in a GitHub repository.
func ListTags(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("list_tags",
mcp.WithDescription(t("TOOL_LIST_TAGS_DESCRIPTION", "List git tags in a GitHub repository")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_LIST_TAGS_USER_TITLE", "List tags"),
ReadOnlyHint: true,
}),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository 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
}
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)
}

tags, resp, err := client.Repositories.ListTags(ctx, owner, repo, opts)
if err != nil {
return nil, fmt.Errorf("failed to list tags: %w", err)
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
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 list tags: %s", string(body))), nil
}

r, err := json.Marshal(tags)
if err != nil {
return nil, fmt.Errorf("failed to marshal response: %w", err)
}

return mcp.NewToolResultText(string(r)), nil
}
}

// GetTag creates a tool to get details about a specific tag in a GitHub repository.
func GetTag(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_tag",
mcp.WithDescription(t("TOOL_GET_TAG_DESCRIPTION", "Get details about a specific git tag in a GitHub repository")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_GET_TAG_USER_TITLE", "Get tag details"),
ReadOnlyHint: true,
}),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository name"),
),
mcp.WithString("tag",
mcp.Required(),
mcp.Description("Tag name"),
),
),
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
}
tag, err := requiredParam[string](request, "tag")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

client, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}

// First get the tag reference
ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/tags/"+tag)
if err != nil {
return nil, fmt.Errorf("failed to get tag reference: %w", err)
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
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 tag reference: %s", string(body))), nil
}

// Then get the tag object
tagObj, resp, err := client.Git.GetTag(ctx, owner, repo, *ref.Object.SHA)
if err != nil {
return nil, fmt.Errorf("failed to get tag object: %w", err)
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
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 tag object: %s", string(body))), nil
}

r, err := json.Marshal(tagObj)
if err != nil {
return nil, fmt.Errorf("failed to marshal response: %w", err)
}

return mcp.NewToolResultText(string(r)), nil
}
}
Loading

[8]ページ先頭

©2009-2025 Movatter.jp