From 3f57f75f151829b73a55e9c8fee3fa411142ae25 Mon Sep 17 00:00:00 2001 From: tommaso-moro Date: Thu, 21 Aug 2025 09:14:14 +0100 Subject: [PATCH 1/5] add get_release_by_tag tool --- pkg/github/repositories.go | 67 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 0925829a1..13d284ef2 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -1441,6 +1441,73 @@ func GetLatestRelease(getClient GetClientFn, t translations.TranslationHelperFun } } +// GetReleaseByTag creates a tool to get a specific release by its tag name in a GitHub repository. +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, From cd51444c3e045f76b410bd4dd0264a637f87aea3 Mon Sep 17 00:00:00 2001 From: tommaso-moro Date: Thu, 21 Aug 2025 09:14:18 +0100 Subject: [PATCH 2/5] add tool --- pkg/github/tools.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/github/tools.go b/pkg/github/tools.go index b50499650..513b93e42 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -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)), From 8770426767477e0fc8691e13d43c4ac09512f863 Mon Sep 17 00:00:00 2001 From: tommaso-moro Date: Thu, 21 Aug 2025 09:24:57 +0100 Subject: [PATCH 3/5] add tests --- pkg/github/repositories_test.go | 165 ++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index 63e577600..f5ebfd32b 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -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 From 8750d996f2d97d65cb4aaf8f9a038b6e5d8405d3 Mon Sep 17 00:00:00 2001 From: tommaso-moro Date: Thu, 21 Aug 2025 09:25:04 +0100 Subject: [PATCH 4/5] autogen --- README.md | 5 ++++ .../__toolsnaps__/get_release_by_tag.snap | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 pkg/github/__toolsnaps__/get_release_by_tag.snap diff --git a/README.md b/README.md index e4543ecf5..b4168a136 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/pkg/github/__toolsnaps__/get_release_by_tag.snap b/pkg/github/__toolsnaps__/get_release_by_tag.snap new file mode 100644 index 000000000..c96d3c30a --- /dev/null +++ b/pkg/github/__toolsnaps__/get_release_by_tag.snap @@ -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" +} \ No newline at end of file From 4b980a7f222f97c5aa98cd451d3884450d1ec0d4 Mon Sep 17 00:00:00 2001 From: tommaso-moro Date: Thu, 21 Aug 2025 09:38:04 +0100 Subject: [PATCH 5/5] remove comment --- pkg/github/repositories.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 13d284ef2..de2c6d01f 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -1441,7 +1441,6 @@ func GetLatestRelease(getClient GetClientFn, t translations.TranslationHelperFun } } -// GetReleaseByTag creates a tool to get a specific release by its tag name in a GitHub repository. 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")), 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