Skip to content

Commit 33eb82c

Browse files
committed
Add tool to star or unstar a repository for the user
1 parent 4ccedee commit 33eb82c

File tree

4 files changed

+214
-0
lines changed

4 files changed

+214
-0
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,11 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
496496
- `page`: Page number (number, optional)
497497
- `perPage`: Results per page (number, optional)
498498

499+
- **toggle_repository_star** - Star or unstar a repository for the authenticated user
500+
- `owner`: Repository owner (string, required)
501+
- `repo`: Repository name (string, required)
502+
- `star`: True to star, false to unstar the repository (boolean, required)
503+
499504
- **create_repository** - Create a new GitHub repository
500505
- `name`: Repository name (string, required)
501506
- `description`: Repository description (string, optional)

pkg/github/repositories.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,78 @@ func ForkRepository(getClient GetClientFn, t translations.TranslationHelperFunc)
556556
}
557557
}
558558

559+
// ToggleRepositoryStar creates a tool to star or unstar a repository.
560+
func ToggleRepositoryStar(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
561+
return mcp.NewTool("toggle_repository_star",
562+
mcp.WithDescription(t("TOOL_TOGGLE_REPOSITORY_STAR_DESCRIPTION", "Star or unstar a GitHub repository with your account")),
563+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
564+
Title: t("TOOL_TOGGLE_REPOSITORY_STAR_USER_TITLE", "Star/unstar repository"),
565+
ReadOnlyHint: toBoolPtr(false),
566+
}),
567+
mcp.WithString("owner",
568+
mcp.Required(),
569+
mcp.Description("Repository owner"),
570+
),
571+
mcp.WithString("repo",
572+
mcp.Required(),
573+
mcp.Description("Repository name"),
574+
),
575+
mcp.WithBoolean("star",
576+
mcp.Required(),
577+
mcp.Description("True to star, false to unstar the repository"),
578+
),
579+
),
580+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
581+
owner, err := requiredParam[string](request, "owner")
582+
if err != nil {
583+
return mcp.NewToolResultError(err.Error()), nil
584+
}
585+
repo, err := requiredParam[string](request, "repo")
586+
if err != nil {
587+
return mcp.NewToolResultError(err.Error()), nil
588+
}
589+
star, err := requiredParam[bool](request, "star")
590+
if err != nil {
591+
return mcp.NewToolResultError(err.Error()), nil
592+
}
593+
594+
client, err := getClient(ctx)
595+
if err != nil {
596+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
597+
}
598+
599+
var resp *github.Response
600+
var action string
601+
602+
if star {
603+
resp, err = client.Activity.Star(ctx, owner, repo)
604+
action = "star"
605+
} else {
606+
resp, err = client.Activity.Unstar(ctx, owner, repo)
607+
action = "unstar"
608+
}
609+
610+
if err != nil {
611+
return nil, fmt.Errorf("failed to %s repository: %w", action, err)
612+
}
613+
defer func() { _ = resp.Body.Close() }()
614+
615+
if resp.StatusCode != http.StatusNoContent {
616+
body, err := io.ReadAll(resp.Body)
617+
if err != nil {
618+
return nil, fmt.Errorf("failed to read response body: %w", err)
619+
}
620+
return mcp.NewToolResultError(fmt.Sprintf("failed to %s repository: %s", action, string(body))), nil
621+
}
622+
623+
resultAction := "starred"
624+
if !star {
625+
resultAction = "unstarred"
626+
}
627+
return mcp.NewToolResultText(fmt.Sprintf("Successfully %s repository %s/%s", resultAction, owner, repo)), nil
628+
}
629+
}
630+
559631
// DeleteFile creates a tool to delete a file in a GitHub repository.
560632
// This tool uses a more roundabout way of deleting a file than just using the client.Repositories.DeleteFile.
561633
// This is because REST file deletion endpoint (and client.Repositories.DeleteFile) don't add commit signing to the deletion commit,

