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

```
├── .github
│   ├── dependabot.yml
│   └── workflows
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .goreleaser.yml
├── Dockerfile
├── filesystemserver
│   ├── handler
│   │   ├── copy_file_test.go
│   │   ├── copy_file.go
│   │   ├── create_directory_test.go
│   │   ├── create_directory.go
│   │   ├── delete_file_test.go
│   │   ├── delete_file.go
│   │   ├── get_file_info_test.go
│   │   ├── get_file_info.go
│   │   ├── handler_test.go
│   │   ├── handler.go
│   │   ├── helper.go
│   │   ├── list_allowed_directories_test.go
│   │   ├── list_allowed_directories.go
│   │   ├── list_directory_test.go
│   │   ├── list_directory.go
│   │   ├── modify_file.go
│   │   ├── move_file.go
│   │   ├── read_file_test.go
│   │   ├── read_file.go
│   │   ├── read_multiple_files_test.go
│   │   ├── read_multiple_files.go
│   │   ├── resources.go
│   │   ├── search_files_test.go
│   │   ├── search_files.go
│   │   ├── search_within_files.go
│   │   ├── tree_test.go
│   │   ├── tree.go
│   │   ├── types.go
│   │   └── write_file.go
│   ├── inprocess_test.go
│   ├── server_test.go
│   ├── server.go
│   └── utils_test.go
├── go.mod
├── go.sum
├── LICENSE
├── main.go
├── README.md
└── smithery.yaml
```

# Files

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

```
 1 | .aider*
 2 | .opencode*
 3 | OpenCode.md
 4 | dist/
 5 | CLAUDE.md
 6 | .claude/
 7 | 
 8 | # go build artifact
 9 | mcp-filesystem-server
10 | 
```

--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------

```yaml
 1 | version: 2
 2 | 
 3 | before:
 4 |   hooks:
 5 |     - go mod tidy
 6 | 
 7 | builds:
 8 |   - id: mcp-filesystem-server
 9 |     env:
10 |       - CGO_ENABLED=0
11 |     goos:
12 |       - linux
13 |       - windows
14 |       - darwin
15 |     goarch:
16 |       - amd64
17 |       - arm64
18 |     ldflags:
19 |       - -s -w -X github.com/mark3labs/mcp-filesystem-server/filesystemserver.Version={{.Version}}
20 |     binary: mcp-filesystem-server
21 |     main: .
22 | 
23 | archives:
24 |   - id: default
25 |     format_overrides:
26 |       - goos: windows
27 |         formats:
28 |           - zip
29 |     name_template: >-
30 |       {{ .ProjectName }}_
31 |       {{- .Os }}_
32 |       {{- .Arch }}
33 |     files:
34 |       - README.md
35 |       - LICENSE*
36 | 
37 | checksum:
38 |   name_template: 'checksums.txt'
39 |   algorithm: sha256
40 | 
41 | # Using new snapshot configuration
42 | snapshot:
43 |   version_template: "{{ .Version }}-SNAPSHOT-{{ .ShortCommit }}"
44 | 
45 | changelog:
46 |   sort: asc
47 |   filters:
48 |     exclude:
49 |       - '^docs:'
50 |       - '^test:'
51 |       - Merge pull request
52 |       - Merge branch
53 | 
54 | release:
55 |   github:
56 |     owner: mark3labs
57 |     name: mcp-filesystem-server
58 |   draft: false
59 |   prerelease: auto
60 |   name_template: "{{ .Tag }}"
61 |   mode: replace
62 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP Filesystem Server
  2 | 
  3 | This MCP server provides secure access to the local filesystem via the Model Context Protocol (MCP).
  4 | 
  5 | ## Components
  6 | 
  7 | ### Resources
  8 | 
  9 | - **file://**
 10 |   - Name: File System
 11 |   - Description: Access to files and directories on the local file system
 12 | 
 13 | ### Tools
 14 | 
 15 | #### File Operations
 16 | 
 17 | - **read_file**
 18 |   - Read the complete contents of a file from the file system
 19 |   - Parameters: `path` (required): Path to the file to read
 20 | 
 21 | - **read_multiple_files**
 22 |   - Read the contents of multiple files in a single operation
 23 |   - Parameters: `paths` (required): List of file paths to read
 24 | 
 25 | - **write_file**
 26 |   - Create a new file or overwrite an existing file with new content
 27 |   - Parameters: `path` (required): Path where to write the file, `content` (required): Content to write to the file
 28 | 
 29 | - **copy_file**
 30 |   - Copy files and directories
 31 |   - Parameters: `source` (required): Source path of the file or directory, `destination` (required): Destination path
 32 | 
 33 | - **move_file**
 34 |   - Move or rename files and directories
 35 |   - Parameters: `source` (required): Source path of the file or directory, `destination` (required): Destination path
 36 | 
 37 | - **delete_file**
 38 |   - Delete a file or directory from the file system
 39 |   - Parameters: `path` (required): Path to the file or directory to delete, `recursive` (optional): Whether to recursively delete directories (default: false)
 40 | 
 41 | - **modify_file**
 42 |   - Update file by finding and replacing text using string matching or regex
 43 |   - Parameters: `path` (required): Path to the file to modify, `find` (required): Text to search for, `replace` (required): Text to replace with, `all_occurrences` (optional): Replace all occurrences (default: true), `regex` (optional): Treat find pattern as regex (default: false)
 44 | 
 45 | #### Directory Operations
 46 | 
 47 | - **list_directory**
 48 |   - Get a detailed listing of all files and directories in a specified path
 49 |   - Parameters: `path` (required): Path of the directory to list
 50 | 
 51 | - **create_directory**
 52 |   - Create a new directory or ensure a directory exists
 53 |   - Parameters: `path` (required): Path of the directory to create
 54 | 
 55 | - **tree**
 56 |   - Returns a hierarchical JSON representation of a directory structure
 57 |   - Parameters: `path` (required): Path of the directory to traverse, `depth` (optional): Maximum depth to traverse (default: 3), `follow_symlinks` (optional): Whether to follow symbolic links (default: false)
 58 | 
 59 | #### Search and Information
 60 | 
 61 | - **search_files**
 62 |   - Recursively search for files and directories matching a pattern
 63 |   - Parameters: `path` (required): Starting path for the search, `pattern` (required): Search pattern to match against file names
 64 | 
 65 | - **search_within_files**
 66 |   - Search for text within file contents across directory trees
 67 |   - Parameters: `path` (required): Starting directory for the search, `substring` (required): Text to search for within file contents, `depth` (optional): Maximum directory depth to search, `max_results` (optional): Maximum number of results to return (default: 1000)
 68 | 
 69 | - **get_file_info**
 70 |   - Retrieve detailed metadata about a file or directory
 71 |   - Parameters: `path` (required): Path to the file or directory
 72 | 
 73 | - **list_allowed_directories**
 74 |   - Returns the list of directories that this server is allowed to access
 75 |   - Parameters: None
 76 | 
 77 | ## Features
 78 | 
 79 | - Secure access to specified directories
 80 | - Path validation to prevent directory traversal attacks
 81 | - Symlink resolution with security checks
 82 | - MIME type detection
 83 | - Support for text, binary, and image files
 84 | - Size limits for inline content and base64 encoding
 85 | 
 86 | ## Getting Started
 87 | 
 88 | ### Installation
 89 | 
 90 | #### Using Go Install
 91 | 
 92 | ```bash
 93 | go install github.com/mark3labs/mcp-filesystem-server@latest
 94 | ```
 95 | 
 96 | ### Usage
 97 | 
 98 | #### As a standalone server
 99 | 
100 | Start the MCP server with allowed directories:
101 | 
102 | ```bash
103 | mcp-filesystem-server /path/to/allowed/directory [/another/allowed/directory ...]
104 | ```
105 | 
106 | #### As a library in your Go project
107 | 
108 | ```go
109 | package main
110 | 
111 | import (
112 | 	"log"
113 | 	"os"
114 | 
115 | 	"github.com/mark3labs/mcp-filesystem-server/filesystemserver"
116 | )
117 | 
118 | func main() {
119 | 	// Create a new filesystem server with allowed directories
120 | 	allowedDirs := []string{"/path/to/allowed/directory", "/another/allowed/directory"}
121 | 	fs, err := filesystemserver.NewFilesystemServer(allowedDirs)
122 | 	if err != nil {
123 | 		log.Fatalf("Failed to create server: %v", err)
124 | 	}
125 | 
126 | 	// Serve requests
127 | 	if err := fs.Serve(); err != nil {
128 | 		log.Fatalf("Server error: %v", err)
129 | 	}
130 | }
131 | ```
132 | 
133 | ### Usage with Model Context Protocol
134 | 
135 | To integrate this server with apps that support MCP:
136 | 
137 | ```json
138 | {
139 |   "mcpServers": {
140 |     "filesystem": {
141 |       "command": "mcp-filesystem-server",
142 |       "args": ["/path/to/allowed/directory", "/another/allowed/directory"]
143 |     }
144 |   }
145 | }
146 | ```
147 | 
148 | ### Docker
149 | 
150 | #### Running with Docker
151 | 
152 | You can run the Filesystem MCP server using Docker:
153 | 
154 | ```bash
155 | docker run -i --rm ghcr.io/mark3labs/mcp-filesystem-server:latest /path/to/allowed/directory
156 | ```
157 | 
158 | #### Docker Configuration with MCP
159 | 
160 | To integrate the Docker image with apps that support MCP:
161 | 
162 | ```json
163 | {
164 |   "mcpServers": {
165 |     "filesystem": {
166 |       "command": "docker",
167 |       "args": [
168 |         "run",
169 |         "-i",
170 |         "--rm",
171 |         "ghcr.io/mark3labs/mcp-filesystem-server:latest",
172 |         "/path/to/allowed/directory"
173 |       ]
174 |     }
175 |   }
176 | }
177 | ```
178 | 
179 | If you need changes made inside the container to reflect on the host filesystem, you can mount a volume. This allows the container to access and modify files on the host system. Here's an example:
180 | 
181 | ```json
182 | {
183 |   "mcpServers": {
184 |     "filesystem": {
185 |       "command": "docker",
186 |       "args": [
187 |         "run",
188 |         "-i",
189 |         "--rm",
190 |         "--volume=/allowed/directory/in/host:/allowed/directory/in/container",
191 |         "ghcr.io/mark3labs/mcp-filesystem-server:latest",
192 |         "/allowed/directory/in/container"
193 |       ]
194 |     }
195 |   }
196 | }
197 | ```
198 | 
199 | ## License
200 | 
201 | See the [LICENSE](LICENSE) file for details.
202 | 
```

--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------

```yaml
 1 | version: 2
 2 | updates:
 3 |   - package-ecosystem: "gomod"
 4 |     directory: "/"
 5 |     schedule:
 6 |       interval: "daily"
 7 |     groups:
 8 |       minor-dependencies:
 9 |         update-types:
10 |           - "minor"
11 |           - "patch"
12 |   - package-ecosystem: "github-actions"
13 |     directory: "/"
14 |     schedule:
15 |       interval: "daily"
16 | 
```

--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Test 
 2 | 
 3 | on: [push]
 4 | 
 5 | jobs:
 6 |   tests:
 7 |     strategy:
 8 |       matrix:
 9 |         os: [ubuntu-latest, windows-latest, macos-latest]
10 |     runs-on: ${{ matrix.os }}
11 |     steps:
12 |       - name: Checkout
13 |         uses: actions/checkout@v4
14 |         with:
15 |           fetch-depth: 0
16 |       
17 |       - name: Set up Go
18 |         uses: actions/setup-go@v5
19 |         with:
20 |           go-version: '>=1.21.0'
21 |           check-latest: true
22 | 
23 |       - name: Run tests
24 |         run: go test -race ./...
25 | 
```

--------------------------------------------------------------------------------
/filesystemserver/inprocess_test.go:
--------------------------------------------------------------------------------

```go
 1 | package filesystemserver_test
 2 | 
 3 | import (
 4 | 	"testing"
 5 | 
 6 | 	"github.com/mark3labs/mcp-filesystem-server/filesystemserver"
 7 | 	"github.com/stretchr/testify/assert"
 8 | 	"github.com/stretchr/testify/require"
 9 | )
10 | 
11 | func TestInProcess(t *testing.T) {
12 | 	fss, err := filesystemserver.NewFilesystemServer([]string{"."})
13 | 	require.NoError(t, err)
14 | 
15 | 	mcpClient := startTestClient(t, fss)
16 | 
17 | 	// just check for a specific tool
18 | 	tool := getTool(t, mcpClient, "read_file")
19 | 	assert.NotNil(t, tool, "read_file tool not found in the list of tools")
20 | }
21 | 
```

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

```go
 1 | package main
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"log"
 6 | 	"os"
 7 | 
 8 | 	"github.com/mark3labs/mcp-filesystem-server/filesystemserver"
 9 | 	"github.com/mark3labs/mcp-go/server"
10 | )
11 | 
12 | func main() {
13 | 	// Parse command line arguments
14 | 	if len(os.Args) < 2 {
15 | 		fmt.Fprintf(
16 | 			os.Stderr,
17 | 			"Usage: %s <allowed-directory> [additional-directories...]\n",
18 | 			os.Args[0],
19 | 		)
20 | 		os.Exit(1)
21 | 	}
22 | 
23 | 	// Create and start the server
24 | 	fss, err := filesystemserver.NewFilesystemServer(os.Args[1:])
25 | 	if err != nil {
26 | 		log.Fatalf("Failed to create server: %v", err)
27 | 	}
28 | 
29 | 	// Serve requests
30 | 	if err := server.ServeStdio(fss); err != nil {
31 | 		log.Fatalf("Server error: %v", err)
32 | 	}
33 | }
34 | 
```

--------------------------------------------------------------------------------
/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 | 
```

--------------------------------------------------------------------------------
/filesystemserver/server_test.go:
--------------------------------------------------------------------------------

```go
 1 | package filesystemserver_test
 2 | 
 3 | import (
 4 | 	"testing"
 5 | 
 6 | 	"github.com/mark3labs/mcp-filesystem-server/filesystemserver"
 7 | 	"github.com/stretchr/testify/assert"
 8 | 	"github.com/stretchr/testify/require"
 9 | )
10 | 
11 | // regression test for invalid schema => missing items in array definition
12 | func TestReadMultipleFilesSchema(t *testing.T) {
13 | 	fsserver, err := filesystemserver.NewFilesystemServer([]string{t.TempDir()})
14 | 	require.NoError(t, err)
15 | 
16 | 	mcpClient := startTestClient(t, fsserver)
17 | 
18 | 	tool := getTool(t, mcpClient, "read_multiple_files")
19 | 	require.NotNil(t, tool)
20 | 
21 | 	// make sure that the tool has the correct schema
22 | 	paths, ok := tool.InputSchema.Properties["paths"]
23 | 	assert.True(t, ok)
24 | 	pathsMap, ok := paths.(map[string]any)
25 | 	assert.True(t, ok)
26 | 	_, ok = pathsMap["items"]
27 | 	assert.True(t, ok)
28 | }
29 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/handler_test.go:
--------------------------------------------------------------------------------

```go
 1 | package handler
 2 | 
 3 | import (
 4 | 	"path/filepath"
 5 | 	"testing"
 6 | 
 7 | 	"github.com/stretchr/testify/require"
 8 | )
 9 | 
10 | // resolveAllowedDirs generates a list of allowed paths, including their resolved symlinks.
11 | // This ensures both the original paths and their symlink-resolved counterparts are included,
12 | // which is useful when paths may be symlinks (e.g., t.TempDir() on some Unix systems).
13 | func resolveAllowedDirs(t *testing.T, dirs ...string) []string {
14 | 	t.Helper()
15 | 	allowedDirs := make([]string, 0)
16 | 	for _, dir := range dirs {
17 | 		allowedDirs = append(allowedDirs, dir)
18 | 
19 | 		resolvedPath, err := filepath.EvalSymlinks(dir)
20 | 		require.NoError(t, err, "Failed to resolve symlinks for directory: %s", dir)
21 | 
22 | 		if resolvedPath != dir {
23 | 			allowedDirs = append(allowedDirs, resolvedPath)
24 | 		}
25 | 	}
26 | 	return allowedDirs
27 | }
28 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/list_allowed_directories.go:
--------------------------------------------------------------------------------

```go
 1 | package handler
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"fmt"
 6 | 	"path/filepath"
 7 | 	"strings"
 8 | 
 9 | 	"github.com/mark3labs/mcp-go/mcp"
10 | )
11 | 
12 | func (fs *FilesystemHandler) HandleListAllowedDirectories(
13 | 	ctx context.Context,
14 | 	request mcp.CallToolRequest,
15 | ) (*mcp.CallToolResult, error) {
16 | 	// Remove the trailing separator for display purposes
17 | 	displayDirs := make([]string, len(fs.allowedDirs))
18 | 	for i, dir := range fs.allowedDirs {
19 | 		displayDirs[i] = strings.TrimSuffix(dir, string(filepath.Separator))
20 | 	}
21 | 
22 | 	var result strings.Builder
23 | 	result.WriteString("Allowed directories:\n\n")
24 | 
25 | 	for _, dir := range displayDirs {
26 | 		resourceURI := pathToResourceURI(dir)
27 | 		result.WriteString(fmt.Sprintf("%s (%s)\n", dir, resourceURI))
28 | 	}
29 | 
30 | 	return &mcp.CallToolResult{
31 | 		Content: []mcp.Content{
32 | 			mcp.TextContent{
33 | 				Type: "text",
34 | 				Text: result.String(),
35 | 			},
36 | 		},
37 | 	}, nil
38 | }
```

--------------------------------------------------------------------------------
/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 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/handler.go:
--------------------------------------------------------------------------------

```go
 1 | package handler
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"os"
 6 | 	"path/filepath"
 7 | )
 8 | 
 9 | type FilesystemHandler struct {
10 | 	allowedDirs []string
11 | }
12 | 
13 | func NewFilesystemHandler(allowedDirs []string) (*FilesystemHandler, error) {
14 | 	// Normalize and validate directories
15 | 	normalized := make([]string, 0, len(allowedDirs))
16 | 	for _, dir := range allowedDirs {
17 | 		abs, err := filepath.Abs(dir)
18 | 		if err != nil {
19 | 			return nil, fmt.Errorf("failed to resolve path %s: %w", dir, err)
20 | 		}
21 | 
22 | 		info, err := os.Stat(abs)
23 | 		if err != nil {
24 | 			return nil, fmt.Errorf(
25 | 				"failed to access directory %s: %w",
26 | 				abs,
27 | 				err,
28 | 			)
29 | 		}
30 | 		if !info.IsDir() {
31 | 			return nil, fmt.Errorf("path is not a directory: %s", abs)
32 | 		}
33 | 
34 | 		// Ensure the path ends with a separator to prevent prefix matching issues
35 | 		// For example, /tmp/foo should not match /tmp/foobar
36 | 		normalized = append(normalized, filepath.Clean(abs)+string(filepath.Separator))
37 | 	}
38 | 	return &FilesystemHandler{
39 | 		allowedDirs: normalized,
40 | 	}, nil
41 | }
42 | 
43 | // pathToResourceURI converts a file path to a resource URI
44 | func pathToResourceURI(path string) string {
45 | 	return "file://" + path
46 | }
47 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/types.go:
--------------------------------------------------------------------------------

```go
 1 | package handler
 2 | 
 3 | import "time"
 4 | 
 5 | const (
 6 | 	// Maximum size for inline content (5MB)
 7 | 	MAX_INLINE_SIZE = 5 * 1024 * 1024
 8 | 	// Maximum size for base64 encoding (1MB)
 9 | 	MAX_BASE64_SIZE = 1 * 1024 * 1024
10 | 	// Maximum number of search results to return (prevent excessive output)
11 | 	MAX_SEARCH_RESULTS = 1000
12 | 	// Maximum file size in bytes to search within (10MB)
13 | 	MAX_SEARCHABLE_SIZE = 10 * 1024 * 1024
14 | )
15 | 
16 | type FileInfo struct {
17 | 	Size        int64     `json:"size"`
18 | 	Created     time.Time `json:"created"`
19 | 	Modified    time.Time `json:"modified"`
20 | 	Accessed    time.Time `json:"accessed"`
21 | 	IsDirectory bool      `json:"isDirectory"`
22 | 	IsFile      bool      `json:"isFile"`
23 | 	Permissions string    `json:"permissions"`
24 | }
25 | 
26 | // FileNode represents a node in the file tree
27 | type FileNode struct {
28 | 	Name     string      `json:"name"`
29 | 	Path     string      `json:"path"`
30 | 	Type     string      `json:"type"` // "file" or "directory"
31 | 	Size     int64       `json:"size,omitempty"`
32 | 	Modified time.Time   `json:"modified,omitempty"`
33 | 	Children []*FileNode `json:"children,omitempty"`
34 | }
35 | 
36 | // SearchResult represents a single match in a file
37 | type SearchResult struct {
38 | 	FilePath    string
39 | 	LineNumber  int
40 | 	LineContent string
41 | 	ResourceURI string
42 | }
43 | 
```

--------------------------------------------------------------------------------
/filesystemserver/utils_test.go:
--------------------------------------------------------------------------------

```go
 1 | package filesystemserver_test
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"testing"
 6 | 
 7 | 	"github.com/mark3labs/mcp-filesystem-server/filesystemserver"
 8 | 	"github.com/mark3labs/mcp-go/client"
 9 | 	"github.com/mark3labs/mcp-go/mcp"
