main
1package filesystem
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8
9 "github.com/xlgmokha/mcp/pkg/mcp"
10)
11
12func New(allowedDirs []string) *mcp.Server {
13 tree := NewTree(allowedDirs)
14
15 builder := mcp.NewServerBuilder("filesystem", "0.2.0")
16
17 // Add tools
18 builder.AddTool(mcp.NewTool("read_file", "Read the contents of a file", map[string]interface{}{
19 "type": "object",
20 "properties": map[string]interface{}{
21 "path": map[string]interface{}{
22 "type": "string",
23 },
24 },
25 "required": []string{"path"},
26 }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
27 pathStr, ok := req.Arguments["path"].(string)
28 if !ok {
29 return mcp.NewToolError("path is required"), nil
30 }
31
32 validPath, err := tree.validatePath(pathStr)
33 if err != nil {
34 return mcp.NewToolError(err.Error()), nil
35 }
36
37 content, err := os.ReadFile(validPath)
38 if err != nil {
39 return mcp.NewToolError(fmt.Sprintf("Failed to read file: %v", err)), nil
40 }
41
42 return mcp.NewToolResult(mcp.NewTextContent(string(content))), nil
43 }))
44
45 builder.AddTool(mcp.NewTool("write_file", "Write content to a file", map[string]interface{}{
46 "type": "object",
47 "properties": map[string]interface{}{
48 "path": map[string]interface{}{
49 "type": "string",
50 },
51 "content": map[string]interface{}{
52 "type": "string",
53 },
54 },
55 "required": []string{"path", "content"},
56 }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
57 pathStr, ok := req.Arguments["path"].(string)
58 if !ok {
59 return mcp.NewToolError("path is required"), nil
60 }
61
62 content, ok := req.Arguments["content"].(string)
63 if !ok {
64 return mcp.NewToolError("content is required"), nil
65 }
66
67 validPath, err := tree.validatePath(pathStr)
68 if err != nil {
69 return mcp.NewToolError(err.Error()), nil
70 }
71
72 err = os.WriteFile(validPath, []byte(content), 0644)
73 if err != nil {
74 return mcp.NewToolError(fmt.Sprintf("Failed to write file: %v", err)), nil
75 }
76
77 return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Successfully wrote to %s", pathStr))), nil
78 }))
79
80 // Add pattern resource for file:// scheme
81 builder.AddResource(mcp.NewResource(
82 "file://",
83 "File System",
84 "",
85 func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
86 if !strings.HasPrefix(req.URI, "file://") {
87 return mcp.ReadResourceResult{}, fmt.Errorf("invalid file URI: %s", req.URI)
88 }
89
90 filePath := req.URI[7:]
91 // Ensure absolute path for file:// URIs
92 if !strings.HasPrefix(filePath, "/") {
93 filePath = "/" + filePath
94 }
95 validPath, err := tree.validatePath(filePath)
96 if err != nil {
97 return mcp.ReadResourceResult{}, fmt.Errorf("access denied: %v", err)
98 }
99
100 content, err := os.ReadFile(validPath)
101 if err != nil {
102 return mcp.ReadResourceResult{}, fmt.Errorf("failed to read file: %v", err)
103 }
104
105 if isBinaryContent(content) {
106 return mcp.ReadResourceResult{
107 Contents: []mcp.Content{
108 mcp.TextContent{
109 Type: "text",
110 Text: fmt.Sprintf("Binary file (size: %d bytes)", len(content)),
111 },
112 },
113 }, nil
114 }
115
116 return mcp.ReadResourceResult{
117 Contents: []mcp.Content{
118 mcp.NewTextContent(string(content)),
119 },
120 }, nil
121 },
122 ))
123
124 // Add directory resources and discover files
125 for _, dir := range tree.Directories {
126 fileURI := "file://" + dir
127 dirName := filepath.Base(dir)
128 if dirName == "." || dirName == "/" {
129 dirName = dir
130 }
131
132 builder.AddResource(mcp.NewResource(
133 fileURI,
134 fmt.Sprintf("Directory: %s", dirName),
135 "inode/directory",
136 func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
137 return mcp.ReadResourceResult{
138 Contents: []mcp.Content{
139 mcp.NewTextContent(fmt.Sprintf("Directory: %s", dir)),
140 },
141 }, nil
142 },
143 ))
144
145 // Discover files in this directory at construction time
146 fileResources := tree.discoverFiles(dir)
147 for _, resource := range fileResources {
148 builder.AddResource(resource)
149 }
150 }
151
152 // Add roots
153 for _, dir := range tree.Directories {
154 fileURI := "file://" + dir
155 dirName := filepath.Base(dir)
156 if dirName == "." || dirName == "/" {
157 dirName = dir
158 }
159 builder.AddRoot(mcp.NewRoot(fileURI, fmt.Sprintf("Directory: %s", dirName)))
160 }
161
162 return builder.Build()
163}