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

Add get_release_by_tag tool#938

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
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
5 changes: 5 additions & 0 deletionsREADME.md
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -846,6 +846,11 @@ The following sets of tools are available (all are on by default):
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)

- **get_release_by_tag** - Get a release by tag name
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
- `tag`: Tag name (e.g., 'v1.0.0') (string, required)

- **get_tag** - Get tag details
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
Expand Down
30 changes: 30 additions & 0 deletionspkg/github/__toolsnaps__/get_release_by_tag.snap
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
{
"annotations": {
"title": "Get a release by tag name",
"readOnlyHint": true
},
"description": "Get a specific release by its tag name in a GitHub repository",
"inputSchema": {
"properties": {
"owner": {
"description": "Repository owner",
"type": "string"
},
"repo": {
"description": "Repository name",
"type": "string"
},
"tag": {
"description": "Tag name (e.g., 'v1.0.0')",
"type": "string"
}
},
"required": [
"owner",
"repo",
"tag"
],
"type": "object"
},
"name": "get_release_by_tag"
}
66 changes: 66 additions & 0 deletionspkg/github/repositories.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -1441,6 +1441,72 @@ func GetLatestRelease(getClient GetClientFn, t translations.TranslationHelperFun
}
}

func GetReleaseByTag(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_release_by_tag",
mcp.WithDescription(t("TOOL_GET_RELEASE_BY_TAG_DESCRIPTION", "Get a specific release by its tag name in a GitHub repository")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_GET_RELEASE_BY_TAG_USER_TITLE", "Get a release by tag name"),
ReadOnlyHint: ToBoolPtr(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 (e.g., 'v1.0.0')"),
),
),
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)
}

release, resp, err := client.Repositories.GetReleaseByTag(ctx, owner, repo, tag)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
fmt.Sprintf("failed to get release by tag: %s", tag),
resp,
err,
), nil
}
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 release by tag: %s", string(body))), nil
}

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

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

// filterPaths filters the entries in a GitHub tree to find paths that
// match the given suffix.
// maxResults limits the number of results returned to first maxResults entries,
Expand Down
165 changes: 165 additions & 0 deletionspkg/github/repositories_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2287,6 +2287,171 @@ func Test_GetLatestRelease(t *testing.T) {
}
}

func Test_GetReleaseByTag(t *testing.T) {
mockClient := github.NewClient(nil)
tool, _ := GetReleaseByTag(stubGetClientFn(mockClient), translations.NullTranslationHelper)
require.NoError(t, toolsnaps.Test(tool.Name, tool))

assert.Equal(t, "get_release_by_tag", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.Contains(t, tool.InputSchema.Properties, "owner")
assert.Contains(t, tool.InputSchema.Properties, "repo")
assert.Contains(t, tool.InputSchema.Properties, "tag")
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "tag"})

mockRelease := &github.RepositoryRelease{
ID: github.Ptr(int64(1)),
TagName: github.Ptr("v1.0.0"),
Name: github.Ptr("Release v1.0.0"),
Body: github.Ptr("This is the first stable release."),
Assets: []*github.ReleaseAsset{
{
ID: github.Ptr(int64(1)),
Name: github.Ptr("release-v1.0.0.tar.gz"),
},
},
}

tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]interface{}
expectError bool
expectedResult *github.RepositoryRelease
expectedErrMsg string
}{
{
name: "successful release by tag fetch",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.GetReposReleasesTagsByOwnerByRepoByTag,
mockRelease,
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"tag": "v1.0.0",
},
expectError: false,
expectedResult: mockRelease,
},
{
name: "missing owner parameter",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]interface{}{
"repo": "repo",
"tag": "v1.0.0",
},
expectError: false, // Returns tool error, not Go error
expectedErrMsg: "missing required parameter: owner",
},
{
name: "missing repo parameter",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]interface{}{
"owner": "owner",
"tag": "v1.0.0",
},
expectError: false, // Returns tool error, not Go error
expectedErrMsg: "missing required parameter: repo",
},
{
name: "missing tag parameter",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
},
expectError: false, // Returns tool error, not Go error
expectedErrMsg: "missing required parameter: tag",
},
{
name: "release by tag not found",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposReleasesTagsByOwnerByRepoByTag,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
}),
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"tag": "v999.0.0",
},
expectError: false, // API errors return tool errors, not Go errors
expectedErrMsg: "failed to get release by tag: v999.0.0",
},
{
name: "server error",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposReleasesTagsByOwnerByRepoByTag,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"message": "Internal Server Error"}`))
}),
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"tag": "v1.0.0",
},
expectError: false, // API errors return tool errors, not Go errors
expectedErrMsg: "failed to get release by tag: v1.0.0",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := github.NewClient(tc.mockedClient)
_, handler := GetReleaseByTag(stubGetClientFn(client), translations.NullTranslationHelper)

request := createMCPRequest(tc.requestArgs)

result, err := handler(context.Background(), request)

if tc.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedErrMsg)
return
}

require.NoError(t, err)

if tc.expectedErrMsg != "" {
require.True(t, result.IsError)
errorContent := getErrorResult(t, result)
assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
return
}

require.False(t, result.IsError)

textContent := getTextResult(t, result)

var returnedRelease github.RepositoryRelease
err = json.Unmarshal([]byte(textContent.Text), &returnedRelease)
require.NoError(t, err)

assert.Equal(t, *tc.expectedResult.ID, *returnedRelease.ID)
assert.Equal(t, *tc.expectedResult.TagName, *returnedRelease.TagName)
assert.Equal(t, *tc.expectedResult.Name, *returnedRelease.Name)
if tc.expectedResult.Body != nil {
assert.Equal(t, *tc.expectedResult.Body, *returnedRelease.Body)
}
if len(tc.expectedResult.Assets) > 0 {
require.Len(t, returnedRelease.Assets, len(tc.expectedResult.Assets))
assert.Equal(t, *tc.expectedResult.Assets[0].Name, *returnedRelease.Assets[0].Name)
}
})
}
}

func Test_filterPaths(t *testing.T) {
tests := []struct {
name string
Expand Down
1 change: 1 addition & 0 deletionspkg/github/tools.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,6 +33,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
toolsets.NewServerTool(GetTag(getClient, t)),
toolsets.NewServerTool(ListReleases(getClient, t)),
toolsets.NewServerTool(GetLatestRelease(getClient, t)),
toolsets.NewServerTool(GetReleaseByTag(getClient, t)),
).
AddWriteTools(
toolsets.NewServerTool(CreateOrUpdateFile(getClient, t)),
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp