diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 8c337163..45167641 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -408,6 +408,90 @@ func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFun } } +// UpdateRepository update the metadata for GitHub repository +func UpdateRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("update_repository", + mcp.WithDescription(t("TOOL_UPDATE_REPOSITORY_DESCRIPTION", "Update a GitHub repository's metadata")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_UPDATE_REPOSITORY_USER_TITLE", "Update repository"), + ReadOnlyHint: toBoolPtr(false), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner (username or organization)"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithString("name", + mcp.Description("The name to change the repository to"), + ), + mcp.WithString("description", + mcp.Description("A short description of the repository"), + ), + mcp.WithString("default_branch", + mcp.Description("The default branch of the repository"), + ), + mcp.WithBoolean("archived", + mcp.Description("Whether to archive this repository"), + ), + mcp.WithBoolean("allow_forking", + mcp.Description("Whether to allow forking this repository"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + var paramsErr error + var owner, repo, name, description, defaultBranch string + var archived, allowForking bool + + owner, paramsErr = requiredParam[string](request, "owner") + repo, paramsErr = requiredParam[string](request, "repo") + name, paramsErr = OptionalParam[string](request, "name") + description, paramsErr = OptionalParam[string](request, "description") + defaultBranch, paramsErr = OptionalParam[string](request, "default_branch") + archived, paramsErr = OptionalParam[bool](request, "archived") + allowForking, paramsErr = OptionalParam[bool](request, "allow_forking") + + if paramsErr != nil { + return mcp.NewToolResultError(paramsErr.Error()), nil + } + + repoObj := &github.Repository{ + Name: github.Ptr(name), + Description: github.Ptr(description), + DefaultBranch: github.Ptr(defaultBranch), + Archived: github.Ptr(archived), + AllowForking: github.Ptr(allowForking), + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + updatedRepo, resp, err := client.Repositories.Edit(ctx, owner, repo, repoObj) + if err != nil { + return nil, fmt.Errorf("failed to update repository: %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 update repository: %s", string(body))), nil + } + + r, err := json.Marshal(updatedRepo) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + // GetFileContents creates a tool to get the contents of a file or directory from a GitHub repository. func GetFileContents(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_file_contents", diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index e4edeee8..e37b8ae4 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -464,6 +464,99 @@ func Test_CreateBranch(t *testing.T) { } } +func Test_UpdateRepository(t *testing.T) { + mockClient := github.NewClient(nil) + tool, _ := UpdateRepository(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "update_repository", 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, "name") + assert.Contains(t, tool.InputSchema.Properties, "description") + assert.Contains(t, tool.InputSchema.Properties, "default_branch") + assert.Contains(t, tool.InputSchema.Properties, "archived") + assert.Contains(t, tool.InputSchema.Properties, "allow_forking") + + mockRepo := &github.Repository{ + ID: github.Ptr(int64(123456)), + Name: github.Ptr("new-repo"), + FullName: github.Ptr("owner/repo"), + DefaultBranch: github.Ptr("main"), + Archived: github.Ptr(true), + AllowForking: github.Ptr(true), + Description: github.Ptr("new description"), + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedRepo *github.Repository + expectedErrMsg string + }{ + { + name: "successful repository update", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PatchReposByOwnerByRepo, + mockResponse(t, http.StatusOK, mockRepo), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "name": "new-repo", + "description": "new description", + "default_branch": "new-branch", + "archived": true, + "allow_forking": true, + }, + expectError: false, + expectedRepo: mockRepo, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := UpdateRepository(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) + + // Unmarshal and verify the result + var returnedRepo github.Repository + err = json.Unmarshal([]byte(textContent.Text), &returnedRepo) + require.NoError(t, err) + assert.Equal(t, *tc.expectedRepo.ID, *returnedRepo.ID) + assert.Equal(t, *tc.expectedRepo.Name, *returnedRepo.Name) + assert.Equal(t, *tc.expectedRepo.FullName, *returnedRepo.FullName) + assert.Equal(t, *tc.expectedRepo.DefaultBranch, *returnedRepo.DefaultBranch) + assert.Equal(t, *tc.expectedRepo.Archived, *returnedRepo.Archived) + assert.Equal(t, *tc.expectedRepo.AllowForking, *returnedRepo.AllowForking) + assert.Equal(t, *tc.expectedRepo.Description, *returnedRepo.Description) + }) + } +} + func Test_GetCommit(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 9c1ab34a..f047a79d 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -39,6 +39,7 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn, toolsets.NewServerTool(CreateBranch(getClient, t)), toolsets.NewServerTool(PushFiles(getClient, t)), toolsets.NewServerTool(DeleteFile(getClient, t)), + toolsets.NewServerTool(UpdateRepository(getClient, t)), ) issues := toolsets.NewToolset("issues", "GitHub Issues related tools"). AddReadTools(