This is page 2 of 2. Use http://codebase.md/gojue/moling?page={x} to view the full context. # Directory Structure ``` ├── .github │ └── workflows │ ├── go-test.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── bin │ └── .gitkeep ├── CHANGELOG.md ├── cli │ ├── cmd │ │ ├── client.go │ │ ├── config.go │ │ ├── perrun.go │ │ ├── root.go │ │ └── utils.go │ ├── cobrautl │ │ └── help.go │ └── main.go ├── client │ ├── client_config_windows.go │ ├── client_config.go │ ├── client_test.go │ └── client.go ├── dist │ └── .gitkeep ├── functions.mk ├── go.mod ├── go.sum ├── images │ ├── logo-colorful.png │ ├── logo.svg │ └── screenshot_claude.png ├── install │ ├── install.ps1 │ └── install.sh ├── LICENSE ├── main.go ├── Makefile ├── Makefile.release ├── pkg │ ├── comm │ │ ├── comm.go │ │ └── errors.go │ ├── config │ │ ├── config_test.go │ │ ├── config_test.json │ │ └── config.go │ ├── server │ │ ├── server_test.go │ │ └── server.go │ ├── services │ │ ├── abstract │ │ │ ├── abstract.go │ │ │ ├── mlservice_test.go │ │ │ └── mlservice.go │ │ ├── browser │ │ │ ├── browser_config.go │ │ │ ├── browser_debugger.go │ │ │ ├── browser_test.go │ │ │ └── browser.go │ │ ├── command │ │ │ ├── command_config.go │ │ │ ├── command_exec_test.go │ │ │ ├── command_exec_windows.go │ │ │ ├── command_exec.go │ │ │ └── command.go │ │ ├── filesystem │ │ │ ├── file_system_config.go │ │ │ ├── file_system_windows.go │ │ │ └── file_system.go │ │ └── register.go │ └── utils │ ├── pid_unix.go │ ├── pid_windows.go │ ├── pid.go │ ├── rotewriter.go │ └── utils.go ├── prompts │ ├── filesystem.md │ ├── browser.md │ └── command.md ├── README_JA_JP.md ├── README_ZH_HANS.md ├── README.md └── variables.mk ``` # Files -------------------------------------------------------------------------------- /pkg/services/filesystem/file_system.go: -------------------------------------------------------------------------------- ```go // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // Repository: https://github.com/gojue/moling // Source: https://github.com/mark3labs/mcp-filesystem-server // Package services provides the implementation of the FileSystemServer, which allows access to files and directories on the local file system. package filesystem import ( "context" "encoding/base64" "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "github.com/mark3labs/mcp-go/mcp" "github.com/rs/zerolog" "github.com/gojue/moling/pkg/comm" "github.com/gojue/moling/pkg/config" "github.com/gojue/moling/pkg/services/abstract" "github.com/gojue/moling/pkg/utils" ) const ( // MaxInlineSize Maximum size for inline content (5MB) MaxInlineSize = 1024 * 1024 * 5 // MaxBase64Size Maximum size for base64 encoding (1MB) MaxBase64Size = 1024 * 1024 * 1 ) const ( FilesystemServerName comm.MoLingServerType = "FileSystem" ) type FileInfo struct { Size int64 `json:"size"` Created time.Time `json:"created"` Modified time.Time `json:"modified"` Accessed time.Time `json:"accessed"` IsDirectory bool `json:"isDirectory"` IsFile bool `json:"isFile"` Permissions string `json:"permissions"` } type FilesystemServer struct { abstract.MLService config *FileSystemConfig } func NewFilesystemServer(ctx context.Context) (abstract.Service, error) { // Validate the config var err error globalConf := ctx.Value(comm.MoLingConfigKey).(*config.MoLingConfig) userDataDir := filepath.Join(globalConf.BasePath, "data") fc := NewFileSystemConfig(userDataDir) lger, ok := ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger) if !ok { return nil, fmt.Errorf("FilesystemServer: invalid logger type") } loggerNameHook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) { e.Str("Service", string(FilesystemServerName)) }) fs := &FilesystemServer{ MLService: abstract.NewMLService(ctx, lger.Hook(loggerNameHook), globalConf), config: fc, } err = fs.InitResources() if err != nil { return nil, fmt.Errorf("failed to initialize filesystem server: %w", err) } return fs, nil } func (fs *FilesystemServer) Init() error { // Register resource handlers fs.AddResource(mcp.NewResource("file://", "File System", mcp.WithResourceDescription("Access to files and directories on the local file system"), ), fs.handleReadResource) pe := abstract.PromptEntry{ PromptVar: mcp.Prompt{ Name: "filesystem_prompt", Description: "Get the relevant functions and prompts of the FileSystem MCP Server.", }, HandlerFunc: fs.handlePrompt, } fs.AddPrompt(pe) // Register tool handlers fs.AddTool(mcp.NewTool("read_file", mcp.WithDescription("Read the complete contents of a file from the file system."), mcp.WithString("path", mcp.Description("Relative path to the file to read"), mcp.Required(), ), ), fs.handleReadFile) fs.AddTool(mcp.NewTool( "write_file", mcp.WithDescription("Create a new file or overwrite an existing file with new content."), mcp.WithString("path", mcp.Description("Relative Path where to write the file"), mcp.Required(), ), mcp.WithString("content", mcp.Description("Content to write to the file"), mcp.Required(), ), ), fs.handleWriteFile) fs.AddTool(mcp.NewTool( "list_directory", mcp.WithDescription("Get a detailed listing of all files and directories in a specified path."), mcp.WithString("path", mcp.Description("Relative Path of the directory to list"), mcp.Required(), ), ), fs.handleListDirectory) fs.AddTool(mcp.NewTool( "create_directory", mcp.WithDescription("Create a new directory or ensure a directory exists."), mcp.WithString("path", mcp.Description("Relative Path of the directory to create"), mcp.Required(), ), ), fs.handleCreateDirectory) fs.AddTool(mcp.NewTool( "move_file", mcp.WithDescription("Move or rename files and directories."), mcp.WithString("source", mcp.Description("Relative Source path of the file or directory"), mcp.Required(), ), mcp.WithString("destination", mcp.Description("Relative Destination path"), mcp.Required(), ), ), fs.handleMoveFile) fs.AddTool(mcp.NewTool( "search_files", mcp.WithDescription("Recursively search for files and directories matching a pattern."), mcp.WithString("path", mcp.Description("Relative Starting path for the search"), mcp.Required(), ), mcp.WithString("pattern", mcp.Description("Relative Search pattern to match against file names"), mcp.Required(), ), ), fs.handleSearchFiles) fs.AddTool(mcp.NewTool( "get_file_info", mcp.WithDescription("Retrieve detailed metadata about a file or directory."), mcp.WithString("path", mcp.Description("Relative Path to the file or directory"), mcp.Required(), ), ), fs.handleGetFileInfo) fs.AddTool(mcp.NewTool( "list_allowed_directories", mcp.WithDescription("Returns the list of directories that this server is allowed to access."), ), fs.handleListAllowedDirectories) return nil } // handlePrompt handles the prompt request for the FilesystemServer func (fs *FilesystemServer) handlePrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { return &mcp.GetPromptResult{ Description: "", Messages: []mcp.PromptMessage{ { Role: mcp.RoleUser, Content: mcp.TextContent{ Type: "text", Text: fs.config.prompt, }, }, }, }, nil } // isPathInAllowedDirs checks if a path is within any of the allowed directories func (fs *FilesystemServer) isPathInAllowedDirs(path string) bool { // Ensure path is absolute and clean absPath, err := filepath.Abs(path) if err != nil { return false } // Add trailing separator to ensure we're checking a directory or a file within a directory // and not a prefix match (e.g., /tmp/foo should not match /tmp/foobar) if !strings.HasSuffix(absPath, string(filepath.Separator)) { // If it'fss a file, we need to check its directory if info, err := os.Stat(absPath); err == nil && !info.IsDir() { absPath = filepath.Dir(absPath) + string(filepath.Separator) } else { absPath = absPath + string(filepath.Separator) } } // Check if the path is within any of the allowed directories for _, dir := range fs.config.allowedDirs { if strings.HasPrefix(absPath, dir) { return true } } return false } func (fs *FilesystemServer) validatePath(requestedPath string) (string, error) { // Always convert to absolute path first var hasPrefix bool var firstDir string for _, dir := range fs.config.allowedDirs { if firstDir == "" { firstDir = dir } if strings.HasPrefix(requestedPath, dir) { hasPrefix = true break } } if !hasPrefix { requestedPath = filepath.Join(firstDir, requestedPath) } abs, err := filepath.Abs(requestedPath) if err != nil { return "", fmt.Errorf("invalid path: %w", err) } // Check if path is within allowed directories if !fs.isPathInAllowedDirs(abs) { return "", fmt.Errorf("access denied - path outside allowed directories: %s", abs) } // Handle symlinks realPath, err := filepath.EvalSymlinks(abs) if err != nil { if !os.IsNotExist(err) { return "", err } // For new files, check parent directory parent := filepath.Dir(abs) realParent, err := filepath.EvalSymlinks(parent) if err != nil { return "", fmt.Errorf("parent directory does not exist: %s", parent) } if !fs.isPathInAllowedDirs(realParent) { return "", fmt.Errorf( "access denied - parent directory outside allowed directories", ) } return abs, nil } // Check if the real path (after resolving symlinks) is still within allowed directories if !fs.isPathInAllowedDirs(realPath) { return "", fmt.Errorf( "access denied - symlink target outside allowed directories", ) } return realPath, nil } func (fs *FilesystemServer) getFileStats(path string) (FileInfo, error) { info, err := os.Stat(path) if err != nil { return FileInfo{}, err } return FileInfo{ Size: info.Size(), Created: info.ModTime(), // Note: ModTime used as birth time isn't always available Modified: info.ModTime(), Accessed: info.ModTime(), // Note: Access time isn't always available IsDirectory: info.IsDir(), IsFile: !info.IsDir(), Permissions: fmt.Sprintf("%o", info.Mode().Perm()), }, nil } func (fs *FilesystemServer) searchFiles(rootPath, pattern string) ([]string, error) { var results []string pattern = strings.ToLower(pattern) err := filepath.Walk( rootPath, func(path string, info os.FileInfo, err error) error { if err != nil { return nil // Skip errors and continue } // Try to validate path if _, err := fs.validatePath(path); err != nil { return nil // Skip invalid paths } if strings.Contains(strings.ToLower(info.Name()), pattern) { results = append(results, path) } return nil }, ) if err != nil { return nil, err } return results, nil } // Resource handler func (fs *FilesystemServer) handleReadResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { uri := request.Params.URI fs.Logger.Debug().Str("uri", uri).Msg("handleReadResource") // Check if it'fss a file:// URI if !strings.HasPrefix(uri, "file://") { return nil, fmt.Errorf("unsupported URI scheme: %s", uri) } // Extract the path from the URI path := strings.TrimPrefix(uri, "file://") // Validate the path validPath, err := fs.validatePath(path) if err != nil { return nil, err } // Get file info fileInfo, err := os.Stat(validPath) if err != nil { return nil, err } // If it'fss a directory, return a listing if fileInfo.IsDir() { entries, err := os.ReadDir(validPath) if err != nil { return nil, err } var result strings.Builder result.WriteString(fmt.Sprintf("Directory listing for: %s\n\n", validPath)) for _, entry := range entries { entryPath := filepath.Join(validPath, entry.Name()) entryURI := utils.PathToResourceURI(entryPath) if entry.IsDir() { result.WriteString(fmt.Sprintf("[DIR] %s (%s)\n", entry.Name(), entryURI)) } else { info, err := entry.Info() if err == nil { result.WriteString(fmt.Sprintf("[FILE] %s (%s) - %d bytes\n", entry.Name(), entryURI, info.Size())) } else { result.WriteString(fmt.Sprintf("[FILE] %s (%s)\n", entry.Name(), entryURI)) } } } return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: uri, MIMEType: "text/plain", Text: result.String(), }, }, nil } // It'fss a file, determine how to handle it mimeType := utils.DetectMimeType(validPath) // Check file size if fileInfo.Size() > MaxInlineSize { // File is too large to inline, return a reference instead return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: uri, MIMEType: "text/plain", Text: fmt.Sprintf("File is too large to display inline (%d bytes). Use the read_file tool to access specific portions.", fileInfo.Size()), }, }, nil } // Read the file content content, err := os.ReadFile(validPath) if err != nil { return nil, err } // Handle based on content type if utils.IsTextFile(mimeType) { // It'fss a text file, return as text return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: uri, MIMEType: mimeType, Text: string(content), }, }, nil } else { // It'fss a binary file if fileInfo.Size() <= MaxBase64Size { // Small enough for base64 encoding return []mcp.ResourceContents{ mcp.BlobResourceContents{ URI: uri, MIMEType: mimeType, Blob: base64.StdEncoding.EncodeToString(content), }, }, nil } else { // Too large for base64, return a reference return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: uri, MIMEType: "text/plain", Text: fmt.Sprintf("Binary file (%s, %d bytes). Use the read_file tool to access specific portions.", mimeType, fileInfo.Size()), }, }, nil } } } // Tool handlers func (fs *FilesystemServer) handleReadFile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := request.GetArguments() path, ok := args["path"].(string) if !ok { return mcp.NewToolResultError("Path must be a string"), nil } // 判断 前缀是不是已经包含了 //path = filepath.Join(fss.config.CachePath, path) validPath, err := fs.validatePath(path) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("validate Path Error: %v", err)), nil } // Check if it'fss a directory info, err := os.Stat(validPath) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("check directory error: %v", err)), nil } if info.IsDir() { // For directories, return a resource reference instead resourceURI := utils.PathToResourceURI(validPath) return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("This is a directory. Use the resource URI to browse its contents: %s", resourceURI), }, mcp.EmbeddedResource{ Type: "resource", Resource: mcp.TextResourceContents{ URI: resourceURI, MIMEType: "text/plain", Text: fmt.Sprintf("Directory: %s", validPath), }, }, }, }, nil } // Determine MIME type mimeType := utils.DetectMimeType(validPath) // Check file size if info.Size() > MaxInlineSize { // File is too large to inline, return a resource reference resourceURI := utils.PathToResourceURI(validPath) return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("File is too large to display inline (%d bytes). Access it via resource URI: %s", info.Size(), resourceURI), }, mcp.EmbeddedResource{ Type: "resource", Resource: mcp.TextResourceContents{ URI: resourceURI, MIMEType: "text/plain", Text: fmt.Sprintf("Large file: %s (%s, %d bytes)", validPath, mimeType, info.Size()), }, }, }, }, nil } // Read file content content, err := os.ReadFile(validPath) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Error reading file: %v", err)), nil } // Handle based on content type if utils.IsTextFile(mimeType) { // It'fss a text file, return as text return mcp.NewToolResultText(string(content)), nil } else if utils.IsImageFile(mimeType) { // It'fss an image file, return as image content if info.Size() <= MaxBase64Size { return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Image file: %s (%s, %d bytes)", validPath, mimeType, info.Size()), }, mcp.ImageContent{ Type: "image", Data: base64.StdEncoding.EncodeToString(content), MIMEType: mimeType, }, }, }, nil } else { // Too large for base64, return a reference resourceURI := utils.PathToResourceURI(validPath) return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Image file is too large to display inline (%d bytes). Access it via resource URI: %s", info.Size(), resourceURI), }, mcp.EmbeddedResource{ Type: "resource", Resource: mcp.TextResourceContents{ URI: resourceURI, MIMEType: "text/plain", Text: fmt.Sprintf("Large image: %s (%s, %d bytes)", validPath, mimeType, info.Size()), }, }, }, }, nil } } else { // It'fss another type of binary file resourceURI := utils.PathToResourceURI(validPath) if info.Size() <= MaxBase64Size { // Small enough for base64 encoding return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Binary file: %s (%s, %d bytes)", validPath, mimeType, info.Size()), }, mcp.EmbeddedResource{ Type: "resource", Resource: mcp.BlobResourceContents{ URI: resourceURI, MIMEType: mimeType, Blob: base64.StdEncoding.EncodeToString(content), }, }, }, }, nil } else { // Too large for base64, return a reference return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Binary file: %s (%s, %d bytes). Access it via resource URI: %s", validPath, mimeType, info.Size(), resourceURI), }, mcp.EmbeddedResource{ Type: "resource", Resource: mcp.TextResourceContents{ URI: resourceURI, MIMEType: "text/plain", Text: fmt.Sprintf("Binary file: %s (%s, %d bytes)", validPath, mimeType, info.Size()), }, }, }, }, nil } } } func (fs *FilesystemServer) handleWriteFile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := request.GetArguments() path, ok := args["path"].(string) if !ok { return mcp.NewToolResultError("Path must be a string"), nil } content, ok := args["content"].(string) if !ok { return mcp.NewToolResultError("Content must be a string"), nil } //path = filepath.Join(fss.config.CachePath, path) validPath, err := fs.validatePath(path) if err != nil { return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Error: %v", err), }, }, IsError: true, }, nil } // Check if it'fss a directory if info, err := os.Stat(validPath); err == nil && info.IsDir() { return mcp.NewToolResultError(fmt.Sprintf("Error: Cannot write to a directory:%s", validPath)), nil } // Create parent directories if they don't exist parentDir := filepath.Dir(validPath) if err := os.MkdirAll(parentDir, 0755); err != nil { return mcp.NewToolResultError(fmt.Sprintf("Error creating parent directories: %v", err)), nil } if err := os.WriteFile(validPath, []byte(content), 0644); err != nil { return mcp.NewToolResultError(fmt.Sprintf("Error writing file: %v", err)), nil } // Get file info for the response info, err := os.Stat(validPath) if err != nil { // File was written but we couldn't get info return mcp.NewToolResultText(fmt.Sprintf("Successfully wrote to %s", path)), nil } resourceURI := utils.PathToResourceURI(validPath) return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Successfully wrote %d bytes to %s", info.Size(), path), }, mcp.EmbeddedResource{ Type: "resource", Resource: mcp.TextResourceContents{ URI: resourceURI, MIMEType: "text/plain", Text: fmt.Sprintf("File: %s (%d bytes)", validPath, info.Size()), }, }, }, }, nil } func (fs *FilesystemServer) handleListDirectory(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := request.GetArguments() path, ok := args["path"].(string) if !ok { return mcp.NewToolResultError("Path must be a string"), nil } validPath, err := fs.validatePath(path) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("validate path error: %v, path:%s", err, validPath)), nil } // Check if it'fss a directory info, err := os.Stat(validPath) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Check directory %s Error: %v", validPath, err)), nil } if !info.IsDir() { return mcp.NewToolResultError(fmt.Sprintf("Error: Path is not a directory:%s", validPath)), nil } entries, err := os.ReadDir(validPath) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Error reading directory: %v", err)), nil } var result strings.Builder result.WriteString(fmt.Sprintf("Directory listing for: %s\n\n", validPath)) for _, entry := range entries { entryPath := filepath.Join(validPath, entry.Name()) resourceURI := utils.PathToResourceURI(entryPath) if entry.IsDir() { result.WriteString(fmt.Sprintf("[DIR] %s (%s)\n", entry.Name(), resourceURI)) } else { info, err := entry.Info() if err == nil { result.WriteString(fmt.Sprintf("[FILE] %s (%s) - %d bytes\n", entry.Name(), resourceURI, info.Size())) } else { result.WriteString(fmt.Sprintf("[FILE] %s (%s)\n", entry.Name(), resourceURI)) } } } // Return both text content and embedded resource resourceURI := utils.PathToResourceURI(validPath) return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: result.String(), }, mcp.EmbeddedResource{ Type: "resource", Resource: mcp.TextResourceContents{ URI: resourceURI, MIMEType: "text/plain", Text: fmt.Sprintf("Directory: %s", validPath), }, }, }, }, nil } func (fs *FilesystemServer) handleCreateDirectory(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := request.GetArguments() path, ok := args["path"].(string) if !ok { return mcp.NewToolResultError("path must be a string"), nil } validPath, err := fs.validatePath(path) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Error: %v", err)), nil } // Check if path already exists if info, err := os.Stat(validPath); err == nil { if info.IsDir() { resourceURI := utils.PathToResourceURI(validPath) return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Directory already exists: %s", path), }, mcp.EmbeddedResource{ Type: "resource", Resource: mcp.TextResourceContents{ URI: resourceURI, MIMEType: "text/plain", Text: fmt.Sprintf("Directory: %s", validPath), }, }, }, }, nil } return mcp.NewToolResultError(fmt.Sprintf("Error: Path exists but is not a directory: %s", path)), nil } if err := os.MkdirAll(validPath, 0755); err != nil { return mcp.NewToolResultError(fmt.Sprintf("Error creating directory: %v", err)), nil } resourceURI := utils.PathToResourceURI(validPath) return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Successfully created directory %s", path), }, mcp.EmbeddedResource{ Type: "resource", Resource: mcp.TextResourceContents{ URI: resourceURI, MIMEType: "text/plain", Text: fmt.Sprintf("Directory: %s", validPath), }, }, }, }, nil } func (fs *FilesystemServer) handleMoveFile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := request.GetArguments() source, ok := args["source"].(string) if !ok { return mcp.NewToolResultError("source must be a string"), nil } destination, ok := args["destination"].(string) if !ok { return mcp.NewToolResultError("destination must be a string"), nil } validSource, err := fs.validatePath(source) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Error with source path: %v", err)), nil } // Check if source exists if _, err := os.Stat(validSource); os.IsNotExist(err) { return mcp.NewToolResultError(fmt.Sprintf("Error: Source does not exist: %s", source)), nil } validDest, err := fs.validatePath(destination) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Error with destination path: %v", err)), nil } // Create parent directory for destination if it doesn't exist destDir := filepath.Dir(validDest) if err := os.MkdirAll(destDir, 0755); err != nil { return mcp.NewToolResultError(fmt.Sprintf("Error creating destination directory: %v", err)), nil } if err := os.Rename(validSource, validDest); err != nil { return mcp.NewToolResultError(fmt.Sprintf("Error moving file: %v", err)), nil } resourceURI := utils.PathToResourceURI(validDest) return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf( "Successfully moved %s to %s", source, destination, ), }, mcp.EmbeddedResource{ Type: "resource", Resource: mcp.TextResourceContents{ URI: resourceURI, MIMEType: "text/plain", Text: fmt.Sprintf("Moved file: %s", validDest), }, }, }, }, nil } func (fs *FilesystemServer) handleSearchFiles(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := request.GetArguments() path, ok := args["path"].(string) if !ok { return mcp.NewToolResultError("path must be a string"), nil } pattern, ok := args["pattern"].(string) if !ok { return mcp.NewToolResultError("pattern must be a string"), nil } validPath, err := fs.validatePath(path) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Error: %v", err)), nil } // Check if it'fss a directory info, err := os.Stat(validPath) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Error: %v", err)), nil } if !info.IsDir() { return mcp.NewToolResultError("Error: Search path must be a directory"), nil } results, err := fs.searchFiles(validPath, pattern) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Error searching files: %v", err)), nil } if len(results) == 0 { return mcp.NewToolResultText(fmt.Sprintf("No files found matching pattern '%s' in %s", pattern, path)), nil } // Format results with resource URIs var formattedResults strings.Builder formattedResults.WriteString(fmt.Sprintf("Found %d results:\n\n", len(results))) for _, result := range results { resourceURI := utils.PathToResourceURI(result) info, err := os.Stat(result) if err == nil { if info.IsDir() { formattedResults.WriteString(fmt.Sprintf("[DIR] %s (%s)\n", result, resourceURI)) } else { formattedResults.WriteString(fmt.Sprintf("[FILE] %s (%s) - %d bytes\n", result, resourceURI, info.Size())) } } else { formattedResults.WriteString(fmt.Sprintf("%s (%s)\n", result, resourceURI)) } } return mcp.NewToolResultText(formattedResults.String()), nil } func (fs *FilesystemServer) handleGetFileInfo(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := request.GetArguments() path, ok := args["path"].(string) if !ok { return mcp.NewToolResultError(fmt.Errorf("path %v must be a string", args["path"]).Error()), nil } validPath, err := fs.validatePath(path) if err != nil { return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Error: %v", err), }, }, IsError: true, }, nil } info, err := fs.getFileStats(validPath) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Error getting file info: %v", err)), nil } // Get MIME type for files mimeType := "directory" if info.IsFile { mimeType = utils.DetectMimeType(validPath) } resourceURI := utils.PathToResourceURI(validPath) // Determine file type text var fileTypeText string if info.IsDirectory { fileTypeText = "Directory" } else { fileTypeText = "File" } return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf( "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", validPath, info.Size, info.Created.Format(time.RFC3339), info.Modified.Format(time.RFC3339), info.Accessed.Format(time.RFC3339), info.IsDirectory, info.IsFile, info.Permissions, mimeType, resourceURI, ), }, mcp.EmbeddedResource{ Type: "resource", Resource: mcp.TextResourceContents{ URI: resourceURI, MIMEType: "text/plain", Text: fmt.Sprintf("%s: %s (%s, %d bytes)", fileTypeText, validPath, mimeType, info.Size), }, }, }, }, nil } func (fs *FilesystemServer) handleListAllowedDirectories(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Remove the trailing separator for display purposes displayDirs := make([]string, len(fs.config.allowedDirs)) for i, dir := range fs.config.allowedDirs { displayDirs[i] = strings.TrimSuffix(dir, string(filepath.Separator)) } var result strings.Builder result.WriteString("Allowed directories:") for _, dir := range displayDirs { resourceURI := utils.PathToResourceURI(dir) result.WriteString(fmt.Sprintf("%s (%s)\n", dir, resourceURI)) } return mcp.NewToolResultText(result.String()), nil } // Config returns the configuration of the service as a string. func (fs *FilesystemServer) Config() string { fs.config.AllowedDir = strings.Join(fs.config.allowedDirs, ",") cfg, err := json.Marshal(fs.config) if err != nil { fs.Logger.Err(err).Msg("failed to marshal config") return "{}" } return string(cfg) } func (fs *FilesystemServer) Name() comm.MoLingServerType { return FilesystemServerName } func (fs *FilesystemServer) Close() error { // Cancel the context to stop the browser fs.Logger.Debug().Msg("closing FilesystemServer") return nil } // LoadConfig loads the configuration from a JSON object. func (fs *FilesystemServer) LoadConfig(jsonData map[string]any) error { err := utils.MergeJSONToStruct(fs.config, jsonData) if err != nil { return err } fs.config.allowedDirs = strings.Split(fs.config.AllowedDir, ",") return fs.config.Check() } ```