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}