@@ -3,6 +3,7 @@ package main
3
3
import (
4
4
"context"
5
5
"encoding/base64"
6
+ "encoding/json"
6
7
"fmt"
7
8
"log"
8
9
"mime"
@@ -33,6 +34,16 @@ type FileInfo struct {
33
34
Permissions string `json:"permissions"`
34
35
}
35
36
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
+
36
47
type FilesystemServer struct {
37
48
allowedDirs []string
38
49
server * server.MCPServer
@@ -161,6 +172,21 @@ func NewFilesystemServer(allowedDirs []string) (*FilesystemServer, error) {
161
172
mcp .WithDescription ("Returns the list of directories that this server is allowed to access." ),
162
173
), s .handleListAllowedDirectories )
163
174
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
+
164
190
return s , nil
165
191
}
166
192
@@ -192,6 +218,85 @@ func (s *FilesystemServer) isPathInAllowedDirs(path string) bool {
192
218
return false
193
219
}
194
220
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
+
195
300
func (s * FilesystemServer ) validatePath (requestedPath string ) (string , error ) {
196
301
// Always convert to absolute path first
197
302
abs , err := filepath .Abs (requestedPath )
@@ -1259,6 +1364,139 @@ func (s *FilesystemServer) handleSearchFiles(
1259
1364
}, nil
1260
1365
}
1261
1366
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
+
1262
1500
func (s * FilesystemServer ) handleGetFileInfo (
1263
1501
ctx context.Context ,
1264
1502
request mcp.CallToolRequest ,
0 commit comments