10 | 	"github.com/mark3labs/mcp-go/server"
11 | 
12 | 	"github.com/stretchr/testify/assert"
13 | 	"github.com/stretchr/testify/require"
14 | )
15 | 
16 | func startTestClient(t *testing.T, fss *server.MCPServer) client.MCPClient {
17 | 	t.Helper()
18 | 
19 | 	mcpClient, err := client.NewInProcessClient(fss)
20 | 	require.NoError(t, err)
21 | 	t.Cleanup(func() { mcpClient.Close() })
22 | 
23 | 	err = mcpClient.Start(context.Background())
24 | 	require.NoError(t, err)
25 | 
26 | 	// Initialize the client
27 | 	initRequest := mcp.InitializeRequest{}
28 | 	initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
29 | 	initRequest.Params.ClientInfo = mcp.Implementation{
30 | 		Name:    "test-client",
31 | 		Version: "1.0.0",
32 | 	}
33 | 	result, err := mcpClient.Initialize(context.Background(), initRequest)
34 | 	require.NoError(t, err)
35 | 	assert.Equal(t, "secure-filesystem-server", result.ServerInfo.Name)
36 | 	assert.Equal(t, filesystemserver.Version, result.ServerInfo.Version)
37 | 
38 | 	return mcpClient
39 | }
40 | 
41 | func getTool(t *testing.T, mcpClient client.MCPClient, toolName string) *mcp.Tool {
42 | 	result, err := mcpClient.ListTools(context.Background(), mcp.ListToolsRequest{})
43 | 	require.NoError(t, err)
44 | 	for _, tool := range result.Tools {
45 | 		if tool.Name == toolName {
46 | 			return &tool
47 | 		}
48 | 	}
49 | 	require.Fail(t, "Tool not found", toolName)
50 | 	return nil
51 | }
52 | 
```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Release
 2 | 
 3 | on:
 4 |   push:
 5 |     tags:
 6 |       - 'v*'
 7 | 
 8 | permissions:
 9 |   contents: write
10 |   packages: write
11 | 
12 | jobs:
13 |   goreleaser:
14 |     runs-on: ubuntu-latest
15 |     steps:
16 |       - name: Checkout
17 |         uses: actions/checkout@v4
18 |         with:
19 |           fetch-depth: 0
20 |       
21 |       - name: Set up Go
22 |         uses: actions/setup-go@v5
23 |         with:
24 |           go-version: '>=1.21.0'
25 |           check-latest: true
26 |       
27 |       - name: Run GoReleaser
28 |         uses: goreleaser/goreleaser-action@v6
29 |         with:
30 |           distribution: goreleaser
31 |           version: '~> v2'
32 |           args: release --clean
33 |         env:
34 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 |       
36 |       - name: Extract version
37 |         id: get-version
38 |         run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
39 |       
40 |       - name: Login to GitHub Container Registry
41 |         uses: docker/login-action@v3
42 |         with:
43 |           registry: ghcr.io
44 |           username: ${{ github.repository_owner }}
45 |           password: ${{ secrets.GITHUB_TOKEN }}
46 |       
47 |       - name: Set up Docker Buildx
48 |         uses: docker/setup-buildx-action@v3
49 |       
50 |       - name: Build and push Docker image
51 |         uses: docker/build-push-action@v6
52 |         with:
53 |           context: .
54 |           push: true
55 |           platforms: linux/amd64,linux/arm64
56 |           tags: |
57 |             ghcr.io/${{ github.repository }}:latest
58 |             ghcr.io/${{ github.repository }}:${{ steps.get-version.outputs.VERSION }}
59 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/search_files_test.go:
--------------------------------------------------------------------------------

```go
 1 | package handler
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"os"
 6 | 	"path/filepath"
 7 | 	"testing"
 8 | 
 9 | 	"github.com/mark3labs/mcp-go/mcp"
10 | 	"github.com/stretchr/testify/assert"
11 | 	"github.com/stretchr/testify/require"
12 | )
13 | 
14 | func TestSearchFiles_Pattern(t *testing.T) {
15 | 
16 | 	// setting up test folder
17 | 	// tmpDir/
18 | 	// - foo/
19 | 	//   - bar.h
20 | 	//   - test.c
21 | 	// - test.h
22 | 	// - test.c
23 | 
24 | 	dir := t.TempDir()
25 | 	test_h := filepath.Join(dir, "test.h")
26 | 	err := os.WriteFile(test_h, []byte("foo"), 0644)
27 | 	require.NoError(t, err)
28 | 
29 | 	test_c := filepath.Join(dir, "test.c")
30 | 	err = os.WriteFile(test_c, []byte("foo"), 0644)
31 | 	require.NoError(t, err)
32 | 
33 | 	fooDir := filepath.Join(dir, "foo")
34 | 	err = os.MkdirAll(fooDir, 0755)
35 | 	require.NoError(t, err)
36 | 
37 | 	foo_bar_h := filepath.Join(fooDir, "bar.h")
38 | 	err = os.WriteFile(foo_bar_h, []byte("foo"), 0644)
39 | 	require.NoError(t, err)
40 | 
41 | 	foo_test_c := filepath.Join(fooDir, "test.c")
42 | 	err = os.WriteFile(foo_test_c, []byte("foo"), 0644)
43 | 	require.NoError(t, err)
44 | 
45 | 	handler, err := NewFilesystemHandler(resolveAllowedDirs(t, dir))
46 | 	require.NoError(t, err)
47 | 
48 | 	tests := []struct {
49 | 		info    string
50 | 		pattern string
51 | 		matches []string
52 | 	}{
53 | 		{info: "use placeholder with extension", pattern: "*.h", matches: []string{test_h, foo_bar_h}},
54 | 		{info: "use placeholder with name", pattern: "test.*", matches: []string{test_h, test_c}},
55 | 		{info: "same filename", pattern: "test.c", matches: []string{test_c, foo_test_c}},
56 | 	}
57 | 
58 | 	for _, test := range tests {
59 | 		t.Run(test.info, func(t *testing.T) {
60 | 			request := mcp.CallToolRequest{}
61 | 			request.Params.Name = "search_files"
62 | 			request.Params.Arguments = map[string]any{
63 | 				"path":    dir,
64 | 				"pattern": test.pattern,
65 | 			}
66 | 
67 | 			result, err := handler.HandleSearchFiles(context.Background(), request)
68 | 			require.NoError(t, err)
69 | 			assert.False(t, result.IsError)
70 | 			assert.Len(t, result.Content, 1)
71 | 
72 | 			for _, match := range test.matches {
73 | 				assert.Contains(t, result.Content[0].(mcp.TextContent).Text, match)
74 | 			}
75 | 		})
76 | 	}
77 | }
78 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/read_file_test.go:
--------------------------------------------------------------------------------

```go
 1 | package handler
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"fmt"
 6 | 	"os"
 7 | 	"path/filepath"
 8 | 	"testing"
 9 | 
10 | 	"github.com/mark3labs/mcp-go/mcp"
11 | 	"github.com/stretchr/testify/assert"
12 | 	"github.com/stretchr/testify/require"
13 | )
14 | 
15 | func TestReadfile_Valid(t *testing.T) {
16 | 	// prepare temp directory
17 | 	dir := t.TempDir()
18 | 	content := "test-content"
19 | 	err := os.WriteFile(filepath.Join(dir, "test"), []byte(content), 0644)
20 | 	require.NoError(t, err)
21 | 
22 | 	handler, err := NewFilesystemHandler(resolveAllowedDirs(t, dir))
23 | 	require.NoError(t, err)
24 | 	request := mcp.CallToolRequest{}
25 | 	request.Params.Name = "read_file"
26 | 	request.Params.Arguments = map[string]any{
27 | 		"path": filepath.Join(dir, "test"),
28 | 	}
29 | 
30 | 	result, err := handler.HandleReadFile(context.Background(), request)
31 | 	require.NoError(t, err)
32 | 	assert.Len(t, result.Content, 1)
33 | 	assert.Equal(t, content, result.Content[0].(mcp.TextContent).Text)
34 | }
35 | 
36 | func TestReadfile_Invalid(t *testing.T) {
37 | 	dir := t.TempDir()
38 | 	handler, err := NewFilesystemHandler(resolveAllowedDirs(t, dir))
39 | 	require.NoError(t, err)
40 | 
41 | 	request := mcp.CallToolRequest{}
42 | 	request.Params.Name = "read_file"
43 | 	request.Params.Arguments = map[string]any{
44 | 		"path": filepath.Join(dir, "test"),
45 | 	}
46 | 
47 | 	result, err := handler.HandleReadFile(context.Background(), request)
48 | 	require.NoError(t, err)
49 | 	assert.True(t, result.IsError)
50 | 	assert.Contains(t, fmt.Sprint(result.Content[0]), "no such file or directory")
51 | }
52 | 
53 | func TestReadfile_NoAccess(t *testing.T) {
54 | 	dir1 := t.TempDir()
55 | 	dir2 := t.TempDir()
56 | 
57 | 	handler, err := NewFilesystemHandler(resolveAllowedDirs(t, dir1))
58 | 	require.NoError(t, err)
59 | 
60 | 	request := mcp.CallToolRequest{}
61 | 	request.Params.Name = "read_file"
62 | 	request.Params.Arguments = map[string]any{
63 | 		"path": filepath.Join(dir2, "test"),
64 | 	}
65 | 
66 | 	result, err := handler.HandleReadFile(context.Background(), request)
67 | 	require.NoError(t, err)
68 | 	assert.True(t, result.IsError)
69 | 	assert.Contains(t, fmt.Sprint(result.Content[0]), "access denied - path outside allowed directories")
70 | }
71 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/list_allowed_directories_test.go:
--------------------------------------------------------------------------------

```go
 1 | package handler
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"testing"
 6 | 
 7 | 	"github.com/mark3labs/mcp-go/mcp"
 8 | 	"github.com/stretchr/testify/assert"
 9 | 	"github.com/stretchr/testify/require"
10 | )
11 | 
12 | func TestHandleListAllowedDirectories(t *testing.T) {
13 | 	// Setup multiple temporary directories for the test
14 | 	tmpDir1 := t.TempDir()
15 | 	tmpDir2 := t.TempDir()
16 | 
17 | 	// Create a handler with multiple allowed directories
18 | 	allowedDirs := resolveAllowedDirs(t, tmpDir1, tmpDir2)
19 | 	fsHandler, err := NewFilesystemHandler(allowedDirs)
20 | 	require.NoError(t, err)
21 | 
22 | 	ctx := context.Background()
23 | 
24 | 	t.Run("list allowed directories", func(t *testing.T) {
25 | 		req := mcp.CallToolRequest{
26 | 			Params: mcp.CallToolParams{
27 | 				Arguments: map[string]interface{}{},
28 | 			},
29 | 		}
30 | 
31 | 		res, err := fsHandler.HandleListAllowedDirectories(ctx, req)
32 | 		require.NoError(t, err)
33 | 		require.False(t, res.IsError)
34 | 
35 | 		// Verify the response contains the allowed directories
36 | 		require.Len(t, res.Content, 1)
37 | 		textContent := res.Content[0].(mcp.TextContent)
38 | 		assert.Contains(t, textContent.Text, "Allowed directories:")
39 | 		assert.Contains(t, textContent.Text, tmpDir1)
40 | 		assert.Contains(t, textContent.Text, tmpDir2)
41 | 		assert.Contains(t, textContent.Text, "file://")
42 | 	})
43 | 
44 | 	t.Run("single allowed directory", func(t *testing.T) {
45 | 		singleDir := t.TempDir()
46 | 		singleAllowedDirs := resolveAllowedDirs(t, singleDir)
47 | 		singleFsHandler, err := NewFilesystemHandler(singleAllowedDirs)
48 | 		require.NoError(t, err)
49 | 
50 | 		req := mcp.CallToolRequest{
51 | 			Params: mcp.CallToolParams{
52 | 				Arguments: map[string]interface{}{},
53 | 			},
54 | 		}
55 | 
56 | 		res, err := singleFsHandler.HandleListAllowedDirectories(ctx, req)
57 | 		require.NoError(t, err)
58 | 		require.False(t, res.IsError)
59 | 
60 | 		// Verify the response contains the single allowed directory
61 | 		require.Len(t, res.Content, 1)
62 | 		textContent := res.Content[0].(mcp.TextContent)
63 | 		assert.Contains(t, textContent.Text, "Allowed directories:")
64 | 		assert.Contains(t, textContent.Text, singleDir)
65 | 		assert.Contains(t, textContent.Text, "file://")
66 | 	})
67 | }
68 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/create_directory_test.go:
--------------------------------------------------------------------------------

```go
 1 | package handler
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"os"
 6 | 	"path/filepath"
 7 | 	"testing"
 8 | 
 9 | 	"github.com/mark3labs/mcp-go/mcp"
10 | 	"github.com/stretchr/testify/assert"
11 | 	"github.com/stretchr/testify/require"
12 | )
13 | 
14 | func TestHandleCreateDirectory(t *testing.T) {
15 | 	// Setup a temporary directory for the test
16 | 	tmpDir := t.TempDir()
17 | 
18 | 	// Create a handler with the temp dir as an allowed path
19 | 	allowedDirs := resolveAllowedDirs(t, tmpDir)
20 | 	fsHandler, err := NewFilesystemHandler(allowedDirs)
21 | 	require.NoError(t, err)
22 | 
23 | 	ctx := context.Background()
24 | 
25 | 	t.Run("create a new directory", func(t *testing.T) {
26 | 		newDirPath := filepath.Join(tmpDir, "new_directory")
27 | 		req := mcp.CallToolRequest{
28 | 			Params: mcp.CallToolParams{
29 | 				Arguments: map[string]interface{}{
30 | 					"path": newDirPath,
31 | 				},
32 | 			},
33 | 		}
34 | 
35 | 		res, err := fsHandler.HandleCreateDirectory(ctx, req)
36 | 		require.NoError(t, err)
37 | 		require.False(t, res.IsError)
38 | 
39 | 		// Verify the directory was created
40 | 		info, err := os.Stat(newDirPath)
41 | 		require.NoError(t, err)
42 | 		assert.True(t, info.IsDir())
43 | 	})
44 | 
45 | 	t.Run("directory already exists", func(t *testing.T) {
46 | 		existingDirPath := filepath.Join(tmpDir, "existing_directory")
47 | 		err := os.Mkdir(existingDirPath, 0755)
48 | 		require.NoError(t, err)
49 | 
50 | 		req := mcp.CallToolRequest{
51 | 			Params: mcp.CallToolParams{
52 | 				Arguments: map[string]interface{}{
53 | 					"path": existingDirPath,
54 | 				},
55 | 			},
56 | 		}
57 | 
58 | 		res, err := fsHandler.HandleCreateDirectory(ctx, req)
59 | 		require.NoError(t, err)
60 | 		require.False(t, res.IsError) // Should not be an error, just a message that it already exists
61 | 	})
62 | 
63 | 	t.Run("path exists but is not a directory", func(t *testing.T) {
64 | 		filePath := filepath.Join(tmpDir, "existing_file.txt")
65 | 		err := os.WriteFile(filePath, []byte("content"), 0644)
66 | 		require.NoError(t, err)
67 | 
68 | 		req := mcp.CallToolRequest{
69 | 			Params: mcp.CallToolParams{
70 | 				Arguments: map[string]interface{}{
71 | 					"path": filePath,
72 | 				},
73 | 			},
74 | 		}
75 | 
76 | 		res, err := fsHandler.HandleCreateDirectory(ctx, req)
77 | 		require.NoError(t, err)
78 | 		require.True(t, res.IsError)
79 | 	})
80 | 
81 | 	t.Run("path is in a non-allowed directory", func(t *testing.T) {
82 | 		otherDir := t.TempDir()
83 | 
84 | 		req := mcp.CallToolRequest{
85 | 			Params: mcp.CallToolParams{
86 | 				Arguments: map[string]interface{}{
87 | 					"path": filepath.Join(otherDir, "new_directory"),
88 | 				},
89 | 			},
90 | 		}
91 | 
92 | 		res, err := fsHandler.HandleCreateDirectory(ctx, req)
93 | 		require.NoError(t, err)
94 | 		require.True(t, res.IsError)
95 | 	})
96 | }
97 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/create_directory.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"os"
  7 | 
  8 | 	"github.com/mark3labs/mcp-go/mcp"
  9 | )
 10 | 
 11 | func (fs *FilesystemHandler) HandleCreateDirectory(
 12 | 	ctx context.Context,
 13 | 	request mcp.CallToolRequest,
 14 | ) (*mcp.CallToolResult, error) {
 15 | 	path, err := request.RequireString("path")
 16 | 	if err != nil {
 17 | 		return nil, err
 18 | 	}
 19 | 
 20 | 	// Handle empty or relative paths like "." or "./" by converting to absolute path
 21 | 	if path == "." || path == "./" {
 22 | 		// Get current working directory
 23 | 		cwd, err := os.Getwd()
 24 | 		if err != nil {
 25 | 			return &mcp.CallToolResult{
 26 | 				Content: []mcp.Content{
 27 | 					mcp.TextContent{
 28 | 						Type: "text",
 29 | 						Text: fmt.Sprintf("Error resolving current directory: %v", err),
 30 | 					},
 31 | 				},
 32 | 				IsError: true,
 33 | 			}, nil
 34 | 		}
 35 | 		path = cwd
 36 | 	}
 37 | 
 38 | 	validPath, err := fs.validatePath(path)
 39 | 	if err != nil {
 40 | 		return &mcp.CallToolResult{
 41 | 			Content: []mcp.Content{
 42 | 				mcp.TextContent{
 43 | 					Type: "text",
 44 | 					Text: fmt.Sprintf("Error: %v", err),
 45 | 				},
 46 | 			},
 47 | 			IsError: true,
 48 | 		}, nil
 49 | 	}
 50 | 
 51 | 	// Check if path already exists
 52 | 	if info, err := os.Stat(validPath); err == nil {
 53 | 		if info.IsDir() {
 54 | 			resourceURI := pathToResourceURI(validPath)
 55 | 			return &mcp.CallToolResult{
 56 | 				Content: []mcp.Content{
 57 | 					mcp.TextContent{
 58 | 						Type: "text",
 59 | 						Text: fmt.Sprintf("Directory already exists: %s", path),
 60 | 					},
 61 | 					mcp.EmbeddedResource{
 62 | 						Type: "resource",
 63 | 						Resource: mcp.TextResourceContents{
 64 | 							URI:      resourceURI,
 65 | 							MIMEType: "text/plain",
 66 | 							Text:     fmt.Sprintf("Directory: %s", validPath),
 67 | 						},
 68 | 					},
 69 | 				},
 70 | 			}, nil
 71 | 		}
 72 | 		return &mcp.CallToolResult{
 73 | 			Content: []mcp.Content{
 74 | 				mcp.TextContent{
 75 | 					Type: "text",
 76 | 					Text: fmt.Sprintf("Error: Path exists but is not a directory: %s", path),
 77 | 				},
 78 | 			},
 79 | 			IsError: true,
 80 | 		}, nil
 81 | 	}
 82 | 
 83 | 	if err := os.MkdirAll(validPath, 0755); err != nil {
 84 | 		return &mcp.CallToolResult{
 85 | 			Content: []mcp.Content{
 86 | 				mcp.TextContent{
 87 | 					Type: "text",
 88 | 					Text: fmt.Sprintf("Error creating directory: %v", err),
 89 | 				},
 90 | 			},
 91 | 			IsError: true,
 92 | 		}, nil
 93 | 	}
 94 | 
 95 | 	resourceURI := pathToResourceURI(validPath)
 96 | 	return &mcp.CallToolResult{
 97 | 		Content: []mcp.Content{
 98 | 			mcp.TextContent{
 99 | 				Type: "text",
100 | 				Text: fmt.Sprintf("Successfully created directory %s", path),
101 | 			},
102 | 			mcp.EmbeddedResource{
103 | 				Type: "resource",
104 | 				Resource: mcp.TextResourceContents{
105 | 					URI:      resourceURI,
106 | 					MIMEType: "text/plain",
107 | 					Text:     fmt.Sprintf("Directory: %s", validPath),
108 | 				},
109 | 			},
110 | 		},
111 | 	}, nil
112 | }
```

--------------------------------------------------------------------------------
/filesystemserver/handler/write_file.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"os"
  7 | 	"path/filepath"
  8 | 
  9 | 	"github.com/mark3labs/mcp-go/mcp"
 10 | )
 11 | 
 12 | func (fs *FilesystemHandler) HandleWriteFile(
 13 | 	ctx context.Context,
 14 | 	request mcp.CallToolRequest,
 15 | ) (*mcp.CallToolResult, error) {
 16 | 	path, err := request.RequireString("path")
 17 | 	if err != nil {
 18 | 		return nil, err
 19 | 	}
 20 | 	content, err := request.RequireString("content")
 21 | 	if err != nil {
 22 | 		return nil, err
 23 | 	}
 24 | 
 25 | 	// Handle empty or relative paths like "." or "./" by converting to absolute path
 26 | 	if path == "." || path == "./" {
 27 | 		// Get current working directory
 28 | 		cwd, err := os.Getwd()
 29 | 		if err != nil {
 30 | 			return &mcp.CallToolResult{
 31 | 				Content: []mcp.Content{
 32 | 					mcp.TextContent{
 33 | 						Type: "text",
 34 | 						Text: fmt.Sprintf("Error resolving current directory: %v", err),
 35 | 					},
 36 | 				},
 37 | 				IsError: true,
 38 | 			}, nil
 39 | 		}
 40 | 		path = cwd
 41 | 	}
 42 | 
 43 | 	validPath, err := fs.validatePath(path)
 44 | 	if err != nil {
 45 | 		return &mcp.CallToolResult{
 46 | 			Content: []mcp.Content{
 47 | 				mcp.TextContent{
 48 | 					Type: "text",
 49 | 					Text: fmt.Sprintf("Error: %v", err),
 50 | 				},
 51 | 			},
 52 | 			IsError: true,
 53 | 		}, nil
 54 | 	}
 55 | 
 56 | 	// Check if it's a directory
 57 | 	if info, err := os.Stat(validPath); err == nil && info.IsDir() {
 58 | 		return &mcp.CallToolResult{
 59 | 			Content: []mcp.Content{
 60 | 				mcp.TextContent{
 61 | 					Type: "text",
 62 | 					Text: "Error: Cannot write to a directory",
 63 | 				},
 64 | 			},
 65 | 			IsError: true,
 66 | 		}, nil
 67 | 	}
 68 | 
 69 | 	// Create parent directories if they don't exist
 70 | 	parentDir := filepath.Dir(validPath)
 71 | 	if err := os.MkdirAll(parentDir, 0755); err != nil {
 72 | 		return &mcp.CallToolResult{
 73 | 			Content: []mcp.Content{
 74 | 				mcp.TextContent{
 75 | 					Type: "text",
 76 | 					Text: fmt.Sprintf("Error creating parent directories: %v", err),
 77 | 				},
 78 | 			},
 79 | 			IsError: true,
 80 | 		}, nil
 81 | 	}
 82 | 
 83 | 	if err := os.WriteFile(validPath, []byte(content), 0644); err != nil {
 84 | 		return &mcp.CallToolResult{
 85 | 			Content: []mcp.Content{
 86 | 				mcp.TextContent{
 87 | 					Type: "text",
 88 | 					Text: fmt.Sprintf("Error writing file: %v", err),
 89 | 				},
 90 | 			},
 91 | 			IsError: true,
 92 | 		}, nil
 93 | 	}
 94 | 
 95 | 	// Get file info for the response
 96 | 	info, err := os.Stat(validPath)
 97 | 	if err != nil {
 98 | 		// File was written but we couldn't get info
 99 | 		return &mcp.CallToolResult{
100 | 			Content: []mcp.Content{
101 | 				mcp.TextContent{
102 | 					Type: "text",
103 | 					Text: fmt.Sprintf("Successfully wrote to %s", path),
104 | 				},
105 | 			},
106 | 		}, nil
107 | 	}
108 | 
109 | 	resourceURI := pathToResourceURI(validPath)
110 | 	return &mcp.CallToolResult{
111 | 		Content: []mcp.Content{
112 | 			mcp.TextContent{
113 | 				Type: "text",
114 | 				Text: fmt.Sprintf("Successfully wrote %d bytes to %s", info.Size(), path),
115 | 			},
116 | 			mcp.EmbeddedResource{
117 | 				Type: "resource",
118 | 				Resource: mcp.TextResourceContents{
119 | 					URI:      resourceURI,
120 | 					MIMEType: "text/plain",
121 | 					Text:     fmt.Sprintf("File: %s (%d bytes)", validPath, info.Size()),
122 | 				},
123 | 			},
124 | 		},
125 | 	}, nil
126 | }
```

--------------------------------------------------------------------------------
/filesystemserver/handler/list_directory.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"os"
  7 | 	"path/filepath"
  8 | 	"strings"
  9 | 
 10 | 	"github.com/mark3labs/mcp-go/mcp"
 11 | )
 12 | 
 13 | func (fs *FilesystemHandler) HandleListDirectory(
 14 | 	ctx context.Context,
 15 | 	request mcp.CallToolRequest,
 16 | ) (*mcp.CallToolResult, error) {
 17 | 	path, err := request.RequireString("path")
 18 | 	if err != nil {
 19 | 		return nil, err
 20 | 	}
 21 | 
 22 | 	// Handle empty or relative paths like "." or "./" by converting to absolute path
 23 | 	if path == "." || path == "./" {
 24 | 		// Get current working directory
 25 | 		cwd, err := os.Getwd()
 26 | 		if err != nil {
 27 | 			return &mcp.CallToolResult{
 28 | 				Content: []mcp.Content{
 29 | 					mcp.TextContent{
 30 | 						Type: "text",
 31 | 						Text: fmt.Sprintf("Error resolving current directory: %v", err),
 32 | 					},
 33 | 				},
 34 | 				IsError: true,
 35 | 			}, nil
 36 | 		}
 37 | 		path = cwd
 38 | 	}
 39 | 
 40 | 	validPath, err := fs.validatePath(path)
 41 | 	if err != nil {
 42 | 		return &mcp.CallToolResult{
 43 | 			Content: []mcp.Content{
 44 | 				mcp.TextContent{
 45 | 					Type: "text",
 46 | 					Text: fmt.Sprintf("Error: %v", err),
 47 | 				},
 48 | 			},
 49 | 			IsError: true,
 50 | 		}, nil
 51 | 	}
 52 | 
 53 | 	// Check if it's a directory
 54 | 	info, err := os.Stat(validPath)
 55 | 	if err != nil {
 56 | 		return &mcp.CallToolResult{
 57 | 			Content: []mcp.Content{
 58 | 				mcp.TextContent{
 59 | 					Type: "text",
 60 | 					Text: fmt.Sprintf("Error: %v", err),
 61 | 				},
 62 | 			},
 63 | 			IsError: true,
 64 | 		}, nil
 65 | 	}
 66 | 
 67 | 	if !info.IsDir() {
 68 | 		return &mcp.CallToolResult{
 69 | 			Content: []mcp.Content{
 70 | 				mcp.TextContent{
 71 | 					Type: "text",
 72 | 					Text: "Error: Path is not a directory",
 73 | 				},
 74 | 			},
 75 | 			IsError: true,
 76 | 		}, nil
 77 | 	}
 78 | 
 79 | 	entries, err := os.ReadDir(validPath)
 80 | 	if err != nil {
 81 | 		return &mcp.CallToolResult{
 82 | 			Content: []mcp.Content{
 83 | 				mcp.TextContent{
 84 | 					Type: "text",
 85 | 					Text: fmt.Sprintf("Error reading directory: %v", err),
 86 | 				},
 87 | 			},
 88 | 			IsError: true,
 89 | 		}, nil
 90 | 	}
 91 | 
 92 | 	var result strings.Builder
 93 | 	result.WriteString(fmt.Sprintf("Directory listing for: %s\n\n", validPath))
 94 | 
 95 | 	for _, entry := range entries {
 96 | 		entryPath := filepath.Join(validPath, entry.Name())
 97 | 		resourceURI := pathToResourceURI(entryPath)
 98 | 
 99 | 		if entry.IsDir() {
100 | 			result.WriteString(fmt.Sprintf("[DIR]  %s (%s)\n", entry.Name(), resourceURI))
101 | 		} else {
102 | 			info, err := entry.Info()
103 | 			if err == nil {
104 | 				result.WriteString(fmt.Sprintf("[FILE] %s (%s) - %d bytes\n",
105 | 					entry.Name(), resourceURI, info.Size()))
106 | 			} else {
107 | 				result.WriteString(fmt.Sprintf("[FILE] %s (%s)\n", entry.Name(), resourceURI))
108 | 			}
109 | 		}
110 | 	}
111 | 
112 | 	// Return both text content and embedded resource
113 | 	resourceURI := pathToResourceURI(validPath)
114 | 	return &mcp.CallToolResult{
115 | 		Content: []mcp.Content{
116 | 			mcp.TextContent{
117 | 				Type: "text",
118 | 				Text: result.String(),
119 | 			},
120 | 			mcp.EmbeddedResource{
121 | 				Type: "resource",
122 | 				Resource: mcp.TextResourceContents{
123 | 					URI:      resourceURI,
124 | 					MIMEType: "text/plain",
125 | 					Text:     fmt.Sprintf("Directory: %s", validPath),
126 | 				},
127 | 			},
128 | 		},
129 | 	}, nil
130 | }
```

