@@ -185,6 +185,15 @@ func NewFilesystemServer(allowedDirs []string) (*FilesystemServer, error) {
185185 mcp .WithDescription ("Returns the list of directories that this server is allowed to access." ),
186186 ), s .handleListAllowedDirectories )
187187
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+
188197 s .server .AddTool (mcp .NewTool (
189198 "tree" ,
190199 mcp .WithDescription ("Returns a hierarchical JSON representation of a directory structure." ),
@@ -1879,6 +1888,191 @@ func (s *FilesystemServer) handleGetFileInfo(
18791888 }, nil
18801889}
18811890
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+
18822076func (s * FilesystemServer ) handleListAllowedDirectories (
18832077 ctx context.Context ,
18842078 request mcp.CallToolRequest ,
0 commit comments