pkg/github/repositories_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1963,3 +1963,139 @@ func Test_GetTag(t *testing.T) {
19631963
})
19641964
}
19651965
}
1966+
1967+
func Test_ToggleRepositoryStar(t *testing.T) {
1968+
// Verify tool definition
1969+
mockClient := github.NewClient(nil)
1970+
tool, _ := ToggleRepositoryStar(stubGetClientFn(mockClient), translations.NullTranslationHelper)
1971+
1972+
assert.Equal(t, "toggle_repository_star", tool.Name)
1973+
assert.NotEmpty(t, tool.Description)
1974+
assert.Contains(t, tool.InputSchema.Properties, "owner")
1975+
assert.Contains(t, tool.InputSchema.Properties, "repo")
1976+
assert.Contains(t, tool.InputSchema.Properties, "star")
1977+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "star"})
1978+
1979+
tests := []struct {
1980+
name string
1981+
mockedClient *http.Client
1982+
requestArgs map[string]interface{}
1983+
expectError bool
1984+
expectedErrMsg string
1985+
expectedResult string
1986+
}{
1987+
{
1988+
name: "successfully star repository",
1989+
mockedClient: mock.NewMockedHTTPClient(
1990+
mock.WithRequestMatchHandler(
1991+
mock.EndpointPattern{
1992+
Pattern: "/user/starred/owner/repo",
1993+
Method: "PUT",
1994+
},
1995+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1996+
w.WriteHeader(http.StatusNoContent)
1997+
}),
1998+
),
1999+
),
2000+
requestArgs: map[string]interface{}{
2001+
"owner": "owner",
2002+
"repo": "repo",
2003+
"star": true,
2004+
},
2005+
expectError: false,
2006+
expectedResult: "Successfully starred repository owner/repo",
2007+
},
2008+
{
2009+
name: "successfully unstar repository",
2010+
mockedClient: mock.NewMockedHTTPClient(
2011+
mock.WithRequestMatchHandler(
2012+
mock.EndpointPattern{
2013+
Pattern: "/user/starred/owner/repo",
2014+
Method: "DELETE",
2015+
},
2016+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
2017+
w.WriteHeader(http.StatusNoContent)
2018+
}),
2019+
),
2020+
),
2021+
requestArgs: map[string]interface{}{
2022+
"owner": "owner",
2023+
"repo": "repo",
2024+
"star": false,
2025+
},
2026+
expectError: false,
2027+
expectedResult: "Successfully unstarred repository owner/repo",
2028+
},
2029+
{
2030+
name: "star repository fails with unauthorized",
2031+
mockedClient: mock.NewMockedHTTPClient(
2032+
mock.WithRequestMatchHandler(
2033+
mock.EndpointPattern{
2034+
Pattern: "/user/starred/owner/repo",
2035+
Method: "PUT",
2036+
},
2037+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
2038+
w.WriteHeader(http.StatusUnauthorized)
2039+
_, _ = w.Write([]byte(`{"message": "Requires authentication"}`))
2040+
}),
2041+
),
2042+
),
2043+
requestArgs: map[string]interface{}{
2044+
"owner": "owner",
2045+
"repo": "repo",
2046+
"star": true,
2047+
},
2048+
expectError: true,
2049+
expectedErrMsg: "failed to star repository",
2050+
},
2051+
{
2052+
name: "unstar repository fails with unauthorized",
2053+
mockedClient: mock.NewMockedHTTPClient(
2054+
mock.WithRequestMatchHandler(
2055+
mock.EndpointPattern{
2056+
Pattern: "/user/starred/owner/repo",
2057+
Method: "DELETE",
2058+
},
2059+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
2060+
w.WriteHeader(http.StatusUnauthorized)
2061+
_, _ = w.Write([]byte(`{"message": "Requires authentication"}`))
2062+
}),
2063+
),
2064+
),
2065+
requestArgs: map[string]interface{}{
2066+
"owner": "owner",
2067+
"repo": "repo",
2068+
"star": false,
2069+
},
2070+
expectError: true,
2071+
expectedErrMsg: "failed to unstar repository",
2072+
},
2073+
}
2074+
2075+
for _, tc := range tests {
2076+
t.Run(tc.name, func(t *testing.T) {
2077+
// Setup client with mock
2078+
client := github.NewClient(tc.mockedClient)
2079+
_, handler := ToggleRepositoryStar(stubGetClientFn(client), translations.NullTranslationHelper)
2080+
2081+
// Create call request
2082+
request := createMCPRequest(tc.requestArgs)
2083+
2084+
// Call handler
2085+
result, err := handler(context.Background(), request)
2086+
2087+
// Verify results
2088+
if tc.expectError {
2089+
require.Error(t, err)
2090+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
2091+
return
2092+
}
2093+
2094+
require.NoError(t, err)
2095+
2096+
// Parse the result and get the text content if no error
2097+
textContent := getTextResult(t, result)
2098+
assert.Equal(t, tc.expectedResult, textContent.Text)
2099+
})
2100+
}
2101+
}

pkg/github/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
3939
toolsets.NewServerTool(CreateBranch(getClient, t)),
4040
toolsets.NewServerTool(PushFiles(getClient, t)),
4141
toolsets.NewServerTool(DeleteFile(getClient, t)),
42+
toolsets.NewServerTool(ToggleRepositoryStar(getClient, t)),
4243
)
4344
issues := toolsets.NewToolset("issues", "GitHub Issues related tools").
4445
AddReadTools(

0 commit comments

Comments
 (0)
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