Skip to content

Commit 432a0b5

Browse files
committed
feat: add create repo from template
1 parent 865f9bf commit 432a0b5

File tree

3 files changed

+242
-1
lines changed

3 files changed

+242
-1
lines changed

pkg/github/repositories.go

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ func CreateOrUpdateFile(getClient GetClientFn, t translations.TranslationHelperF
321321
// CreateRepository creates a tool to create a new GitHub repository.
322322
func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
323323
return mcp.NewTool("create_repository",
324-
mcp.WithDescription(t("TOOL_CREATE_REPOSITORY_DESCRIPTION", "Create a new GitHub repository in your account")),
324+
mcp.WithDescription(t("TOOL_CREATE_REPOSITORY_DESCRIPTION", "Create a new GitHub repository in your account without using a template")),
325325
mcp.WithString("name",
326326
mcp.Required(),
327327
mcp.Description("Repository name"),
@@ -388,6 +388,92 @@ func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFun
388388
}
389389
}
390390

391+
// CreateRepositoryFromTemplate creates a tool to create a new GitHub repository from a template.
392+
func CreateRepositoryFromTemplate(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
393+
return mcp.NewTool("create_repository_from_template",
394+
mcp.WithDescription(t("TOOL_CREATE_REPOSITORY_FROM_TEMPLATE_DESCRIPTION", "Create a new GitHub repository from a template in your account")),
395+
mcp.WithString("name",
396+
mcp.Required(),
397+
mcp.Description("Repository name"),
398+
),
399+
mcp.WithString("description",
400+
mcp.Description("Repository description"),
401+
),
402+
mcp.WithBoolean("private",
403+
mcp.Description("Whether repo should be private"),
404+
),
405+
mcp.WithBoolean("includeAllBranches",
406+
mcp.Description("Include all branches from template"),
407+
),
408+
mcp.WithString("templateOwner",
409+
mcp.Required(),
410+
mcp.Description("Template repository owner"),
411+
),
412+
mcp.WithString("templateRepo",
413+
mcp.Required(),
414+
mcp.Description("Template repository name"),
415+
),
416+
),
417+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
418+
name, err := requiredParam[string](request, "name")
419+
if err != nil {
420+
return mcp.NewToolResultError(err.Error()), nil
421+
}
422+
description, err := OptionalParam[string](request, "description")
423+
if err != nil {
424+
return mcp.NewToolResultError(err.Error()), nil
425+
}
426+
private, err := OptionalParam[bool](request, "private")
427+
if err != nil {
428+
return mcp.NewToolResultError(err.Error()), nil
429+
}
430+
includeAllBranches, err := OptionalParam[bool](request, "includeAllBranches")
431+
if err != nil {
432+
return mcp.NewToolResultError(err.Error()), nil
433+
}
434+
templateOwner, err := requiredParam[string](request, "templateOwner")
435+
if err != nil {
436+
return mcp.NewToolResultError(err.Error()), nil
437+
}
438+
templateRepo, err := requiredParam[string](request, "templateRepo")
439+
if err != nil {
440+
return mcp.NewToolResultError(err.Error()), nil
441+
}
442+
443+
templateReq := &github.TemplateRepoRequest{
444+
Name: github.Ptr(name),
445+
Description: github.Ptr(description),
446+
Private: github.Ptr(private),
447+
IncludeAllBranches: github.Ptr(includeAllBranches),
448+
}
449+
450+
client, err := getClient(ctx)
451+
if err != nil {
452+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
453+
}
454+
createdRepo, resp, err := client.Repositories.CreateFromTemplate(ctx, templateOwner, templateRepo, templateReq)
455+
if err != nil {
456+
return nil, fmt.Errorf("failed to create repository from template: %w", err)
457+
}
458+
defer func() { _ = resp.Body.Close() }()
459+
460+
if resp.StatusCode != http.StatusCreated {
461+
body, err := io.ReadAll(resp.Body)
462+
if err != nil {
463+
return nil, fmt.Errorf("failed to read response body: %w", err)
464+
}
465+
return mcp.NewToolResultError(fmt.Sprintf("failed to create repository from template: %s", string(body))), nil
466+
}
467+
468+
r, err := json.Marshal(createdRepo)
469+
if err != nil {
470+
return nil, fmt.Errorf("failed to marshal response: %w", err)
471+
}
472+
473+
return mcp.NewToolResultText(string(r)), nil
474+
}
475+
}
476+
391477
// GetFileContents creates a tool to get the contents of a file or directory from a GitHub repository.
392478
func GetFileContents(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
393479
return mcp.NewTool("get_file_contents",

pkg/github/repositories_test.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,3 +1528,157 @@ func Test_ListBranches(t *testing.T) {
15281528
})
15291529
}
15301530
}
1531+
1532+
func Test_CreateRepositoryFromTemplate(t *testing.T) {
1533+
// Verify tool definition once
1534+
mockClient := github.NewClient(nil)
1535+
tool, _ := CreateRepositoryFromTemplate(stubGetClientFn(mockClient), translations.NullTranslationHelper)
1536+
1537+
assert.Equal(t, "create_repository_from_template", tool.Name)
1538+
assert.NotEmpty(t, tool.Description)
1539+
assert.Contains(t, tool.InputSchema.Properties, "name")
1540+
assert.Contains(t, tool.InputSchema.Properties, "description")
1541+
assert.Contains(t, tool.InputSchema.Properties, "private")
1542+
assert.Contains(t, tool.InputSchema.Properties, "includeAllBranches")
1543+
assert.Contains(t, tool.InputSchema.Properties, "templateOwner")
1544+
assert.Contains(t, tool.InputSchema.Properties, "templateRepo")
1545+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"name", "templateOwner", "templateRepo"})
1546+
1547+
// Setup mock repository response
1548+
mockRepo := &github.Repository{
1549+
Name: github.Ptr("test-repo"),
1550+
Description: github.Ptr("Test repository"),
1551+
Private: github.Ptr(true),
1552+
HTMLURL: github.Ptr("https://github.com/testuser/test-repo"),
1553+
CloneURL: github.Ptr("https://github.com/testuser/test-repo.git"),
1554+
CreatedAt: &github.Timestamp{Time: time.Now()},
1555+
Owner: &github.User{
1556+
Login: github.Ptr("testuser"),
1557+
},
1558+
}
1559+
1560+
tests := []struct {
1561+
name string
1562+
mockedClient *http.Client
1563+
requestArgs map[string]interface{}
1564+
expectError bool
1565+
expectedRepo *github.Repository
1566+
expectedErrMsg string
1567+
}{
1568+
{
1569+
name: "successful repository creation from template with all params",
1570+
mockedClient: mock.NewMockedHTTPClient(
1571+
mock.WithRequestMatchHandler(
1572+
mock.EndpointPattern{
1573+
Pattern: "/repos/template-owner/template-repo/generate",
1574+
Method: "POST",
1575+
},
1576+
expectRequestBody(t, map[string]interface{}{
1577+
"name": "test-repo",
1578+
"description": "Test repository",
1579+
"private": true,
1580+
"include_all_branches": true,
1581+
}).andThen(
1582+
mockResponse(t, http.StatusCreated, mockRepo),
1583+
),
1584+
),
1585+
),
1586+
requestArgs: map[string]interface{}{
1587+
"name": "test-repo",
1588+
"description": "Test repository",
1589+
"private": true,
1590+
"includeAllBranches": true,
1591+
"templateOwner": "template-owner",
1592+
"templateRepo": "template-repo",
1593+
},
1594+
expectError: false,
1595+
expectedRepo: mockRepo,
1596+
},
1597+
{
1598+
name: "successful repository creation from template with minimal params",
1599+
mockedClient: mock.NewMockedHTTPClient(
1600+
mock.WithRequestMatchHandler(
1601+
mock.EndpointPattern{
1602+
Pattern: "/repos/template-owner/template-repo/generate",
1603+
Method: "POST",
1604+
},
1605+
expectRequestBody(t, map[string]interface{}{
1606+
"name": "test-repo",
1607+
"description": "",
1608+
"private": false,
1609+
"include_all_branches": false,
1610+
}).andThen(
1611+
mockResponse(t, http.StatusCreated, mockRepo),
1612+
),
1613+
),
1614+
),
1615+
requestArgs: map[string]interface{}{
1616+
"name": "test-repo",
1617+
"templateOwner": "template-owner",
1618+
"templateRepo": "template-repo",
1619+
},
1620+
expectError: false,
1621+
expectedRepo: mockRepo,
1622+
},
1623+
{
1624+
name: "repository creation from template fails",
1625+
mockedClient: mock.NewMockedHTTPClient(
1626+
mock.WithRequestMatchHandler(
1627+
mock.EndpointPattern{
1628+
Pattern: "/repos/template-owner/template-repo/generate",
1629+
Method: "POST",
1630+
},
1631+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1632+
w.WriteHeader(http.StatusUnprocessableEntity)
1633+
_, _ = w.Write([]byte(`{"message": "Repository creation from template failed"}`))
1634+
}),
1635+
),
1636+
),
1637+
requestArgs: map[string]interface{}{
1638+
"name": "invalid-repo",
1639+
"templateOwner": "template-owner",
1640+
"templateRepo": "template-repo",
1641+
},
1642+
expectError: true,
1643+
expectedErrMsg: "failed to create repository from template",
1644+
},
1645+
}
1646+
1647+
for _, tc := range tests {
1648+
t.Run(tc.name, func(t *testing.T) {
1649+
// Setup client with mock
1650+
client := github.NewClient(tc.mockedClient)
1651+
_, handler := CreateRepositoryFromTemplate(stubGetClientFn(client), translations.NullTranslationHelper)
1652+
1653+
// Create call request
1654+
request := createMCPRequest(tc.requestArgs)
1655+
1656+
// Call handler
1657+
result, err := handler(context.Background(), request)
1658+
1659+
// Verify results
1660+
if tc.expectError {
1661+
require.Error(t, err)
1662+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
1663+
return
1664+
}
1665+
1666+
require.NoError(t, err)
1667+
1668+
// Parse the result and get the text content if no error
1669+
textContent := getTextResult(t, result)
1670+
1671+
// Unmarshal and verify the result
1672+
var returnedRepo github.Repository
1673+
err = json.Unmarshal([]byte(textContent.Text), &returnedRepo)
1674+
assert.NoError(t, err)
1675+
1676+
// Verify repository details
1677+
assert.Equal(t, *tc.expectedRepo.Name, *returnedRepo.Name)
1678+
assert.Equal(t, *tc.expectedRepo.Description, *returnedRepo.Description)
1679+
assert.Equal(t, *tc.expectedRepo.Private, *returnedRepo.Private)
1680+
assert.Equal(t, *tc.expectedRepo.HTMLURL, *returnedRepo.HTMLURL)
1681+
assert.Equal(t, *tc.expectedRepo.Owner.Login, *returnedRepo.Owner.Login)
1682+
})
1683+
}
1684+
}

pkg/github/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ func NewServer(getClient GetClientFn, version string, readOnly bool, t translati
7474
if !readOnly {
7575
s.AddTool(CreateOrUpdateFile(getClient, t))
7676
s.AddTool(CreateRepository(getClient, t))
77+
s.AddTool(CreateRepositoryFromTemplate(getClient, t))
7778
s.AddTool(ForkRepository(getClient, t))
7879
s.AddTool(CreateBranch(getClient, t))
7980
s.AddTool(PushFiles(getClient, t))

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