|
5 | 5 | "encoding/base64"
|
6 | 6 | "encoding/json"
|
7 | 7 | "fmt"
|
| 8 | + "io" |
8 | 9 | "mime"
|
9 | 10 | "os"
|
10 | 11 | "path/filepath"
|
@@ -131,6 +132,19 @@ func NewFilesystemServer(allowedDirs []string) (*FilesystemServer, error) {
|
131 | 132 | ),
|
132 | 133 | ), s.handleCreateDirectory)
|
133 | 134 |
|
| 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 | + |
134 | 148 | s.server.AddTool(mcp.NewTool(
|
135 | 149 | "move_file",
|
136 | 150 | mcp.WithDescription("Move or rename files and directories."),
|
@@ -1132,6 +1146,242 @@ func (s *FilesystemServer) handleCreateDirectory(
|
1132 | 1146 | }, nil
|
1133 | 1147 | }
|
1134 | 1148 |
|
| 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 | + |
1135 | 1385 | func (s *FilesystemServer) handleMoveFile(
|
1136 | 1386 | ctx context.Context,
|
1137 | 1387 | request mcp.CallToolRequest,
|
|
0 commit comments