--------------------------------------------------------------------------------
/filesystemserver/handler/delete_file.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"os"
  7 | 
  8 | 	"github.com/mark3labs/mcp-go/mcp"
  9 | )
 10 | 
 11 | func (fs *FilesystemHandler) HandleDeleteFile(
 12 | 	ctx context.Context,
 13 | 	request mcp.CallToolRequest,
 14 | ) (*mcp.CallToolResult, error) {
 15 | 	path, err := request.RequireString("path")
 16 | 	if err != nil {
 17 | 		return nil, err
 18 | 	}
 19 | 
 20 | 	// Handle empty or relative paths like "." or "./" by converting to absolute path
 21 | 	if path == "." || path == "./" {
 22 | 		// Get current working directory
 23 | 		cwd, err := os.Getwd()
 24 | 		if err != nil {
 25 | 			return &mcp.CallToolResult{
 26 | 				Content: []mcp.Content{
 27 | 					mcp.TextContent{
 28 | 						Type: "text",
 29 | 						Text: fmt.Sprintf("Error resolving current directory: %v", err),
 30 | 					},
 31 | 				},
 32 | 				IsError: true,
 33 | 			}, nil
 34 | 		}
 35 | 		path = cwd
 36 | 	}
 37 | 
 38 | 	validPath, err := fs.validatePath(path)
 39 | 	if err != nil {
 40 | 		return &mcp.CallToolResult{
 41 | 			Content: []mcp.Content{
 42 | 				mcp.TextContent{
 43 | 					Type: "text",
 44 | 					Text: fmt.Sprintf("Error: %v", err),
 45 | 				},
 46 | 			},
 47 | 			IsError: true,
 48 | 		}, nil
 49 | 	}
 50 | 
 51 | 	// Check if path exists
 52 | 	info, err := os.Stat(validPath)
 53 | 	if os.IsNotExist(err) {
 54 | 		return &mcp.CallToolResult{
 55 | 			Content: []mcp.Content{
 56 | 				mcp.TextContent{
 57 | 					Type: "text",
 58 | 					Text: fmt.Sprintf("Error: Path does not exist: %s", path),
 59 | 				},
 60 | 			},
 61 | 			IsError: true,
 62 | 		}, nil
 63 | 	} else if err != nil {
 64 | 		return &mcp.CallToolResult{
 65 | 			Content: []mcp.Content{
 66 | 				mcp.TextContent{
 67 | 					Type: "text",
 68 | 					Text: fmt.Sprintf("Error accessing path: %v", err),
 69 | 				},
 70 | 			},
 71 | 			IsError: true,
 72 | 		}, nil
 73 | 	}
 74 | 
 75 | 	// Extract recursive parameter (optional, default: false)
 76 | 	recursive := false
 77 | 	if recursiveParam, err := request.RequireBool("recursive"); err == nil {
 78 | 		recursive = recursiveParam
 79 | 	}
 80 | 
 81 | 	// Check if it's a directory and handle accordingly
 82 | 	if info.IsDir() {
 83 | 		if !recursive {
 84 | 			return &mcp.CallToolResult{
 85 | 				Content: []mcp.Content{
 86 | 					mcp.TextContent{
 87 | 						Type: "text",
 88 | 						Text: fmt.Sprintf("Error: %s is a directory. Use recursive=true to delete directories.", path),
 89 | 					},
 90 | 				},
 91 | 				IsError: true,
 92 | 			}, nil
 93 | 		}
 94 | 
 95 | 		// It's a directory and recursive is true, so remove it
 96 | 		if err := os.RemoveAll(validPath); err != nil {
 97 | 			return &mcp.CallToolResult{
 98 | 				Content: []mcp.Content{
 99 | 					mcp.TextContent{
100 | 						Type: "text",
101 | 						Text: fmt.Sprintf("Error deleting directory: %v", err),
102 | 					},
103 | 				},
104 | 				IsError: true,
105 | 			}, nil
106 | 		}
107 | 
108 | 		return &mcp.CallToolResult{
109 | 			Content: []mcp.Content{
110 | 				mcp.TextContent{
111 | 					Type: "text",
112 | 					Text: fmt.Sprintf("Successfully deleted directory %s", path),
113 | 				},
114 | 			},
115 | 		}, nil
116 | 	}
117 | 
118 | 	// It's a file, delete it
119 | 	if err := os.Remove(validPath); err != nil {
120 | 		return &mcp.CallToolResult{
121 | 			Content: []mcp.Content{
122 | 				mcp.TextContent{
123 | 					Type: "text",
124 | 					Text: fmt.Sprintf("Error deleting file: %v", err),
125 | 				},
126 | 			},
127 | 			IsError: true,
128 | 		}, nil
129 | 	}
130 | 
131 | 	return &mcp.CallToolResult{
132 | 		Content: []mcp.Content{
133 | 			mcp.TextContent{
134 | 				Type: "text",
135 | 				Text: fmt.Sprintf("Successfully deleted file %s", path),
136 | 			},
137 | 		},
138 | 	}, nil
139 | }
140 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/get_file_info.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"os"
  7 | 	"time"
  8 | 
  9 | 	"github.com/djherbis/times"
 10 | 	"github.com/mark3labs/mcp-go/mcp"
 11 | )
 12 | 
 13 | func (fs *FilesystemHandler) HandleGetFileInfo(
 14 | 	ctx context.Context,
 15 | 	request mcp.CallToolRequest,
 16 | ) (*mcp.CallToolResult, error) {
 17 | 	path, err := request.RequireString("path")
 18 | 	if err != nil {
 19 | 		return nil, err
 20 | 	}
 21 | 
 22 | 	// Handle empty or relative paths like "." or "./" by converting to absolute path
 23 | 	if path == "." || path == "./" {
 24 | 		// Get current working directory
 25 | 		cwd, err := os.Getwd()
 26 | 		if err != nil {
 27 | 			return &mcp.CallToolResult{
 28 | 				Content: []mcp.Content{
 29 | 					mcp.TextContent{
 30 | 						Type: "text",
 31 | 						Text: fmt.Sprintf("Error resolving current directory: %v", err),
 32 | 					},
 33 | 				},
 34 | 				IsError: true,
 35 | 			}, nil
 36 | 		}
 37 | 		path = cwd
 38 | 	}
 39 | 
 40 | 	validPath, err := fs.validatePath(path)
 41 | 	if err != nil {
 42 | 		return &mcp.CallToolResult{
 43 | 			Content: []mcp.Content{
 44 | 				mcp.TextContent{
 45 | 					Type: "text",
 46 | 					Text: fmt.Sprintf("Error: %v", err),
 47 | 				},
 48 | 			},
 49 | 			IsError: true,
 50 | 		}, nil
 51 | 	}
 52 | 
 53 | 	info, err := fs.getFileStats(validPath)
 54 | 	if err != nil {
 55 | 		return &mcp.CallToolResult{
 56 | 			Content: []mcp.Content{
 57 | 				mcp.TextContent{
 58 | 					Type: "text",
 59 | 					Text: fmt.Sprintf("Error getting file info: %v", err),
 60 | 				},
 61 | 			},
 62 | 			IsError: true,
 63 | 		}, nil
 64 | 	}
 65 | 
 66 | 	// Get MIME type for files
 67 | 	mimeType := "directory"
 68 | 	if info.IsFile {
 69 | 		mimeType = detectMimeType(validPath)
 70 | 	}
 71 | 
 72 | 	resourceURI := pathToResourceURI(validPath)
 73 | 
 74 | 	// Determine file type text
 75 | 	var fileTypeText string
 76 | 	if info.IsDirectory {
 77 | 		fileTypeText = "Directory"
 78 | 	} else {
 79 | 		fileTypeText = "File"
 80 | 	}
 81 | 
 82 | 	return &mcp.CallToolResult{
 83 | 		Content: []mcp.Content{
 84 | 			mcp.TextContent{
 85 | 				Type: "text",
 86 | 				Text: fmt.Sprintf(
 87 | 					"File information for: %s\n\nSize: %d bytes\nCreated: %s\nModified: %s\nAccessed: %s\nIsDirectory: %v\nIsFile: %v\nPermissions: %s\nMIME Type: %s\nResource URI: %s",
 88 | 					validPath,
 89 | 					info.Size,
 90 | 					info.Created.Format(time.RFC3339),
 91 | 					info.Modified.Format(time.RFC3339),
 92 | 					info.Accessed.Format(time.RFC3339),
 93 | 					info.IsDirectory,
 94 | 					info.IsFile,
 95 | 					info.Permissions,
 96 | 					mimeType,
 97 | 					resourceURI,
 98 | 				),
 99 | 			},
100 | 			mcp.EmbeddedResource{
101 | 				Type: "resource",
102 | 				Resource: mcp.TextResourceContents{
103 | 					URI:      resourceURI,
104 | 					MIMEType: "text/plain",
105 | 					Text: fmt.Sprintf("%s: %s (%s, %d bytes)",
106 | 						fileTypeText,
107 | 						validPath,
108 | 						mimeType,
109 | 						info.Size),
110 | 				},
111 | 			},
112 | 		},
113 | 	}, nil
114 | }
115 | 
116 | func (fs *FilesystemHandler) getFileStats(path string) (FileInfo, error) {
117 | 	info, err := os.Stat(path)
118 | 	if err != nil {
119 | 		return FileInfo{}, err
120 | 	}
121 | 
122 | 	timespec, err := times.Stat(path)
123 | 	if err != nil {
124 | 		return FileInfo{}, fmt.Errorf("failed to get file times: %w", err)
125 | 	}
126 | 
127 | 	createdTime := time.Time{}
128 | 	if timespec.HasBirthTime() {
129 | 		createdTime = timespec.BirthTime()
130 | 	}
131 | 
132 | 	return FileInfo{
133 | 		Size:        info.Size(),
134 | 		Created:     createdTime,
135 | 		Modified:    timespec.ModTime(),
136 | 		Accessed:    timespec.AccessTime(),
137 | 		IsDirectory: info.IsDir(),
138 | 		IsFile:      !info.IsDir(),
139 | 		Permissions: fmt.Sprintf("%o", info.Mode().Perm()),
140 | 	}, nil
141 | }
142 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/get_file_info_test.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"os"
  6 | 	"path/filepath"
  7 | 	"testing"
  8 | 
  9 | 	"github.com/mark3labs/mcp-go/mcp"
 10 | 	"github.com/stretchr/testify/assert"
 11 | 	"github.com/stretchr/testify/require"
 12 | )
 13 | 
 14 | func TestHandleGetFileInfo(t *testing.T) {
 15 | 	// Setup a temporary directory for the test
 16 | 	tmpDir := t.TempDir()
 17 | 
 18 | 	// Create a handler with the temp dir as an allowed path
 19 | 	allowedDirs := resolveAllowedDirs(t, tmpDir)
 20 | 	fsHandler, err := NewFilesystemHandler(allowedDirs)
 21 | 	require.NoError(t, err)
 22 | 
 23 | 	ctx := context.Background()
 24 | 
 25 | 	t.Run("get file info for a file", func(t *testing.T) {
 26 | 		filePath := filepath.Join(tmpDir, "test_file.txt")
 27 | 		fileContent := "Hello, world!"
 28 | 		err := os.WriteFile(filePath, []byte(fileContent), 0644)
 29 | 		require.NoError(t, err)
 30 | 
 31 | 		req := mcp.CallToolRequest{
 32 | 			Params: mcp.CallToolParams{
 33 | 				Arguments: map[string]interface{}{
 34 | 					"path": filePath,
 35 | 				},
 36 | 			},
 37 | 		}
 38 | 
 39 | 		res, err := fsHandler.HandleGetFileInfo(ctx, req)
 40 | 		require.NoError(t, err)
 41 | 		require.False(t, res.IsError)
 42 | 
 43 | 		// Verify the response contains file information
 44 | 		require.Len(t, res.Content, 2)
 45 | 		textContent := res.Content[0].(mcp.TextContent)
 46 | 		assert.Contains(t, textContent.Text, "File information for:")
 47 | 		assert.Contains(t, textContent.Text, filePath)
 48 | 		assert.Contains(t, textContent.Text, "IsFile: true")
 49 | 		assert.Contains(t, textContent.Text, "IsDirectory: false")
 50 | 		assert.Contains(t, textContent.Text, "Size: 13 bytes") // Length of "Hello, world!"
 51 | 	})
 52 | 
 53 | 	t.Run("get file info for a directory", func(t *testing.T) {
 54 | 		dirPath := filepath.Join(tmpDir, "test_directory")
 55 | 		err := os.Mkdir(dirPath, 0755)
 56 | 		require.NoError(t, err)
 57 | 
 58 | 		req := mcp.CallToolRequest{
 59 | 			Params: mcp.CallToolParams{
 60 | 				Arguments: map[string]interface{}{
 61 | 					"path": dirPath,
 62 | 				},
 63 | 			},
 64 | 		}
 65 | 
 66 | 		res, err := fsHandler.HandleGetFileInfo(ctx, req)
 67 | 		require.NoError(t, err)
 68 | 		require.False(t, res.IsError)
 69 | 
 70 | 		// Verify the response contains directory information
 71 | 		require.Len(t, res.Content, 2)
 72 | 		textContent := res.Content[0].(mcp.TextContent)
 73 | 		assert.Contains(t, textContent.Text, "File information for:")
 74 | 		assert.Contains(t, textContent.Text, dirPath)
 75 | 		assert.Contains(t, textContent.Text, "IsFile: false")
 76 | 		assert.Contains(t, textContent.Text, "IsDirectory: true")
 77 | 		assert.Contains(t, textContent.Text, "MIME Type: directory")
 78 | 	})
 79 | 
 80 | 	t.Run("file does not exist", func(t *testing.T) {
 81 | 		nonExistentPath := filepath.Join(tmpDir, "non_existent_file.txt")
 82 | 
 83 | 		req := mcp.CallToolRequest{
 84 | 			Params: mcp.CallToolParams{
 85 | 				Arguments: map[string]interface{}{
 86 | 					"path": nonExistentPath,
 87 | 				},
 88 | 			},
 89 | 		}
 90 | 
 91 | 		res, err := fsHandler.HandleGetFileInfo(ctx, req)
 92 | 		require.NoError(t, err)
 93 | 		require.True(t, res.IsError)
 94 | 	})
 95 | 
 96 | 	t.Run("path is in a non-allowed directory", func(t *testing.T) {
 97 | 		otherDir := t.TempDir()
 98 | 
 99 | 		req := mcp.CallToolRequest{
100 | 			Params: mcp.CallToolParams{
101 | 				Arguments: map[string]interface{}{
102 | 					"path": filepath.Join(otherDir, "some_file.txt"),
103 | 				},
104 | 			},
105 | 		}
106 | 
107 | 		res, err := fsHandler.HandleGetFileInfo(ctx, req)
108 | 		require.NoError(t, err)
109 | 		require.True(t, res.IsError)
110 | 	})
111 | }
112 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/resources.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"encoding/base64"
  6 | 	"fmt"
  7 | 	"os"
  8 | 	"path/filepath"
  9 | 	"strings"
 10 | 
 11 | 	"github.com/mark3labs/mcp-go/mcp"
 12 | )
 13 | 
 14 | // HandleReadResource handles the MCP resource reading functionality
 15 | func (fs *FilesystemHandler) HandleReadResource(
 16 | 	ctx context.Context,
 17 | 	request mcp.ReadResourceRequest,
 18 | ) ([]mcp.ResourceContents, error) {
 19 | 	uri := request.Params.URI
 20 | 
 21 | 	// Check if it's a file:// URI
 22 | 	if !strings.HasPrefix(uri, "file://") {
 23 | 		return nil, fmt.Errorf("unsupported URI scheme: %s", uri)
 24 | 	}
 25 | 
 26 | 	// Extract the path from the URI
 27 | 	path := strings.TrimPrefix(uri, "file://")
 28 | 
 29 | 	// Validate the path
 30 | 	validPath, err := fs.validatePath(path)
 31 | 	if err != nil {
 32 | 		return nil, err
 33 | 	}
 34 | 
 35 | 	// Get file info
 36 | 	fileInfo, err := os.Stat(validPath)
 37 | 	if err != nil {
 38 | 		return nil, err
 39 | 	}
 40 | 
 41 | 	// If it's a directory, return a listing
 42 | 	if fileInfo.IsDir() {
 43 | 		entries, err := os.ReadDir(validPath)
 44 | 		if err != nil {
 45 | 			return nil, err
 46 | 		}
 47 | 
 48 | 		var result strings.Builder
 49 | 		result.WriteString(fmt.Sprintf("Directory listing for: %s\n\n", validPath))
 50 | 
 51 | 		for _, entry := range entries {
 52 | 			entryPath := filepath.Join(validPath, entry.Name())
 53 | 			entryURI := pathToResourceURI(entryPath)
 54 | 
 55 | 			if entry.IsDir() {
 56 | 				result.WriteString(fmt.Sprintf("[DIR]  %s (%s)\n", entry.Name(), entryURI))
 57 | 			} else {
 58 | 				info, err := entry.Info()
 59 | 				if err == nil {
 60 | 					result.WriteString(fmt.Sprintf("[FILE] %s (%s) - %d bytes\n",
 61 | 						entry.Name(), entryURI, info.Size()))
 62 | 				} else {
 63 | 					result.WriteString(fmt.Sprintf("[FILE] %s (%s)\n", entry.Name(), entryURI))
 64 | 				}
 65 | 			}
 66 | 		}
 67 | 
 68 | 		return []mcp.ResourceContents{
 69 | 			mcp.TextResourceContents{
 70 | 				URI:      uri,
 71 | 				MIMEType: "text/plain",
 72 | 				Text:     result.String(),
 73 | 			},
 74 | 		}, nil
 75 | 	}
 76 | 
 77 | 	// It's a file, determine how to handle it
 78 | 	mimeType := detectMimeType(validPath)
 79 | 
 80 | 	// Check file size
 81 | 	if fileInfo.Size() > MAX_INLINE_SIZE {
 82 | 		// File is too large to inline, return a reference instead
 83 | 		return []mcp.ResourceContents{
 84 | 			mcp.TextResourceContents{
 85 | 				URI:      uri,
 86 | 				MIMEType: "text/plain",
 87 | 				Text:     fmt.Sprintf("File is too large to display inline (%d bytes). Use the read_file tool to access specific portions.", fileInfo.Size()),
 88 | 			},
 89 | 		}, nil
 90 | 	}
 91 | 
 92 | 	// Read the file content
 93 | 	content, err := os.ReadFile(validPath)
 94 | 	if err != nil {
 95 | 		return nil, err
 96 | 	}
 97 | 
 98 | 	// Handle based on content type
 99 | 	if isTextFile(mimeType) {
100 | 		// It's a text file, return as text
101 | 		return []mcp.ResourceContents{
102 | 			mcp.TextResourceContents{
103 | 				URI:      uri,
104 | 				MIMEType: mimeType,
105 | 				Text:     string(content),
106 | 			},
107 | 		}, nil
108 | 	} else {
109 | 		// It's a binary file
110 | 		if fileInfo.Size() <= MAX_BASE64_SIZE {
111 | 			// Small enough for base64 encoding
112 | 			return []mcp.ResourceContents{
113 | 				mcp.BlobResourceContents{
114 | 					URI:      uri,
115 | 					MIMEType: mimeType,
116 | 					Blob:     base64.StdEncoding.EncodeToString(content),
117 | 				},
118 | 			}, nil
119 | 		} else {
120 | 			// Too large for base64, return a reference
121 | 			return []mcp.ResourceContents{
122 | 				mcp.TextResourceContents{
123 | 					URI:      uri,
124 | 					MIMEType: "text/plain",
125 | 					Text:     fmt.Sprintf("Binary file (%s, %d bytes). Use the read_file tool to access specific portions.", mimeType, fileInfo.Size()),
126 | 				},
127 | 			}, nil
128 | 		}
129 | 	}
130 | }
```

--------------------------------------------------------------------------------
/filesystemserver/handler/copy_file_test.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"os"
  6 | 	"path/filepath"
  7 | 	"testing"
  8 | 
  9 | 	"github.com/mark3labs/mcp-go/mcp"
 10 | 	"github.com/stretchr/testify/assert"
 11 | 	"github.com/stretchr/testify/require"
 12 | )
 13 | 
 14 | func TestHandleCopyFile(t *testing.T) {
 15 | 	// Setup a temporary directory for the test
 16 | 	tmpDir := t.TempDir()
 17 | 
 18 | 	// Create a handler with the temp dir as an allowed path
 19 | 	allowedDirs := resolveAllowedDirs(t, tmpDir)
 20 | 	fsHandler, err := NewFilesystemHandler(allowedDirs)
 21 | 	require.NoError(t, err)
 22 | 
 23 | 	ctx := context.Background()
 24 | 
 25 | 	// Create a source file
 26 | 	sourceFilePath := filepath.Join(tmpDir, "source.txt")
 27 | 	err = os.WriteFile(sourceFilePath, []byte("hello world"), 0644)
 28 | 	require.NoError(t, err)
 29 | 
 30 | 	// Create a source directory
 31 | 	sourceDirPath := filepath.Join(tmpDir, "source_dir")
 32 | 	err = os.Mkdir(sourceDirPath, 0755)
 33 | 	require.NoError(t, err)
 34 | 
 35 | 	// Create a file inside the source directory
 36 | 	nestedFilePath := filepath.Join(sourceDirPath, "nested.txt")
 37 | 	err = os.WriteFile(nestedFilePath, []byte("nested hello"), 0644)
 38 | 	require.NoError(t, err)
 39 | 
 40 | 	t.Run("copy a single file", func(t *testing.T) {
 41 | 		destinationPath := filepath.Join(tmpDir, "destination.txt")
 42 | 		req := mcp.CallToolRequest{
 43 | 			Params: mcp.CallToolParams{
 44 | 				Arguments: map[string]interface{}{
 45 | 					"source":      sourceFilePath,
 46 | 					"destination": destinationPath,
 47 | 				},
 48 | 			},
 49 | 		}
 50 | 
 51 | 		res, err := fsHandler.HandleCopyFile(ctx, req)
 52 | 		require.NoError(t, err)
 53 | 		require.False(t, res.IsError)
 54 | 
 55 | 		// Verify the file was copied
 56 | 		_, err = os.Stat(destinationPath)
 57 | 		require.NoError(t, err)
 58 | 
 59 | 		content, err := os.ReadFile(destinationPath)
 60 | 		require.NoError(t, err)
 61 | 		assert.Equal(t, "hello world", string(content))
 62 | 	})
 63 | 
 64 | 	t.Run("copy a directory", func(t *testing.T) {
 65 | 		destinationPath := filepath.Join(tmpDir, "destination_dir")
 66 | 		req := mcp.CallToolRequest{
 67 | 			Params: mcp.CallToolParams{
 68 | 				Arguments: map[string]interface{}{
 69 | 					"source":      sourceDirPath,
 70 | 					"destination": destinationPath,
 71 | 				},
 72 | 			},
 73 | 		}
 74 | 
 75 | 		res, err := fsHandler.HandleCopyFile(ctx, req)
 76 | 		require.NoError(t, err)
 77 | 		require.False(t, res.IsError)
 78 | 
 79 | 		// Verify the directory was copied
 80 | 		_, err = os.Stat(destinationPath)
 81 | 		require.NoError(t, err)
 82 | 
 83 | 		// Verify the nested file was copied
 84 | 		nestedDestPath := filepath.Join(destinationPath, "nested.txt")
 85 | 		_, err = os.Stat(nestedDestPath)
 86 | 		require.NoError(t, err)
 87 | 
 88 | 		content, err := os.ReadFile(nestedDestPath)
 89 | 		require.NoError(t, err)
 90 | 		assert.Equal(t, "nested hello", string(content))
 91 | 	})
 92 | 
 93 | 	t.Run("source does not exist", func(t *testing.T) {
 94 | 		req := mcp.CallToolRequest{
 95 | 			Params: mcp.CallToolParams{
 96 | 				Arguments: map[string]interface{}{
 97 | 					"source":      filepath.Join(tmpDir, "non-existent-file.txt"),
 98 | 					"destination": filepath.Join(tmpDir, "destination.txt"),
 99 | 				},
100 | 			},
101 | 		}
102 | 
103 | 		res, err := fsHandler.HandleCopyFile(ctx, req)
104 | 		require.NoError(t, err)
105 | 		require.True(t, res.IsError)
106 | 	})
107 | 
108 | 	t.Run("destination is in a non-allowed directory", func(t *testing.T) {
109 | 		// Setup a temporary directory for the test
110 | 		otherDir := t.TempDir()
111 | 
112 | 		req := mcp.CallToolRequest{
113 | 			Params: mcp.CallToolParams{
114 | 				Arguments: map[string]interface{}{
115 | 					"source":      sourceFilePath,
116 | 					"destination": filepath.Join(otherDir, "destination.txt"),
117 | 				},
118 | 			},
119 | 		}
120 | 
121 | 		res, err := fsHandler.HandleCopyFile(ctx, req)
122 | 		require.NoError(t, err)
123 | 		require.True(t, res.IsError)
124 | 	})
125 | }
126 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/search_files.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"os"
  7 | 	"path/filepath"
  8 | 	"strings"
  9 | 
 10 | 	"github.com/gobwas/glob"
 11 | 	"github.com/mark3labs/mcp-go/mcp"
 12 | )
 13 | 
 14 | func (fs *FilesystemHandler) HandleSearchFiles(
 15 | 	ctx context.Context,
 16 | 	request mcp.CallToolRequest,
 17 | ) (*mcp.CallToolResult, error) {
 18 | 	path, err := request.RequireString("path")
 19 | 	if err != nil {
 20 | 		return nil, err
 21 | 	}
 22 | 	pattern, err := request.RequireString("pattern")
 23 | 	if err != nil {
 24 | 		return nil, err
 25 | 	}
 26 | 
 27 | 	// Handle empty or relative paths like "." or "./" by converting to absolute path
 28 | 	if path == "." || path == "./" {
 29 | 		// Get current working directory
 30 | 		cwd, err := os.Getwd()
 31 | 		if err != nil {
 32 | 			return &mcp.CallToolResult{
 33 | 				Content: []mcp.Content{
 34 | 					mcp.TextContent{
 35 | 						Type: "text",
 36 | 						Text: fmt.Sprintf("Error resolving current directory: %v", err),
 37 | 					},
 38 | 				},
 39 | 				IsError: true,
 40 | 			}, nil
 41 | 		}
 42 | 		path = cwd
 43 | 	}
 44 | 
 45 | 	validPath, err := fs.validatePath(path)
 46 | 	if err != nil {
 47 | 		return &mcp.CallToolResult{
 48 | 			Content: []mcp.Content{
 49 | 				mcp.TextContent{
 50 | 					Type: "text",
 51 | 					Text: fmt.Sprintf("Error: %v", err),
 52 | 				},
 53 | 			},
 54 | 			IsError: true,
 55 | 		}, nil
 56 | 	}
 57 | 
 58 | 	// Check if it's a directory
 59 | 	info, err := os.Stat(validPath)
 60 | 	if err != nil {
 61 | 		return &mcp.CallToolResult{
 62 | 			Content: []mcp.Content{
 63 | 				mcp.TextContent{
 64 | 					Type: "text",
 65 | 					Text: fmt.Sprintf("Error: %v", err),
 66 | 				},
 67 | 			},
 68 | 			IsError: true,
 69 | 		}, nil
 70 | 	}
 71 | 
 72 | 	if !info.IsDir() {
 73 | 		return &mcp.CallToolResult{
 74 | 			Content: []mcp.Content{
 75 | 				mcp.TextContent{
 76 | 					Type: "text",
 77 | 					Text: "Error: Search path must be a directory",
 78 | 				},
 79 | 			},
 80 | 			IsError: true,
 81 | 		}, nil
 82 | 	}
 83 | 
 84 | 	results, err := searchFiles(validPath, pattern, fs)
 85 | 	if err != nil {
 86 | 		return &mcp.CallToolResult{
 87 | 			Content: []mcp.Content{
 88 | 				mcp.TextContent{
 89 | 					Type: "text",
 90 | 					Text: fmt.Sprintf("Error searching files: %v",
 91 | 						err),
 92 | 				},
 93 | 			},
 94 | 			IsError: true,
 95 | 		}, nil
 96 | 	}
 97 | 
 98 | 	if len(results) == 0 {
 99 | 		return &mcp.CallToolResult{
100 | 			Content: []mcp.Content{
101 | 				mcp.TextContent{
102 | 					Type: "text",
103 | 					Text: fmt.Sprintf("No files found matching pattern '%s' in %s", pattern, path),
104 | 				},
105 | 			},
106 | 		}, nil
107 | 	}
108 | 
109 | 	// Format results with resource URIs
110 | 	var formattedResults strings.Builder
111 | 	formattedResults.WriteString(fmt.Sprintf("Found %d results:\n\n", len(results)))
112 | 
113 | 	for _, result := range results {
114 | 		resourceURI := pathToResourceURI(result)
115 | 		info, err := os.Stat(result)
116 | 		if err == nil {
117 | 			if info.IsDir() {
118 | 				formattedResults.WriteString(fmt.Sprintf("[DIR]  %s (%s)\n", result, resourceURI))
119 | 			} else {
120 | 				formattedResults.WriteString(fmt.Sprintf("[FILE] %s (%s) - %d bytes\n",
121 | 					result, resourceURI, info.Size()))
122 | 			}
123 | 		} else {
124 | 			formattedResults.WriteString(fmt.Sprintf("%s (%s)\n", result, resourceURI))
125 | 		}
126 | 	}
127 | 
128 | 	return &mcp.CallToolResult{
129 | 		Content: []mcp.Content{
130 | 			mcp.TextContent{
131 | 				Type: "text",
132 | 				Text: formattedResults.String(),
133 | 			},
134 | 		},
135 | 	}, nil
136 | }
137 | 
138 | func searchFiles(rootPath, pattern string, fs *FilesystemHandler) ([]string, error) {
139 | 	var results []string
140 | 	globPattern := glob.MustCompile(pattern)
141 | 
142 | 	err := filepath.Walk(
143 | 		rootPath,
144 | 		func(path string, info os.FileInfo, err error) error {
145 | 			if err != nil {
146 | 				return nil // Skip errors and continue
147 | 			}
148 | 
149 | 			// Try to validate path
150 | 			if _, err := fs.validatePath(path); err != nil {
151 | 				return nil // Skip invalid paths
152 | 			}
153 | 
154 | 			if globPattern.Match(info.Name()) {
155 | 				results = append(results, path)
156 | 			}
157 | 			return nil
158 | 		},
159 | 	)
160 | 	if err != nil {
161 | 		return nil, err
162 | 	}
163 | 	return results, nil
164 | }
```

