#
tokens: 6408/50000 6/6 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .idea
│   ├── .gitignore
│   ├── filesystem-server.iml
│   ├── modules.xml
│   └── vcs.xml
├── Dockerfile
├── go.mod
├── go.sum
├── main.go
└── smithery.yaml
```

# Files

--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------

```
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 | 
```

--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------

```
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <project version="4">
3 |   <component name="VcsDirectoryMappings" defaultProject="true" />
4 | </project>
```

--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------

```
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <project version="4">
3 |   <component name="ProjectModuleManager">
4 |     <modules>
5 |       <module fileurl="file://$PROJECT_DIR$/.idea/filesystem-server.iml" filepath="$PROJECT_DIR$/.idea/filesystem-server.iml" />
6 |     </modules>
7 |   </component>
8 | </project>
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | FROM golang:1.23-alpine AS builder
 3 | 
 4 | WORKDIR /app
 5 | 
 6 | # Copy go.mod and go.sum first for caching dependencies
 7 | COPY go.mod go.sum ./
 8 | 
 9 | # Download dependencies
10 | RUN go mod download
11 | 
12 | # Copy the source code
13 | COPY . .
14 | 
15 | # Build the application
16 | RUN go build -ldflags="-s -w" -o server .
17 | 
18 | FROM alpine:latest
19 | 
20 | WORKDIR /app
21 | 
22 | # Copy the built binary from the builder stage
23 | COPY --from=builder /app/server ./
24 | 
25 | # The container will by default pass '/app' as the allowed directory if no other command line arguments are provided
26 | ENTRYPOINT ["./server"]
27 | CMD ["/app"]
28 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - allowedDirectory
10 |     properties:
11 |       allowedDirectory:
12 |         type: string
13 |         description: The absolute path to an allowed directory for the filesystem
14 |           server. For example, in the Docker container '/app' is a good default.
15 |       additionalDirectories:
16 |         type: array
17 |         items:
18 |           type: string
19 |         description: Optional additional allowed directories.
20 |   commandFunction:
21 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
22 |     |-
23 |     (config) => { const args = [config.allowedDirectory]; if (config.additionalDirectories && Array.isArray(config.additionalDirectories)) { args.push(...config.additionalDirectories); } return { command: './server', args: args }; }
24 |   exampleConfig:
25 |     allowedDirectory: /app
26 |     additionalDirectories: []
27 | 
```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"log"
  6 | 	"os"
  7 | 	"path/filepath"
  8 | 	"strings"
  9 | 	"time"
 10 | 
 11 | 	"github.com/mark3labs/mcp-go/mcp"
 12 | 	"github.com/mark3labs/mcp-go/server"
 13 | )
 14 | 
 15 | type FileInfo struct {
 16 | 	Size        int64     `json:"size"`
 17 | 	Created     time.Time `json:"created"`
 18 | 	Modified    time.Time `json:"modified"`
 19 | 	Accessed    time.Time `json:"accessed"`
 20 | 	IsDirectory bool      `json:"isDirectory"`
 21 | 	IsFile      bool      `json:"isFile"`
 22 | 	Permissions string    `json:"permissions"`
 23 | }
 24 | 
 25 | type FilesystemServer struct {
 26 | 	allowedDirs []string
 27 | 	server      *server.MCPServer
 28 | }
 29 | 
 30 | func NewFilesystemServer(allowedDirs []string) (*FilesystemServer, error) {
 31 | 	// Normalize and validate directories
 32 | 	normalized := make([]string, 0, len(allowedDirs))
 33 | 	for _, dir := range allowedDirs {
 34 | 		abs, err := filepath.Abs(dir)
 35 | 		if err != nil {
 36 | 			return nil, fmt.Errorf("failed to resolve path %s: %w", dir, err)
 37 | 		}
 38 | 
 39 | 		info, err := os.Stat(abs)
 40 | 		if err != nil {
 41 | 			return nil, fmt.Errorf(
 42 | 				"failed to access directory %s: %w",
 43 | 				abs,
 44 | 				err,
 45 | 			)
 46 | 		}
 47 | 		if !info.IsDir() {
 48 | 			return nil, fmt.Errorf("path is not a directory: %s", abs)
 49 | 		}
 50 | 
 51 | 		normalized = append(normalized, filepath.Clean(strings.ToLower(abs)))
 52 | 	}
 53 | 
 54 | 	s := &FilesystemServer{
 55 | 		allowedDirs: normalized,
 56 | 		server: server.NewMCPServer(
 57 | 			"secure-filesystem-server",
 58 | 			"0.2.0",
 59 | 			server.WithToolCapabilities(true),
 60 | 		),
 61 | 	}
 62 | 
 63 | 	// Register tool handlers
 64 | 	s.server.AddTool(mcp.Tool{
 65 | 		Name:        "read_file",
 66 | 		Description: "Read the complete contents of a file from the file system.",
 67 | 		InputSchema: mcp.ToolInputSchema{
 68 | 			Type: "object",
 69 | 			Properties: map[string]interface{}{
 70 | 				"path": map[string]interface{}{
 71 | 					"type":        "string",
 72 | 					"description": "Path to the file to read",
 73 | 				},
 74 | 			},
 75 | 		},
 76 | 	}, s.handleReadFile)
 77 | 
 78 | 	s.server.AddTool(mcp.Tool{
 79 | 		Name:        "write_file",
 80 | 		Description: "Create a new file or overwrite an existing file with new content.",
 81 | 		InputSchema: mcp.ToolInputSchema{
 82 | 			Type: "object",
 83 | 			Properties: map[string]interface{}{
 84 | 				"path": map[string]interface{}{
 85 | 					"type":        "string",
 86 | 					"description": "Path where to write the file",
 87 | 				},
 88 | 				"content": map[string]interface{}{
 89 | 					"type":        "string",
 90 | 					"description": "Content to write to the file",
 91 | 				},
 92 | 			},
 93 | 		},
 94 | 	}, s.handleWriteFile)
 95 | 
 96 | 	s.server.AddTool(mcp.Tool{
 97 | 		Name:        "list_directory",
 98 | 		Description: "Get a detailed listing of all files and directories in a specified path.",
 99 | 		InputSchema: mcp.ToolInputSchema{
100 | 			Type: "object",
101 | 			Properties: map[string]interface{}{
102 | 				"path": map[string]interface{}{
103 | 					"type":        "string",
104 | 					"description": "Path of the directory to list",
105 | 				},
106 | 			},
107 | 		},
108 | 	}, s.handleListDirectory)
109 | 
110 | 	s.server.AddTool(mcp.Tool{
111 | 		Name:        "create_directory",
112 | 		Description: "Create a new directory or ensure a directory exists.",
113 | 		InputSchema: mcp.ToolInputSchema{
114 | 			Type: "object",
115 | 			Properties: map[string]interface{}{
116 | 				"path": map[string]interface{}{
117 | 					"type":        "string",
118 | 					"description": "Path of the directory to create",
119 | 				},
120 | 			},
121 | 		},
122 | 	}, s.handleCreateDirectory)
123 | 
124 | 	s.server.AddTool(mcp.Tool{
125 | 		Name:        "move_file",
126 | 		Description: "Move or rename files and directories.",
127 | 		InputSchema: mcp.ToolInputSchema{
128 | 			Type: "object",
129 | 			Properties: map[string]interface{}{
130 | 				"source": map[string]interface{}{
131 | 					"type":        "string",
132 | 					"description": "Source path of the file or directory",
133 | 				},
134 | 				"destination": map[string]interface{}{
135 | 					"type":        "string",
136 | 					"description": "Destination path",
137 | 				},
138 | 			},
139 | 		},
140 | 	}, s.handleMoveFile)
141 | 
142 | 	s.server.AddTool(mcp.Tool{
143 | 		Name:        "search_files",
144 | 		Description: "Recursively search for files and directories matching a pattern.",
145 | 		InputSchema: mcp.ToolInputSchema{
146 | 			Type: "object",
147 | 			Properties: map[string]interface{}{
148 | 				"path": map[string]interface{}{
149 | 					"type":        "string",
150 | 					"description": "Starting path for the search",
151 | 				},
152 | 				"pattern": map[string]interface{}{
153 | 					"type":        "string",
154 | 					"description": "Search pattern to match against file names",
155 | 				},
156 | 			},
157 | 		},
158 | 	}, s.handleSearchFiles)
159 | 
160 | 	s.server.AddTool(mcp.Tool{
161 | 		Name:        "get_file_info",
162 | 		Description: "Retrieve detailed metadata about a file or directory.",
163 | 		InputSchema: mcp.ToolInputSchema{
164 | 			Type: "object",
165 | 			Properties: map[string]interface{}{
166 | 				"path": map[string]interface{}{
167 | 					"type":        "string",
168 | 					"description": "Path to the file or directory",
169 | 				},
170 | 			},
171 | 		},
172 | 	}, s.handleGetFileInfo)
173 | 
174 | 	s.server.AddTool(mcp.Tool{
175 | 		Name:        "list_allowed_directories",
176 | 		Description: "Returns the list of directories that this server is allowed to access.",
177 | 		InputSchema: mcp.ToolInputSchema{
178 | 			Type:       "object",
179 | 			Properties: map[string]interface{}{},
180 | 		},
181 | 	}, s.handleListAllowedDirectories)
182 | 
183 | 	return s, nil
184 | }
185 | 
186 | func (s *FilesystemServer) validatePath(requestedPath string) (string, error) {
187 | 	abs, err := filepath.Abs(requestedPath)
188 | 	if err != nil {
189 | 		return "", fmt.Errorf("invalid path: %w", err)
190 | 	}
191 | 
192 | 	normalized := filepath.Clean(strings.ToLower(abs))
193 | 
194 | 	// Check if path is within allowed directories
195 | 	allowed := false
196 | 	for _, dir := range s.allowedDirs {
197 | 		if strings.HasPrefix(normalized, dir) {
198 | 			allowed = true
199 | 			break
200 | 		}
201 | 	}
202 | 	if !allowed {
203 | 		return "", fmt.Errorf(
204 | 			"access denied - path outside allowed directories: %s",
205 | 			abs,
206 | 		)
207 | 	}
208 | 
209 | 	// Handle symlinks
210 | 	realPath, err := filepath.EvalSymlinks(abs)
211 | 	if err != nil {
212 | 		if !os.IsNotExist(err) {
213 | 			return "", err
214 | 		}
215 | 		// For new files, check parent directory
216 | 		parent := filepath.Dir(abs)
217 | 		realParent, err := filepath.EvalSymlinks(parent)
218 | 		if err != nil {
219 | 			return "", fmt.Errorf("parent directory does not exist: %s", parent)
220 | 		}
221 | 		normalizedParent := filepath.Clean(strings.ToLower(realParent))
222 | 		for _, dir := range s.allowedDirs {
223 | 			if strings.HasPrefix(normalizedParent, dir) {
224 | 				return abs, nil
225 | 			}
226 | 		}
227 | 		return "", fmt.Errorf(
228 | 			"access denied - parent directory outside allowed directories",
229 | 		)
230 | 	}
231 | 
232 | 	normalizedReal := filepath.Clean(strings.ToLower(realPath))
233 | 	for _, dir := range s.allowedDirs {
234 | 		if strings.HasPrefix(normalizedReal, dir) {
235 | 			return realPath, nil
236 | 		}
237 | 	}
238 | 	return "", fmt.Errorf(
239 | 		"access denied - symlink target outside allowed directories",
240 | 	)
241 | }
242 | 
243 | func (s *FilesystemServer) getFileStats(path string) (FileInfo, error) {
244 | 	info, err := os.Stat(path)
245 | 	if err != nil {
246 | 		return FileInfo{}, err
247 | 	}
248 | 
249 | 	return FileInfo{
250 | 		Size:        info.Size(),
251 | 		Created:     info.ModTime(), // Note: ModTime used as birth time isn't always available
252 | 		Modified:    info.ModTime(),
253 | 		Accessed:    info.ModTime(), // Note: Access time isn't always available
254 | 		IsDirectory: info.IsDir(),
255 | 		IsFile:      !info.IsDir(),
256 | 		Permissions: fmt.Sprintf("%o", info.Mode().Perm()),
257 | 	}, nil
258 | }
259 | 
260 | func (s *FilesystemServer) searchFiles(
261 | 	rootPath, pattern string,
262 | ) ([]string, error) {
263 | 	var results []string
264 | 	pattern = strings.ToLower(pattern)
265 | 
266 | 	err := filepath.Walk(
267 | 		rootPath,
268 | 		func(path string, info os.FileInfo, err error) error {
269 | 			if err != nil {
270 | 				return nil // Skip errors and continue
271 | 			}
272 | 
273 | 			// Try to validate path
274 | 			if _, err := s.validatePath(path); err != nil {
275 | 				return nil // Skip invalid paths
276 | 			}
277 | 
278 | 			if strings.Contains(strings.ToLower(info.Name()), pattern) {
279 | 				results = append(results, path)
280 | 			}
281 | 			return nil
282 | 		},
283 | 	)
284 | 	if err != nil {
285 | 		return nil, err
286 | 	}
287 | 	return results, nil
288 | }
289 | 
290 | // Tool handlers
291 | 
292 | func (s *FilesystemServer) handleReadFile(
293 | 	arguments map[string]interface{},
294 | ) (*mcp.CallToolResult, error) {
295 | 	path, ok := arguments["path"].(string)
296 | 	if !ok {
297 | 		return nil, fmt.Errorf("path must be a string")
298 | 	}
299 | 
300 | 	validPath, err := s.validatePath(path)
301 | 	if err != nil {
302 | 		return nil, err
303 | 	}
304 | 
305 | 	content, err := os.ReadFile(validPath)
306 | 	if err != nil {
307 | 		return &mcp.CallToolResult{
308 | 			Content: []interface{}{
309 | 				mcp.TextContent{
310 | 					Type: "text",
311 | 					Text: fmt.Sprintf("Error reading file: %v", err),
312 | 				},
313 | 			},
314 | 			IsError: true,
315 | 		}, nil
316 | 	}
317 | 
318 | 	return &mcp.CallToolResult{
319 | 		Content: []interface{}{
320 | 			mcp.TextContent{
321 | 				Type: "text",
322 | 				Text: string(content),
323 | 			},
324 | 		},
325 | 	}, nil
326 | }
327 | 
328 | func (s *FilesystemServer) handleWriteFile(
329 | 	arguments map[string]interface{},
330 | ) (*mcp.CallToolResult, error) {
331 | 	path, ok := arguments["path"].(string)
332 | 	if !ok {
333 | 		return nil, fmt.Errorf("path must be a string")
334 | 	}
335 | 	content, ok := arguments["content"].(string)
336 | 	if !ok {
337 | 		return nil, fmt.Errorf("content must be a string")
338 | 	}
339 | 
340 | 	validPath, err := s.validatePath(path)
341 | 	if err != nil {
342 | 		return nil, err
343 | 	}
344 | 
345 | 	if err := os.WriteFile(validPath, []byte(content), 0644); err != nil {
346 | 		return &mcp.CallToolResult{
347 | 			Content: []interface{}{
348 | 				mcp.TextContent{
349 | 					Type: "text",
350 | 					Text: fmt.Sprintf("Error writing file: %v", err),
351 | 				},
352 | 			},
353 | 			IsError: true,
354 | 		}, nil
355 | 	}
356 | 
357 | 	return &mcp.CallToolResult{
358 | 		Content: []interface{}{
359 | 			mcp.TextContent{
360 | 				Type: "text",
361 | 				Text: fmt.Sprintf("Successfully wrote to %s", path),
362 | 			},
363 | 		},
364 | 	}, nil
365 | }
366 | 
367 | func (s *FilesystemServer) handleListDirectory(
368 | 	arguments map[string]interface{},
369 | ) (*mcp.CallToolResult, error) {
370 | 	path, ok := arguments["path"].(string)
371 | 	if !ok {
372 | 		return nil, fmt.Errorf("path must be a string")
373 | 	}
374 | 
375 | 	validPath, err := s.validatePath(path)
376 | 	if err != nil {
377 | 		return nil, err
378 | 	}
379 | 
380 | 	entries, err := os.ReadDir(validPath)
381 | 	if err != nil {
382 | 		return &mcp.CallToolResult{
383 | 			Content: []interface{}{
384 | 				mcp.TextContent{
385 | 					Type: "text",
386 | 					Text: fmt.Sprintf("Error reading directory: %v", err),
387 | 				},
388 | 			},
389 | 			IsError: true,
390 | 		}, nil
391 | 	}
392 | 
393 | 	var result strings.Builder
394 | 	for _, entry := range entries {
395 | 		prefix := "[FILE]"
396 | 		if entry.IsDir() {
397 | 			prefix = "[DIR]"
398 | 		}
399 | 		fmt.Fprintf(&result, "%s %s\n", prefix, entry.Name())
400 | 	}
401 | 
402 | 	return &mcp.CallToolResult{
403 | 		Content: []interface{}{
404 | 			mcp.TextContent{
405 | 				Type: "text",
406 | 				Text: result.String(),
407 | 			},
408 | 		},
409 | 	}, nil
410 | }
411 | 
412 | func (s *FilesystemServer) handleCreateDirectory(
413 | 	arguments map[string]interface{},
414 | ) (*mcp.CallToolResult, error) {
415 | 	path, ok := arguments["path"].(string)
416 | 	if !ok {
417 | 		return nil, fmt.Errorf("path must be a string")
418 | 	}
419 | 
420 | 	validPath, err := s.validatePath(path)
421 | 	if err != nil {
422 | 		return nil, err
423 | 	}
424 | 
425 | 	if err := os.MkdirAll(validPath, 0755); err != nil {
426 | 		return &mcp.CallToolResult{
427 | 			Content: []interface{}{
428 | 				mcp.TextContent{
429 | 					Type: "text",
430 | 					Text: fmt.Sprintf("Error creating directory: %v", err),
431 | 				},
432 | 			},
433 | 			IsError: true,
434 | 		}, nil
435 | 	}
436 | 
437 | 	return &mcp.CallToolResult{
438 | 		Content: []interface{}{
439 | 			mcp.TextContent{
440 | 				Type: "text",
441 | 				Text: fmt.Sprintf("Successfully created directory %s", path),
442 | 			},
443 | 		},
444 | 	}, nil
445 | }
446 | 
447 | func (s *FilesystemServer) handleMoveFile(
448 | 	arguments map[string]interface{},
449 | ) (*mcp.CallToolResult, error) {
450 | 	source, ok := arguments["source"].(string)
451 | 	if !ok {
452 | 		return nil, fmt.Errorf("source must be a string")
453 | 	}
454 | 	destination, ok := arguments["destination"].(string)
455 | 	if !ok {
456 | 		return nil, fmt.Errorf("destination must be a string")
457 | 	}
458 | 
459 | 	validSource, err := s.validatePath(source)
460 | 	if err != nil {
461 | 		return nil, err
462 | 	}
463 | 	validDest, err := s.validatePath(destination)
464 | 	if err != nil {
465 | 		return nil, err
466 | 	}
467 | 
468 | 	if err := os.Rename(validSource, validDest); err != nil {
469 | 		return &mcp.CallToolResult{
470 | 			Content: []interface{}{
471 | 				mcp.TextContent{
472 | 					Type: "text",
473 | 					Text: fmt.Sprintf("Error moving file: %v", err),
474 | 				},
475 | 			},
476 | 			IsError: true,
477 | 		}, nil
478 | 	}
479 | 
480 | 	return &mcp.CallToolResult{
481 | 		Content: []interface{}{
482 | 			mcp.TextContent{
483 | 				Type: "text",
484 | 				Text: fmt.Sprintf(
485 | 					"Successfully moved %s to %s",
486 | 					source,
487 | 					destination,
488 | 				),
489 | 			},
490 | 		},
491 | 	}, nil
492 | }
493 | 
494 | func (s *FilesystemServer) handleSearchFiles(
495 | 	arguments map[string]interface{},
496 | ) (*mcp.CallToolResult, error) {
497 | 	path, ok := arguments["path"].(string)
498 | 	if !ok {
499 | 		return nil, fmt.Errorf("path must be a string")
500 | 	}
501 | 	pattern, ok := arguments["pattern"].(string)
502 | 	if !ok {
503 | 		return nil, fmt.Errorf("pattern must be a string")
504 | 	}
505 | 
506 | 	validPath, err := s.validatePath(path)
507 | 	if err != nil {
508 | 		return nil, err
509 | 	}
510 | 
511 | 	results, err := s.searchFiles(validPath, pattern)
512 | 	if err != nil {
513 | 		return &mcp.CallToolResult{
514 | 			Content: []interface{}{
515 | 				mcp.TextContent{
516 | 					Type: "text",
517 | 					Text: fmt.Sprintf("Error searching files: %v",
518 | 						err),
519 | 				},
520 | 			},
521 | 			IsError: true,
522 | 		}, nil
523 | 	}
524 | 
525 | 	return &mcp.CallToolResult{
526 | 		Content: []interface{}{
527 | 			mcp.TextContent{
528 | 				Type: "text",
529 | 				Text: strings.Join(results, "\n"),
530 | 			},
531 | 		},
532 | 	}, nil
533 | }
534 | 
535 | func (s *FilesystemServer) handleGetFileInfo(
536 | 	arguments map[string]interface{},
537 | ) (*mcp.CallToolResult, error) {
538 | 	path, ok := arguments["path"].(string)
539 | 	if !ok {
540 | 		return nil, fmt.Errorf("path must be a string")
541 | 	}
542 | 
543 | 	validPath, err := s.validatePath(path)
544 | 	if err != nil {
545 | 		return nil, err
546 | 	}
547 | 
548 | 	info, err := s.getFileStats(validPath)
549 | 	if err != nil {
550 | 		return &mcp.CallToolResult{
551 | 			Content: []interface{}{
552 | 				mcp.TextContent{
553 | 					Type: "text",
554 | 					Text: fmt.Sprintf("Error getting file info: %v", err),
555 | 				},
556 | 			},
557 | 			IsError: true,
558 | 		}, nil
559 | 	}
560 | 
561 | 	return &mcp.CallToolResult{
562 | 		Content: []interface{}{
563 | 			mcp.TextContent{
564 | 				Type: "text",
565 | 				Text: fmt.Sprintf(
566 | 					"Size: %d\nCreated: %s\nModified: %s\nAccessed: %s\nIsDirectory: %v\nIsFile: %v\nPermissions: %s",
567 | 					info.Size,
568 | 					info.Created.Format(time.RFC3339),
569 | 					info.Modified.Format(time.RFC3339),
570 | 					info.Accessed.Format(time.RFC3339),
571 | 					info.IsDirectory,
572 | 					info.IsFile,
573 | 					info.Permissions,
574 | 				),
575 | 			},
576 | 		},
577 | 	}, nil
578 | }
579 | 
580 | func (s *FilesystemServer) handleListAllowedDirectories(
581 | 	arguments map[string]interface{},
582 | ) (*mcp.CallToolResult, error) {
583 | 	return &mcp.CallToolResult{
584 | 		Content: []interface{}{
585 | 			mcp.TextContent{
586 | 				Type: "text",
587 | 				Text: fmt.Sprintf(
588 | 					"Allowed directories:\n%s",
589 | 					strings.Join(s.allowedDirs, "\n"),
590 | 				),
591 | 			},
592 | 		},
593 | 	}, nil
594 | }
595 | 
596 | func (s *FilesystemServer) Serve() error {
597 | 	return server.ServeStdio(s.server)
598 | }
599 | 
600 | func main() {
601 | 	// Parse command line arguments
602 | 	if len(os.Args) < 2 {
603 | 		fmt.Fprintf(
604 | 			os.Stderr,
605 | 			"Usage: %s <allowed-directory> [additional-directories...]\n",
606 | 			os.Args[0],
607 | 		)
608 | 		os.Exit(1)
609 | 	}
610 | 
611 | 	// Create and start the server
612 | 	fs, err := NewFilesystemServer(os.Args[1:])
613 | 	if err != nil {
614 | 		log.Fatalf("Failed to create server: %v", err)
615 | 	}
616 | 
617 | 	// Serve requests
618 | 	if err := fs.Serve(); err != nil {
619 | 		log.Fatalf("Server error: %v", err)
620 | 	}
621 | }
622 | 
```