@@ -185,6 +185,15 @@ func NewFilesystemServer(allowedDirs []string) (*FilesystemServer, error) {
185
185
mcp .WithDescription ("Returns the list of directories that this server is allowed to access." ),
186
186
), s .handleListAllowedDirectories )
187
187
188
+ s .server .AddTool (mcp .NewTool (
189
+ "read_multiple_files" ,
190
+ mcp .WithDescription ("Read the contents of multiple files in a single operation." ),
191
+ mcp .WithArray ("paths" ,
192
+ mcp .Description ("List of file paths to read" ),
193
+ mcp .Required (),
194
+ ),
195
+ ), s .handleReadMultipleFiles )
196
+
188
197
s .server .AddTool (mcp .NewTool (
189
198
"tree" ,
190
199
mcp .WithDescription ("Returns a hierarchical JSON representation of a directory structure." ),
@@ -1879,6 +1888,191 @@ func (s *FilesystemServer) handleGetFileInfo(
1879
1888
}, nil
1880
1889
}
1881
1890
1891
+ func (s * FilesystemServer ) handleReadMultipleFiles (
1892
+ ctx context.Context ,
1893
+ request mcp.CallToolRequest ,
1894
+ ) (* mcp.CallToolResult , error ) {
1895
+ pathsParam , ok := request .Params .Arguments ["paths" ]
1896
+ if ! ok {
1897
+ return nil , fmt .Errorf ("paths parameter is required" )
1898
+ }
1899
+
1900
+ // Convert the paths parameter to a string slice
1901
+ pathsSlice , ok := pathsParam .([]interface {})
1902
+ if ! ok {
1903
+ return nil , fmt .Errorf ("paths must be an array of strings" )
1904
+ }
1905
+
1906
+ if len (pathsSlice ) == 0 {
1907
+ return & mcp.CallToolResult {
1908
+ Content : []mcp.Content {
1909
+ mcp.TextContent {
1910
+ Type : "text" ,
1911
+ Text : "No files specified to read" ,
1912
+ },
1913
+ },
1914
+ IsError : true ,
1915
+ }, nil
1916
+ }
1917
+
1918
+ // Maximum number of files to read in a single request
1919
+ const maxFiles = 50
1920
+ if len (pathsSlice ) > maxFiles {
1921
+ return & mcp.CallToolResult {
1922
+ Content : []mcp.Content {
1923
+ mcp.TextContent {
1924
+ Type : "text" ,
1925
+ Text : fmt .Sprintf ("Too many files requested. Maximum is %d files per request." , maxFiles ),
1926
+ },
1927
+ },
1928
+ IsError : true ,
1929
+ }, nil
1930
+ }
1931
+
1932
+ // Process each file
1933
+ var results []mcp.Content
1934
+ for _ , pathInterface := range pathsSlice {
1935
+ path , ok := pathInterface .(string )
1936
+ if ! ok {
1937
+ return nil , fmt .Errorf ("each path must be a string" )
1938
+ }
1939
+
1940
+ // Handle empty or relative paths like "." or "./" by converting to absolute path
1941
+ if path == "." || path == "./" {
1942
+ // Get current working directory
1943
+ cwd , err := os .Getwd ()
1944
+ if err != nil {
1945
+ results = append (results , mcp.TextContent {
1946
+ Type : "text" ,
1947
+ Text : fmt .Sprintf ("Error resolving current directory for path '%s': %v" , path , err ),
1948
+ })
1949
+ continue
1950
+ }
1951
+ path = cwd
1952
+ }
1953
+
1954
+ validPath , err := s .validatePath (path )
1955
+ if err != nil {
1956
+ results = append (results , mcp.TextContent {
1957
+ Type : "text" ,
1958
+ Text : fmt .Sprintf ("Error with path '%s': %v" , path , err ),
1959
+ })
1960
+ continue
1961
+ }
1962
+
1963
+ // Check if it's a directory
1964
+ info , err := os .Stat (validPath )
1965
+ if err != nil {
1966
+ results = append (results , mcp.TextContent {
1967
+ Type : "text" ,
1968
+ Text : fmt .Sprintf ("Error accessing '%s': %v" , path , err ),
1969
+ })
1970
+ continue
1971
+ }
1972
+
1973
+ if info .IsDir () {
1974
+ // For directories, return a resource reference instead
1975
+ resourceURI := pathToResourceURI (validPath )
1976
+ results = append (results , mcp.TextContent {
1977
+ Type : "text" ,
1978
+ Text : fmt .Sprintf ("'%s' is a directory. Use list_directory tool or resource URI: %s" , path , resourceURI ),
1979
+ })
1980
+ continue
1981
+ }
1982
+
1983
+ // Determine MIME type
1984
+ mimeType := detectMimeType (validPath )
1985
+
1986
+ // Check file size
1987
+ if info .Size () > MAX_INLINE_SIZE {
1988
+ // File is too large to inline, return a resource reference
1989
+ resourceURI := pathToResourceURI (validPath )
1990
+ results = append (results , mcp.TextContent {
1991
+ Type : "text" ,
1992
+ Text : fmt .Sprintf ("File '%s' is too large to display inline (%d bytes). Access it via resource URI: %s" ,
1993
+ path , info .Size (), resourceURI ),
1994
+ })
1995
+ continue
1996
+ }
1997
+
1998
+ // Read file content
1999
+ content , err := os .ReadFile (validPath )
2000
+ if err != nil {
2001
+ results = append (results , mcp.TextContent {
2002
+ Type : "text" ,
2003
+ Text : fmt .Sprintf ("Error reading file '%s': %v" , path , err ),
2004
+ })
2005
+ continue
2006
+ }
2007
+
2008
+ // Add file header
2009
+ results = append (results , mcp.TextContent {
2010
+ Type : "text" ,
2011
+ Text : fmt .Sprintf ("--- File: %s ---" , path ),
2012
+ })
2013
+
2014
+ // Check if it's a text file
2015
+ if isTextFile (mimeType ) {
2016
+ // It's a text file, return as text
2017
+ results = append (results , mcp.TextContent {
2018
+ Type : "text" ,
2019
+ Text : string (content ),
2020
+ })
2021
+ } else if isImageFile (mimeType ) {
2022
+ // It's an image file, return as image content
2023
+ if info .Size () <= MAX_BASE64_SIZE {
2024
+ results = append (results , mcp.TextContent {
2025
+ Type : "text" ,
2026
+ Text : fmt .Sprintf ("Image file: %s (%s, %d bytes)" , path , mimeType , info .Size ()),
2027
+ })
2028
+ results = append (results , mcp.ImageContent {
2029
+ Type : "image" ,
2030
+ Data : base64 .StdEncoding .EncodeToString (content ),
2031
+ MIMEType : mimeType ,
2032
+ })
2033
+ } else {
2034
+ // Too large for base64, return a reference
2035
+ resourceURI := pathToResourceURI (validPath )
2036
+ results = append (results , mcp.TextContent {
2037
+ Type : "text" ,
2038
+ Text : fmt .Sprintf ("Image file '%s' is too large to display inline (%d bytes). Access it via resource URI: %s" ,
2039
+ path , info .Size (), resourceURI ),
2040
+ })
2041
+ }
2042
+ } else {
2043
+ // It's another type of binary file
2044
+ resourceURI := pathToResourceURI (validPath )
2045
+
2046
+ if info .Size () <= MAX_BASE64_SIZE {
2047
+ // Small enough for base64 encoding
2048
+ results = append (results , mcp.TextContent {
2049
+ Type : "text" ,
2050
+ Text : fmt .Sprintf ("Binary file: %s (%s, %d bytes)" , path , mimeType , info .Size ()),
2051
+ })
2052
+ results = append (results , mcp.EmbeddedResource {
2053
+ Type : "resource" ,
2054
+ Resource : mcp.BlobResourceContents {
2055
+ URI : resourceURI ,
2056
+ MIMEType : mimeType ,
2057
+ Blob : base64 .StdEncoding .EncodeToString (content ),
2058
+ },
2059
+ })
2060
+ } else {
2061
+ // Too large for base64, return a reference
2062
+ results = append (results , mcp.TextContent {
2063
+ Type : "text" ,
2064
+ Text : fmt .Sprintf ("Binary file '%s' (%s, %d bytes). Access it via resource URI: %s" ,
2065
+ path , mimeType , info .Size (), resourceURI ),
2066
+ })
2067
+ }
2068
+ }
2069
+ }
2070
+
2071
+ return & mcp.CallToolResult {
2072
+ Content : results ,
2073
+ }, nil
2074
+ }
2075
+
1882
2076
func (s * FilesystemServer ) handleListAllowedDirectories (
1883
2077
ctx context.Context ,
1884
2078
request mcp.CallToolRequest ,
0 commit comments