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}