Repository: github/github-mcp-server

Issue:
Add support for tags operations
### Describe the feature or problem you’d like to solve
I'd like the server to be able to get the repository tags 

### Proposed solution
Add list_tags tool

### Additional context

![Image](https://github.com/user-attachments/assets/c623e5a9-1615-42fc-8dff-1a260887d154)

![Image](https://github.com/user-attachments/assets/d5a88162-98eb-4e32-834b-c9a26710c59e)

Discussion:

Solution patch:
diff --git a/e2e/README.md b/e2e/README.md
index 21b65bf..bb93b32 100644
--- a/e2e/README.md
+++ b/e2e/README.md
@@ -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.
diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go
index 7c1bc23..beaab7c 100644
--- a/pkg/github/repositories.go
+++ b/pkg/github/repositories.go
@@ -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
+		}
+}
diff --git a/pkg/github/tools.go b/pkg/github/tools.go
index 1a4a3b4..3776a12 100644
--- a/pkg/github/tools.go
+++ b/pkg/github/tools.go
@@ -27,6 +27,8 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
 			toolsets.NewServerTool(SearchCode(getClient, t)),
 			toolsets.NewServerTool(GetCommit(getClient, t)),
 			toolsets.NewServerTool(ListBranches(getClient, t)),
+			toolsets.NewServerTool(ListTags(getClient, t)),
+			toolsets.NewServerTool(GetTag(getClient, t)),
 		).
 		AddWriteTools(
 			toolsets.NewServerTool(CreateOrUpdateFile(getClient, t)),



Test patch:
diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go
index 757dd5c..5da6379 100644
--- a/e2e/e2e_test.go
+++ b/e2e/e2e_test.go
@@ -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")
+}
diff --git a/pkg/github/helper_test.go b/pkg/github/helper_test.go
index 40fc0b9..f241d33 100644
--- a/pkg/github/helper_test.go
+++ b/pkg/github/helper_test.go
@@ -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 {
+	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 {
@@ -29,7 +38,9 @@ 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
 }
@@ -37,12 +48,8 @@ type partialMock struct {
 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 {
@@ -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)
 	}
 }
diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go
index 5b8129f..59d19fc 100644
--- a/pkg/github/repositories_test.go
+++ b/pkg/github/repositories_test.go
@@ -1528,3 +1528,272 @@ func Test_ListBranches(t *testing.T) {
 		})
 	}
 }
