Skip to main content
Glama

Filesystem MCP Server

by mark3labs
package handler import ( "bufio" "context" "fmt" "os" "path/filepath" "strings" "github.com/mark3labs/mcp-go/mcp" ) func (fs *FilesystemHandler) HandleSearchWithinFiles( ctx context.Context, request mcp.CallToolRequest, ) (*mcp.CallToolResult, error) { // Extract and validate parameters path, err := request.RequireString("path") if err != nil { return nil, err } substring, err := request.RequireString("substring") if err != nil { return nil, err } if substring == "" { return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: "Error: substring cannot be empty", }, }, IsError: true, }, nil } // Extract optional depth parameter maxDepth := 0 // 0 means unlimited if depthArg, err := request.RequireFloat("depth"); err == nil { maxDepth = int(depthArg) if maxDepth < 0 { return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: "Error: depth cannot be negative", }, }, IsError: true, }, nil } } // Extract optional max_results parameter maxResults := MAX_SEARCH_RESULTS // default limit if maxResultsArg, err := request.RequireFloat("max_results"); err == nil { maxResults = int(maxResultsArg) if maxResults <= 0 { return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: "Error: max_results must be positive", }, }, IsError: true, }, nil } } // Handle empty or relative paths like "." or "./" by converting to absolute path if path == "." || path == "./" { // Get current working directory cwd, err := os.Getwd() if err != nil { return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Error resolving current directory: %v", err), }, }, IsError: true, }, nil } path = cwd } validPath, err := fs.validatePath(path) if err != nil { return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Error: %v", err), }, }, IsError: true, }, nil } // Check if the path is a directory info, err := os.Stat(validPath) if err != nil { return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Error: %v", err), }, }, IsError: true, }, nil } if !info.IsDir() { return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: "Error: search path must be a directory", }, }, IsError: true, }, nil } // Perform the search results, err := searchWithinFiles(validPath, substring, maxDepth, maxResults, fs) if err != nil { return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Error searching within files: %v", err), }, }, IsError: true, }, nil } if len(results) == 0 { return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("No occurrences of '%s' found in files under %s", substring, path), }, }, }, nil } // Format search results var formattedResults strings.Builder formattedResults.WriteString(fmt.Sprintf("Found %d occurrences of '%s':\n\n", len(results), substring)) // Group results by file for easier readability fileResultsMap := make(map[string][]SearchResult) for _, result := range results { fileResultsMap[result.FilePath] = append(fileResultsMap[result.FilePath], result) } // Display results grouped by file for filePath, fileResults := range fileResultsMap { resourceURI := pathToResourceURI(filePath) formattedResults.WriteString(fmt.Sprintf("File: %s (%s)\n", filePath, resourceURI)) for _, result := range fileResults { // Truncate line content if too long (keeping context around the match) lineContent := result.LineContent if len(lineContent) > 100 { // Find the substring position substrPos := strings.Index(strings.ToLower(lineContent), strings.ToLower(substring)) // Calculate start and end positions for context contextStart := max(0, substrPos-30) contextEnd := min(len(lineContent), substrPos+len(substring)+30) if contextStart > 0 { lineContent = "..." + lineContent[contextStart:contextEnd] } else { lineContent = lineContent[:contextEnd] } if contextEnd < len(result.LineContent) { lineContent += "..." } } formattedResults.WriteString(fmt.Sprintf(" Line %d: %s\n", result.LineNumber, lineContent)) } formattedResults.WriteString("\n") } // If results were limited, note this in the output if len(results) >= maxResults { formattedResults.WriteString(fmt.Sprintf("\nNote: Results limited to %d matches. There may be more occurrences.", maxResults)) } return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: formattedResults.String(), }, }, }, nil } // searchWithinFiles searches for a substring within file contents func searchWithinFiles( rootPath, substring string, maxDepth int, maxResults int, fs *FilesystemHandler, ) ([]SearchResult, error) { var results []SearchResult resultCount := 0 currentDepth := 0 // Walk the directory tree err := filepath.Walk( rootPath, func(path string, info os.FileInfo, err error) error { if err != nil { return nil // Skip errors and continue } // Check if we've reached the maximum number of results if resultCount >= maxResults { return filepath.SkipDir } // Try to validate path validPath, err := fs.validatePath(path) if err != nil { return nil // Skip invalid paths } // Skip directories, only search files if info.IsDir() { // Calculate depth for this directory relPath, err := filepath.Rel(rootPath, path) if err != nil { return nil // Skip on error } // Count separators to determine depth (empty or "." means we're at rootPath) if relPath == "" || relPath == "." { currentDepth = 0 } else { currentDepth = strings.Count(relPath, string(filepath.Separator)) + 1 } // Skip directories beyond max depth if specified if maxDepth > 0 && currentDepth >= maxDepth { return filepath.SkipDir } return nil } // Skip files that are too large if info.Size() > MAX_SEARCHABLE_SIZE { return nil } // Determine MIME type and skip non-text files mimeType := detectMimeType(validPath) if !isTextFile(mimeType) { return nil } // Open the file and search for the substring file, err := os.Open(validPath) if err != nil { return nil // Skip files that can't be opened } defer file.Close() // Create a scanner to read the file line by line scanner := bufio.NewScanner(file) lineNum := 0 // Scan each line for scanner.Scan() { lineNum++ line := scanner.Text() // Check if the line contains the substring if strings.Contains(line, substring) { // Add to results results = append(results, SearchResult{ FilePath: validPath, LineNumber: lineNum, LineContent: line, ResourceURI: pathToResourceURI(validPath), }) resultCount++ // Check if we've reached the maximum results if resultCount >= maxResults { return filepath.SkipDir } } } // Check for scanner errors if err := scanner.Err(); err != nil { return nil // Skip files with scanning errors } return nil }, ) if err != nil { return nil, err } return results, nil } // Helper function since Go < 1.21 doesn't have min/max functions func min(a, b int) int { if a < b { return a } return b } // Helper function since Go < 1.21 doesn't have min/max functions func max(a, b int) int { if a > b { return a } return b }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mark3labs/mcp-filesystem-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server