main
  1package filesystem
  2
  3import (
  4	"fmt"
  5	"mime"
  6	"os"
  7	"path/filepath"
  8	"strings"
  9
 10	"github.com/xlgmokha/mcp/pkg/mcp"
 11)
 12
 13type Tree struct {
 14	Directories []string
 15}
 16
 17func NewTree(allowedDirectories []string) *Tree {
 18	return &Tree{
 19		Directories: normalizeDirectories(allowedDirectories),
 20	}
 21}
 22
 23func (fs *Tree) discoverFiles(dirPath string) []mcp.Resource {
 24	var resources []mcp.Resource
 25
 26	programmingMimeTypes := map[string]string{
 27		// Go
 28		".go":   "text/x-go",
 29		".mod":  "text/x-go-mod",
 30		".sum":  "text/plain",
 31		
 32		// Rust
 33		".rs":   "text/x-rust",
 34		".rlib": "application/x-rust-library",
 35		
 36		// Python
 37		".py":   "text/x-python",
 38		".pyc":  "application/x-python-bytecode",
 39		".pyo":  "application/x-python-bytecode",
 40		".pyd":  "application/x-python-extension",
 41		".pyw":  "text/x-python",
 42		".pyz":  "application/x-python-archive",
 43		".whl":  "application/x-wheel+zip",
 44		
 45		// Java ecosystem
 46		".java":       "text/x-java",
 47		".class":      "application/java-vm",
 48		".jar":        "application/java-archive",
 49		".war":        "application/java-archive",
 50		".scala":      "text/x-scala",
 51		".kotlin":     "text/x-kotlin",
 52		".kt":         "text/x-kotlin",
 53		".kts":        "text/x-kotlin",
 54		".gradle":     "text/x-gradle",
 55		".groovy":     "text/x-groovy",
 56		".clj":        "text/x-clojure",
 57		".cljs":       "text/x-clojure",
 58		
 59		// C/C++ family
 60		".c":     "text/x-c",
 61		".cpp":   "text/x-c++",
 62		".cxx":   "text/x-c++",
 63		".cc":    "text/x-c++",
 64		".c++":   "text/x-c++",
 65		".h":     "text/x-c",
 66		".hpp":   "text/x-c++",
 67		".hxx":   "text/x-c++",
 68		".hh":    "text/x-c++",
 69		".h++":   "text/x-c++",
 70		".inc":   "text/x-c",
 71		".cs":    "text/x-csharp",
 72		".vb":    "text/x-vb",
 73		".fs":    "text/x-fsharp",
 74		
 75		// JavaScript/TypeScript ecosystem
 76		".js":    "text/javascript",
 77		".mjs":   "text/javascript",
 78		".cjs":   "text/javascript",
 79		".jsx":   "text/jsx",
 80		".ts":    "text/typescript",
 81		".tsx":   "text/tsx",
 82		".vue":   "text/x-vue",
 83		".svelte": "text/x-svelte",
 84		
 85		// Ruby
 86		".rb":      "text/x-ruby",
 87		".rbw":     "text/x-ruby",
 88		".rake":    "text/x-ruby",
 89		".gemspec": "text/x-ruby",
 90		".ru":      "text/x-ruby",
 91		
 92		// PHP
 93		".php":   "text/x-php",
 94		".php3":  "text/x-php",
 95		".php4":  "text/x-php",
 96		".php5":  "text/x-php",
 97		".phtml": "text/x-php",
 98		
 99		// Shell scripting
100		".sh":    "text/x-shellscript",
101		".bash":  "text/x-shellscript",
102		".zsh":   "text/x-shellscript",
103		".fish":  "text/x-shellscript",
104		".ksh":   "text/x-shellscript",
105		".csh":   "text/x-shellscript",
106		".tcsh":  "text/x-shellscript",
107		".bat":   "text/x-msdos-batch",
108		".cmd":   "text/x-msdos-batch",
109		".ps1":   "text/x-powershell",
110		".psm1":  "text/x-powershell",
111		
112		// Perl
113		".pl":   "text/x-perl",
114		".pm":   "text/x-perl",
115		".pod":  "text/x-perl",
116		".t":    "text/x-perl",
117		
118		// Swift
119		".swift": "text/x-swift",
120		
121		// Objective-C
122		".m":  "text/x-objc",
123		".mm": "text/x-objc++",
124		
125		// Functional languages
126		".hs":   "text/x-haskell",
127		".lhs":  "text/x-haskell",
128		".ml":   "text/x-ocaml",
129		".mli":  "text/x-ocaml",
130		".elm":  "text/x-elm",
131		".erl":  "text/x-erlang",
132		".hrl":  "text/x-erlang",
133		".ex":   "text/x-elixir",
134		".exs":  "text/x-elixir",
135		".f":    "text/x-fortran",
136		".f77":  "text/x-fortran",
137		".f90":  "text/x-fortran",
138		".f95":  "text/x-fortran",
139		".for":  "text/x-fortran",
140		
141		// Lisp family
142		".lisp": "text/x-lisp",
143		".lsp":  "text/x-lisp",
144		".cl":   "text/x-common-lisp",
145		".scm":  "text/x-scheme",
146		".ss":   "text/x-scheme",
147		
148		// R and statistics
149		".r":   "text/x-r",
150		".R":   "text/x-r",
151		".rmd": "text/x-r-markdown",
152		".Rmd": "text/x-r-markdown",
153		
154		// Assembly
155		".asm": "text/x-asm",
156		".s":   "text/x-asm",
157		".S":   "text/x-asm",
158		
159		// Other programming languages
160		".lua":    "text/x-lua",
161		".dart":   "text/x-dart",
162		".pas":    "text/x-pascal",
163		".pp":     "text/x-pascal",
164		".dpr":    "text/x-pascal",
165		".v":      "text/x-verilog",
166		".vhd":    "text/x-vhdl",
167		".vhdl":   "text/x-vhdl",
168		".ada":    "text/x-ada",
169		".adb":    "text/x-ada",
170		".ads":    "text/x-ada",
171		".cob":    "text/x-cobol",
172		".cbl":    "text/x-cobol",
173		".d":      "text/x-d",
174		".nim":    "text/x-nim",
175		".nims":   "text/x-nim",
176		".zig":    "text/x-zig",
177		".jl":     "text/x-julia",
178		".cr":     "text/x-crystal",
179		".tcl":    "text/x-tcl",
180		".tk":     "text/x-tcl",
181		
182		// Markup and data formats
183		".html":   "text/html",
184		".htm":    "text/html",
185		".xhtml":  "application/xhtml+xml",
186		".xml":    "text/xml",
187		".xsl":    "text/xsl",
188		".xslt":   "text/xsl",
189		".json":   "application/json",
190		".jsonl":  "application/jsonl",
191		".ndjson": "application/x-ndjson",
192		".yaml":   "text/yaml",
193		".yml":    "text/yaml",
194		".toml":   "application/toml",
195		".ini":    "text/plain",
196		".cfg":    "text/plain",
197		".conf":   "text/plain",
198		".config": "text/plain",
199		".properties": "text/plain",
200		".env":    "text/plain",
201		".csv":    "text/csv",
202		".tsv":    "text/tab-separated-values",
203		
204		// Stylesheets
205		".css":  "text/css",
206		".scss": "text/x-scss",
207		".sass": "text/x-sass",
208		".less": "text/x-less",
209		".styl": "text/x-stylus",
210		
211		// Documentation
212		".md":       "text/markdown",
213		".markdown": "text/markdown",
214		".mdown":    "text/markdown",
215		".mkd":      "text/markdown",
216		".rst":      "text/x-rst",
217		".asciidoc": "text/x-asciidoc",
218		".adoc":     "text/x-asciidoc",
219		".tex":      "text/x-tex",
220		".latex":    "text/x-tex",
221		".org":      "text/x-org",
222		
223		// Database
224		".sql":  "text/x-sql",
225		".ddl":  "text/x-sql",
226		".dml":  "text/x-sql",
227		".nosql": "text/x-nosql",
228		
229		// Plain text variations
230		".txt":  "text/plain",
231		".text": "text/plain",
232		".log":  "text/plain",
233		".out":  "text/plain",
234		".err":  "text/plain",
235		
236		// Protocol buffers and IDL
237		".proto": "text/x-protobuf",
238		".thrift": "text/x-thrift",
239		".avro":   "text/x-avro",
240		".idl":    "text/x-idl",
241		
242		// Build and deployment
243		".dockerfile": "text/x-dockerfile",
244		".containerfile": "text/x-dockerfile",
245		".makefile": "text/x-makefile",
246		".mk":       "text/x-makefile",
247		".cmake":    "text/x-cmake",
248		".bazel":    "text/x-bazel",
249		".bzl":      "text/x-bazel",
250		".ant":      "text/xml",
251		".maven":    "text/xml",
252		
253		// Specialized formats
254		".graphql": "application/graphql",
255		".gql":     "application/graphql",
256		".prisma":  "text/x-prisma",
257		".tf":      "text/x-terraform",
258		".tfvars":  "text/x-terraform",
259		".hcl":     "text/x-hcl",
260		".nomad":   "text/x-nomad",
261		".consul":  "text/x-consul",
262	}
263
264	specialFiles := map[string]string{
265		"Makefile":     "text/x-makefile",
266		"README":       "text/plain",
267		"LICENSE":      "text/plain",
268		"go.mod":       "text/x-go-mod",
269		"go.sum":       "text/plain",
270		"Cargo.toml":   "application/toml",
271		"Cargo.lock":   "application/toml",
272		"package.json": "application/json",
273		"package-lock.json": "application/json",
274		"yarn.lock":    "text/plain",
275		"Gemfile":      "text/x-ruby",
276		"Gemfile.lock": "text/plain",
277		"requirements.txt": "text/plain",
278		"Dockerfile":   "text/x-dockerfile",
279		"docker-compose.yml": "text/yaml",
280		"docker-compose.yaml": "text/yaml",
281		".gitignore":   "text/plain",
282		".gitattributes": "text/plain",
283	}
284
285	const maxFiles = 500
286	fileCount := 0
287
288	err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
289		if err != nil || fileCount >= maxFiles {
290			return filepath.SkipDir
291		}
292
293		if strings.HasPrefix(info.Name(), ".") {
294			if info.IsDir() {
295				return filepath.SkipDir
296			}
297			return nil
298		}
299
300		if info.IsDir() {
301			dirName := strings.ToLower(info.Name())
302			skipDirs := []string{"node_modules", "vendor", "target", "build", "dist", ".git"}
303			for _, skip := range skipDirs {
304				if dirName == skip {
305					return filepath.SkipDir
306				}
307			}
308			return nil
309		}
310
311		fileName := info.Name()
312		ext := strings.ToLower(filepath.Ext(fileName))
313
314		var mimeType string
315
316		if progType, exists := programmingMimeTypes[ext]; exists {
317			mimeType = progType
318		} else if specialType, exists := specialFiles[fileName]; exists {
319			mimeType = specialType
320		} else {
321			mimeType = mime.TypeByExtension(ext)
322			if mimeType == "" {
323				// Default to text/plain for common text-like extensions
324				textExtensions := []string{".conf", ".config", ".ini", ".log", ".env", ".properties", ".cfg", ".spec"}
325				for _, textExt := range textExtensions {
326					if ext == textExt {
327						mimeType = "text/plain"
328						break
329					}
330				}
331				// If still no MIME type and it looks like a text file, include it
332				if mimeType == "" && (ext == "" || len(ext) <= 4) {
333					// Skip files that are likely binary based on extension
334					binaryExtensions := []string{".exe", ".bin", ".so", ".dll", ".dylib", ".img", ".iso", ".zip", ".tar", ".gz", ".bz2", ".xz", ".7z", ".rar", ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".ico", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx"}
335					isBinary := false
336					for _, binExt := range binaryExtensions {
337						if ext == binExt {
338							isBinary = true
339							break
340						}
341					}
342					if !isBinary {
343						mimeType = "text/plain"
344					}
345				}
346				// If still no MIME type, skip the file
347				if mimeType == "" {
348					return nil
349				}
350			}
351		}
352
353		fileURI := "file://" + path
354		relPath, _ := filepath.Rel(dirPath, path)
355
356		resource := mcp.NewResource(
357			fileURI,
358			relPath,
359			mimeType,
360			func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
361				return mcp.ReadResourceResult{}, fmt.Errorf("individual file resources are handled by the pattern handler")
362			},
363		)
364
365		resources = append(resources, resource)
366		fileCount++
367
368		return nil
369	})
370
371	if err != nil {
372		fmt.Printf("Warning: Error discovering files in %s: %v\n", dirPath, err)
373	}
374
375	return resources
376}
377
378func isBinaryContent(content []byte) bool {
379	checkBytes := content
380	if len(content) > 512 {
381		checkBytes = content[:512]
382	}
383
384	for _, b := range checkBytes {
385		if b == 0 {
386			return true
387		}
388	}
389
390	return false
391}
392
393func (fs *Tree) validatePath(requestedPath string) (string, error) {
394	expandedPath := expandHome(requestedPath)
395	var absolute string
396
397	if filepath.IsAbs(expandedPath) {
398		absolute = filepath.Clean(expandedPath)
399	} else {
400		wd, _ := os.Getwd()
401		absolute = filepath.Clean(filepath.Join(wd, expandedPath))
402	}
403
404	allowed := false
405	for _, dir := range fs.Directories {
406		if strings.HasPrefix(absolute, dir) {
407			allowed = true
408			break
409		}
410	}
411
412	if !allowed {
413		return "", fmt.Errorf("access denied: %s", absolute)
414	}
415
416	realPath, err := filepath.EvalSymlinks(absolute)
417	if err != nil {
418		return absolute, nil
419	}
420
421	for _, dir := range fs.Directories {
422		if strings.HasPrefix(realPath, dir) {
423			return realPath, nil
424		}
425	}
426
427	return "", fmt.Errorf("access denied")
428}
429
430func expandHome(filePath string) string {
431	if strings.HasPrefix(filePath, "~/") || filePath == "~" {
432		homeDir, _ := os.UserHomeDir()
433		if filePath == "~" {
434			return homeDir
435		}
436		return filepath.Join(homeDir, filePath[2:])
437	}
438	return filePath
439}
440
441func normalizeDirectories(dirs []string) []string {
442	normalizedDirs := make([]string, len(dirs))
443	for i, dir := range dirs {
444		absPath, err := filepath.Abs(expandHome(dir))
445		if err != nil {
446			panic(fmt.Sprintf("Invalid directory: %s", dir))
447		}
448		normalizedDirs[i] = filepath.Clean(absPath)
449	}
450	return normalizedDirs
451}