--------------------------------------------------------------------------------
/filesystemserver/handler/move_file.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"os"
  7 | 	"path/filepath"
  8 | 
  9 | 	"github.com/mark3labs/mcp-go/mcp"
 10 | )
 11 | 
 12 | func (fs *FilesystemHandler) HandleMoveFile(
 13 | 	ctx context.Context,
 14 | 	request mcp.CallToolRequest,
 15 | ) (*mcp.CallToolResult, error) {
 16 | 	source, err := request.RequireString("source")
 17 | 	if err != nil {
 18 | 		return nil, err
 19 | 	}
 20 | 	destination, err := request.RequireString("destination")
 21 | 	if err != nil {
 22 | 		return nil, err
 23 | 	}
 24 | 
 25 | 	// Handle empty or relative paths for source
 26 | 	if source == "." || source == "./" {
 27 | 		// Get current working directory
 28 | 		cwd, err := os.Getwd()
 29 | 		if err != nil {
 30 | 			return &mcp.CallToolResult{
 31 | 				Content: []mcp.Content{
 32 | 					mcp.TextContent{
 33 | 						Type: "text",
 34 | 						Text: fmt.Sprintf("Error resolving current directory: %v", err),
 35 | 					},
 36 | 				},
 37 | 				IsError: true,
 38 | 			}, nil
 39 | 		}
 40 | 		source = cwd
 41 | 	}
 42 | 
 43 | 	// Handle empty or relative paths for destination
 44 | 	if destination == "." || destination == "./" {
 45 | 		// Get current working directory
 46 | 		cwd, err := os.Getwd()
 47 | 		if err != nil {
 48 | 			return &mcp.CallToolResult{
 49 | 				Content: []mcp.Content{
 50 | 					mcp.TextContent{
 51 | 						Type: "text",
 52 | 						Text: fmt.Sprintf("Error resolving current directory: %v", err),
 53 | 					},
 54 | 				},
 55 | 				IsError: true,
 56 | 			}, nil
 57 | 		}
 58 | 		destination = cwd
 59 | 	}
 60 | 
 61 | 	validSource, err := fs.validatePath(source)
 62 | 	if err != nil {
 63 | 		return &mcp.CallToolResult{
 64 | 			Content: []mcp.Content{
 65 | 				mcp.TextContent{
 66 | 					Type: "text",
 67 | 					Text: fmt.Sprintf("Error with source path: %v", err),
 68 | 				},
 69 | 			},
 70 | 			IsError: true,
 71 | 		}, nil
 72 | 	}
 73 | 
 74 | 	// Check if source exists
 75 | 	if _, err := os.Stat(validSource); os.IsNotExist(err) {
 76 | 		return &mcp.CallToolResult{
 77 | 			Content: []mcp.Content{
 78 | 				mcp.TextContent{
 79 | 					Type: "text",
 80 | 					Text: fmt.Sprintf("Error: Source does not exist: %s", source),
 81 | 				},
 82 | 			},
 83 | 			IsError: true,
 84 | 		}, nil
 85 | 	}
 86 | 
 87 | 	// For destination path, validate the parent directory first and create it if needed
 88 | 	destDir := filepath.Dir(destination)
 89 | 	validDestDir, err := fs.validatePath(destDir)
 90 | 	if err != nil {
 91 | 		return &mcp.CallToolResult{
 92 | 			Content: []mcp.Content{
 93 | 				mcp.TextContent{
 94 | 					Type: "text",
 95 | 					Text: fmt.Sprintf("Error with destination directory path: %v", err),
 96 | 				},
 97 | 			},
 98 | 			IsError: true,
 99 | 		}, nil
100 | 	}
101 | 
102 | 	// Create parent directory for destination if it doesn't exist
103 | 	if err := os.MkdirAll(validDestDir, 0755); err != nil {
104 | 		return &mcp.CallToolResult{
105 | 			Content: []mcp.Content{
106 | 				mcp.TextContent{
107 | 					Type: "text",
108 | 					Text: fmt.Sprintf("Error creating destination directory: %v", err),
109 | 				},
110 | 			},
111 | 			IsError: true,
112 | 		}, nil
113 | 	}
114 | 
115 | 	// Now validate the full destination path
116 | 	validDest, err := fs.validatePath(destination)
117 | 	if err != nil {
118 | 		return &mcp.CallToolResult{
119 | 			Content: []mcp.Content{
120 | 				mcp.TextContent{
121 | 					Type: "text",
122 | 					Text: fmt.Sprintf("Error with destination path: %v", err),
123 | 				},
124 | 			},
125 | 			IsError: true,
126 | 		}, nil
127 | 	}
128 | 
129 | 	if err := os.Rename(validSource, validDest); err != nil {
130 | 		return &mcp.CallToolResult{
131 | 			Content: []mcp.Content{
132 | 				mcp.TextContent{
133 | 					Type: "text",
134 | 					Text: fmt.Sprintf("Error moving file: %v", err),
135 | 				},
136 | 			},
137 | 			IsError: true,
138 | 		}, nil
139 | 	}
140 | 
141 | 	resourceURI := pathToResourceURI(validDest)
142 | 	return &mcp.CallToolResult{
143 | 		Content: []mcp.Content{
144 | 			mcp.TextContent{
145 | 				Type: "text",
146 | 				Text: fmt.Sprintf(
147 | 					"Successfully moved %s to %s",
148 | 					source,
149 | 					destination,
150 | 				),
151 | 			},
152 | 			mcp.EmbeddedResource{
153 | 				Type: "resource",
154 | 				Resource: mcp.TextResourceContents{
155 | 					URI:      resourceURI,
156 | 					MIMEType: "text/plain",
157 | 					Text:     fmt.Sprintf("Moved file: %s", validDest),
158 | 				},
159 | 			},
160 | 		},
161 | 	}, nil
162 | }
163 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/list_directory_test.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"os"
  6 | 	"path/filepath"
  7 | 	"testing"
  8 | 
  9 | 	"github.com/mark3labs/mcp-go/mcp"
 10 | 	"github.com/stretchr/testify/assert"
 11 | 	"github.com/stretchr/testify/require"
 12 | )
 13 | 
 14 | func TestHandleListDirectory(t *testing.T) {
 15 | 	// Setup a temporary directory for the test
 16 | 	tmpDir := t.TempDir()
 17 | 
 18 | 	// Create a handler with the temp dir as an allowed path
 19 | 	allowedDirs := resolveAllowedDirs(t, tmpDir)
 20 | 	fsHandler, err := NewFilesystemHandler(allowedDirs)
 21 | 	require.NoError(t, err)
 22 | 
 23 | 	ctx := context.Background()
 24 | 
 25 | 	// Create test directory structure
 26 | 	subDir := filepath.Join(tmpDir, "subdirectory")
 27 | 	err = os.Mkdir(subDir, 0755)
 28 | 	require.NoError(t, err)
 29 | 
 30 | 	testFile := filepath.Join(tmpDir, "test_file.txt")
 31 | 	err = os.WriteFile(testFile, []byte("hello world"), 0644)
 32 | 	require.NoError(t, err)
 33 | 
 34 | 	t.Run("list directory with files and subdirectories", func(t *testing.T) {
 35 | 		req := mcp.CallToolRequest{
 36 | 			Params: mcp.CallToolParams{
 37 | 				Arguments: map[string]interface{}{
 38 | 					"path": tmpDir,
 39 | 				},
 40 | 			},
 41 | 		}
 42 | 
 43 | 		res, err := fsHandler.HandleListDirectory(ctx, req)
 44 | 		require.NoError(t, err)
 45 | 		require.False(t, res.IsError)
 46 | 
 47 | 		// Verify the response contains directory listing
 48 | 		require.Len(t, res.Content, 2)
 49 | 		textContent := res.Content[0].(mcp.TextContent)
 50 | 		assert.Contains(t, textContent.Text, "Directory listing for:")
 51 | 		assert.Contains(t, textContent.Text, tmpDir)
 52 | 		assert.Contains(t, textContent.Text, "[DIR]  subdirectory")
 53 | 		assert.Contains(t, textContent.Text, "[FILE] test_file.txt")
 54 | 		assert.Contains(t, textContent.Text, "11 bytes") // Length of "hello world"
 55 | 		assert.Contains(t, textContent.Text, "file://")
 56 | 
 57 | 		// Verify embedded resource
 58 | 		embeddedResource := res.Content[1].(mcp.EmbeddedResource)
 59 | 		assert.Equal(t, "resource", embeddedResource.Type)
 60 | 	})
 61 | 
 62 | 	t.Run("list empty directory", func(t *testing.T) {
 63 | 		emptyDir := filepath.Join(tmpDir, "empty_directory")
 64 | 		err := os.Mkdir(emptyDir, 0755)
 65 | 		require.NoError(t, err)
 66 | 
 67 | 		req := mcp.CallToolRequest{
 68 | 			Params: mcp.CallToolParams{
 69 | 				Arguments: map[string]interface{}{
 70 | 					"path": emptyDir,
 71 | 				},
 72 | 			},
 73 | 		}
 74 | 
 75 | 		res, err := fsHandler.HandleListDirectory(ctx, req)
 76 | 		require.NoError(t, err)
 77 | 		require.False(t, res.IsError)
 78 | 
 79 | 		// Verify the response contains directory listing for empty directory
 80 | 		require.Len(t, res.Content, 2)
 81 | 		textContent := res.Content[0].(mcp.TextContent)
 82 | 		assert.Contains(t, textContent.Text, "Directory listing for:")
 83 | 		assert.Contains(t, textContent.Text, emptyDir)
 84 | 	})
 85 | 
 86 | 	t.Run("try to list a file instead of directory", func(t *testing.T) {
 87 | 		req := mcp.CallToolRequest{
 88 | 			Params: mcp.CallToolParams{
 89 | 				Arguments: map[string]interface{}{
 90 | 					"path": testFile,
 91 | 				},
 92 | 			},
 93 | 		}
 94 | 
 95 | 		res, err := fsHandler.HandleListDirectory(ctx, req)
 96 | 		require.NoError(t, err)
 97 | 		require.True(t, res.IsError)
 98 | 
 99 | 		// Verify error message
100 | 		require.Len(t, res.Content, 1)
101 | 		textContent := res.Content[0].(mcp.TextContent)
102 | 		assert.Contains(t, textContent.Text, "Path is not a directory")
103 | 	})
104 | 
105 | 	t.Run("try to list non-existent directory", func(t *testing.T) {
106 | 		nonExistentPath := filepath.Join(tmpDir, "non_existent_directory")
107 | 
108 | 		req := mcp.CallToolRequest{
109 | 			Params: mcp.CallToolParams{
110 | 				Arguments: map[string]interface{}{
111 | 					"path": nonExistentPath,
112 | 				},
113 | 			},
114 | 		}
115 | 
116 | 		res, err := fsHandler.HandleListDirectory(ctx, req)
117 | 		require.NoError(t, err)
118 | 		require.True(t, res.IsError)
119 | 	})
120 | 
121 | 	t.Run("path is in a non-allowed directory", func(t *testing.T) {
122 | 		otherDir := t.TempDir()
123 | 
124 | 		req := mcp.CallToolRequest{
125 | 			Params: mcp.CallToolParams{
126 | 				Arguments: map[string]interface{}{
127 | 					"path": otherDir,
128 | 				},
129 | 			},
130 | 		}
131 | 
132 | 		res, err := fsHandler.HandleListDirectory(ctx, req)
133 | 		require.NoError(t, err)
134 | 		require.True(t, res.IsError)
135 | 	})
136 | }
137 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/helper.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"mime"
  6 | 	"os"
  7 | 	"path/filepath"
  8 | 	"slices"
  9 | 	"strings"
 10 | 
 11 | 	"github.com/gabriel-vasile/mimetype"
 12 | )
 13 | 
 14 | // isPathInAllowedDirs checks if a path is within any of the allowed directories
 15 | func (fs *FilesystemHandler) isPathInAllowedDirs(path string) bool {
 16 | 	// Ensure path is absolute and clean
 17 | 	absPath, err := filepath.Abs(path)
 18 | 	if err != nil {
 19 | 		return false
 20 | 	}
 21 | 
 22 | 	// Add trailing separator to ensure we're checking a directory or a file within a directory
 23 | 	// and not a prefix match (e.g., /tmp/foo should not match /tmp/foobar)
 24 | 	if !strings.HasSuffix(absPath, string(filepath.Separator)) {
 25 | 		// If it's a file, we need to check its directory
 26 | 		if info, err := os.Stat(absPath); err == nil && !info.IsDir() {
 27 | 			absPath = filepath.Dir(absPath) + string(filepath.Separator)
 28 | 		} else {
 29 | 			absPath = absPath + string(filepath.Separator)
 30 | 		}
 31 | 	}
 32 | 
 33 | 	// Check if the path is within any of the allowed directories
 34 | 	for _, dir := range fs.allowedDirs {
 35 | 		if strings.HasPrefix(absPath, dir) {
 36 | 			return true
 37 | 		}
 38 | 	}
 39 | 	return false
 40 | }
 41 | 
 42 | func (fs *FilesystemHandler) validatePath(requestedPath string) (string, error) {
 43 | 	// Always convert to absolute path first
 44 | 	abs, err := filepath.Abs(requestedPath)
 45 | 	if err != nil {
 46 | 		return "", fmt.Errorf("invalid path: %w", err)
 47 | 	}
 48 | 
 49 | 	// Check if path is within allowed directories
 50 | 	if !fs.isPathInAllowedDirs(abs) {
 51 | 		return "", fmt.Errorf(
 52 | 			"access denied - path outside allowed directories: %s",
 53 | 			abs,
 54 | 		)
 55 | 	}
 56 | 
 57 | 	// Handle symlinks
 58 | 	realPath, err := filepath.EvalSymlinks(abs)
 59 | 	if err != nil {
 60 | 		if !os.IsNotExist(err) {
 61 | 			return "", err
 62 | 		}
 63 | 		// For new files, check parent directory
 64 | 		parent := filepath.Dir(abs)
 65 | 		realParent, err := filepath.EvalSymlinks(parent)
 66 | 		if err != nil {
 67 | 			return "", fmt.Errorf("parent directory does not exist: %s", parent)
 68 | 		}
 69 | 
 70 | 		if !fs.isPathInAllowedDirs(realParent) {
 71 | 			return "", fmt.Errorf(
 72 | 				"access denied - parent directory outside allowed directories",
 73 | 			)
 74 | 		}
 75 | 		return abs, nil
 76 | 	}
 77 | 
 78 | 	// Check if the real path (after resolving symlinks) is still within allowed directories
 79 | 	if !fs.isPathInAllowedDirs(realPath) {
 80 | 		return "", fmt.Errorf(
 81 | 			"access denied - symlink target outside allowed directories",
 82 | 		)
 83 | 	}
 84 | 
 85 | 	return realPath, nil
 86 | }
 87 | 
 88 | // detectMimeType tries to determine the MIME type of a file
 89 | func detectMimeType(path string) string {
 90 | 	// Use mimetype library for more accurate detection
 91 | 	mtype, err := mimetype.DetectFile(path)
 92 | 	if err != nil {
 93 | 		// Fallback to extension-based detection if file can't be read
 94 | 		ext := filepath.Ext(path)
 95 | 		if ext != "" {
 96 | 			mimeType := mime.TypeByExtension(ext)
 97 | 			if mimeType != "" {
 98 | 				return mimeType
 99 | 			}
100 | 		}
101 | 		return "application/octet-stream" // Default
102 | 	}
103 | 
104 | 	return mtype.String()
105 | }
106 | 
107 | // isTextFile determines if a file is likely a text file based on MIME type
108 | func isTextFile(mimeType string) bool {
109 | 	// Check for common text MIME types
110 | 	if strings.HasPrefix(mimeType, "text/") {
111 | 		return true
112 | 	}
113 | 
114 | 	// Common application types that are text-based
115 | 	textApplicationTypes := []string{
116 | 		"application/json",
117 | 		"application/xml",
118 | 		"application/javascript",
119 | 		"application/x-javascript",
120 | 		"application/typescript",
121 | 		"application/x-typescript",
122 | 		"application/x-yaml",
123 | 		"application/yaml",
124 | 		"application/toml",
125 | 		"application/x-sh",
126 | 		"application/x-shellscript",
127 | 	}
128 | 
129 | 	if slices.Contains(textApplicationTypes, mimeType) {
130 | 		return true
131 | 	}
132 | 
133 | 	// Check for +format types
134 | 	if strings.Contains(mimeType, "+xml") ||
135 | 		strings.Contains(mimeType, "+json") ||
136 | 		strings.Contains(mimeType, "+yaml") {
137 | 		return true
138 | 	}
139 | 
140 | 	// Common code file types that might be misidentified
141 | 	if strings.HasPrefix(mimeType, "text/x-") {
142 | 		return true
143 | 	}
144 | 
145 | 	if strings.HasPrefix(mimeType, "application/x-") &&
146 | 		(strings.Contains(mimeType, "script") ||
147 | 			strings.Contains(mimeType, "source") ||
148 | 			strings.Contains(mimeType, "code")) {
149 | 		return true
150 | 	}
151 | 
152 | 	return false
153 | }
154 | 
155 | // isImageFile determines if a file is an image based on MIME type
156 | func isImageFile(mimeType string) bool {
157 | 	return strings.HasPrefix(mimeType, "image/") ||
158 | 		(mimeType == "application/xml" && strings.HasSuffix(strings.ToLower(mimeType), ".svg"))
159 | }
160 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/delete_file_test.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"os"
  6 | 	"path/filepath"
  7 | 	"testing"
  8 | 
  9 | 	"github.com/mark3labs/mcp-go/mcp"
 10 | 	"github.com/stretchr/testify/assert"
 11 | 	"github.com/stretchr/testify/require"
 12 | )
 13 | 
 14 | func TestHandleDeleteFile(t *testing.T) {
 15 | 	// Setup a temporary directory for the test
 16 | 	tmpDir := t.TempDir()
 17 | 
 18 | 	// Create a handler with the temp dir as an allowed path
 19 | 	allowedDirs := resolveAllowedDirs(t, tmpDir)
 20 | 	fsHandler, err := NewFilesystemHandler(allowedDirs)
 21 | 	require.NoError(t, err)
 22 | 
 23 | 	ctx := context.Background()
 24 | 
 25 | 	t.Run("delete a file", func(t *testing.T) {
 26 | 		filePath := filepath.Join(tmpDir, "test_file.txt")
 27 | 		err := os.WriteFile(filePath, []byte("test content"), 0644)
 28 | 		require.NoError(t, err)
 29 | 
 30 | 		// Verify file exists before deletion
 31 | 		_, err = os.Stat(filePath)
 32 | 		require.NoError(t, err)
 33 | 
 34 | 		req := mcp.CallToolRequest{
 35 | 			Params: mcp.CallToolParams{
 36 | 				Arguments: map[string]interface{}{
 37 | 					"path": filePath,
 38 | 				},
 39 | 			},
 40 | 		}
 41 | 
 42 | 		res, err := fsHandler.HandleDeleteFile(ctx, req)
 43 | 		require.NoError(t, err)
 44 | 		require.False(t, res.IsError)
 45 | 
 46 | 		// Verify file was deleted
 47 | 		_, err = os.Stat(filePath)
 48 | 		assert.True(t, os.IsNotExist(err))
 49 | 	})
 50 | 
 51 | 	t.Run("delete an empty directory with recursive=true", func(t *testing.T) {
 52 | 		dirPath := filepath.Join(tmpDir, "empty_directory")
 53 | 		err := os.Mkdir(dirPath, 0755)
 54 | 		require.NoError(t, err)
 55 | 
 56 | 		// Verify directory exists before deletion
 57 | 		_, err = os.Stat(dirPath)
 58 | 		require.NoError(t, err)
 59 | 
 60 | 		req := mcp.CallToolRequest{
 61 | 			Params: mcp.CallToolParams{
 62 | 				Arguments: map[string]interface{}{
 63 | 					"path":      dirPath,
 64 | 					"recursive": true,
 65 | 				},
 66 | 			},
 67 | 		}
 68 | 
 69 | 		res, err := fsHandler.HandleDeleteFile(ctx, req)
 70 | 		require.NoError(t, err)
 71 | 		require.False(t, res.IsError)
 72 | 
 73 | 		// Verify directory was deleted
 74 | 		_, err = os.Stat(dirPath)
 75 | 		assert.True(t, os.IsNotExist(err))
 76 | 	})
 77 | 
 78 | 	t.Run("delete a directory with contents using recursive=true", func(t *testing.T) {
 79 | 		dirPath := filepath.Join(tmpDir, "directory_with_contents")
 80 | 		err := os.Mkdir(dirPath, 0755)
 81 | 		require.NoError(t, err)
 82 | 
 83 | 		// Create a file inside the directory
 84 | 		filePath := filepath.Join(dirPath, "nested_file.txt")
 85 | 		err = os.WriteFile(filePath, []byte("nested content"), 0644)
 86 | 		require.NoError(t, err)
 87 | 
 88 | 		// Create a subdirectory
 89 | 		subDirPath := filepath.Join(dirPath, "subdirectory")
 90 | 		err = os.Mkdir(subDirPath, 0755)
 91 | 		require.NoError(t, err)
 92 | 
 93 | 		// Verify directory and contents exist before deletion
 94 | 		_, err = os.Stat(dirPath)
 95 | 		require.NoError(t, err)
 96 | 		_, err = os.Stat(filePath)
 97 | 		require.NoError(t, err)
 98 | 		_, err = os.Stat(subDirPath)
 99 | 		require.NoError(t, err)
100 | 
101 | 		req := mcp.CallToolRequest{
102 | 			Params: mcp.CallToolParams{
103 | 				Arguments: map[string]interface{}{
104 | 					"path":      dirPath,
105 | 					"recursive": true,
106 | 				},
107 | 			},
108 | 		}
109 | 
110 | 		res, err := fsHandler.HandleDeleteFile(ctx, req)
111 | 		require.NoError(t, err)
112 | 		require.False(t, res.IsError)
113 | 
114 | 		// Verify directory and all contents were deleted
115 | 		_, err = os.Stat(dirPath)
116 | 		assert.True(t, os.IsNotExist(err))
117 | 	})
118 | 
119 | 	t.Run("try to delete directory without recursive flag", func(t *testing.T) {
120 | 		dirPath := filepath.Join(tmpDir, "directory_no_recursive")
121 | 		err := os.Mkdir(dirPath, 0755)
122 | 		require.NoError(t, err)
123 | 
124 | 		req := mcp.CallToolRequest{
125 | 			Params: mcp.CallToolParams{
126 | 				Arguments: map[string]interface{}{
127 | 					"path": dirPath,
128 | 				},
129 | 			},
130 | 		}
131 | 
132 | 		res, err := fsHandler.HandleDeleteFile(ctx, req)
133 | 		require.NoError(t, err)
134 | 		require.True(t, res.IsError)
135 | 
136 | 		// Verify directory still exists
137 | 		_, err = os.Stat(dirPath)
138 | 		require.NoError(t, err)
139 | 	})
140 | 
141 | 	t.Run("try to delete non-existent file", func(t *testing.T) {
142 | 		nonExistentPath := filepath.Join(tmpDir, "non_existent_file.txt")
143 | 
144 | 		req := mcp.CallToolRequest{
145 | 			Params: mcp.CallToolParams{
146 | 				Arguments: map[string]interface{}{
147 | 					"path": nonExistentPath,
148 | 				},
149 | 			},
150 | 		}
151 | 
152 | 		res, err := fsHandler.HandleDeleteFile(ctx, req)
153 | 		require.NoError(t, err)
154 | 		require.True(t, res.IsError)
155 | 	})
156 | 
157 | 	t.Run("path is in a non-allowed directory", func(t *testing.T) {
158 | 		otherDir := t.TempDir()
159 | 
160 | 		req := mcp.CallToolRequest{
161 | 			Params: mcp.CallToolParams{
162 | 				Arguments: map[string]interface{}{
163 | 					"path": filepath.Join(otherDir, "some_file.txt"),
164 | 				},
165 | 			},
166 | 		}
167 | 
168 | 		res, err := fsHandler.HandleDeleteFile(ctx, req)
169 | 		require.NoError(t, err)
170 | 		require.True(t, res.IsError)
171 | 	})
172 | }
173 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/read_multiple_files.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"encoding/base64"
  6 | 	"fmt"
  7 | 	"os"
  8 | 
  9 | 	"github.com/mark3labs/mcp-go/mcp"
 10 | )
 11 | 
 12 | func (fs *FilesystemHandler) HandleReadMultipleFiles(
 13 | 	ctx context.Context,
 14 | 	request mcp.CallToolRequest,
 15 | ) (*mcp.CallToolResult, error) {
 16 | 	pathsSlice, err := request.RequireStringSlice("paths")
 17 | 	if err != nil {
 18 | 		return nil, err
 19 | 	}
 20 | 
 21 | 	if len(pathsSlice) == 0 {
 22 | 		return &mcp.CallToolResult{
 23 | 			Content: []mcp.Content{
 24 | 				mcp.TextContent{
 25 | 					Type: "text",
 26 | 					Text: "No files specified to read",
 27 | 				},
 28 | 			},
 29 | 			IsError: true,
 30 | 		}, nil
 31 | 	}
 32 | 
 33 | 	// Maximum number of files to read in a single request
 34 | 	const maxFiles = 50
 35 | 	if len(pathsSlice) > maxFiles {
 36 | 		return &mcp.CallToolResult{
 37 | 			Content: []mcp.Content{
 38 | 				mcp.TextContent{
 39 | 					Type: "text",
 40 | 					Text: fmt.Sprintf("Too many files requested. Maximum is %d files per request.", maxFiles),
 41 | 				},
 42 | 			},
 43 | 			IsError: true,
 44 | 		}, nil
 45 | 	}
 46 | 
 47 | 	// Process each file
 48 | 	var results []mcp.Content
 49 | 	for _, path := range pathsSlice {
 50 | 		// Handle empty or relative paths like "." or "./" by converting to absolute path
 51 | 		if path == "." || path == "./" {
 52 | 			// Get current working directory
 53 | 			cwd, err := os.Getwd()
 54 | 			if err != nil {
 55 | 				results = append(results, mcp.TextContent{
 56 | 					Type: "text",
 57 | 					Text: fmt.Sprintf("Error resolving current directory for path '%s': %v", path, err),
 58 | 				})
 59 | 				continue
 60 | 			}
 61 | 			path = cwd
 62 | 		}
 63 | 
 64 | 		validPath, err := fs.validatePath(path)
 65 | 		if err != nil {
 66 | 			results = append(results, mcp.TextContent{
 67 | 				Type: "text",
 68 | 				Text: fmt.Sprintf("Error with path '%s': %v", path, err),
 69 | 			})
 70 | 			continue
 71 | 		}
 72 | 
 73 | 		// Check if it's a directory
 74 | 		info, err := os.Stat(validPath)
 75 | 		if err != nil {
 76 | 			results = append(results, mcp.TextContent{
 77 | 				Type: "text",
 78 | 				Text: fmt.Sprintf("Error accessing '%s': %v", path, err),
 79 | 			})
 80 | 			continue
 81 | 		}
 82 | 
 83 | 		if info.IsDir() {
 84 | 			// For directories, return a resource reference instead
 85 | 			resourceURI := pathToResourceURI(validPath)
 86 | 			results = append(results, mcp.TextContent{
 87 | 				Type: "text",
 88 | 				Text: fmt.Sprintf("'%s' is a directory. Use list_directory tool or resource URI: %s", path, resourceURI),
 89 | 			})
 90 | 			continue
 91 | 		}
 92 | 
 93 | 		// Determine MIME type
 94 | 		mimeType := detectMimeType(validPath)
 95 | 
 96 | 		// Check file size
 97 | 		if info.Size() > MAX_INLINE_SIZE {
 98 | 			// File is too large to inline, return a resource reference
 99 | 			resourceURI := pathToResourceURI(validPath)
100 | 			results = append(results, mcp.TextContent{
101 | 				Type: "text",
102 | 				Text: fmt.Sprintf("File '%s' is too large to display inline (%d bytes). Access it via resource URI: %s",
103 | 					path, info.Size(), resourceURI),
104 | 			})
105 | 			continue
106 | 		}
107 | 
108 | 		// Read file content
109 | 		content, err := os.ReadFile(validPath)
110 | 		if err != nil {
111 | 			results = append(results, mcp.TextContent{
112 | 				Type: "text",
113 | 				Text: fmt.Sprintf("Error reading file '%s': %v", path, err),
114 | 			})
115 | 			continue
116 | 		}
117 | 
118 | 		// Add file header
119 | 		results = append(results, mcp.TextContent{
120 | 			Type: "text",
121 | 			Text: fmt.Sprintf("--- File: %s ---", path),
122 | 		})
123 | 
124 | 		// Check if it's a text file
125 | 		if isTextFile(mimeType) {
126 | 			// It's a text file, return as text
127 | 			results = append(results, mcp.TextContent{
128 | 				Type: "text",
129 | 				Text: string(content),
130 | 			})
131 | 		} else if isImageFile(mimeType) {
132 | 			// It's an image file, return as image content
133 | 			if info.Size() <= MAX_BASE64_SIZE {
134 | 				results = append(results, mcp.TextContent{
135 | 					Type: "text",
136 | 					Text: fmt.Sprintf("Image file: %s (%s, %d bytes)", path, mimeType, info.Size()),
137 | 				})
138 | 				results = append(results, mcp.ImageContent{
139 | 					Type:     "image",
140 | 					Data:     base64.StdEncoding.EncodeToString(content),
141 | 					MIMEType: mimeType,
142 | 				})
143 | 			} else {
144 | 				// Too large for base64, return a reference
145 | 				resourceURI := pathToResourceURI(validPath)
146 | 				results = append(results, mcp.TextContent{
147 | 					Type: "text",
148 | 					Text: fmt.Sprintf("Image file '%s' is too large to display inline (%d bytes). Access it via resource URI: %s",
149 | 						path, info.Size(), resourceURI),
150 | 				})
151 | 			}
152 | 		} else {
153 | 			// It's another type of binary file
154 | 			resourceURI := pathToResourceURI(validPath)
155 | 
156 | 			if info.Size() <= MAX_BASE64_SIZE {
157 | 				// Small enough for base64 encoding
158 | 				results = append(results, mcp.TextContent{
159 | 					Type: "text",
160 | 					Text: fmt.Sprintf("Binary file: %s (%s, %d bytes)", path, mimeType, info.Size()),
161 | 				})
162 | 				results = append(results, mcp.EmbeddedResource{
163 | 					Type: "resource",
164 | 					Resource: mcp.BlobResourceContents{
165 | 						URI:      resourceURI,
166 | 						MIMEType: mimeType,
167 | 						Blob:     base64.StdEncoding.EncodeToString(content),
168 | 					},
169 | 				})
170 | 			} else {
171 | 				// Too large for base64, return a reference
172 | 				results = append(results, mcp.TextContent{
173 | 					Type: "text",
174 | 					Text: fmt.Sprintf("Binary file '%s' (%s, %d bytes). Access it via resource URI: %s",
175 | 						path, mimeType, info.Size(), resourceURI),
176 | 				})
177 | 			}
178 | 		}
179 | 	}
180 | 
181 | 	return &mcp.CallToolResult{
182 | 		Content: results,
183 | 	}, nil
184 | }
```

--------------------------------------------------------------------------------
/filesystemserver/handler/tree.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"encoding/json"
  6 | 	"fmt"
  7 | 	"os"
  8 | 	"path/filepath"
  9 | 
 10 | 	"github.com/mark3labs/mcp-go/mcp"
 11 | )
 12 | 
 13 | func (fs *FilesystemHandler) HandleTree(
 14 | 	ctx context.Context,
 15 | 	request mcp.CallToolRequest,
 16 | ) (*mcp.CallToolResult, error) {
 17 | 	path, err := request.RequireString("path")
 18 | 	if err != nil {
 19 | 		return nil, err
 20 | 	}
 21 | 
 22 | 	// Handle empty or relative paths like "." or "./" by converting to absolute path
 23 | 	if path == "." || path == "./" {
 24 | 		// Get current working directory
 25 | 		cwd, err := os.Getwd()
 26 | 		if err != nil {
 27 | 			return &mcp.CallToolResult{
 28 | 				Content: []mcp.Content{
 29 | 					mcp.TextContent{
 30 | 						Type: "text",
 31 | 						Text: fmt.Sprintf("Error resolving current directory: %v", err),
 32 | 					},
 33 | 				},
 34 | 				IsError: true,
 35 | 			}, nil
 36 | 		}
 37 | 		path = cwd
 38 | 	}
 39 | 
 40 | 	// Extract depth parameter (optional, default: 3)
 41 | 	depth := 3 // Default value
 42 | 	if depthParam, err := request.RequireFloat("depth"); err == nil {
 43 | 		depth = int(depthParam)
 44 | 	}
 45 | 
 46 | 	// Extract follow_symlinks parameter (optional, default: false)
 47 | 	followSymlinks := false // Default value
 48 | 	if followParam, err := request.RequireBool("follow_symlinks"); err == nil {
 49 | 		followSymlinks = followParam
 50 | 	}
 51 | 
 52 | 	// Validate the path is within allowed directories
 53 | 	validPath, err := fs.validatePath(path)
 54 | 	if err != nil {
 55 | 		return &mcp.CallToolResult{
 56 | 			Content: []mcp.Content{
 57 | 				mcp.TextContent{
 58 | 					Type: "text",
 59 | 					Text: fmt.Sprintf("Error: %v", err),
 60 | 				},
 61 | 			},
 62 | 			IsError: true,
 63 | 		}, nil
 64 | 	}
 65 | 
 66 | 	// Check if it's a directory
 67 | 	info, err := os.Stat(validPath)
 68 | 	if err != nil {
 69 | 		return &mcp.CallToolResult{
 70 | 			Content: []mcp.Content{
 71 | 				mcp.TextContent{
 72 | 					Type: "text",
 73 | 					Text: fmt.Sprintf("Error: %v", err),
 74 | 				},
 75 | 			},
 76 | 			IsError: true,
 77 | 		}, nil
 78 | 	}
 79 | 
 80 | 	if !info.IsDir() {
 81 | 		return &mcp.CallToolResult{
 82 | 			Content: []mcp.Content{
 83 | 				mcp.TextContent{
 84 | 					Type: "text",
 85 | 					Text: "Error: The specified path is not a directory",
 86 | 				},
 87 | 			},
 88 | 			IsError: true,
 89 | 		}, nil
 90 | 	}
 91 | 
 92 | 	// Build the tree structure
 93 | 	tree, err := fs.buildTree(validPath, depth, 0, followSymlinks)
 94 | 	if err != nil {
 95 | 		return &mcp.CallToolResult{
 96 | 			Content: []mcp.Content{
 97 | 				mcp.TextContent{
 98 | 					Type: "text",
 99 | 					Text: fmt.Sprintf("Error building directory tree: %v", err),
100 | 				},
101 | 			},
102 | 			IsError: true,
103 | 		}, nil
104 | 	}
105 | 
106 | 	// Convert to JSON
107 | 	jsonData, err := json.MarshalIndent(tree, "", "  ")
108 | 	if err != nil {
109 | 		return &mcp.CallToolResult{
110 | 			Content: []mcp.Content{
111 | 				mcp.TextContent{
112 | 					Type: "text",
113 | 					Text: fmt.Sprintf("Error generating JSON: %v", err),
114 | 				},
115 | 			},
116 | 			IsError: true,
117 | 		}, nil
118 | 	}
119 | 
120 | 	// Create resource URI for the directory
121 | 	resourceURI := pathToResourceURI(validPath)
122 | 
123 | 	// Return the result
124 | 	return &mcp.CallToolResult{
125 | 		Content: []mcp.Content{
126 | 			mcp.TextContent{
127 | 				Type: "text",
128 | 				Text: fmt.Sprintf("Directory tree for %s (max depth: %d):\n\n%s", validPath, depth, string(jsonData)),
129 | 			},
130 | 			mcp.EmbeddedResource{
131 | 				Type: "resource",
132 | 				Resource: mcp.TextResourceContents{
133 | 					URI:      resourceURI,
134 | 					MIMEType: "application/json",
135 | 					Text:     string(jsonData),
136 | 				},
137 | 			},
138 | 		},
139 | 	}, nil
140 | }
141 | 
142 | // buildTree builds a tree representation of the filesystem starting at the given path
143 | func (fs *FilesystemHandler) buildTree(path string, maxDepth int, currentDepth int, followSymlinks bool) (*FileNode, error) {
144 | 	// Validate the path
145 | 	validPath, err := fs.validatePath(path)
146 | 	if err != nil {
147 | 		return nil, err
148 | 	}
149 | 
150 | 	// Get file info
151 | 	info, err := os.Stat(validPath)
152 | 	if err != nil {
153 | 		return nil, err
154 | 	}
155 | 
156 | 	// Create the node
157 | 	node := &FileNode{
158 | 		Name:     filepath.Base(validPath),
159 | 		Path:     validPath,
160 | 		Modified: info.ModTime(),
161 | 	}
162 | 
163 | 	// Set type and size
164 | 	if info.IsDir() {
165 | 		node.Type = "directory"
166 | 
167 | 		// If we haven't reached the max depth, process children
168 | 		if currentDepth < maxDepth {
169 | 			// Read directory entries
170 | 			entries, err := os.ReadDir(validPath)
171 | 			if err != nil {
172 | 				return nil, err
173 | 			}
174 | 
175 | 			// Process each entry
176 | 			for _, entry := range entries {
177 | 				entryPath := filepath.Join(validPath, entry.Name())
178 | 
179 | 				// Handle symlinks
180 | 				if entry.Type()&os.ModeSymlink != 0 {
181 | 					if !followSymlinks {
182 | 						// Skip symlinks if not following them
183 | 						continue
184 | 					}
185 | 
186 | 					// Resolve symlink
187 | 					linkDest, err := filepath.EvalSymlinks(entryPath)
188 | 					if err != nil {
189 | 						// Skip invalid symlinks
190 | 						continue
191 | 					}
192 | 
193 | 					// Validate the symlink destination is within allowed directories
194 | 					if !fs.isPathInAllowedDirs(linkDest) {
195 | 						// Skip symlinks pointing outside allowed directories
196 | 						continue
197 | 					}
198 | 
199 | 					entryPath = linkDest
200 | 				}
201 | 
202 | 				// Recursively build child node
203 | 				childNode, err := fs.buildTree(entryPath, maxDepth, currentDepth+1, followSymlinks)
204 | 				if err != nil {
205 | 					// Skip entries with errors
206 | 					continue
207 | 				}
208 | 
209 | 				// Add child to the current node
210 | 				node.Children = append(node.Children, childNode)
211 | 			}
212 | 		}
213 | 	} else {
214 | 		node.Type = "file"
215 | 		node.Size = info.Size()
216 | 	}
217 | 
218 | 	return node, nil
219 | }
220 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/modify_file.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"os"
  7 | 	"regexp"
  8 | 	"strings"
  9 | 
 10 | 	"github.com/mark3labs/mcp-go/mcp"
 11 | )
 12 | 
 13 | // handleModifyFile handles the modify_file tool request
 14 | func (fs *FilesystemHandler) HandleModifyFile(
 15 | 	ctx context.Context,
 16 | 	request mcp.CallToolRequest,
 17 | ) (*mcp.CallToolResult, error) {
 18 | 	// Extract arguments
 19 | 	path, err := request.RequireString("path")
 20 | 	if err != nil {
 21 | 		return nil, err
 22 | 	}
 23 | 
 24 | 	find, err := request.RequireString("find")
 25 | 	if err != nil {
 26 | 		return nil, err
 27 | 	}
 28 | 
 29 | 	replace, err := request.RequireString("replace")
 30 | 	if err != nil {
 31 | 		return nil, err
 32 | 	}
 33 | 
 34 | 	// Extract optional arguments with defaults
 35 | 	allOccurrences := true // Default value
 36 | 	if val, err := request.RequireBool("all_occurrences"); err == nil {
 37 | 		allOccurrences = val
 38 | 	}
 39 | 
 40 | 	useRegex := false // Default value
 41 | 	if val, err := request.RequireBool("regex"); err == nil {
 42 | 		useRegex = val
 43 | 	}
 44 | 
 45 | 	// Handle empty or relative paths like "." or "./" by converting to absolute path
 46 | 	if path == "." || path == "./" {
 47 | 		// Get current working directory
 48 | 		cwd, err := os.Getwd()
 49 | 		if err != nil {
 50 | 			return &mcp.CallToolResult{
 51 | 				Content: []mcp.Content{
 52 | 					mcp.TextContent{
 53 | 						Type: "text",
 54 | 						Text: fmt.Sprintf("Error resolving current directory: %v", err),
 55 | 					},
 56 | 				},
 57 | 				IsError: true,
 58 | 			}, nil
 59 | 		}
 60 | 		path = cwd
 61 | 	}
 62 | 
 63 | 	// Validate path is within allowed directories
 64 | 	validPath, err := fs.validatePath(path)
 65 | 	if err != nil {
 66 | 		return &mcp.CallToolResult{
 67 | 			Content: []mcp.Content{
 68 | 				mcp.TextContent{
 69 | 					Type: "text",
 70 | 					Text: fmt.Sprintf("Error: %v", err),
 71 | 				},
 72 | 			},
 73 | 			IsError: true,
 74 | 		}, nil
 75 | 	}
 76 | 
 77 | 	// Check if it's a directory
 78 | 	if info, err := os.Stat(validPath); err == nil && info.IsDir() {
 79 | 		return &mcp.CallToolResult{
 80 | 			Content: []mcp.Content{
 81 | 				mcp.TextContent{
 82 | 					Type: "text",
 83 | 					Text: "Error: Cannot modify a directory",
 84 | 				},
 85 | 			},
 86 | 			IsError: true,
 87 | 		}, nil
 88 | 	}
 89 | 
 90 | 	// Check if file exists
 91 | 	if _, err := os.Stat(validPath); os.IsNotExist(err) {
 92 | 		return &mcp.CallToolResult{
 93 | 			Content: []mcp.Content{
 94 | 				mcp.TextContent{
 95 | 					Type: "text",
 96 | 					Text: fmt.Sprintf("Error: File not found: %s", path),
 97 | 				},
 98 | 			},
 99 | 			IsError: true,
100 | 		}, nil
101 | 	}
102 | 
103 | 	// Read file content
104 | 	content, err := os.ReadFile(validPath)
105 | 	if err != nil {
106 | 		return &mcp.CallToolResult{
107 | 			Content: []mcp.Content{
108 | 				mcp.TextContent{
109 | 					Type: "text",
110 | 					Text: fmt.Sprintf("Error reading file: %v", err),
111 | 				},
112 | 			},
113 | 			IsError: true,
114 | 		}, nil
115 | 	}
116 | 
117 | 	originalContent := string(content)
118 | 	modifiedContent := ""
119 | 	replacementCount := 0
120 | 
121 | 	// Perform the replacement
122 | 	if useRegex {
123 | 		re, err := regexp.Compile(find)
124 | 		if err != nil {
125 | 			return &mcp.CallToolResult{
126 | 				Content: []mcp.Content{
127 | 					mcp.TextContent{
128 | 						Type: "text",
129 | 						Text: fmt.Sprintf("Error: Invalid regular expression: %v", err),
130 | 					},
131 | 				},
132 | 				IsError: true,
133 | 			}, nil
134 | 		}
135 | 
136 | 		if allOccurrences {
137 | 			modifiedContent = re.ReplaceAllString(originalContent, replace)
138 | 			replacementCount = len(re.FindAllString(originalContent, -1))
139 | 		} else {
140 | 			matched := re.FindStringIndex(originalContent)
141 | 			if matched != nil {
142 | 				replacementCount = 1
143 | 				modifiedContent = originalContent[:matched[0]] + replace + originalContent[matched[1]:]
144 | 			} else {
145 | 				modifiedContent = originalContent
146 | 				replacementCount = 0
147 | 			}
148 | 		}
149 | 	} else {
150 | 		if allOccurrences {
151 | 			replacementCount = strings.Count(originalContent, find)
152 | 			modifiedContent = strings.ReplaceAll(originalContent, find, replace)
153 | 		} else {
154 | 			if index := strings.Index(originalContent, find); index != -1 {
155 | 				replacementCount = 1
156 | 				modifiedContent = originalContent[:index] + replace + originalContent[index+len(find):]
157 | 			} else {
158 | 				modifiedContent = originalContent
159 | 				replacementCount = 0
160 | 			}
161 | 		}
162 | 	}
163 | 
164 | 	// Write modified content back to file
165 | 	if err := os.WriteFile(validPath, []byte(modifiedContent), 0644); err != nil {
166 | 		return &mcp.CallToolResult{
167 | 			Content: []mcp.Content{
168 | 				mcp.TextContent{
169 | 					Type: "text",
170 | 					Text: fmt.Sprintf("Error writing to file: %v", err),
171 | 				},
172 | 			},
173 | 			IsError: true,
174 | 		}, nil
175 | 	}
176 | 
177 | 	// Create response
178 | 	resourceURI := pathToResourceURI(validPath)
179 | 
180 | 	// Get file info for the response
181 | 	info, err := os.Stat(validPath)
182 | 	if err != nil {
183 | 		// File was written but we couldn't get info
184 | 		return &mcp.CallToolResult{
185 | 			Content: []mcp.Content{
186 | 				mcp.TextContent{
187 | 					Type: "text",
188 | 					Text: fmt.Sprintf("File modified successfully. Made %d replacement(s).", replacementCount),
189 | 				},
190 | 			},
191 | 		}, nil
192 | 	}
193 | 
194 | 	return &mcp.CallToolResult{
195 | 		Content: []mcp.Content{
196 | 			mcp.TextContent{
197 | 				Type: "text",
198 | 				Text: fmt.Sprintf("File modified successfully. Made %d replacement(s) in %s (file size: %d bytes)",
199 | 					replacementCount, path, info.Size()),
200 | 			},
201 | 			mcp.EmbeddedResource{
202 | 				Type: "resource",
203 | 				Resource: mcp.TextResourceContents{
204 | 					URI:      resourceURI,
205 | 					MIMEType: "text/plain",
206 | 					Text:     fmt.Sprintf("Modified file: %s (%d bytes)", validPath, info.Size()),
207 | 				},
208 | 			},
209 | 		},
210 | 	}, nil
211 | }
```

