Skip to content

Commit 84f5607

Browse files
ashwin-antclaude
andcommitted
feat: add DeleteFile tool to delete files from GitHub repositories
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent a7d741c commit 84f5607

File tree

3 files changed

+227
-0
lines changed

3 files changed

+227
-0
lines changed

pkg/github/repositories.go

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

559+
// DeleteFile creates a tool to delete a file in a GitHub repository.
560+
func DeleteFile(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
561+
return mcp.NewTool("delete_file",
562+
mcp.WithDescription(t("TOOL_DELETE_FILE_DESCRIPTION", "Delete a file from a GitHub repository")),
563+
mcp.WithString("owner",
564+
mcp.Required(),
565+
mcp.Description("Repository owner (username or organization)"),
566+
),
567+
mcp.WithString("repo",
568+
mcp.Required(),
569+
mcp.Description("Repository name"),
570+
),
571+
mcp.WithString("path",
572+
mcp.Required(),
573+
mcp.Description("Path to the file to delete"),
574+
),
575+
mcp.WithString("message",
576+
mcp.Required(),
577+
mcp.Description("Commit message"),
578+
),
579+
mcp.WithString("branch",
580+
mcp.Required(),
581+
mcp.Description("Branch to delete the file from"),
582+
),
583+
mcp.WithString("sha",
584+
mcp.Required(),
585+
mcp.Description("SHA of the file being deleted"),
586+
),
587+
),
588+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
589+
owner, err := requiredParam[string](request, "owner")
590+
if err != nil {
591+
return mcp.NewToolResultError(err.Error()), nil
592+
}
593+
repo, err := requiredParam[string](request, "repo")
594+
if err != nil {
595+
return mcp.NewToolResultError(err.Error()), nil
596+
}
597+
path, err := requiredParam[string](request, "path")
598+
if err != nil {
599+
return mcp.NewToolResultError(err.Error()), nil
600+
}
601+
message, err := requiredParam[string](request, "message")
602+
if err != nil {
603+
return mcp.NewToolResultError(err.Error()), nil
604+
}
605+
branch, err := requiredParam[string](request, "branch")
606+
if err != nil {
607+
return mcp.NewToolResultError(err.Error()), nil
608+
}
609+
sha, err := requiredParam[string](request, "sha")
610+
if err != nil {
611+
return mcp.NewToolResultError(err.Error()), nil
612+
}
613+
614+
// Create the file options
615+
opts := &github.RepositoryContentFileOptions{
616+
Message: github.Ptr(message),
617+
Branch: github.Ptr(branch),
618+
SHA: github.Ptr(sha),
619+
}
620+
621+
// Delete the file
622+
client, err := getClient(ctx)
623+
if err != nil {
624+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
625+
}
626+
deleteResponse, resp, err := client.Repositories.DeleteFile(ctx, owner, repo, path, opts)
627+
if err != nil {
628+
return nil, fmt.Errorf("failed to delete file: %w", err)
629+
}
630+
defer func() { _ = resp.Body.Close() }()
631+
632+
if resp.StatusCode != 200 && resp.StatusCode != 204 {
633+
body, err := io.ReadAll(resp.Body)
634+
if err != nil {
635+
return nil, fmt.Errorf("failed to read response body: %w", err)
636+
}
637+
return mcp.NewToolResultError(fmt.Sprintf("failed to delete file: %s", string(body))), nil
638+
}
639+
640+
r, err := json.Marshal(deleteResponse)
641+
if err != nil {
642+
return nil, fmt.Errorf("failed to marshal response: %w", err)
643+
}
644+
645+
return mcp.NewToolResultText(string(r)), nil
646+
}
647+
}
648+
559649
// CreateBranch creates a tool to create a new branch.
560650
func CreateBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
561651
return mcp.NewTool("create_branch",

pkg/github/repositories_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,3 +1528,139 @@ func Test_ListBranches(t *testing.T) {
15281528
})
15291529
}
15301530
}
1531+
1532+
func Test_DeleteFile(t *testing.T) {
1533+
// Verify tool definition once
1534+
mockClient := github.NewClient(nil)
1535+
tool, _ := DeleteFile(stubGetClientFn(mockClient), translations.NullTranslationHelper)
1536+
1537+
assert.Equal(t, "delete_file", tool.Name)
1538+
assert.NotEmpty(t, tool.Description)
1539+
assert.Contains(t, tool.InputSchema.Properties, "owner")
1540+
assert.Contains(t, tool.InputSchema.Properties, "repo")
1541+
assert.Contains(t, tool.InputSchema.Properties, "path")
1542+
assert.Contains(t, tool.InputSchema.Properties, "message")
1543+
assert.Contains(t, tool.InputSchema.Properties, "branch")
1544+
assert.Contains(t, tool.InputSchema.Properties, "sha")
1545+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "path", "message", "branch", "sha"})
1546+
1547+
// Setup mock delete response
1548+
mockDeleteResponse := &github.RepositoryContentResponse{
1549+
Content: &github.RepositoryContent{
1550+
Name: github.Ptr("example.md"),
1551+
Path: github.Ptr("docs/example.md"),
1552+
SHA: github.Ptr("abc123def456"),
1553+
HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/docs/example.md"),
1554+
},
1555+
Commit: github.Commit{
1556+
SHA: github.Ptr("def456abc789"),
1557+
Message: github.Ptr("Delete example file"),
1558+
Author: &github.CommitAuthor{
1559+
Name: github.Ptr("Test User"),
1560+
Email: github.Ptr("[email protected]"),
1561+
Date: &github.Timestamp{Time: time.Now()},
1562+
},
1563+
HTMLURL: github.Ptr("https://github.com/owner/repo/commit/def456abc789"),
1564+
},
1565+
}
1566+
1567+
tests := []struct {
1568+
name string
1569+
mockedClient *http.Client
1570+
requestArgs map[string]interface{}
1571+
expectError bool
1572+
expectedResult *github.RepositoryContentResponse
1573+
expectedErrMsg string
1574+
}{
1575+
{
1576+
name: "successful file deletion",
1577+
mockedClient: mock.NewMockedHTTPClient(
1578+
mock.WithRequestMatchHandler(
1579+
mock.DeleteReposContentsByOwnerByRepoByPath,
1580+
expectRequestBody(t, map[string]interface{}{
1581+
"message": "Delete example file",
1582+
"branch": "main",
1583+
"sha": "abc123def456",
1584+
"content": interface{}(nil),
1585+
}).andThen(
1586+
mockResponse(t, http.StatusOK, mockDeleteResponse),
1587+
),
1588+
),
1589+
),
1590+
requestArgs: map[string]interface{}{
1591+
"owner": "owner",
1592+
"repo": "repo",
1593+
"path": "docs/example.md",
1594+
"message": "Delete example file",
1595+
"branch": "main",
1596+
"sha": "abc123def456",
1597+
},
1598+
expectError: false,
1599+
expectedResult: mockDeleteResponse,
1600+
},
1601+
{
1602+
name: "file deletion fails",
1603+
mockedClient: mock.NewMockedHTTPClient(
1604+
mock.WithRequestMatchHandler(
1605+
mock.DeleteReposContentsByOwnerByRepoByPath,
1606+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1607+
w.WriteHeader(http.StatusNotFound)
1608+
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
1609+
}),
1610+
),
1611+
),
1612+
requestArgs: map[string]interface{}{
1613+
"owner": "owner",
1614+
"repo": "repo",
1615+
"path": "docs/nonexistent.md",
1616+
"message": "Delete nonexistent file",
1617+
"branch": "main",
1618+
"sha": "abc123def456",
1619+
},
1620+
expectError: true,
1621+
expectedErrMsg: "failed to delete file",
1622+
},
1623+
}
1624+
1625+
for _, tc := range tests {
1626+
t.Run(tc.name, func(t *testing.T) {
1627+
// Setup client with mock
1628+
client := github.NewClient(tc.mockedClient)
1629+
_, handler := DeleteFile(stubGetClientFn(client), translations.NullTranslationHelper)
1630+
1631+
// Create call request
1632+
request := createMCPRequest(tc.requestArgs)
1633+
1634+
// Call handler
1635+
result, err := handler(context.Background(), request)
1636+
1637+
// Verify results
1638+
if tc.expectError {
1639+
require.Error(t, err)
1640+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
1641+
return
1642+
}
1643+
1644+
require.NoError(t, err)
1645+
1646+
// Parse the result and get the text content if no error
1647+
textContent := getTextResult(t, result)
1648+
1649+
// Unmarshal and verify the result
1650+
var returnedContent github.RepositoryContentResponse
1651+
err = json.Unmarshal([]byte(textContent.Text), &returnedContent)
1652+
require.NoError(t, err)
1653+
1654+
// Verify content
1655+
if tc.expectedResult.Content != nil {
1656+
assert.Equal(t, *tc.expectedResult.Content.Name, *returnedContent.Content.Name)
1657+
assert.Equal(t, *tc.expectedResult.Content.Path, *returnedContent.Content.Path)
1658+
assert.Equal(t, *tc.expectedResult.Content.SHA, *returnedContent.Content.SHA)
1659+
}
1660+
1661+
// Verify commit
1662+
assert.Equal(t, *tc.expectedResult.Commit.SHA, *returnedContent.Commit.SHA)
1663+
assert.Equal(t, *tc.expectedResult.Commit.Message, *returnedContent.Commit.Message)
1664+
})
1665+
}
1666+
}

pkg/github/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
3434
toolsets.NewServerTool(ForkRepository(getClient, t)),
3535
toolsets.NewServerTool(CreateBranch(getClient, t)),
3636
toolsets.NewServerTool(PushFiles(getClient, t)),
37+
toolsets.NewServerTool(DeleteFile(getClient, t)),
3738
)
3839
issues := toolsets.NewToolset("issues", "GitHub Issues related tools").
3940
AddReadTools(

0 commit comments

Comments
 (0)