Commit 2f82819

mo khan <mo@mokhan.ca>
2025-08-14 23:41:17
feat: return every file in the filesystem resources/list endpoint
1 parent f847fd0
Changed files (1)
pkg
filesystem
pkg/filesystem/server.go
@@ -1,6 +1,7 @@
 package filesystem
 
 import (
+	"encoding/json"
 	"fmt"
 	"os"
 	"path/filepath"
@@ -41,6 +42,9 @@ func New(allowedDirs []string) *Server {
 	fsServer.registerPrompts()
 	fsServer.registerResources()
 	fsServer.registerRoots()
+	
+	// Set up custom resource handling to use our dynamic discovery
+	fsServer.setupResourceHandling()
 
 	return fsServer
 }
@@ -151,6 +155,223 @@ func (fs *Server) registerRoots() {
 	}
 }
 
+// setupResourceHandling configures custom resource handling for dynamic file discovery
+func (fs *Server) setupResourceHandling() {
+	// Use reflection to access the private createSuccessResponse method
+	// Custom handler that calls our ListResources method
+	customListResourcesHandler := func(req mcp.JSONRPCRequest) mcp.JSONRPCResponse {
+		resources := fs.ListResources()
+		result := mcp.ListResourcesResult{Resources: resources}
+		
+		// Create JSON response manually since we can't access private method
+		id := req.ID
+		bytes, _ := json.Marshal(result)
+		rawMsg := json.RawMessage(bytes)
+		resultBytes := &rawMsg
+		
+		return mcp.JSONRPCResponse{
+			JSONRPC: "2.0",
+			ID:      id,
+			Result:  resultBytes,
+		}
+	}
+	
+	handlers := map[string]func(mcp.JSONRPCRequest) mcp.JSONRPCResponse{
+		"resources/list": customListResourcesHandler,
+	}
+	fs.SetCustomRequestHandler(handlers)
+}
+
+// ListResources returns all available file resources in allowed directories
+func (fs *Server) ListResources() []mcp.Resource {
+	var resources []mcp.Resource
+	
+	// Include the base directory resources from parent
+	parentResources := fs.Server.ListResources()
+	resources = append(resources, parentResources...)
+	
+	// Discover programming files in each allowed directory
+	for _, dir := range fs.allowedDirectories {
+		fileResources := fs.discoverProgrammingFiles(dir)
+		resources = append(resources, fileResources...)
+	}
+	
+	return resources
+}
+
+// discoverProgrammingFiles finds programming-related files in a directory
+func (fs *Server) discoverProgrammingFiles(dirPath string) []mcp.Resource {
+	var resources []mcp.Resource
+	
+	// Programming file extensions to include
+	programmingExts := map[string]string{
+		".go":     "text/x-go",
+		".js":     "text/javascript", 
+		".ts":     "text/typescript",
+		".py":     "text/x-python",
+		".rs":     "text/x-rust",
+		".java":   "text/x-java",
+		".c":      "text/x-c",
+		".cpp":    "text/x-c++",
+		".h":      "text/x-c",
+		".hpp":    "text/x-c++",
+		".cs":     "text/x-csharp",
+		".php":    "text/x-php",
+		".rb":     "text/x-ruby",
+		".swift":  "text/x-swift",
+		".kt":     "text/x-kotlin",
+		".scala":  "text/x-scala",
+		".sh":     "text/x-shellscript",
+		".bash":   "text/x-shellscript",
+		".zsh":    "text/x-shellscript",
+		".fish":   "text/x-shellscript",
+		".ps1":    "text/x-powershell",
+		".sql":    "text/x-sql",
+		".md":     "text/markdown",
+		".txt":    "text/plain",
+		".json":   "application/json",
+		".xml":    "application/xml",
+		".yaml":   "application/x-yaml",
+		".yml":    "application/x-yaml",
+		".toml":   "application/toml",
+		".ini":    "text/plain",
+		".cfg":    "text/plain",
+		".conf":   "text/plain",
+		".config": "application/json",
+		".html":   "text/html",
+		".htm":    "text/html",
+		".css":    "text/css",
+		".scss":   "text/x-scss",
+		".sass":   "text/x-sass",
+		".less":   "text/x-less",
+		".vue":    "text/x-vue",
+		".jsx":    "text/jsx",
+		".tsx":    "text/tsx",
+		".svelte": "text/x-svelte",
+		".dockerfile": "text/x-dockerfile",
+		".gitignore":  "text/plain",
+		".gitattributes": "text/plain",
+		".editorconfig": "text/plain",
+		".env":    "text/plain",
+	}
+	
+	// Special filenames to include (without extension)
+	specialFiles := map[string]string{
+		"Makefile":      "text/x-makefile",
+		"makefile":      "text/x-makefile", 
+		"Dockerfile":    "text/x-dockerfile",
+		"docker-compose.yml": "application/x-yaml",
+		"docker-compose.yaml": "application/x-yaml",
+		"README":        "text/plain",
+		"LICENSE":       "text/plain",
+		"CHANGELOG":     "text/plain",
+		"AUTHORS":       "text/plain",
+		"CONTRIBUTORS":  "text/plain",
+		"COPYING":       "text/plain",
+		"INSTALL":       "text/plain",
+		"NEWS":          "text/plain",
+		"TODO":          "text/plain",
+		"package.json":  "application/json",
+		"package-lock.json": "application/json",
+		"yarn.lock":     "text/plain",
+		"Cargo.toml":    "application/toml",
+		"Cargo.lock":    "application/toml",
+		"go.mod":        "text/x-go-mod",
+		"go.sum":        "text/plain",
+		"requirements.txt": "text/plain",
+		"setup.py":      "text/x-python",
+		"pyproject.toml": "application/toml",
+		"pom.xml":       "application/xml",
+		"build.gradle":  "text/x-gradle",
+		"CMakeLists.txt": "text/x-cmake",
+		".travis.yml":   "application/x-yaml",
+		".github":       "text/plain",
+		"tsconfig.json": "application/json",
+		"webpack.config.js": "text/javascript",
+		"rollup.config.js": "text/javascript",
+		"vite.config.js": "text/javascript",
+		"jest.config.js": "text/javascript",
+		"eslint.config.js": "text/javascript",
+		".eslintrc":     "application/json",
+		".prettierrc":   "application/json",
+	}
+	
+	// Walk through directory tree (limit to prevent performance issues)
+	const maxFiles = 500
+	fileCount := 0
+	
+	err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
+		if err != nil || fileCount >= maxFiles {
+			return filepath.SkipDir
+		}
+		
+		// Skip hidden files and directories
+		if strings.HasPrefix(info.Name(), ".") && info.Name() != ".gitignore" && 
+		   info.Name() != ".gitattributes" && info.Name() != ".editorconfig" && 
+		   info.Name() != ".env" && info.Name() != ".eslintrc" && info.Name() != ".prettierrc" {
+			if info.IsDir() {
+				return filepath.SkipDir
+			}
+			return nil
+		}
+		
+		// Skip common non-programming directories
+		if info.IsDir() {
+			dirName := strings.ToLower(info.Name())
+			skipDirs := []string{"node_modules", "vendor", "target", "build", "dist", 
+								 ".git", ".svn", ".hg", "__pycache__", ".pytest_cache",
+								 "coverage", ".coverage", ".nyc_output", "logs"}
+			for _, skip := range skipDirs {
+				if dirName == skip {
+					return filepath.SkipDir
+				}
+			}
+			return nil
+		}
+		
+		// Check if it's a programming file
+		fileName := info.Name()
+		ext := strings.ToLower(filepath.Ext(fileName))
+		
+		var mimeType string
+		var isProgFile bool
+		
+		// Check by extension
+		if mimeType, isProgFile = programmingExts[ext]; isProgFile {
+			// Found by extension
+		} else if mimeType, isProgFile = specialFiles[fileName]; isProgFile {
+			// Found by special filename
+		} else if mimeType, isProgFile = specialFiles[strings.ToLower(fileName)]; isProgFile {
+			// Found by special filename (case insensitive)
+		} else {
+			return nil // Not a programming file
+		}
+		
+		// Create resource for this file
+		fileURI := "file://" + path
+		relPath, _ := filepath.Rel(dirPath, path)
+		
+		resource := mcp.Resource{
+			URI:         fileURI,
+			Name:        relPath,
+			Description: fmt.Sprintf("Programming file: %s", relPath),
+			MimeType:    mimeType,
+		}
+		
+		resources = append(resources, resource)
+		fileCount++
+		
+		return nil
+	})
+	
+	if err != nil {
+		// Log error but don't fail completely
+		fmt.Printf("Warning: Error discovering files in %s: %v\n", dirPath, err)
+	}
+	
+	return resources
+}
+
 // HandleFileResource handles file:// resource requests
 func (fs *Server) HandleFileResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
 	// Extract file path from file:// URI