@@ -3,6 +3,7 @@ package main
33import  (
44	"context" 
55	"encoding/base64" 
6+ 	"encoding/json" 
67	"fmt" 
78	"log" 
89	"mime" 
@@ -33,6 +34,16 @@ type FileInfo struct {
3334	Permissions  string     `json:"permissions"` 
3435}
3536
37+ // FileNode represents a node in the file tree 
38+ type  FileNode  struct  {
39+ 	Name      string       `json:"name"` 
40+ 	Path      string       `json:"path"` 
41+ 	Type      string       `json:"type"`  // "file" or "directory" 
42+ 	Size      int64        `json:"size,omitempty"` 
43+ 	Modified  time.Time    `json:"modified,omitempty"` 
44+ 	Children  []* FileNode  `json:"children,omitempty"` 
45+ }
46+ 
3647type  FilesystemServer  struct  {
3748	allowedDirs  []string 
3849	server       * server.MCPServer 
@@ -161,6 +172,21 @@ func NewFilesystemServer(allowedDirs []string) (*FilesystemServer, error) {
161172		mcp .WithDescription ("Returns the list of directories that this server is allowed to access." ),
162173	), s .handleListAllowedDirectories )
163174
175+ 	s .server .AddTool (mcp .NewTool (
176+ 		"tree" ,
177+ 		mcp .WithDescription ("Returns a hierarchical JSON representation of a directory structure." ),
178+ 		mcp .WithString ("path" ,
179+ 			mcp .Description ("Path of the directory to traverse" ),
180+ 			mcp .Required (),
181+ 		),
182+ 		mcp .WithNumber ("depth" ,
183+ 			mcp .Description ("Maximum depth to traverse (default: 3)" ),
184+ 		),
185+ 		mcp .WithBoolean ("follow_symlinks" ,
186+ 			mcp .Description ("Whether to follow symbolic links (default: false)" ),
187+ 		),
188+ 	), s .handleTree )
189+ 
164190	return  s , nil 
165191}
166192
@@ -192,6 +218,85 @@ func (s *FilesystemServer) isPathInAllowedDirs(path string) bool {
192218	return  false 
193219}
194220
221+ // buildTree builds a tree representation of the filesystem starting at the given path 
222+ func  (s  * FilesystemServer ) buildTree (path  string , maxDepth  int , currentDepth  int , followSymlinks  bool ) (* FileNode , error ) {
223+ 	// Validate the path 
224+ 	validPath , err  :=  s .validatePath (path )
225+ 	if  err  !=  nil  {
226+ 		return  nil , err 
227+ 	}
228+ 
229+ 	// Get file info 
230+ 	info , err  :=  os .Stat (validPath )
231+ 	if  err  !=  nil  {
232+ 		return  nil , err 
233+ 	}
234+ 
235+ 	// Create the node 
236+ 	node  :=  & FileNode {
237+ 		Name :     filepath .Base (validPath ),
238+ 		Path :     validPath ,
239+ 		Modified : info .ModTime (),
240+ 	}
241+ 
242+ 	// Set type and size 
243+ 	if  info .IsDir () {
244+ 		node .Type  =  "directory" 
245+ 
246+ 		// If we haven't reached the max depth, process children 
247+ 		if  currentDepth  <  maxDepth  {
248+ 			// Read directory entries 
249+ 			entries , err  :=  os .ReadDir (validPath )
250+ 			if  err  !=  nil  {
251+ 				return  nil , err 
252+ 			}
253+ 
254+ 			// Process each entry 
255+ 			for  _ , entry  :=  range  entries  {
256+ 				entryPath  :=  filepath .Join (validPath , entry .Name ())
257+ 
258+ 				// Handle symlinks 
259+ 				if  entry .Type ()& os .ModeSymlink  !=  0  {
260+ 					if  ! followSymlinks  {
261+ 						// Skip symlinks if not following them 
262+ 						continue 
263+ 					}
264+ 
265+ 					// Resolve symlink 
266+ 					linkDest , err  :=  filepath .EvalSymlinks (entryPath )
267+ 					if  err  !=  nil  {
268+ 						// Skip invalid symlinks 
269+ 						continue 
270+ 					}
271+ 
272+ 					// Validate the symlink destination is within allowed directories 
273+ 					if  ! s .isPathInAllowedDirs (linkDest ) {
274+ 						// Skip symlinks pointing outside allowed directories 
275+ 						continue 
276+ 					}
277+ 
278+ 					entryPath  =  linkDest 
279+ 				}
280+ 
281+ 				// Recursively build child node 
282+ 				childNode , err  :=  s .buildTree (entryPath , maxDepth , currentDepth + 1 , followSymlinks )
283+ 				if  err  !=  nil  {
284+ 					// Skip entries with errors 
285+ 					continue 
286+ 				}
287+ 
288+ 				// Add child to the current node 
289+ 				node .Children  =  append (node .Children , childNode )
290+ 			}
291+ 		}
292+ 	} else  {
293+ 		node .Type  =  "file" 
294+ 		node .Size  =  info .Size ()
295+ 	}
296+ 
297+ 	return  node , nil 
298+ }
299+ 
195300func  (s  * FilesystemServer ) validatePath (requestedPath  string ) (string , error ) {
196301	// Always convert to absolute path first 
197302	abs , err  :=  filepath .Abs (requestedPath )
@@ -1259,6 +1364,139 @@ func (s *FilesystemServer) handleSearchFiles(
12591364	}, nil 
12601365}
12611366
1367+ func  (s  * FilesystemServer ) handleTree (
1368+ 	ctx  context.Context ,
1369+ 	request  mcp.CallToolRequest ,
1370+ ) (* mcp.CallToolResult , error ) {
1371+ 	path , ok  :=  request .Params .Arguments ["path" ].(string )
1372+ 	if  ! ok  {
1373+ 		return  nil , fmt .Errorf ("path must be a string" )
1374+ 	}
1375+ 
1376+ 	// Handle empty or relative paths like "." or "./" by converting to absolute path 
1377+ 	if  path  ==  "."  ||  path  ==  "./"  {
1378+ 		// Get current working directory 
1379+ 		cwd , err  :=  os .Getwd ()
1380+ 		if  err  !=  nil  {
1381+ 			return  & mcp.CallToolResult {
1382+ 				Content : []mcp.Content {
1383+ 					mcp.TextContent {
1384+ 						Type : "text" ,
1385+ 						Text : fmt .Sprintf ("Error resolving current directory: %v" , err ),
1386+ 					},
1387+ 				},
1388+ 				IsError : true ,
1389+ 			}, nil 
1390+ 		}
1391+ 		path  =  cwd 
1392+ 	}
1393+ 
1394+ 	// Extract depth parameter (optional, default: 3) 
1395+ 	depth  :=  3  // Default value 
1396+ 	if  depthParam , ok  :=  request .Params .Arguments ["depth" ]; ok  {
1397+ 		if  d , ok  :=  depthParam .(float64 ); ok  {
1398+ 			depth  =  int (d )
1399+ 		}
1400+ 	}
1401+ 
1402+ 	// Extract follow_symlinks parameter (optional, default: false) 
1403+ 	followSymlinks  :=  false  // Default value 
1404+ 	if  followParam , ok  :=  request .Params .Arguments ["follow_symlinks" ]; ok  {
1405+ 		if  f , ok  :=  followParam .(bool ); ok  {
1406+ 			followSymlinks  =  f 
1407+ 		}
1408+ 	}
1409+ 
1410+ 	// Validate the path is within allowed directories 
1411+ 	validPath , err  :=  s .validatePath (path )
1412+ 	if  err  !=  nil  {
1413+ 		return  & mcp.CallToolResult {
1414+ 			Content : []mcp.Content {
1415+ 				mcp.TextContent {
1416+ 					Type : "text" ,
1417+ 					Text : fmt .Sprintf ("Error: %v" , err ),
1418+ 				},
1419+ 			},
1420+ 			IsError : true ,
1421+ 		}, nil 
1422+ 	}
1423+ 
1424+ 	// Check if it's a directory 
1425+ 	info , err  :=  os .Stat (validPath )
1426+ 	if  err  !=  nil  {
1427+ 		return  & mcp.CallToolResult {
1428+ 			Content : []mcp.Content {
1429+ 				mcp.TextContent {
1430+ 					Type : "text" ,
1431+ 					Text : fmt .Sprintf ("Error: %v" , err ),
1432+ 				},
1433+ 			},
1434+ 			IsError : true ,
1435+ 		}, nil 
1436+ 	}
1437+ 
1438+ 	if  ! info .IsDir () {
1439+ 		return  & mcp.CallToolResult {
1440+ 			Content : []mcp.Content {
1441+ 				mcp.TextContent {
1442+ 					Type : "text" ,
1443+ 					Text : "Error: The specified path is not a directory" ,
1444+ 				},
1445+ 			},
1446+ 			IsError : true ,
1447+ 		}, nil 
1448+ 	}
1449+ 
1450+ 	// Build the tree structure 
1451+ 	tree , err  :=  s .buildTree (validPath , depth , 0 , followSymlinks )
1452+ 	if  err  !=  nil  {
1453+ 		return  & mcp.CallToolResult {
1454+ 			Content : []mcp.Content {
1455+ 				mcp.TextContent {
1456+ 					Type : "text" ,
1457+ 					Text : fmt .Sprintf ("Error building directory tree: %v" , err ),
1458+ 				},
1459+ 			},
1460+ 			IsError : true ,
1461+ 		}, nil 
1462+ 	}
1463+ 
1464+ 	// Convert to JSON 
1465+ 	jsonData , err  :=  json .MarshalIndent (tree , "" , "  " )
1466+ 	if  err  !=  nil  {
1467+ 		return  & mcp.CallToolResult {
1468+ 			Content : []mcp.Content {
1469+ 				mcp.TextContent {
1470+ 					Type : "text" ,
1471+ 					Text : fmt .Sprintf ("Error generating JSON: %v" , err ),
1472+ 				},
1473+ 			},
1474+ 			IsError : true ,
1475+ 		}, nil 
1476+ 	}
1477+ 
1478+ 	// Create resource URI for the directory 
1479+ 	resourceURI  :=  pathToResourceURI (validPath )
1480+ 
1481+ 	// Return the result 
1482+ 	return  & mcp.CallToolResult {
1483+ 		Content : []mcp.Content {
1484+ 			mcp.TextContent {
1485+ 				Type : "text" ,
1486+ 				Text : fmt .Sprintf ("Directory tree for %s (max depth: %d):\n \n %s" , validPath , depth , string (jsonData )),
1487+ 			},
1488+ 			mcp.EmbeddedResource {
1489+ 				Type : "resource" ,
1490+ 				Resource : mcp.TextResourceContents {
1491+ 					URI :      resourceURI ,
1492+ 					MIMEType : "application/json" ,
1493+ 					Text :     string (jsonData ),
1494+ 				},
1495+ 			},
1496+ 		},
1497+ 	}, nil 
1498+ }
1499+ 
12621500func  (s  * FilesystemServer ) handleGetFileInfo (
12631501	ctx  context.Context ,
12641502	request  mcp.CallToolRequest ,
0 commit comments