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

Diff for: pkg/github/repositories.go

+87-1
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",

Diff for: pkg/github/repositories_test.go

+154
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+
}

Diff for: pkg/github/server.go

+1
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)