--------------------------------------------------------------------------------
/filesystemserver/handler/copy_file.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"io"
  7 | 	"os"
  8 | 	"path/filepath"
  9 | 
 10 | 	"github.com/mark3labs/mcp-go/mcp"
 11 | )
 12 | 
 13 | func (fs *FilesystemHandler) HandleCopyFile(
 14 | 	ctx context.Context,
 15 | 	request mcp.CallToolRequest,
 16 | ) (*mcp.CallToolResult, error) {
 17 | 	source, err := request.RequireString("source")
 18 | 	if err != nil {
 19 | 		return nil, err
 20 | 	}
 21 | 	destination, err := request.RequireString("destination")
 22 | 	if err != nil {
 23 | 		return nil, err
 24 | 	}
 25 | 
 26 | 	// Handle empty or relative paths for source
 27 | 	if source == "." || source == "./" {
 28 | 		cwd, err := os.Getwd()
 29 | 		if err != nil {
 30 | 			return &mcp.CallToolResult{
 31 | 				Content: []mcp.Content{
 32 | 					mcp.TextContent{
 33 | 						Type: "text",
 34 | 						Text: fmt.Sprintf("Error resolving current directory: %v", err),
 35 | 					},
 36 | 				},
 37 | 				IsError: true,
 38 | 			}, nil
 39 | 		}
 40 | 		source = cwd
 41 | 	}
 42 | 	if destination == "." || destination == "./" {
 43 | 		cwd, err := os.Getwd()
 44 | 		if err != nil {
 45 | 			return &mcp.CallToolResult{
 46 | 				Content: []mcp.Content{
 47 | 					mcp.TextContent{
 48 | 						Type: "text",
 49 | 						Text: fmt.Sprintf("Error resolving current directory: %v", err),
 50 | 					},
 51 | 				},
 52 | 				IsError: true,
 53 | 			}, nil
 54 | 		}
 55 | 		destination = cwd
 56 | 	}
 57 | 
 58 | 	validSource, err := fs.validatePath(source)
 59 | 	if err != nil {
 60 | 		return &mcp.CallToolResult{
 61 | 			Content: []mcp.Content{
 62 | 				mcp.TextContent{
 63 | 					Type: "text",
 64 | 					Text: fmt.Sprintf("Error with source path: %v", err),
 65 | 				},
 66 | 			},
 67 | 			IsError: true,
 68 | 		}, nil
 69 | 	}
 70 | 
 71 | 	// Check if source exists
 72 | 	srcInfo, err := os.Stat(validSource)
 73 | 	if os.IsNotExist(err) {
 74 | 		return &mcp.CallToolResult{
 75 | 			Content: []mcp.Content{
 76 | 				mcp.TextContent{
 77 | 					Type: "text",
 78 | 					Text: fmt.Sprintf("Error: Source does not exist: %s", source),
 79 | 				},
 80 | 			},
 81 | 			IsError: true,
 82 | 		}, nil
 83 | 	} else if err != nil {
 84 | 		return &mcp.CallToolResult{
 85 | 			Content: []mcp.Content{
 86 | 				mcp.TextContent{
 87 | 					Type: "text",
 88 | 					Text: fmt.Sprintf("Error accessing source: %v", err),
 89 | 				},
 90 | 			},
 91 | 			IsError: true,
 92 | 		}, nil
 93 | 	}
 94 | 
 95 | 	validDest, err := fs.validatePath(destination)
 96 | 	if err != nil {
 97 | 		return &mcp.CallToolResult{
 98 | 			Content: []mcp.Content{
 99 | 				mcp.TextContent{
100 | 					Type: "text",
101 | 					Text: fmt.Sprintf("Error with destination path: %v", err),
102 | 				},
103 | 			},
104 | 			IsError: true,
105 | 		}, nil
106 | 	}
107 | 
108 | 	// Create parent directory for destination if it doesn't exist
109 | 	destDir := filepath.Dir(validDest)
110 | 	if err := os.MkdirAll(destDir, 0755); err != nil {
111 | 		return &mcp.CallToolResult{
112 | 			Content: []mcp.Content{
113 | 				mcp.TextContent{
114 | 					Type: "text",
115 | 					Text: fmt.Sprintf("Error creating destination directory: %v", err),
116 | 				},
117 | 			},
118 | 			IsError: true,
119 | 		}, nil
120 | 	}
121 | 
122 | 	// Perform the copy operation based on whether source is a file or directory
123 | 	if srcInfo.IsDir() {
124 | 		// It's a directory, copy recursively
125 | 		if err := copyDir(validSource, validDest); err != nil {
126 | 			return &mcp.CallToolResult{
127 | 				Content: []mcp.Content{
128 | 					mcp.TextContent{
129 | 						Type: "text",
130 | 						Text: fmt.Sprintf("Error copying directory: %v", err),
131 | 					},
132 | 				},
133 | 				IsError: true,
134 | 			}, nil
135 | 		}
136 | 	} else {
137 | 		// It's a file, copy directly
138 | 		if err := copyFile(validSource, validDest); err != nil {
139 | 			return &mcp.CallToolResult{
140 | 				Content: []mcp.Content{
141 | 					mcp.TextContent{
142 | 						Type: "text",
143 | 						Text: fmt.Sprintf("Error copying file: %v", err),
144 | 					},
145 | 				},
146 | 				IsError: true,
147 | 			}, nil
148 | 		}
149 | 	}
150 | 
151 | 	resourceURI := pathToResourceURI(validDest)
152 | 	return &mcp.CallToolResult{
153 | 		Content: []mcp.Content{
154 | 			mcp.TextContent{
155 | 				Type: "text",
156 | 				Text: fmt.Sprintf(
157 | 					"Successfully copied %s to %s",
158 | 					source,
159 | 					destination,
160 | 				),
161 | 			},
162 | 			mcp.EmbeddedResource{
163 | 				Type: "resource",
164 | 				Resource: mcp.TextResourceContents{
165 | 					URI:      resourceURI,
166 | 					MIMEType: "text/plain",
167 | 					Text:     fmt.Sprintf("Copied file: %s", validDest),
168 | 				},
169 | 			},
170 | 		},
171 | 	}, nil
172 | }
173 | 
174 | // copyFile copies a single file from src to dst
175 | func copyFile(src, dst string) error {
176 | 	// Open the source file
177 | 	sourceFile, err := os.Open(src)
178 | 	if err != nil {
179 | 		return err
180 | 	}
181 | 	defer sourceFile.Close()
182 | 
183 | 	// Create the destination file
184 | 	destFile, err := os.Create(dst)
185 | 	if err != nil {
186 | 		return err
187 | 	}
188 | 	defer destFile.Close()
189 | 
190 | 	// Copy the contents
191 | 	if _, err := io.Copy(destFile, sourceFile); err != nil {
192 | 		return err
193 | 	}
194 | 
195 | 	// Get source file mode
196 | 	sourceInfo, err := os.Stat(src)
197 | 	if err != nil {
198 | 		return err
199 | 	}
200 | 
201 | 	// Set the same file mode on destination
202 | 	return os.Chmod(dst, sourceInfo.Mode())
203 | }
204 | 
205 | // copyDir recursively copies a directory tree from src to dst
206 | func copyDir(src, dst string) error {
207 | 	// Get properties of source dir
208 | 	srcInfo, err := os.Stat(src)
209 | 	if err != nil {
210 | 		return err
211 | 	}
212 | 
213 | 	// Create the destination directory with the same permissions
214 | 	if err = os.MkdirAll(dst, srcInfo.Mode()); err != nil {
215 | 		return err
216 | 	}
217 | 
218 | 	// Read directory entries
219 | 	entries, err := os.ReadDir(src)
220 | 	if err != nil {
221 | 		return err
222 | 	}
223 | 
224 | 	for _, entry := range entries {
225 | 		srcPath := filepath.Join(src, entry.Name())
226 | 		dstPath := filepath.Join(dst, entry.Name())
227 | 
228 | 		// Handle symlinks
229 | 		if entry.Type()&os.ModeSymlink != 0 {
230 | 			// For simplicity, we'll skip symlinks in this implementation
231 | 			continue
232 | 		}
233 | 
234 | 		// Recursively copy subdirectories or copy files
235 | 		if entry.IsDir() {
236 | 			if err = copyDir(srcPath, dstPath); err != nil {
237 | 				return err
238 | 			}
239 | 		} else {
240 | 			if err = copyFile(srcPath, dstPath); err != nil {
241 | 				return err
242 | 			}
243 | 		}
244 | 	}
245 | 
246 | 	return nil
247 | }
```

--------------------------------------------------------------------------------
/filesystemserver/handler/read_file.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"encoding/base64"
  6 | 	"fmt"
  7 | 	"os"
  8 | 
  9 | 	"github.com/mark3labs/mcp-go/mcp"
 10 | )
 11 | 
 12 | func (fs *FilesystemHandler) HandleReadFile(
 13 | 	ctx context.Context,
 14 | 	request mcp.CallToolRequest,
 15 | ) (*mcp.CallToolResult, error) {
 16 | 	path, err := request.RequireString("path")
 17 | 	if err != nil {
 18 | 		return nil, err
 19 | 	}
 20 | 
 21 | 	// Handle empty or relative paths like "." or "./" by converting to absolute path
 22 | 	if path == "." || path == "./" {
 23 | 		// Get current working directory
 24 | 		cwd, err := os.Getwd()
 25 | 		if err != nil {
 26 | 			return &mcp.CallToolResult{
 27 | 				Content: []mcp.Content{
 28 | 					mcp.TextContent{
 29 | 						Type: "text",
 30 | 						Text: fmt.Sprintf("Error resolving current directory: %v", err),
 31 | 					},
 32 | 				},
 33 | 				IsError: true,
 34 | 			}, nil
 35 | 		}
 36 | 		path = cwd
 37 | 	}
 38 | 
 39 | 	validPath, err := fs.validatePath(path)
 40 | 	if err != nil {
 41 | 		return &mcp.CallToolResult{
 42 | 			Content: []mcp.Content{
 43 | 				mcp.TextContent{
 44 | 					Type: "text",
 45 | 					Text: fmt.Sprintf("Error: %v", err),
 46 | 				},
 47 | 			},
 48 | 			IsError: true,
 49 | 		}, nil
 50 | 	}
 51 | 
 52 | 	// Check if it's a directory
 53 | 	info, err := os.Stat(validPath)
 54 | 	if err != nil {
 55 | 		return &mcp.CallToolResult{
 56 | 			Content: []mcp.Content{
 57 | 				mcp.TextContent{
 58 | 					Type: "text",
 59 | 					Text: fmt.Sprintf("Error: %v", err),
 60 | 				},
 61 | 			},
 62 | 			IsError: true,
 63 | 		}, nil
 64 | 	}
 65 | 
 66 | 	if info.IsDir() {
 67 | 		// For directories, return a resource reference instead
 68 | 		resourceURI := pathToResourceURI(validPath)
 69 | 		return &mcp.CallToolResult{
 70 | 			Content: []mcp.Content{
 71 | 				mcp.TextContent{
 72 | 					Type: "text",
 73 | 					Text: fmt.Sprintf("This is a directory. Use the resource URI to browse its contents: %s", resourceURI),
 74 | 				},
 75 | 				mcp.EmbeddedResource{
 76 | 					Type: "resource",
 77 | 					Resource: mcp.TextResourceContents{
 78 | 						URI:      resourceURI,
 79 | 						MIMEType: "text/plain",
 80 | 						Text:     fmt.Sprintf("Directory: %s", validPath),
 81 | 					},
 82 | 				},
 83 | 			},
 84 | 		}, nil
 85 | 	}
 86 | 
 87 | 	// Determine MIME type
 88 | 	mimeType := detectMimeType(validPath)
 89 | 
 90 | 	// Check file size
 91 | 	if info.Size() > MAX_INLINE_SIZE {
 92 | 		// File is too large to inline, return a resource reference
 93 | 		resourceURI := pathToResourceURI(validPath)
 94 | 		return &mcp.CallToolResult{
 95 | 			Content: []mcp.Content{
 96 | 				mcp.TextContent{
 97 | 					Type: "text",
 98 | 					Text: fmt.Sprintf("File is too large to display inline (%d bytes). Access it via resource URI: %s", info.Size(), resourceURI),
 99 | 				},
100 | 				mcp.EmbeddedResource{
101 | 					Type: "resource",
102 | 					Resource: mcp.TextResourceContents{
103 | 						URI:      resourceURI,
104 | 						MIMEType: "text/plain",
105 | 						Text:     fmt.Sprintf("Large file: %s (%s, %d bytes)", validPath, mimeType, info.Size()),
106 | 					},
107 | 				},
108 | 			},
109 | 		}, nil
110 | 	}
111 | 
112 | 	// Read file content
113 | 	content, err := os.ReadFile(validPath)
114 | 	if err != nil {
115 | 		return &mcp.CallToolResult{
116 | 			Content: []mcp.Content{
117 | 				mcp.TextContent{
118 | 					Type: "text",
119 | 					Text: fmt.Sprintf("Error reading file: %v", err),
120 | 				},
121 | 			},
122 | 			IsError: true,
123 | 		}, nil
124 | 	}
125 | 
126 | 	// Check if it's a text file
127 | 	if isTextFile(mimeType) {
128 | 		// It's a text file, return as text
129 | 		return &mcp.CallToolResult{
130 | 			Content: []mcp.Content{
131 | 				mcp.TextContent{
132 | 					Type: "text",
133 | 					Text: string(content),
134 | 				},
135 | 			},
136 | 		}, nil
137 | 	} else if isImageFile(mimeType) {
138 | 		// It's an image file, return as image content
139 | 		if info.Size() <= MAX_BASE64_SIZE {
140 | 			return &mcp.CallToolResult{
141 | 				Content: []mcp.Content{
142 | 					mcp.TextContent{
143 | 						Type: "text",
144 | 						Text: fmt.Sprintf("Image file: %s (%s, %d bytes)", validPath, mimeType, info.Size()),
145 | 					},
146 | 					mcp.ImageContent{
147 | 						Type:     "image",
148 | 						Data:     base64.StdEncoding.EncodeToString(content),
149 | 						MIMEType: mimeType,
150 | 					},
151 | 				},
152 | 			}, nil
153 | 		} else {
154 | 			// Too large for base64, return a reference
155 | 			resourceURI := pathToResourceURI(validPath)
156 | 			return &mcp.CallToolResult{
157 | 				Content: []mcp.Content{
158 | 					mcp.TextContent{
159 | 						Type: "text",
160 | 						Text: fmt.Sprintf("Image file is too large to display inline (%d bytes). Access it via resource URI: %s", info.Size(), resourceURI),
161 | 					},
162 | 					mcp.EmbeddedResource{
163 | 						Type: "resource",
164 | 						Resource: mcp.TextResourceContents{
165 | 							URI:      resourceURI,
166 | 							MIMEType: "text/plain",
167 | 							Text:     fmt.Sprintf("Large image: %s (%s, %d bytes)", validPath, mimeType, info.Size()),
168 | 						},
169 | 					},
170 | 				},
171 | 			}, nil
172 | 		}
173 | 	} else {
174 | 		// It's another type of binary file
175 | 		resourceURI := pathToResourceURI(validPath)
176 | 
177 | 		if info.Size() <= MAX_BASE64_SIZE {
178 | 			// Small enough for base64 encoding
179 | 			return &mcp.CallToolResult{
180 | 				Content: []mcp.Content{
181 | 					mcp.TextContent{
182 | 						Type: "text",
183 | 						Text: fmt.Sprintf("Binary file: %s (%s, %d bytes)", validPath, mimeType, info.Size()),
184 | 					},
185 | 					mcp.EmbeddedResource{
186 | 						Type: "resource",
187 | 						Resource: mcp.BlobResourceContents{
188 | 							URI:      resourceURI,
189 | 							MIMEType: mimeType,
190 | 							Blob:     base64.StdEncoding.EncodeToString(content),
191 | 						},
192 | 					},
193 | 				},
194 | 			}, nil
195 | 		} else {
196 | 			// Too large for base64, return a reference
197 | 			return &mcp.CallToolResult{
198 | 				Content: []mcp.Content{
199 | 					mcp.TextContent{
200 | 						Type: "text",
201 | 						Text: fmt.Sprintf("Binary file: %s (%s, %d bytes). Access it via resource URI: %s", validPath, mimeType, info.Size(), resourceURI),
202 | 					},
203 | 					mcp.EmbeddedResource{
204 | 						Type: "resource",
205 | 						Resource: mcp.TextResourceContents{
206 | 							URI:      resourceURI,
207 | 							MIMEType: "text/plain",
208 | 							Text:     fmt.Sprintf("Binary file: %s (%s, %d bytes)", validPath, mimeType, info.Size()),
209 | 						},
210 | 					},
211 | 				},
212 | 			}, nil
213 | 		}
214 | 	}
215 | }
```

