From 42c575895c739b11dded4c9b1d1667b8e5aa2b38 Mon Sep 17 00:00:00 2001 From: Ueslei Santos Lima Date: Tue, 20 May 2025 17:15:09 +0200 Subject: [PATCH] Including new `create_repository_using_template` tool --- README.md | 8 ++ pkg/github/repositories.go | 90 ++++++++++++++++++ pkg/github/repositories_test.go | 160 +++++++++++++++++++++++++++++++- 3 files changed, 256 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 352bb50eb..5afabdb3c 100644 --- a/README.md +++ b/README.md @@ -502,6 +502,14 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `private`: Whether the repository is private (boolean, optional) - `autoInit`: Auto-initialize with README (boolean, optional) +- **create_repository_using_template** - Create a new GitHub repository using a repository template + - `templateOwner`: Template repository owner (string, required) + - `templateName`: Template repository name (string, required) + - `name`: Repository name (string, required) + - `description`: Repository description (string, optional) + - `private`: Whether the repository is private (boolean, optional) + - `includeAllBranches`: Should include all branches from the template repository (boolean, optional) + - **get_file_contents** - Get contents of a file or directory - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 4403e2a19..b37e52fd1 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -408,6 +408,96 @@ func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFun } } +// CreateRepositoryUsingTemplate creates a tool to create a new GitHub repository using a repository template. +func CreateRepositoryUsingTemplate(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("create_repository_using_template", + mcp.WithDescription(t("TOOL_CREATE_REPOSITORY_USING_TEMPLATE_DESCRIPTION", "Create a new GitHub repository in your account using a repository template")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_CREATE_REPOSITORY_USING_TEMPLATE_USER_TITLE", "Create repository using template"), + ReadOnlyHint: toBoolPtr(false), + }), + mcp.WithString("templateOwner", + mcp.Required(), + mcp.Description("Template repository owner (username or organization)"), + ), + mcp.WithString("templateName", + mcp.Required(), + mcp.Description("Template repository name"), + ), + mcp.WithString("name", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithString("description", + mcp.Description("Repository description"), + ), + mcp.WithBoolean("private", + mcp.Description("Whether repo should be private"), + ), + mcp.WithBoolean("includeAllBranches", + mcp.Description("Should include all branches from the template repository"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + templateOwner, err := requiredParam[string](request, "templateOwner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + templateName, err := requiredParam[string](request, "templateName") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + name, err := requiredParam[string](request, "name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + description, err := OptionalParam[string](request, "description") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + private, err := OptionalParam[bool](request, "private") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + includeAllBranches, err := OptionalParam[bool](request, "includeAllBranches") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + repo := &github.TemplateRepoRequest{ + Name: github.Ptr(name), + Description: github.Ptr(description), + Private: github.Ptr(private), + IncludeAllBranches: github.Ptr(includeAllBranches), + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + createdRepo, resp, err := client.Repositories.CreateFromTemplate(ctx, templateOwner, templateName, repo) + if err != nil { + return nil, fmt.Errorf("failed to create repository using template: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusCreated { + 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 create repository using template: %s", string(body))), nil + } + + r, err := json.Marshal(createdRepo) + 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 e4edeee88..30a767b04 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -1072,6 +1072,162 @@ func Test_CreateRepository(t *testing.T) { } } +func Test_CreateRepositoryUsingTemplate(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := CreateRepositoryUsingTemplate(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "create_repository_using_template", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "templateOwner") + assert.Contains(t, tool.InputSchema.Properties, "templateName") + assert.Contains(t, tool.InputSchema.Properties, "name") + assert.Contains(t, tool.InputSchema.Properties, "description") + assert.Contains(t, tool.InputSchema.Properties, "private") + assert.Contains(t, tool.InputSchema.Properties, "includeAllBranches") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"templateOwner", "templateName", "name"}) + + // Setup mock repository response + mockRepo := &github.Repository{ + Name: github.Ptr("test-repo"), + Description: github.Ptr("Test repository"), + Private: github.Ptr(true), + HTMLURL: github.Ptr("https://github.com/testuser/test-repo"), + CloneURL: github.Ptr("https://github.com/testuser/test-repo.git"), + CreatedAt: &github.Timestamp{Time: time.Now()}, + Owner: &github.User{ + Login: github.Ptr("testuser"), + }, + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedRepo *github.Repository + expectedErrMsg string + }{ + { + name: "successful repository creation with all parameters", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/{template_owner}/{template_repo}/generate", + Method: "POST", + }, + expectPath(t, "/repos/test-repo-template-org/test-repo-template/generate").andThen( + expectRequestBody(t, map[string]interface{}{ + "name": "test-repo", + "description": "Test repository", + "private": true, + "include_all_branches": true, + }).andThen( + mockResponse(t, http.StatusCreated, mockRepo), + )), + ), + ), + requestArgs: map[string]interface{}{ + "templateOwner": "test-repo-template-org", + "templateName": "test-repo-template", + "name": "test-repo", + "description": "Test repository", + "private": true, + "includeAllBranches": true, + }, + expectError: false, + expectedRepo: mockRepo, + }, + { + name: "successful repository creation with minimal parameters", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/{template_owner}/{template_repo}/generate", + Method: "POST", + }, + expectPath(t, "/repos/test-repo-template-org/test-repo-template/generate").andThen( + expectRequestBody(t, map[string]interface{}{ + "name": "test-repo", + "include_all_branches": false, + "description": "", + "private": false, + }).andThen( + mockResponse(t, http.StatusCreated, mockRepo), + )), + ), + ), + requestArgs: map[string]interface{}{ + "templateOwner": "test-repo-template-org", + "templateName": "test-repo-template", + "name": "test-repo", + }, + expectError: false, + expectedRepo: mockRepo, + }, + { + name: "repository creation fails", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/{template_owner}/{template_repo}/generate", + Method: "POST", + }, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnprocessableEntity) + _, _ = w.Write([]byte(`{"message": "Repository creation failed"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "templateOwner": "test-repo-template-org", + "templateName": "test-repo-template", + "name": "invalid-repo", + }, + expectError: true, + expectedErrMsg: "failed to create repository", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := CreateRepositoryUsingTemplate(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) + assert.NoError(t, err) + + // Verify repository details + assert.Equal(t, *tc.expectedRepo.Name, *returnedRepo.Name) + assert.Equal(t, *tc.expectedRepo.Description, *returnedRepo.Description) + assert.Equal(t, *tc.expectedRepo.Private, *returnedRepo.Private) + assert.Equal(t, *tc.expectedRepo.HTMLURL, *returnedRepo.HTMLURL) + assert.Equal(t, *tc.expectedRepo.Owner.Login, *returnedRepo.Owner.Login) + }) + } +} + func Test_PushFiles(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) @@ -1207,9 +1363,9 @@ func Test_PushFiles(t *testing.T) { expectedRef: mockUpdatedRef, }, { - name: "fails when files parameter is invalid", + name: "fails when files parameter is invalid", mockedClient: mock.NewMockedHTTPClient( - // No requests expected + // No requests expected ), requestArgs: map[string]interface{}{ "owner": "owner", 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