Skip to content

Commit 01b8eef

Browse files
committed
Add read_multiple_files tool
1 parent 181178a commit 01b8eef

File tree

2 files changed

+198
-0
lines changed

2 files changed

+198
-0
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ This MCP server provides secure access to the local filesystem via the Model Con
1212
- Read the complete contents of a file from the file system
1313
- Parameters: `path` (required): Path to the file to read
1414

15+
- **read_multiple_files**
16+
- Read the contents of multiple files in a single operation
17+
- Parameters: `paths` (required): List of file paths to read
18+
1519
- **write_file**
1620
- Create a new file or overwrite an existing file with new content
1721
- Parameters: `path` (required): Path where to write the file, `content` (required): Content to write to the file

filesystemserver/server.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
18822076
func (s *FilesystemServer) handleListAllowedDirectories(
18832077
ctx context.Context,
18842078
request mcp.CallToolRequest,

0 commit comments

Comments
 (0)