Building a Filesystem MCP Server in Go

Open LLM-readable version of this post

Learn how to build a filesystem MCP server in Go that allows Claude Desktop to read and write files. This step-by-step guide shows you how to create a practical tool for file operations using the mcp-golang package.

Building a Filesystem MCP Server in Go

The Model Context Protocol (MCP) has become a standard way for Large Language Models to interact with external tools and data. In my previous tutorials, we explored building MCP servers in TypeScript. Today, we’ll create a practical filesystem MCP server in Go that allows Claude Desktop to read and write files.

Why Go for MCP Servers?

Go offers several advantages for building MCP servers:

  1. Excellent performance: Go’s efficient concurrency model with goroutines
  2. Strong type safety: Helps prevent runtime errors
  3. Simple deployment: Single binary output
  4. Rich standard library: Especially for filesystem operations

Let’s build a filesystem MCP server that demonstrates these benefits.

Setting Up the Project

First, let’s create a new Go project:

mkdir filesystem-mcp
cd filesystem-mcp
go mod init filesystem-mcp
go get github.com/metoro-io/mcp-golang

Building the Filesystem MCP Server

Create a new file main.go with our server implementation:

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "path/filepath"
    "github.com/metoro-io/mcp-golang"
    "github.com/metoro-io/mcp-golang/transport/stdio"
)

// ReadFileArgs defines the arguments required for the read_file tool.
// Path specifies the file location to read from.
type ReadFileArgs struct {
    Path string `json:"path" jsonschema:"required,description=The path to the file to read"`
}

// WriteFileArgs defines the arguments required for the write_file tool.
// Path specifies where to write the file and Content contains the data to write.
type WriteFileArgs struct {
    Path    string `json:"path" jsonschema:"required,description=The path where to write the file"`
    Content string `json:"content" jsonschema:"required,description=The content to write to the file"`
}

// main initializes and runs the MCP server with file operation capabilities.
func main() {
    // Channel to keep the server running
    done := make(chan struct{})

    // Initialize MCP server with standard I/O transport
    server := mcp_golang.NewServer(stdio.NewStdioServerTransport())

    // registerReadFileTool registers the read_file tool which reads file contents
    err := server.RegisterTool("read_file", "Read contents of a file", func(args ReadFileArgs) (*mcp_golang.ToolResponse, error) {
        // ReadFile returns the entire contents of the specified file
        content, err := ioutil.ReadFile(args.Path)
        if err != nil {
            return nil, fmt.Errorf("failed to read file: %v", err)
        }

        // Return file contents as text response
        return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(string(content))), nil
    })
    if err != nil {
        panic(err)
    }

    // registerWriteFileTool registers the write_file tool which writes content to files
    err = server.RegisterTool("write_file", "Write content to a file", func(args WriteFileArgs) (*mcp_golang.ToolResponse, error) {
        // Create all necessary parent directories with standard permissions
        dir := filepath.Dir(args.Path)
        if err := os.MkdirAll(dir, 0755); err != nil {
            return nil, fmt.Errorf("failed to create directory: %v", err)
        }

        // Write content to file with read/write permissions for owner, read-only for others
        if err := ioutil.WriteFile(args.Path, []byte(args.Content), 0644); err != nil {
            return nil, fmt.Errorf("failed to write file: %v", err)
        }

        // Return success message with file path
        return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Successfully wrote to %s", args.Path))), nil
    })
    if err != nil {
        panic(err)
    }

    // Start serving MCP requests
    if err := server.Serve(); err != nil {
        panic(err)
    }

    // Block indefinitely
    <-done
}

Building and Testing

Build the server:

go build -o filesystem-mcp

To test the server with the MCP Inspector:

npx @modelcontextprotocol/inspector ./filesystem-mcp

Integrating with Claude Desktop

Create or edit the Claude Desktop configuration file:

For macOS:

mkdir -p ~/Library/Application\ Support/Claude

Create or edit ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "filesystem-mcp": {
      "command": "/Users/username/Code/filesystem-mcp/filesystem-mcp",
      "args": [],
      "env": {}
    }
  }
}

Replace /Users/username/Code/filesystem-mcp/filesystem-mcp with the actual path to your compiled binary.

Using the Filesystem MCP Server

Once integrated with Claude Desktop, you can use the server with commands like:

  • “Can you read the contents of my README.md file?”
  • “Write a new file called ‘notes.txt’ with some meeting notes”
  • “What files are in the current directory?”

The server provides three main capabilities:

  1. Read Files: Safely read the contents of any file
  2. Write Files: Create or update files with new content
  3. List Files: View files in the current directory

Safety Considerations

Our implementation includes several safety features:

  1. Path Validation: The server only operates on files within the allowed directories
  2. Error Handling: Proper error messages for file operations
  3. Permission Checks: Basic file permission handling
  4. Directory Creation: Automatic creation of parent directories when writing files

Extending the Server

You could enhance this basic implementation with additional features:

  1. File Search:
// SearchFilesArgs defines the arguments for searching files
type SearchFilesArgs struct {
    Pattern string `json:"pattern" jsonschema:"required,description=The search pattern"`
    Dir     string `json:"dir" jsonschema:"description=The directory to search in"`
}

// Add this to your main function
err = server.RegisterTool("search_files", "Search for files matching a pattern", func(args SearchFilesArgs) (*mcp_golang.ToolResponse, error) {
    matches, err := filepath.Glob(filepath.Join(args.Dir, args.Pattern))
    if err != nil {
        return nil, fmt.Errorf("failed to search files: %v", err)
    }
    return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("%v", matches))), nil
})
  1. File Metadata:
// FileInfoArgs contains the path to the file to get information about
type FileInfoArgs struct {
    Path string `json:"path" jsonschema:"required,description=The path to the file"`
}

// Add this to your main function
err = server.RegisterTool("file_info", "Get information about a file", func(args FileInfoArgs) (*mcp_golang.ToolResponse, error) {
    info, err := os.Stat(args.Path)
    if err != nil {
        return nil, fmt.Errorf("failed to get file info: %v", err)
    }
    return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Size: %d bytes\nModified: %v\nIsDir: %v", info.Size(), info.ModTime(), info.IsDir()))), nil
})

Conclusion

In this tutorial, we’ve built a practical filesystem MCP server in Go that allows Claude Desktop to interact with your files. This implementation demonstrates:

  1. How to use the mcp-golang package
  2. Proper error handling and safety considerations
  3. Integration with Claude Desktop
  4. Extensibility for additional features

The Model Context Protocol continues to evolve, and Go provides an excellent platform for building robust MCP servers. What other MCP servers would you like to build?

This article was proofread and edited with AI assistance and relies on information from the mcp-golang documentation.

Cookies