--------------------------------------------------------------------------------
/filesystemserver/server.go:
--------------------------------------------------------------------------------

```go
  1 | package filesystemserver
  2 | 
  3 | import (
  4 | 	"github.com/mark3labs/mcp-filesystem-server/filesystemserver/handler"
  5 | 	"github.com/mark3labs/mcp-go/mcp"
  6 | 	"github.com/mark3labs/mcp-go/server"
  7 | )
  8 | 
  9 | var Version = "dev"
 10 | 
 11 | func NewFilesystemServer(allowedDirs []string) (*server.MCPServer, error) {
 12 | 
 13 | 	h, err := handler.NewFilesystemHandler(allowedDirs)
 14 | 	if err != nil {
 15 | 		return nil, err
 16 | 	}
 17 | 
 18 | 	s := server.NewMCPServer(
 19 | 		"secure-filesystem-server",
 20 | 		Version,
 21 | 		server.WithResourceCapabilities(true, true),
 22 | 	)
 23 | 
 24 | 	// Register resource handlers
 25 | 	s.AddResource(mcp.NewResource(
 26 | 		"file://",
 27 | 		"File System",
 28 | 		mcp.WithResourceDescription("Access to files and directories on the local file system"),
 29 | 	), h.HandleReadResource)
 30 | 
 31 | 	// Register tool handlers
 32 | 	s.AddTool(mcp.NewTool(
 33 | 		"read_file",
 34 | 		mcp.WithDescription("Read the complete contents of a file from the file system."),
 35 | 		mcp.WithString("path",
 36 | 			mcp.Description("Path to the file to read"),
 37 | 			mcp.Required(),
 38 | 		),
 39 | 	), h.HandleReadFile)
 40 | 
 41 | 	s.AddTool(mcp.NewTool(
 42 | 		"write_file",
 43 | 		mcp.WithDescription("Create a new file or overwrite an existing file with new content."),
 44 | 		mcp.WithString("path",
 45 | 			mcp.Description("Path where to write the file"),
 46 | 			mcp.Required(),
 47 | 		),
 48 | 		mcp.WithString("content",
 49 | 			mcp.Description("Content to write to the file"),
 50 | 			mcp.Required(),
 51 | 		),
 52 | 	), h.HandleWriteFile)
 53 | 
 54 | 	s.AddTool(mcp.NewTool(
 55 | 		"list_directory",
 56 | 		mcp.WithDescription("Get a detailed listing of all files and directories in a specified path."),
 57 | 		mcp.WithString("path",
 58 | 			mcp.Description("Path of the directory to list"),
 59 | 			mcp.Required(),
 60 | 		),
 61 | 	), h.HandleListDirectory)
 62 | 
 63 | 	s.AddTool(mcp.NewTool(
 64 | 		"create_directory",
 65 | 		mcp.WithDescription("Create a new directory or ensure a directory exists."),
 66 | 		mcp.WithString("path",
 67 | 			mcp.Description("Path of the directory to create"),
 68 | 			mcp.Required(),
 69 | 		),
 70 | 	), h.HandleCreateDirectory)
 71 | 
 72 | 	s.AddTool(mcp.NewTool(
 73 | 		"copy_file",
 74 | 		mcp.WithDescription("Copy files and directories."),
 75 | 		mcp.WithString("source",
 76 | 			mcp.Description("Source path of the file or directory"),
 77 | 			mcp.Required(),
 78 | 		),
 79 | 		mcp.WithString("destination",
 80 | 			mcp.Description("Destination path"),
 81 | 			mcp.Required(),
 82 | 		),
 83 | 	), h.HandleCopyFile)
 84 | 
 85 | 	s.AddTool(mcp.NewTool(
 86 | 		"move_file",
 87 | 		mcp.WithDescription("Move or rename files and directories."),
 88 | 		mcp.WithString("source",
 89 | 			mcp.Description("Source path of the file or directory"),
 90 | 			mcp.Required(),
 91 | 		),
 92 | 		mcp.WithString("destination",
 93 | 			mcp.Description("Destination path"),
 94 | 			mcp.Required(),
 95 | 		),
 96 | 	), h.HandleMoveFile)
 97 | 
 98 | 	s.AddTool(mcp.NewTool(
 99 | 		"search_files",
100 | 		mcp.WithDescription("Recursively search for files and directories matching a pattern."),
101 | 		mcp.WithString("path",
102 | 			mcp.Description("Starting path for the search"),
103 | 			mcp.Required(),
104 | 		),
105 | 		mcp.WithString("pattern",
106 | 			mcp.Description("Search pattern to match against file names"),
107 | 			mcp.Required(),
108 | 		),
109 | 	), h.HandleSearchFiles)
110 | 
111 | 	s.AddTool(mcp.NewTool(
112 | 		"get_file_info",
113 | 		mcp.WithDescription("Retrieve detailed metadata about a file or directory."),
114 | 		mcp.WithString("path",
115 | 			mcp.Description("Path to the file or directory"),
116 | 			mcp.Required(),
117 | 		),
118 | 	), h.HandleGetFileInfo)
119 | 
120 | 	s.AddTool(mcp.NewTool(
121 | 		"list_allowed_directories",
122 | 		mcp.WithDescription("Returns the list of directories that this server is allowed to access."),
123 | 	), h.HandleListAllowedDirectories)
124 | 
125 | 	s.AddTool(mcp.NewTool(
126 | 		"read_multiple_files",
127 | 		mcp.WithDescription("Read the contents of multiple files in a single operation."),
128 | 		mcp.WithArray("paths",
129 | 			mcp.Description("List of file paths to read"),
130 | 			mcp.Required(),
131 | 			mcp.Items(map[string]any{"type": "string"}),
132 | 		),
133 | 	), h.HandleReadMultipleFiles)
134 | 
135 | 	s.AddTool(mcp.NewTool(
136 | 		"tree",
137 | 		mcp.WithDescription("Returns a hierarchical JSON representation of a directory structure."),
138 | 		mcp.WithString("path",
139 | 			mcp.Description("Path of the directory to traverse"),
140 | 			mcp.Required(),
141 | 		),
142 | 		mcp.WithNumber("depth",
143 | 			mcp.Description("Maximum depth to traverse (default: 3)"),
144 | 		),
145 | 		mcp.WithBoolean("follow_symlinks",
146 | 			mcp.Description("Whether to follow symbolic links (default: false)"),
147 | 		),
148 | 	), h.HandleTree)
149 | 
150 | 	s.AddTool(mcp.NewTool(
151 | 		"delete_file",
152 | 		mcp.WithDescription("Delete a file or directory from the file system."),
153 | 		mcp.WithString("path",
154 | 			mcp.Description("Path to the file or directory to delete"),
155 | 			mcp.Required(),
156 | 		),
157 | 		mcp.WithBoolean("recursive",
158 | 			mcp.Description("Whether to recursively delete directories (default: false)"),
159 | 		),
160 | 	), h.HandleDeleteFile)
161 | 
162 | 	s.AddTool(mcp.NewTool(
163 | 		"modify_file",
164 | 		mcp.WithDescription("Update file by finding and replacing text. Provides a simple pattern matching interface without needing exact character positions."),
165 | 		mcp.WithString("path",
166 | 			mcp.Description("Path to the file to modify"),
167 | 			mcp.Required(),
168 | 		),
169 | 		mcp.WithString("find",
170 | 			mcp.Description("Text to search for (exact match or regex pattern)"),
171 | 			mcp.Required(),
172 | 		),
173 | 		mcp.WithString("replace",
174 | 			mcp.Description("Text to replace with"),
175 | 			mcp.Required(),
176 | 		),
177 | 		mcp.WithBoolean("all_occurrences",
178 | 			mcp.Description("Replace all occurrences of the matching text (default: true)"),
179 | 		),
180 | 		mcp.WithBoolean("regex",
181 | 			mcp.Description("Treat the find pattern as a regular expression (default: false)"),
182 | 		),
183 | 	), h.HandleModifyFile)
184 | 
185 | 	s.AddTool(mcp.NewTool(
186 | 		"search_within_files",
187 | 		mcp.WithDescription("Search for text within file contents. Unlike search_files which only searches file names, this tool scans the actual contents of text files for matching substrings. Binary files are automatically excluded from the search. Reports file paths and line numbers where matches are found."),
188 | 		mcp.WithString("path",
189 | 			mcp.Description("Starting path for the search (must be a directory)"),
190 | 			mcp.Required(),
191 | 		),
192 | 		mcp.WithString("substring",
193 | 			mcp.Description("Text to search for within file contents"),
194 | 			mcp.Required(),
195 | 		),
196 | 		mcp.WithNumber("depth",
197 | 			mcp.Description("Maximum directory depth to search (default: unlimited)"),
198 | 		),
199 | 		mcp.WithNumber("max_results",
200 | 			mcp.Description("Maximum number of results to return (default: 1000)"),
201 | 		),
202 | 	), h.HandleSearchWithinFiles)
203 | 
204 | 	return s, nil
205 | }
206 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/tree_test.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"encoding/json"
  6 | 	"os"
  7 | 	"path/filepath"
  8 | 	"strings"
  9 | 	"testing"
 10 | 
 11 | 	"github.com/mark3labs/mcp-go/mcp"
 12 | 	"github.com/stretchr/testify/assert"
 13 | 	"github.com/stretchr/testify/require"
 14 | )
 15 | 
 16 | func TestHandleTree(t *testing.T) {
 17 | 	// Setup a temporary directory for the test
 18 | 	tmpDir := t.TempDir()
 19 | 
 20 | 	// Create a handler with the temp dir as an allowed path
 21 | 	allowedDirs := resolveAllowedDirs(t, tmpDir)
 22 | 	fsHandler, err := NewFilesystemHandler(allowedDirs)
 23 | 	require.NoError(t, err)
 24 | 
 25 | 	ctx := context.Background()
 26 | 
 27 | 	// Create test directory structure
 28 | 	// /tmpDir/
 29 | 	//   ├── file1.txt
 30 | 	//   ├── subdir1/
 31 | 	//   │   ├── file2.txt
 32 | 	//   │   └── subdir2/
 33 | 	//   │       └── file3.txt
 34 | 	//   └── emptydir/
 35 | 
 36 | 	file1Path := filepath.Join(tmpDir, "file1.txt")
 37 | 	err = os.WriteFile(file1Path, []byte("content1"), 0644)
 38 | 	require.NoError(t, err)
 39 | 
 40 | 	subdir1Path := filepath.Join(tmpDir, "subdir1")
 41 | 	err = os.Mkdir(subdir1Path, 0755)
 42 | 	require.NoError(t, err)
 43 | 
 44 | 	file2Path := filepath.Join(subdir1Path, "file2.txt")
 45 | 	err = os.WriteFile(file2Path, []byte("content2"), 0644)
 46 | 	require.NoError(t, err)
 47 | 
 48 | 	subdir2Path := filepath.Join(subdir1Path, "subdir2")
 49 | 	err = os.Mkdir(subdir2Path, 0755)
 50 | 	require.NoError(t, err)
 51 | 
 52 | 	file3Path := filepath.Join(subdir2Path, "file3.txt")
 53 | 	err = os.WriteFile(file3Path, []byte("content3"), 0644)
 54 | 	require.NoError(t, err)
 55 | 
 56 | 	emptydirPath := filepath.Join(tmpDir, "emptydir")
 57 | 	err = os.Mkdir(emptydirPath, 0755)
 58 | 	require.NoError(t, err)
 59 | 
 60 | 	t.Run("tree with default depth", func(t *testing.T) {
 61 | 		req := mcp.CallToolRequest{
 62 | 			Params: mcp.CallToolParams{
 63 | 				Arguments: map[string]interface{}{
 64 | 					"path": tmpDir,
 65 | 				},
 66 | 			},
 67 | 		}
 68 | 
 69 | 		res, err := fsHandler.HandleTree(ctx, req)
 70 | 		require.NoError(t, err)
 71 | 		require.False(t, res.IsError)
 72 | 
 73 | 		// Verify the response contains tree structure
 74 | 		require.Len(t, res.Content, 2)
 75 | 		textContent := res.Content[0].(mcp.TextContent)
 76 | 		assert.Contains(t, textContent.Text, "Directory tree for")
 77 | 		assert.Contains(t, textContent.Text, "max depth: 3")
 78 | 
 79 | 		// Parse the JSON to verify structure
 80 | 		lines := textContent.Text
 81 | 		assert.Contains(t, lines, "file1.txt")
 82 | 		assert.Contains(t, lines, "subdir1")
 83 | 		assert.Contains(t, lines, "file2.txt")
 84 | 		assert.Contains(t, lines, "subdir2")
 85 | 		assert.Contains(t, lines, "file3.txt")
 86 | 		assert.Contains(t, lines, "emptydir")
 87 | 
 88 | 		// Verify embedded resource
 89 | 		embeddedResource := res.Content[1].(mcp.EmbeddedResource)
 90 | 		assert.Equal(t, "resource", embeddedResource.Type)
 91 | 		assert.Equal(t, "application/json", embeddedResource.Resource.(mcp.TextResourceContents).MIMEType)
 92 | 	})
 93 | 
 94 | 	t.Run("tree with custom depth", func(t *testing.T) {
 95 | 		req := mcp.CallToolRequest{
 96 | 			Params: mcp.CallToolParams{
 97 | 				Arguments: map[string]interface{}{
 98 | 					"path":  tmpDir,
 99 | 					"depth": 2.0, // Only go 2 levels deep
100 | 				},
101 | 			},
102 | 		}
103 | 
104 | 		res, err := fsHandler.HandleTree(ctx, req)
105 | 		require.NoError(t, err)
106 | 		require.False(t, res.IsError)
107 | 
108 | 		textContent := res.Content[0].(mcp.TextContent)
109 | 		assert.Contains(t, textContent.Text, "max depth: 2")
110 | 
111 | 		// Should include file1.txt, subdir1, file2.txt, subdir2, emptydir
112 | 		// but NOT file3.txt (which is at depth 3)
113 | 		assert.Contains(t, textContent.Text, "file1.txt")
114 | 		assert.Contains(t, textContent.Text, "subdir1")
115 | 		assert.Contains(t, textContent.Text, "file2.txt")
116 | 		assert.Contains(t, textContent.Text, "subdir2")
117 | 		assert.Contains(t, textContent.Text, "emptydir")
118 | 		// file3.txt should not be included at depth 2
119 | 		assert.NotContains(t, textContent.Text, "file3.txt")
120 | 	})
121 | 
122 | 	t.Run("tree with depth 1", func(t *testing.T) {
123 | 		req := mcp.CallToolRequest{
124 | 			Params: mcp.CallToolParams{
125 | 				Arguments: map[string]interface{}{
126 | 					"path":  tmpDir,
127 | 					"depth": 1.0, // Only show immediate children
128 | 				},
129 | 			},
130 | 		}
131 | 
132 | 		res, err := fsHandler.HandleTree(ctx, req)
133 | 		require.NoError(t, err)
134 | 		require.False(t, res.IsError)
135 | 
136 | 		textContent := res.Content[0].(mcp.TextContent)
137 | 		assert.Contains(t, textContent.Text, "max depth: 1")
138 | 
139 | 		// Should only include immediate children
140 | 		assert.Contains(t, textContent.Text, "file1.txt")
141 | 		assert.Contains(t, textContent.Text, "subdir1")
142 | 		assert.Contains(t, textContent.Text, "emptydir")
143 | 		// Should not include nested files
144 | 		assert.NotContains(t, textContent.Text, "file2.txt")
145 | 		assert.NotContains(t, textContent.Text, "subdir2")
146 | 		assert.NotContains(t, textContent.Text, "file3.txt")
147 | 	})
148 | 
149 | 	t.Run("tree of empty directory", func(t *testing.T) {
150 | 		req := mcp.CallToolRequest{
151 | 			Params: mcp.CallToolParams{
152 | 				Arguments: map[string]interface{}{
153 | 					"path": emptydirPath,
154 | 				},
155 | 			},
156 | 		}
157 | 
158 | 		res, err := fsHandler.HandleTree(ctx, req)
159 | 		require.NoError(t, err)
160 | 		require.False(t, res.IsError)
161 | 
162 | 		textContent := res.Content[0].(mcp.TextContent)
163 | 		assert.Contains(t, textContent.Text, "Directory tree for")
164 | 
165 | 		// Parse JSON to verify it's a directory with no children
166 | 		jsonStart := textContent.Text[strings.Index(textContent.Text, "{"):]
167 | 		var tree FileNode
168 | 		err = json.Unmarshal([]byte(jsonStart), &tree)
169 | 		require.NoError(t, err)
170 | 		assert.Equal(t, "directory", tree.Type)
171 | 		assert.Equal(t, "emptydir", tree.Name)
172 | 		assert.Nil(t, tree.Children)
173 | 	})
174 | 
175 | 	t.Run("try to tree a file instead of directory", func(t *testing.T) {
176 | 		req := mcp.CallToolRequest{
177 | 			Params: mcp.CallToolParams{
178 | 				Arguments: map[string]interface{}{
179 | 					"path": file1Path,
180 | 				},
181 | 			},
182 | 		}
183 | 
184 | 		res, err := fsHandler.HandleTree(ctx, req)
185 | 		require.NoError(t, err)
186 | 		require.True(t, res.IsError)
187 | 
188 | 		require.Len(t, res.Content, 1)
189 | 		textContent := res.Content[0].(mcp.TextContent)
190 | 		assert.Contains(t, textContent.Text, "not a directory")
191 | 	})
192 | 
193 | 	t.Run("try to tree non-existent directory", func(t *testing.T) {
194 | 		nonExistentPath := filepath.Join(tmpDir, "non_existent_directory")
195 | 
196 | 		req := mcp.CallToolRequest{
197 | 			Params: mcp.CallToolParams{
198 | 				Arguments: map[string]interface{}{
199 | 					"path": nonExistentPath,
200 | 				},
201 | 			},
202 | 		}
203 | 
204 | 		res, err := fsHandler.HandleTree(ctx, req)
205 | 		require.NoError(t, err)
206 | 		require.True(t, res.IsError)
207 | 	})
208 | 
209 | 	t.Run("path is in a non-allowed directory", func(t *testing.T) {
210 | 		otherDir := t.TempDir()
211 | 
212 | 		req := mcp.CallToolRequest{
213 | 			Params: mcp.CallToolParams{
214 | 				Arguments: map[string]interface{}{
215 | 					"path": otherDir,
216 | 				},
217 | 			},
218 | 		}
219 | 
220 | 		res, err := fsHandler.HandleTree(ctx, req)
221 | 		require.NoError(t, err)
222 | 		require.True(t, res.IsError)
223 | 	})
224 | }
225 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/read_multiple_files_test.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"os"
  6 | 	"path/filepath"
  7 | 	"strings"
  8 | 	"testing"
  9 | 
 10 | 	"github.com/mark3labs/mcp-go/mcp"
 11 | 	"github.com/stretchr/testify/assert"
 12 | 	"github.com/stretchr/testify/require"
 13 | )
 14 | 
 15 | func TestHandleReadMultipleFiles(t *testing.T) {
 16 | 	// Setup a temporary directory for the test
 17 | 	tmpDir := t.TempDir()
 18 | 
 19 | 	// Create a handler with the temp dir as an allowed path
 20 | 	allowedDirs := resolveAllowedDirs(t, tmpDir)
 21 | 	fsHandler, err := NewFilesystemHandler(allowedDirs)
 22 | 	require.NoError(t, err)
 23 | 
 24 | 	ctx := context.Background()
 25 | 
 26 | 	// Create test files
 27 | 	file1Path := filepath.Join(tmpDir, "file1.txt")
 28 | 	file1Content := "This is the content of file 1"
 29 | 	err = os.WriteFile(file1Path, []byte(file1Content), 0644)
 30 | 	require.NoError(t, err)
 31 | 
 32 | 	file2Path := filepath.Join(tmpDir, "file2.txt")
 33 | 	file2Content := "This is the content of file 2"
 34 | 	err = os.WriteFile(file2Path, []byte(file2Content), 0644)
 35 | 	require.NoError(t, err)
 36 | 
 37 | 	// Create a directory
 38 | 	dirPath := filepath.Join(tmpDir, "test_directory")
 39 | 	err = os.Mkdir(dirPath, 0755)
 40 | 	require.NoError(t, err)
 41 | 
 42 | 	t.Run("read multiple text files", func(t *testing.T) {
 43 | 		req := mcp.CallToolRequest{
 44 | 			Params: mcp.CallToolParams{
 45 | 				Arguments: map[string]interface{}{
 46 | 					"paths": []string{file1Path, file2Path},
 47 | 				},
 48 | 			},
 49 | 		}
 50 | 
 51 | 		res, err := fsHandler.HandleReadMultipleFiles(ctx, req)
 52 | 		require.NoError(t, err)
 53 | 		require.False(t, res.IsError)
 54 | 
 55 | 		// Verify the response contains content from both files
 56 | 		require.GreaterOrEqual(t, len(res.Content), 4) // At least 2 headers + 2 content blocks
 57 | 
 58 | 		// Convert all content to strings for easier checking
 59 | 		var contentTexts []string
 60 | 		for _, content := range res.Content {
 61 | 			if textContent, ok := content.(mcp.TextContent); ok {
 62 | 				contentTexts = append(contentTexts, textContent.Text)
 63 | 			}
 64 | 		}
 65 | 
 66 | 		allText := strings.Join(contentTexts, "\n")
 67 | 		assert.Contains(t, allText, "--- File: "+file1Path+" ---")
 68 | 		assert.Contains(t, allText, "--- File: "+file2Path+" ---")
 69 | 		assert.Contains(t, allText, file1Content)
 70 | 		assert.Contains(t, allText, file2Content)
 71 | 	})
 72 | 
 73 | 	t.Run("read single file", func(t *testing.T) {
 74 | 		req := mcp.CallToolRequest{
 75 | 			Params: mcp.CallToolParams{
 76 | 				Arguments: map[string]interface{}{
 77 | 					"paths": []string{file1Path},
 78 | 				},
 79 | 			},
 80 | 		}
 81 | 
 82 | 		res, err := fsHandler.HandleReadMultipleFiles(ctx, req)
 83 | 		require.NoError(t, err)
 84 | 		require.False(t, res.IsError)
 85 | 
 86 | 		// Verify the response contains content from the file
 87 | 		require.GreaterOrEqual(t, len(res.Content), 2) // At least 1 header + 1 content block
 88 | 
 89 | 		var contentTexts []string
 90 | 		for _, content := range res.Content {
 91 | 			if textContent, ok := content.(mcp.TextContent); ok {
 92 | 				contentTexts = append(contentTexts, textContent.Text)
 93 | 			}
 94 | 		}
 95 | 
 96 | 		allText := strings.Join(contentTexts, "\n")
 97 | 		assert.Contains(t, allText, "--- File: "+file1Path+" ---")
 98 | 		assert.Contains(t, allText, file1Content)
 99 | 	})
100 | 
101 | 	t.Run("try to read a directory", func(t *testing.T) {
102 | 		req := mcp.CallToolRequest{
103 | 			Params: mcp.CallToolParams{
104 | 				Arguments: map[string]interface{}{
105 | 					"paths": []string{dirPath},
106 | 				},
107 | 			},
108 | 		}
109 | 
110 | 		res, err := fsHandler.HandleReadMultipleFiles(ctx, req)
111 | 		require.NoError(t, err)
112 | 		require.False(t, res.IsError)
113 | 
114 | 		// Should get a message about it being a directory
115 | 		require.Len(t, res.Content, 1)
116 | 		textContent := res.Content[0].(mcp.TextContent)
117 | 		assert.Contains(t, textContent.Text, "is a directory")
118 | 		assert.Contains(t, textContent.Text, "Use list_directory tool")
119 | 	})
120 | 
121 | 	t.Run("try to read non-existent file", func(t *testing.T) {
122 | 		nonExistentPath := filepath.Join(tmpDir, "non_existent.txt")
123 | 
124 | 		req := mcp.CallToolRequest{
125 | 			Params: mcp.CallToolParams{
126 | 				Arguments: map[string]interface{}{
127 | 					"paths": []string{nonExistentPath},
128 | 				},
129 | 			},
130 | 		}
131 | 
132 | 		res, err := fsHandler.HandleReadMultipleFiles(ctx, req)
133 | 		require.NoError(t, err)
134 | 		require.False(t, res.IsError) // The operation succeeds but individual files may have errors
135 | 
136 | 		// Should get an error message about the file not existing
137 | 		require.Len(t, res.Content, 1)
138 | 		textContent := res.Content[0].(mcp.TextContent)
139 | 		assert.Contains(t, textContent.Text, "Error accessing")
140 | 		assert.Contains(t, textContent.Text, nonExistentPath)
141 | 	})
142 | 
143 | 	t.Run("mix of valid and invalid files", func(t *testing.T) {
144 | 		nonExistentPath := filepath.Join(tmpDir, "non_existent.txt")
145 | 
146 | 		req := mcp.CallToolRequest{
147 | 			Params: mcp.CallToolParams{
148 | 				Arguments: map[string]interface{}{
149 | 					"paths": []string{file1Path, nonExistentPath, file2Path},
150 | 				},
151 | 			},
152 | 		}
153 | 
154 | 		res, err := fsHandler.HandleReadMultipleFiles(ctx, req)
155 | 		require.NoError(t, err)
156 | 		require.False(t, res.IsError)
157 | 
158 | 		// Should have content for valid files and error messages for invalid ones
159 | 		require.GreaterOrEqual(t, len(res.Content), 5) // At least 2 headers + 2 content blocks + 1 error
160 | 
161 | 		var contentTexts []string
162 | 		for _, content := range res.Content {
163 | 			if textContent, ok := content.(mcp.TextContent); ok {
164 | 				contentTexts = append(contentTexts, textContent.Text)
165 | 			}
166 | 		}
167 | 
168 | 		allText := strings.Join(contentTexts, "\n")
169 | 		assert.Contains(t, allText, "--- File: "+file1Path+" ---")
170 | 		assert.Contains(t, allText, "--- File: "+file2Path+" ---")
171 | 		assert.Contains(t, allText, file1Content)
172 | 		assert.Contains(t, allText, file2Content)
173 | 		assert.Contains(t, allText, "Error accessing")
174 | 		assert.Contains(t, allText, nonExistentPath)
175 | 	})
176 | 
177 | 	t.Run("no files specified", func(t *testing.T) {
178 | 		req := mcp.CallToolRequest{
179 | 			Params: mcp.CallToolParams{
180 | 				Arguments: map[string]interface{}{
181 | 					"paths": []string{},
182 | 				},
183 | 			},
184 | 		}
185 | 
186 | 		res, err := fsHandler.HandleReadMultipleFiles(ctx, req)
187 | 		require.NoError(t, err)
188 | 		require.True(t, res.IsError)
189 | 
190 | 		require.Len(t, res.Content, 1)
191 | 		textContent := res.Content[0].(mcp.TextContent)
192 | 		assert.Contains(t, textContent.Text, "No files specified to read")
193 | 	})
194 | 
195 | 	t.Run("too many files", func(t *testing.T) {
196 | 		// Create a slice with more than 50 files (the maximum)
197 | 		var manyPaths []string
198 | 		for i := 0; i < 51; i++ {
199 | 			manyPaths = append(manyPaths, filepath.Join(tmpDir, "file.txt"))
200 | 		}
201 | 
202 | 		req := mcp.CallToolRequest{
203 | 			Params: mcp.CallToolParams{
204 | 				Arguments: map[string]interface{}{
205 | 					"paths": manyPaths,
206 | 				},
207 | 			},
208 | 		}
209 | 
210 | 		res, err := fsHandler.HandleReadMultipleFiles(ctx, req)
211 | 		require.NoError(t, err)
212 | 		require.True(t, res.IsError)
213 | 
214 | 		require.Len(t, res.Content, 1)
215 | 		textContent := res.Content[0].(mcp.TextContent)
216 | 		assert.Contains(t, textContent.Text, "Too many files requested")
217 | 		assert.Contains(t, textContent.Text, "Maximum is 50")
218 | 	})
219 | 
220 | 	t.Run("path in non-allowed directory", func(t *testing.T) {
221 | 		otherDir := t.TempDir()
222 | 		otherFile := filepath.Join(otherDir, "other.txt")
223 | 
224 | 		req := mcp.CallToolRequest{
225 | 			Params: mcp.CallToolParams{
226 | 				Arguments: map[string]interface{}{
227 | 					"paths": []string{otherFile},
228 | 				},
229 | 			},
230 | 		}
231 | 
232 | 		res, err := fsHandler.HandleReadMultipleFiles(ctx, req)
233 | 		require.NoError(t, err)
234 | 		require.False(t, res.IsError) // The operation succeeds but individual files may have errors
235 | 
236 | 		require.Len(t, res.Content, 1)
237 | 		textContent := res.Content[0].(mcp.TextContent)
238 | 		assert.Contains(t, textContent.Text, "Error with path")
239 | 		assert.Contains(t, textContent.Text, otherFile)
240 | 	})
241 | }
242 | 
```