+
+func Test_ListTags(t *testing.T) {
+	// Verify tool definition once
+	mockClient := github.NewClient(nil)
+	tool, _ := ListTags(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+
+	assert.Equal(t, "list_tags", tool.Name)
+	assert.NotEmpty(t, tool.Description)
+	assert.Contains(t, tool.InputSchema.Properties, "owner")
+	assert.Contains(t, tool.InputSchema.Properties, "repo")
+	assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"})
+
+	// Setup mock tags for success case
+	mockTags := []*github.RepositoryTag{
+		{
+			Name: github.Ptr("v1.0.0"),
+			Commit: &github.Commit{
+				SHA: github.Ptr("v1.0.0-tag-sha"),
+				URL: github.Ptr("https://api.github.com/repos/owner/repo/commits/abc123"),
+			},
+			ZipballURL: github.Ptr("https://github.com/owner/repo/zipball/v1.0.0"),
+			TarballURL: github.Ptr("https://github.com/owner/repo/tarball/v1.0.0"),
+		},
+		{
+			Name: github.Ptr("v0.9.0"),
+			Commit: &github.Commit{
+				SHA: github.Ptr("v0.9.0-tag-sha"),
+				URL: github.Ptr("https://api.github.com/repos/owner/repo/commits/def456"),
+			},
+			ZipballURL: github.Ptr("https://github.com/owner/repo/zipball/v0.9.0"),
+			TarballURL: github.Ptr("https://github.com/owner/repo/tarball/v0.9.0"),
+		},
+	}
+
+	tests := []struct {
+		name           string
+		mockedClient   *http.Client
+		requestArgs    map[string]interface{}
+		expectError    bool
+		expectedTags   []*github.RepositoryTag
+		expectedErrMsg string
+	}{
+		{
+			name: "successful tags list",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.GetReposTagsByOwnerByRepo,
+					expectPath(
+						t,
+						"/repos/owner/repo/tags",
+					).andThen(
+						mockResponse(t, http.StatusOK, mockTags),
+					),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner": "owner",
+				"repo":  "repo",
+			},
+			expectError:  false,
+			expectedTags: mockTags,
+		},
+		{
+			name: "list tags fails",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.GetReposTagsByOwnerByRepo,
+					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",
+			},
+			expectError:    true,
+			expectedErrMsg: "failed to list tags",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			// Setup client with mock
+			client := github.NewClient(tc.mockedClient)
+			_, handler := ListTags(stubGetClientFn(client), translations.NullTranslationHelper)
+
+			// Create call request
+			request := createMCPRequest(tc.requestArgs)
+
+			// Call handler
+			result, err := handler(context.Background(), request)
+
+			// Verify results
+			if tc.expectError {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tc.expectedErrMsg)
+				return
+			}
+
+			require.NoError(t, err)
+
+			// Parse the result and get the text content if no error
+			textContent := getTextResult(t, result)
+
+			// Parse and verify the result
+			var returnedTags []*github.RepositoryTag
+			err = json.Unmarshal([]byte(textContent.Text), &returnedTags)
+			require.NoError(t, err)
+
+			// Verify each tag
+			require.Equal(t, len(tc.expectedTags), len(returnedTags))
+			for i, expectedTag := range tc.expectedTags {
+				assert.Equal(t, *expectedTag.Name, *returnedTags[i].Name)
+				assert.Equal(t, *expectedTag.Commit.SHA, *returnedTags[i].Commit.SHA)
+			}
+		})
+	}
+}
+
+func Test_GetTag(t *testing.T) {
+	// Verify tool definition once
+	mockClient := github.NewClient(nil)
+	tool, _ := GetTag(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+
+	assert.Equal(t, "get_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"})
+
+	mockTagRef := &github.Reference{
+		Ref: github.Ptr("refs/tags/v1.0.0"),
+		Object: &github.GitObject{
+			SHA: github.Ptr("v1.0.0-tag-sha"),
+		},
+	}
+
+	mockTagObj := &github.Tag{
+		SHA:     github.Ptr("v1.0.0-tag-sha"),
+		Tag:     github.Ptr("v1.0.0"),
+		Message: github.Ptr("Release v1.0.0"),
+		Object: &github.GitObject{
+			Type: github.Ptr("commit"),
+			SHA:  github.Ptr("abc123"),
+		},
+	}
+
+	tests := []struct {
+		name           string
+		mockedClient   *http.Client
+		requestArgs    map[string]interface{}
+		expectError    bool
+		expectedTag    *github.Tag
+		expectedErrMsg string
+	}{
+		{
+			name: "successful tag retrieval",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.GetReposGitRefByOwnerByRepoByRef,
+					expectPath(
+						t,
+						"/repos/owner/repo/git/ref/tags/v1.0.0",
+					).andThen(
+						mockResponse(t, http.StatusOK, mockTagRef),
+					),
+				),
+				mock.WithRequestMatchHandler(
+					mock.GetReposGitTagsByOwnerByRepoByTagSha,
+					expectPath(
+						t,
+						"/repos/owner/repo/git/tags/v1.0.0-tag-sha",
+					).andThen(
+						mockResponse(t, http.StatusOK, mockTagObj),
+					),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner": "owner",
+				"repo":  "repo",
+				"tag":   "v1.0.0",
+			},
+			expectError: false,
+			expectedTag: mockTagObj,
+		},
+		{
+			name: "tag reference not found",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.GetReposGitRefByOwnerByRepoByRef,
+					http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+						w.WriteHeader(http.StatusNotFound)
+						_, _ = w.Write([]byte(`{"message": "Reference does not exist"}`))
+					}),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner": "owner",
+				"repo":  "repo",
+				"tag":   "v1.0.0",
+			},
+			expectError:    true,
+			expectedErrMsg: "failed to get tag reference",
+		},
+		{
+			name: "tag object not found",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatch(
+					mock.GetReposGitRefByOwnerByRepoByRef,
+					mockTagRef,
+				),
+				mock.WithRequestMatchHandler(
+					mock.GetReposGitTagsByOwnerByRepoByTagSha,
+					http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+						w.WriteHeader(http.StatusNotFound)
+						_, _ = w.Write([]byte(`{"message": "Tag object does not exist"}`))
+					}),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner": "owner",
+				"repo":  "repo",
+				"tag":   "v1.0.0",
+			},
+			expectError:    true,
+			expectedErrMsg: "failed to get tag object",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			// Setup client with mock
+			client := github.NewClient(tc.mockedClient)
+			_, handler := GetTag(stubGetClientFn(client), translations.NullTranslationHelper)
+
+			// Create call request
+			request := createMCPRequest(tc.requestArgs)
+
+			// Call handler
+			result, err := handler(context.Background(), request)
+
+			// Verify results
+			if tc.expectError {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tc.expectedErrMsg)
+				return
+			}
+
+			require.NoError(t, err)
+
+			// Parse the result and get the text content if no error
+			textContent := getTextResult(t, result)
+
+			// Parse and verify the result
+			var returnedTag github.Tag
+			err = json.Unmarshal([]byte(textContent.Text), &returnedTag)
+			require.NoError(t, err)
+
+			assert.Equal(t, *tc.expectedTag.SHA, *returnedTag.SHA)
+			assert.Equal(t, *tc.expectedTag.Tag, *returnedTag.Tag)
+			assert.Equal(t, *tc.expectedTag.Message, *returnedTag.Message)
+			assert.Equal(t, *tc.expectedTag.Object.Type, *returnedTag.Object.Type)
+			assert.Equal(t, *tc.expectedTag.Object.SHA, *returnedTag.Object.SHA)
+		})
+	}
+}