Skip to content

Commit 181178a

Browse files
committed
Implement copy_file
1 parent 5f02ce4 commit 181178a

File tree

2 files changed

+254
-0
lines changed

2 files changed

+254
-0
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ This MCP server provides secure access to the local filesystem via the Model Con
1616
- Create a new file or overwrite an existing file with new content
1717
- Parameters: `path` (required): Path where to write the file, `content` (required): Content to write to the file
1818

19+
- **copy_file**
20+
- Copy files and directories
21+
- Parameters: `source` (required): Source path of the file or directory, `destination` (required): Destination path
22+
1923
- **move_file**
2024
- Move or rename files and directories
2125
- Parameters: `source` (required): Source path of the file or directory, `destination` (required): Destination path

filesystemserver/server.go

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/base64"
66
"encoding/json"
77
"fmt"
8+
"io"
89
"mime"
910
"os"
1011
"path/filepath"
@@ -131,6 +132,19 @@ func NewFilesystemServer(allowedDirs []string) (*FilesystemServer, error) {
131132
),
132133
), s.handleCreateDirectory)
133134

135+
s.server.AddTool(mcp.NewTool(
136+
"copy_file",
137+
mcp.WithDescription("Copy files and directories."),
138+
mcp.WithString("source",
139+
mcp.Description("Source path of the file or directory"),
140+
mcp.Required(),
141+
),
142+
mcp.WithString("destination",
143+
mcp.Description("Destination path"),
144+
mcp.Required(),
145+
),
146+
), s.handleCopyFile)
147+
134148
s.server.AddTool(mcp.NewTool(
135149
"move_file",
136150
mcp.WithDescription("Move or rename files and directories."),
@@ -1132,6 +1146,242 @@ func (s *FilesystemServer) handleCreateDirectory(
11321146
}, nil
11331147
}
11341148

1149+
func (s *FilesystemServer) handleCopyFile(
1150+
ctx context.Context,
1151+
request mcp.CallToolRequest,
1152+
) (*mcp.CallToolResult, error) {
1153+
source, ok := request.Params.Arguments["source"].(string)
1154+
if !ok {
1155+
return nil, fmt.Errorf("source must be a string")
1156+
}
1157+
destination, ok := request.Params.Arguments["destination"].(string)
1158+
if !ok {
1159+
return nil, fmt.Errorf("destination must be a string")
1160+
}
1161+
1162+
// Handle empty or relative paths for source
1163+
if source == "." || source == "./" {
1164+
cwd, err := os.Getwd()
1165+
if err != nil {
1166+
return &mcp.CallToolResult{
1167+
Content: []mcp.Content{
1168+
mcp.TextContent{
1169+
Type: "text",
1170+
Text: fmt.Sprintf("Error resolving current directory: %v", err),
1171+
},
1172+
},
1173+
IsError: true,
1174+
}, nil
1175+
}
1176+
source = cwd
1177+
}
1178+
if destination == "." || destination == "./" {
1179+
cwd, err := os.Getwd()
1180+
if err != nil {
1181+
return &mcp.CallToolResult{
1182+
Content: []mcp.Content{
1183+
mcp.TextContent{
1184+
Type: "text",
1185+
Text: fmt.Sprintf("Error resolving current directory: %v", err),
1186+
},
1187+
},
1188+
IsError: true,
1189+
}, nil
1190+
}
1191+
destination = cwd
1192+
}
1193+
1194+
validSource, err := s.validatePath(source)
1195+
if err != nil {
1196+
return &mcp.CallToolResult{
1197+
Content: []mcp.Content{
1198+
mcp.TextContent{
1199+
Type: "text",
1200+
Text: fmt.Sprintf("Error with source path: %v", err),
1201+
},
1202+
},
1203+
IsError: true,
1204+
}, nil
1205+
}
1206+
1207+
// Check if source exists
1208+
srcInfo, err := os.Stat(validSource)
1209+
if os.IsNotExist(err) {
1210+
return &mcp.CallToolResult{
1211+
Content: []mcp.Content{
1212+
mcp.TextContent{
1213+
Type: "text",
1214+
Text: fmt.Sprintf("Error: Source does not exist: %s", source),
1215+
},
1216+
},
1217+
IsError: true,
1218+
}, nil
1219+
} else if err != nil {
1220+
return &mcp.CallToolResult{
1221+
Content: []mcp.Content{
1222+
mcp.TextContent{
1223+
Type: "text",
1224+
Text: fmt.Sprintf("Error accessing source: %v", err),
1225+
},
1226+
},
1227+
IsError: true,
1228+
}, nil
1229+
}
1230+
1231+
validDest, err := s.validatePath(destination)
1232+
if err != nil {
1233+
return &mcp.CallToolResult{
1234+
Content: []mcp.Content{
1235+
mcp.TextContent{
1236+
Type: "text",
1237+
Text: fmt.Sprintf("Error with destination path: %v", err),
1238+
},
1239+
},
1240+
IsError: true,
1241+
}, nil
1242+
}
1243+
1244+
// Create parent directory for destination if it doesn't exist
1245+
destDir := filepath.Dir(validDest)
1246+
if err := os.MkdirAll(destDir, 0755); err != nil {
1247+
return &mcp.CallToolResult{
1248+
Content: []mcp.Content{
1249+
mcp.TextContent{
1250+
Type: "text",
1251+
Text: fmt.Sprintf("Error creating destination directory: %v", err),
1252+
},
1253+
},
1254+
IsError: true,
1255+
}, nil
1256+
}
1257+
1258+
// Perform the copy operation based on whether source is a file or directory
1259+
if srcInfo.IsDir() {
1260+
// It's a directory, copy recursively
1261+
if err := copyDir(validSource, validDest); err != nil {
1262+
return &mcp.CallToolResult{
1263+
Content: []mcp.Content{
1264+
mcp.TextContent{
1265+
Type: "text",
1266+
Text: fmt.Sprintf("Error copying directory: %v", err),
1267+
},
1268+
},
1269+
IsError: true,
1270+
}, nil
1271+
}
1272+
} else {
1273+
// It's a file, copy directly
1274+
if err := copyFile(validSource, validDest); err != nil {
1275+
return &mcp.CallToolResult{
1276+
Content: []mcp.Content{
1277+
mcp.TextContent{
1278+
Type: "text",
1279+
Text: fmt.Sprintf("Error copying file: %v", err),
1280+
},
1281+
},
1282+
IsError: true,
1283+
}, nil
1284+
}
1285+
}
1286+
1287+
resourceURI := pathToResourceURI(validDest)
1288+
return &mcp.CallToolResult{
1289+
Content: []mcp.Content{
1290+
mcp.TextContent{
1291+
Type: "text",
1292+
Text: fmt.Sprintf(
1293+
"Successfully copied %s to %s",
1294+
source,
1295+
destination,
1296+
),
1297+
},
1298+
mcp.EmbeddedResource{
1299+
Type: "resource",
1300+
Resource: mcp.TextResourceContents{
1301+
URI: resourceURI,
1302+
MIMEType: "text/plain",
1303+
Text: fmt.Sprintf("Copied file: %s", validDest),
1304+
},
1305+
},
1306+
},
1307+
}, nil
1308+
}
1309+
1310+
// copyFile copies a single file from src to dst
1311+
func copyFile(src, dst string) error {
1312+
// Open the source file
1313+
sourceFile, err := os.Open(src)
1314+
if err != nil {
1315+
return err
1316+
}
1317+
defer sourceFile.Close()
1318+
1319+
// Create the destination file
1320+
destFile, err := os.Create(dst)
1321+
if err != nil {
1322+
return err
1323+
}
1324+
defer destFile.Close()
1325+
1326+
// Copy the contents
1327+
if _, err := io.Copy(destFile, sourceFile); err != nil {
1328+
return err
1329+
}
1330+
1331+
// Get source file mode
1332+
sourceInfo, err := os.Stat(src)
1333+
if err != nil {
1334+
return err
1335+
}
1336+
1337+
// Set the same file mode on destination
1338+
return os.Chmod(dst, sourceInfo.Mode())
1339+
}
1340+
1341+
// copyDir recursively copies a directory tree from src to dst
1342+
func copyDir(src, dst string) error {
1343+
// Get properties of source dir
1344+
srcInfo, err := os.Stat(src)
1345+
if err != nil {
1346+
return err
1347+
}
1348+
1349+
// Create the destination directory with the same permissions
1350+
if err = os.MkdirAll(dst, srcInfo.Mode()); err != nil {
1351+
return err
1352+
}
1353+
1354+
// Read directory entries
1355+
entries, err := os.ReadDir(src)
1356+
if err != nil {
1357+
return err
1358+
}
1359+
1360+
for _, entry := range entries {
1361+
srcPath := filepath.Join(src, entry.Name())
1362+
dstPath := filepath.Join(dst, entry.Name())
1363+
1364+
// Handle symlinks
1365+
if entry.Type()&os.ModeSymlink != 0 {
1366+
// For simplicity, we'll skip symlinks in this implementation
1367+
continue
1368+
}
1369+
1370+
// Recursively copy subdirectories or copy files
1371+
if entry.IsDir() {
1372+
if err = copyDir(srcPath, dstPath); err != nil {
1373+
return err
1374+
}
1375+
} else {
1376+
if err = copyFile(srcPath, dstPath); err != nil {
1377+
return err
1378+
}
1379+
}
1380+
}
1381+
1382+
return nil
1383+
}
1384+
11351385
func (s *FilesystemServer) handleMoveFile(
11361386
ctx context.Context,
11371387
request mcp.CallToolRequest,

0 commit comments

Comments
 (0)