--------------------------------------------------------------------------------
/filesystemserver/handler/search_within_files.go:
--------------------------------------------------------------------------------

```go
  1 | package handler
  2 | 
  3 | import (
  4 | 	"bufio"
  5 | 	"context"
  6 | 	"fmt"
  7 | 	"os"
  8 | 	"path/filepath"
  9 | 	"strings"
 10 | 
 11 | 	"github.com/mark3labs/mcp-go/mcp"
 12 | )
 13 | 
 14 | func (fs *FilesystemHandler) HandleSearchWithinFiles(
 15 | 	ctx context.Context,
 16 | 	request mcp.CallToolRequest,
 17 | ) (*mcp.CallToolResult, error) {
 18 | 	// Extract and validate parameters
 19 | 	path, err := request.RequireString("path")
 20 | 	if err != nil {
 21 | 		return nil, err
 22 | 	}
 23 | 	substring, err := request.RequireString("substring")
 24 | 	if err != nil {
 25 | 		return nil, err
 26 | 	}
 27 | 	if substring == "" {
 28 | 		return &mcp.CallToolResult{
 29 | 			Content: []mcp.Content{
 30 | 				mcp.TextContent{
 31 | 					Type: "text",
 32 | 					Text: "Error: substring cannot be empty",
 33 | 				},
 34 | 			},
 35 | 			IsError: true,
 36 | 		}, nil
 37 | 	}
 38 | 
 39 | 	// Extract optional depth parameter
 40 | 	maxDepth := 0 // 0 means unlimited
 41 | 	if depthArg, err := request.RequireFloat("depth"); err == nil {
 42 | 		maxDepth = int(depthArg)
 43 | 		if maxDepth < 0 {
 44 | 			return &mcp.CallToolResult{
 45 | 				Content: []mcp.Content{
 46 | 					mcp.TextContent{
 47 | 						Type: "text",
 48 | 						Text: "Error: depth cannot be negative",
 49 | 					},
 50 | 				},
 51 | 				IsError: true,
 52 | 			}, nil
 53 | 		}
 54 | 	}
 55 | 
 56 | 	// Extract optional max_results parameter
 57 | 	maxResults := MAX_SEARCH_RESULTS // default limit
 58 | 	if maxResultsArg, err := request.RequireFloat("max_results"); err == nil {
 59 | 		maxResults = int(maxResultsArg)
 60 | 		if maxResults <= 0 {
 61 | 			return &mcp.CallToolResult{
 62 | 				Content: []mcp.Content{
 63 | 					mcp.TextContent{
 64 | 						Type: "text",
 65 | 						Text: "Error: max_results must be positive",
 66 | 					},
 67 | 				},
 68 | 				IsError: true,
 69 | 			}, nil
 70 | 		}
 71 | 	}
 72 | 
 73 | 	// Handle empty or relative paths like "." or "./" by converting to absolute path
 74 | 	if path == "." || path == "./" {
 75 | 		// Get current working directory
 76 | 		cwd, err := os.Getwd()
 77 | 		if err != nil {
 78 | 			return &mcp.CallToolResult{
 79 | 				Content: []mcp.Content{
 80 | 					mcp.TextContent{
 81 | 						Type: "text",
 82 | 						Text: fmt.Sprintf("Error resolving current directory: %v", err),
 83 | 					},
 84 | 				},
 85 | 				IsError: true,
 86 | 			}, nil
 87 | 		}
 88 | 		path = cwd
 89 | 	}
 90 | 
 91 | 	validPath, err := fs.validatePath(path)
 92 | 	if err != nil {
 93 | 		return &mcp.CallToolResult{
 94 | 			Content: []mcp.Content{
 95 | 				mcp.TextContent{
 96 | 					Type: "text",
 97 | 					Text: fmt.Sprintf("Error: %v", err),
 98 | 				},
 99 | 			},
100 | 			IsError: true,
101 | 		}, nil
102 | 	}
103 | 
104 | 	// Check if the path is a directory
105 | 	info, err := os.Stat(validPath)
106 | 	if err != nil {
107 | 		return &mcp.CallToolResult{
108 | 			Content: []mcp.Content{
109 | 				mcp.TextContent{
110 | 					Type: "text",
111 | 					Text: fmt.Sprintf("Error: %v", err),
112 | 				},
113 | 			},
114 | 			IsError: true,
115 | 		}, nil
116 | 	}
117 | 
118 | 	if !info.IsDir() {
119 | 		return &mcp.CallToolResult{
120 | 			Content: []mcp.Content{
121 | 				mcp.TextContent{
122 | 					Type: "text",
123 | 					Text: "Error: search path must be a directory",
124 | 				},
125 | 			},
126 | 			IsError: true,
127 | 		}, nil
128 | 	}
129 | 
130 | 	// Perform the search
131 | 	results, err := searchWithinFiles(validPath, substring, maxDepth, maxResults, fs)
132 | 	if err != nil {
133 | 		return &mcp.CallToolResult{
134 | 			Content: []mcp.Content{
135 | 				mcp.TextContent{
136 | 					Type: "text",
137 | 					Text: fmt.Sprintf("Error searching within files: %v", err),
138 | 				},
139 | 			},
140 | 			IsError: true,
141 | 		}, nil
142 | 	}
143 | 
144 | 	if len(results) == 0 {
145 | 		return &mcp.CallToolResult{
146 | 			Content: []mcp.Content{
147 | 				mcp.TextContent{
148 | 					Type: "text",
149 | 					Text: fmt.Sprintf("No occurrences of '%s' found in files under %s", substring, path),
150 | 				},
151 | 			},
152 | 		}, nil
153 | 	}
154 | 
155 | 	// Format search results
156 | 	var formattedResults strings.Builder
157 | 	formattedResults.WriteString(fmt.Sprintf("Found %d occurrences of '%s':\n\n", len(results), substring))
158 | 
159 | 	// Group results by file for easier readability
160 | 	fileResultsMap := make(map[string][]SearchResult)
161 | 	for _, result := range results {
162 | 		fileResultsMap[result.FilePath] = append(fileResultsMap[result.FilePath], result)
163 | 	}
164 | 
165 | 	// Display results grouped by file
166 | 	for filePath, fileResults := range fileResultsMap {
167 | 		resourceURI := pathToResourceURI(filePath)
168 | 		formattedResults.WriteString(fmt.Sprintf("File: %s (%s)\n", filePath, resourceURI))
169 | 
170 | 		for _, result := range fileResults {
171 | 			// Truncate line content if too long (keeping context around the match)
172 | 			lineContent := result.LineContent
173 | 			if len(lineContent) > 100 {
174 | 				// Find the substring position
175 | 				substrPos := strings.Index(strings.ToLower(lineContent), strings.ToLower(substring))
176 | 
177 | 				// Calculate start and end positions for context
178 | 				contextStart := max(0, substrPos-30)
179 | 				contextEnd := min(len(lineContent), substrPos+len(substring)+30)
180 | 
181 | 				if contextStart > 0 {
182 | 					lineContent = "..." + lineContent[contextStart:contextEnd]
183 | 				} else {
184 | 					lineContent = lineContent[:contextEnd]
185 | 				}
186 | 
187 | 				if contextEnd < len(result.LineContent) {
188 | 					lineContent += "..."
189 | 				}
190 | 			}
191 | 
192 | 			formattedResults.WriteString(fmt.Sprintf("  Line %d: %s\n", result.LineNumber, lineContent))
193 | 		}
194 | 		formattedResults.WriteString("\n")
195 | 	}
196 | 
197 | 	// If results were limited, note this in the output
198 | 	if len(results) >= maxResults {
199 | 		formattedResults.WriteString(fmt.Sprintf("\nNote: Results limited to %d matches. There may be more occurrences.", maxResults))
200 | 	}
201 | 
202 | 	return &mcp.CallToolResult{
203 | 		Content: []mcp.Content{
204 | 			mcp.TextContent{
205 | 				Type: "text",
206 | 				Text: formattedResults.String(),
207 | 			},
208 | 		},
209 | 	}, nil
210 | }
211 | 
212 | // searchWithinFiles searches for a substring within file contents
213 | func searchWithinFiles(
214 | 	rootPath, substring string, maxDepth int, maxResults int, fs *FilesystemHandler,
215 | ) ([]SearchResult, error) {
216 | 	var results []SearchResult
217 | 	resultCount := 0
218 | 	currentDepth := 0
219 | 
220 | 	// Walk the directory tree
221 | 	err := filepath.Walk(
222 | 		rootPath,
223 | 		func(path string, info os.FileInfo, err error) error {
224 | 			if err != nil {
225 | 				return nil // Skip errors and continue
226 | 			}
227 | 
228 | 			// Check if we've reached the maximum number of results
229 | 			if resultCount >= maxResults {
230 | 				return filepath.SkipDir
231 | 			}
232 | 
233 | 			// Try to validate path
234 | 			validPath, err := fs.validatePath(path)
235 | 			if err != nil {
236 | 				return nil // Skip invalid paths
237 | 			}
238 | 
239 | 			// Skip directories, only search files
240 | 			if info.IsDir() {
241 | 				// Calculate depth for this directory
242 | 				relPath, err := filepath.Rel(rootPath, path)
243 | 				if err != nil {
244 | 					return nil // Skip on error
245 | 				}
246 | 
247 | 				// Count separators to determine depth (empty or "." means we're at rootPath)
248 | 				if relPath == "" || relPath == "." {
249 | 					currentDepth = 0
250 | 				} else {
251 | 					currentDepth = strings.Count(relPath, string(filepath.Separator)) + 1
252 | 				}
253 | 
254 | 				// Skip directories beyond max depth if specified
255 | 				if maxDepth > 0 && currentDepth >= maxDepth {
256 | 					return filepath.SkipDir
257 | 				}
258 | 				return nil
259 | 			}
260 | 
261 | 			// Skip files that are too large
262 | 			if info.Size() > MAX_SEARCHABLE_SIZE {
263 | 				return nil
264 | 			}
265 | 
266 | 			// Determine MIME type and skip non-text files
267 | 			mimeType := detectMimeType(validPath)
268 | 			if !isTextFile(mimeType) {
269 | 				return nil
270 | 			}
271 | 
272 | 			// Open the file and search for the substring
273 | 			file, err := os.Open(validPath)
274 | 			if err != nil {
275 | 				return nil // Skip files that can't be opened
276 | 			}
277 | 			defer file.Close()
278 | 
279 | 			// Create a scanner to read the file line by line
280 | 			scanner := bufio.NewScanner(file)
281 | 			lineNum := 0
282 | 
283 | 			// Scan each line
284 | 			for scanner.Scan() {
285 | 				lineNum++
286 | 				line := scanner.Text()
287 | 
288 | 				// Check if the line contains the substring
289 | 				if strings.Contains(line, substring) {
290 | 					// Add to results
291 | 					results = append(results, SearchResult{
292 | 						FilePath:    validPath,
293 | 						LineNumber:  lineNum,
294 | 						LineContent: line,
295 | 						ResourceURI: pathToResourceURI(validPath),
296 | 					})
297 | 					resultCount++
298 | 
299 | 					// Check if we've reached the maximum results
300 | 					if resultCount >= maxResults {
301 | 						return filepath.SkipDir
302 | 					}
303 | 				}
304 | 			}
305 | 
306 | 			// Check for scanner errors
307 | 			if err := scanner.Err(); err != nil {
308 | 				return nil // Skip files with scanning errors
309 | 			}
310 | 
311 | 			return nil
312 | 		},
313 | 	)
314 | 
315 | 	if err != nil {
316 | 		return nil, err
317 | 	}
318 | 
319 | 	return results, nil
320 | }
321 | 
322 | // Helper function since Go < 1.21 doesn't have min/max functions
323 | func min(a, b int) int {
324 | 	if a < b {
325 | 		return a
326 | 	}
327 | 	return b
328 | }
329 | 
330 | // Helper function since Go < 1.21 doesn't have min/max functions
331 | func max(a, b int) int {
332 | 	if a > b {
333 | 		return a
334 | 	}
335 | 	return b
336 | }
337 | 
```