The Model Context Protocol (MCP) is revolutionizing how AI assistants interact with tools and data sources. In this comprehensive guide, we’ll build a production-ready MCP server in Go that provides powerful file operations-think of it as the strings utility package, but for files and AI assistants.
What is MCP?
MCP (Model Context Protocol) is a standardized way for AI assistants to interact with external tools and data sources. Instead of custom integrations for each tool, MCP provides a universal protocol that works with any compliant server.
Why build MCP tools in Go?
- ✅ Excellent performance for file I/O operations
- ✅ Built-in concurrency for handling multiple requests
- ✅ Strong typing prevents runtime errors
- ✅ Easy deployment (single binary)
- ✅ Cross-platform compatibility
What We’re Building
An MCP server that provides:
- Search files: Find files by name pattern
- Show results: List search results with metadata
- Fetch content: Read file contents
- Modify file: Write or update files
- Search within files: Find text patterns in file contents
- Replace content: Find and replace text in files
Architecture Overview
Project Structure
mcp-file-tool/
├── cmd/
│ └── server/
│ └── main.go # Entry point
├── internal/
│ ├── mcp/
│ │ ├── protocol.go # MCP protocol types
│ │ └── server.go # MCP server implementation
│ └── tools/
│ ├── search.go # File search tool
│ ├── content.go # Content operations
│ ├── modify.go # File modification
│ └── replace.go # Content replacement
├── go.mod
└── go.sum
Implementation
Step 1: MCP Protocol Types
// internal/mcp/protocol.go
package mcp
import "encoding/json"
// Request represents an MCP request
type Request struct {
ID string `json:"id"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
}
// Response represents an MCP response
type Response struct {
ID string `json:"id"`
Result interface{} `json:"result,omitempty"`
Error *Error `json:"error,omitempty"`
}
// Error represents an MCP error
type Error struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// ToolDefinition describes a tool's capabilities
type ToolDefinition struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema map[string]interface{} `json:"inputSchema"`
}
// Standard error codes
const (
ErrCodeParseError = -32700
ErrCodeInvalidRequest = -32600
ErrCodeMethodNotFound = -32601
ErrCodeInvalidParams = -32602
ErrCodeInternalError = -32603
)
Step 2: File Search Tool
// internal/tools/search.go
package tools
import (
"fmt"
"os"
"path/filepath"
"regexp"
"time"
)
// FileSearchParams defines search parameters
type FileSearchParams struct {
Pattern string `json:"pattern"` // Glob pattern or regex
Path string `json:"path"` // Root path to search
MaxDepth int `json:"maxDepth"` // Maximum directory depth
MaxResults int `json:"maxResults"` // Limit results
}
// FileInfo represents a found file
type FileInfo struct {
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
ModTime time.Time `json:"modTime"`
IsDir bool `json:"isDir"`
Extension string `json:"extension"`
}
// SearchResult contains search results
type SearchResult struct {
Files []FileInfo `json:"files"`
TotalFound int `json:"totalFound"`
Truncated bool `json:"truncated"`
}
// SearchFiles searches for files matching a pattern
func SearchFiles(params FileSearchParams) (*SearchResult, error) {
if params.Path == "" {
params.Path = "."
}
if params.MaxResults == 0 {
params.MaxResults = 100 // Default limit
}
if params.MaxDepth == 0 {
params.MaxDepth = 10 // Default depth
}
var files []FileInfo
currentDepth := 0
// Compile pattern if it's a regex
var pattern *regexp.Regexp
var err error
if isRegexPattern(params.Pattern) {
pattern, err = regexp.Compile(params.Pattern)
if err != nil {
return nil, fmt.Errorf("invalid regex pattern: %w", err)
}
}
err = filepath.Walk(params.Path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil // Skip files we can't access
}
// Check depth
depth := getDepth(params.Path, path)
if depth > params.MaxDepth {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
// Match pattern
matched := false
if pattern != nil {
matched = pattern.MatchString(info.Name())
} else {
matched, _ = filepath.Match(params.Pattern, info.Name())
}
if matched {
files = append(files, FileInfo{
Path: path,
Name: info.Name(),
Size: info.Size(),
ModTime: info.ModTime(),
IsDir: info.IsDir(),
Extension: filepath.Ext(info.Name()),
})
}
// Limit results
if len(files) >= params.MaxResults {
return filepath.SkipAll
}
return nil
})
if err != nil {
return nil, fmt.Errorf("search failed: %w", err)
}
return &SearchResult{
Files: files,
TotalFound: len(files),
Truncated: len(files) >= params.MaxResults,
}, nil
}
// Helper functions
func getDepth(root, path string) int {
rel, _ := filepath.Rel(root, path)
if rel == "." {
return 0
}
return len(filepath.SplitList(rel))
}
func isRegexPattern(pattern string) bool {
// Simple heuristic: if it contains regex metacharacters, treat as regex
regexChars := []string{"^", "$", "(", ")", "[", "]", "{", "}", "|", "+", "?"}
for _, char := range regexChars {
if contains(pattern, char) {
return true
}
}
return false
}
func contains(s, substr string) bool {
return len(s) > 0 && len(substr) > 0 && s != substr && len(s) >= len(substr) &&
s[0:len(substr)] == substr || (len(s) > len(substr) && contains(s[1:], substr))
}
Step 3: Content Operations
// internal/tools/content.go
package tools
import (
"bufio"
"fmt"
"os"
)
// FetchContentParams defines content fetch parameters
type FetchContentParams struct {
Path string `json:"path"`
StartLine int `json:"startLine,omitempty"` // 1-indexed
EndLine int `json:"endLine,omitempty"` // 1-indexed, 0 = end of file
MaxSize int64 `json:"maxSize,omitempty"` // Bytes
}
// ContentResult contains file content
type ContentResult struct {
Path string `json:"path"`
Content string `json:"content"`
Lines []string `json:"lines,omitempty"`
TotalLines int `json:"totalLines"`
Truncated bool `json:"truncated"`
}
// FetchContent reads file content
func FetchContent(params FetchContentParams) (*ContentResult, error) {
// Check file exists and is readable
info, err := os.Stat(params.Path)
if err != nil {
return nil, fmt.Errorf("file not found: %w", err)
}
if info.IsDir() {
return nil, fmt.Errorf("path is a directory, not a file")
}
// Check size limit
if params.MaxSize > 0 && info.Size() > params.MaxSize {
return nil, fmt.Errorf("file size (%d bytes) exceeds limit (%d bytes)",
info.Size(), params.MaxSize)
}
// Read file
file, err := os.Open(params.Path)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
var lines []string
scanner := bufio.NewScanner(file)
lineNum := 0
for scanner.Scan() {
lineNum++
// Handle line range
if params.StartLine > 0 && lineNum < params.StartLine {
continue
}
if params.EndLine > 0 && lineNum > params.EndLine {
break
}
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading file: %w", err)
}
// Build content string
content := ""
for _, line := range lines {
content += line + "\n"
}
return &ContentResult{
Path: params.Path,
Content: content,
Lines: lines,
TotalLines: lineNum,
Truncated: params.EndLine > 0 && lineNum > params.EndLine,
}, nil
}
Step 4: File Modification
// internal/tools/modify.go
package tools
import (
"fmt"
"os"
"path/filepath"
)
// ModifyFileParams defines modification parameters
type ModifyFileParams struct {
Path string `json:"path"`
Content string `json:"content"`
Mode string `json:"mode"` // "write", "append", "insert"
LineNum int `json:"lineNum,omitempty"` // For insert mode
Backup bool `json:"backup"` // Create backup before modifying
}
// ModifyResult contains modification result
type ModifyResult struct {
Path string `json:"path"`
BytesWritten int `json:"bytesWritten"`
BackupPath string `json:"backupPath,omitempty"`
Success bool `json:"success"`
}
// ModifyFile writes or modifies a file
func ModifyFile(params ModifyFileParams) (*ModifyResult, error) {
// Validate mode
if params.Mode == "" {
params.Mode = "write"
}
validModes := map[string]bool{"write": true, "append": true, "insert": true}
if !validModes[params.Mode] {
return nil, fmt.Errorf("invalid mode: %s (use: write, append, insert)", params.Mode)
}
// Create backup if requested
backupPath := ""
if params.Backup {
if _, err := os.Stat(params.Path); err == nil {
backupPath = params.Path + ".backup"
if err := copyFile(params.Path, backupPath); err != nil {
return nil, fmt.Errorf("failed to create backup: %w", err)
}
}
}
var bytesWritten int
var err error
switch params.Mode {
case "write":
bytesWritten, err = writeFile(params.Path, params.Content)
case "append":
bytesWritten, err = appendFile(params.Path, params.Content)
case "insert":
bytesWritten, err = insertAtLine(params.Path, params.Content, params.LineNum)
}
if err != nil {
// Restore backup on failure
if backupPath != "" {
copyFile(backupPath, params.Path)
}
return nil, err
}
return &ModifyResult{
Path: params.Path,
BytesWritten: bytesWritten,
BackupPath: backupPath,
Success: true,
}, nil
}
// Helper functions
func writeFile(path, content string) (int, error) {
// Ensure directory exists
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return 0, fmt.Errorf("failed to create directory: %w", err)
}
err := os.WriteFile(path, []byte(content), 0644)
if err != nil {
return 0, fmt.Errorf("failed to write file: %w", err)
}
return len(content), nil
}
func appendFile(path, content string) (int, error) {
file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return 0, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
n, err := file.WriteString(content)
if err != nil {
return 0, fmt.Errorf("failed to append: %w", err)
}
return n, nil
}
func insertAtLine(path, content string, lineNum int) (int, error) {
// Read existing content
existing, err := os.ReadFile(path)
if err != nil && !os.IsNotExist(err) {
return 0, fmt.Errorf("failed to read file: %w", err)
}
// Split into lines
lines := splitLines(string(existing))
// Insert content
if lineNum < 0 || lineNum > len(lines) {
return 0, fmt.Errorf("line number %d out of range (1-%d)", lineNum, len(lines))
}
newLines := append(lines[:lineNum], append([]string{content}, lines[lineNum:]...)...)
newContent := joinLines(newLines)
// Write back
return writeFile(path, newContent)
}
func copyFile(src, dst string) error {
data, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dst, data, 0644)
}
func splitLines(s string) []string {
var lines []string
start := 0
for i := 0; i < len(s); i++ {
if s[i] == '\n' {
lines = append(lines, s[start:i])
start = i + 1
}
}
if start < len(s) {
lines = append(lines, s[start:])
}
return lines
}
func joinLines(lines []string) string {
result := ""
for i, line := range lines {
result += line
if i < len(lines)-1 {
result += "\n"
}
}
return result
}
Step 5: Content Search and Replace
// internal/tools/replace.go
package tools
import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
)
// SearchContentParams defines content search parameters
type SearchContentParams struct {
Path string `json:"path"`
Pattern string `json:"pattern"`
Regex bool `json:"regex"`
IgnoreCase bool `json:"ignoreCase"`
MaxMatches int `json:"maxMatches"`
}
// SearchMatch represents a match
type SearchMatch struct {
Line int `json:"line"`
Column int `json:"column"`
Text string `json:"text"`
Context string `json:"context"`
}
// SearchContentResult contains search results
type SearchContentResult struct {
Matches []SearchMatch `json:"matches"`
TotalFound int `json:"totalFound"`
Truncated bool `json:"truncated"`
}
// SearchContent searches for text within a file
func SearchContent(params SearchContentParams) (*SearchContentResult, error) {
file, err := os.Open(params.Path)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
if params.MaxMatches == 0 {
params.MaxMatches = 100
}
var pattern *regexp.Regexp
if params.Regex {
flags := ""
if params.IgnoreCase {
flags = "(?i)"
}
pattern, err = regexp.Compile(flags + params.Pattern)
if err != nil {
return nil, fmt.Errorf("invalid regex: %w", err)
}
}
var matches []SearchMatch
scanner := bufio.NewScanner(file)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
var found bool
var col int
if pattern != nil {
loc := pattern.FindStringIndex(line)
found = loc != nil
if found {
col = loc[0]
}
} else {
searchText := params.Pattern
searchLine := line
if params.IgnoreCase {
searchText = strings.ToLower(searchText)
searchLine = strings.ToLower(line)
}
col = strings.Index(searchLine, searchText)
found = col >= 0
}
if found {
matches = append(matches, SearchMatch{
Line: lineNum,
Column: col,
Text: params.Pattern,
Context: line,
})
if len(matches) >= params.MaxMatches {
break
}
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error scanning file: %w", err)
}
return &SearchContentResult{
Matches: matches,
TotalFound: len(matches),
Truncated: len(matches) >= params.MaxMatches,
}, nil
}
// ReplaceContentParams defines replacement parameters
type ReplaceContentParams struct {
Path string `json:"path"`
Pattern string `json:"pattern"`
Replacement string `json:"replacement"`
Regex bool `json:"regex"`
IgnoreCase bool `json:"ignoreCase"`
ReplaceAll bool `json:"replaceAll"`
Backup bool `json:"backup"`
}
// ReplaceResult contains replacement results
type ReplaceResult struct {
Path string `json:"path"`
Replacements int `json:"replacements"`
BackupPath string `json:"backupPath,omitempty"`
Success bool `json:"success"`
}
// ReplaceContent finds and replaces text in a file
func ReplaceContent(params ReplaceContentParams) (*ReplaceResult, error) {
// Read file
content, err := os.ReadFile(params.Path)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
text := string(content)
var newText string
var count int
// Perform replacement
if params.Regex {
flags := ""
if params.IgnoreCase {
flags = "(?i)"
}
pattern, err := regexp.Compile(flags + params.Pattern)
if err != nil {
return nil, fmt.Errorf("invalid regex: %w", err)
}
if params.ReplaceAll {
newText = pattern.ReplaceAllString(text, params.Replacement)
count = len(pattern.FindAllString(text, -1))
} else {
newText = pattern.ReplaceAllStringFunc(text, func(match string) string {
if count == 0 {
count++
return params.Replacement
}
return match
})
}
} else {
oldText := params.Pattern
if params.IgnoreCase {
// Case-insensitive non-regex replacement is complex
// For simplicity, we'll use regex
pattern := regexp.MustCompile("(?i)" + regexp.QuoteMeta(params.Pattern))
if params.ReplaceAll {
newText = pattern.ReplaceAllString(text, params.Replacement)
count = len(pattern.FindAllString(text, -1))
} else {
count = 0
newText = pattern.ReplaceAllStringFunc(text, func(match string) string {
if count == 0 {
count++
return params.Replacement
}
return match
})
}
} else {
if params.ReplaceAll {
count = strings.Count(text, oldText)
newText = strings.ReplaceAll(text, oldText, params.Replacement)
} else {
newText = strings.Replace(text, oldText, params.Replacement, 1)
if newText != text {
count = 1
}
}
}
}
// Create backup if requested
backupPath := ""
if params.Backup {
backupPath = params.Path + ".backup"
if err := os.WriteFile(backupPath, content, 0644); err != nil {
return nil, fmt.Errorf("failed to create backup: %w", err)
}
}
// Write modified content
if err := os.WriteFile(params.Path, []byte(newText), 0644); err != nil {
return nil, fmt.Errorf("failed to write file: %w", err)
}
return &ReplaceResult{
Path: params.Path,
Replacements: count,
BackupPath: backupPath,
Success: true,
}, nil
}
Step 6: MCP Server Implementation
// internal/mcp/server.go
package mcp
import (
"encoding/json"
"fmt"
"io"
"log"
"mcp-file-tool/internal/tools"
)
// Server is the MCP server
type Server struct {
tools map[string]ToolHandler
}
// ToolHandler processes tool calls
type ToolHandler func(params json.RawMessage) (interface{}, error)
// NewServer creates a new MCP server
func NewServer() *Server {
s := &Server{
tools: make(map[string]ToolHandler),
}
// Register tools
s.registerTools()
return s
}
// registerTools registers all available tools
func (s *Server) registerTools() {
s.tools["search_files"] = s.handleSearchFiles
s.tools["fetch_content"] = s.handleFetchContent
s.tools["modify_file"] = s.handleModifyFile
s.tools["search_content"] = s.handleSearchContent
s.tools["replace_content"] = s.handleReplaceContent
}
// GetTools returns tool definitions
func (s *Server) GetTools() []ToolDefinition {
return []ToolDefinition{
{
Name: "search_files",
Description: "Search for files matching a pattern",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"pattern": map[string]string{"type": "string"},
"path": map[string]string{"type": "string"},
"maxResults": map[string]string{"type": "integer"},
},
"required": []string{"pattern"},
},
},
{
Name: "fetch_content",
Description: "Fetch content from a file",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"path": map[string]string{"type": "string"},
"startLine": map[string]string{"type": "integer"},
"endLine": map[string]string{"type": "integer"},
},
"required": []string{"path"},
},
},
{
Name: "modify_file",
Description: "Write or modify a file",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"path": map[string]string{"type": "string"},
"content": map[string]string{"type": "string"},
"mode": map[string]string{"type": "string"},
"backup": map[string]string{"type": "boolean"},
},
"required": []string{"path", "content"},
},
},
{
Name: "search_content",
Description: "Search for text within a file",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"path": map[string]string{"type": "string"},
"pattern": map[string]string{"type": "string"},
"regex": map[string]string{"type": "boolean"},
},
"required": []string{"path", "pattern"},
},
},
{
Name: "replace_content",
Description: "Find and replace text in a file",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"path": map[string]string{"type": "string"},
"pattern": map[string]string{"type": "string"},
"replacement": map[string]string{"type": "string"},
"regex": map[string]string{"type": "boolean"},
"backup": map[string]string{"type": "boolean"},
},
"required": []string{"path", "pattern", "replacement"},
},
},
}
}
// Tool handlers
func (s *Server) handleSearchFiles(params json.RawMessage) (interface{}, error) {
var p tools.FileSearchParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, err
}
return tools.SearchFiles(p)
}
func (s *Server) handleFetchContent(params json.RawMessage) (interface{}, error) {
var p tools.FetchContentParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, err
}
return tools.FetchContent(p)
}
func (s *Server) handleModifyFile(params json.RawMessage) (interface{}, error) {
var p tools.ModifyFileParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, err
}
return tools.ModifyFile(p)
}
func (s *Server) handleSearchContent(params json.RawMessage) (interface{}, error) {
var p tools.SearchContentParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, err
}
return tools.SearchContent(p)
}
func (s *Server) handleReplaceContent(params json.RawMessage) (interface{}, error) {
var p tools.ReplaceContentParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, err
}
return tools.ReplaceContent(p)
}
// HandleRequest processes an MCP request
func (s *Server) HandleRequest(req Request) Response {
switch req.Method {
case "tools/list":
return Response{
ID: req.ID,
Result: map[string]interface{}{"tools": s.GetTools()},
}
case "tools/call":
var callParams struct {
Name string `json:"name"`
Arguments json.RawMessage `json:"arguments"`
}
if err := json.Unmarshal(req.Params, &callParams); err != nil {
return s.errorResponse(req.ID, ErrCodeInvalidParams, "Invalid parameters")
}
handler, exists := s.tools[callParams.Name]
if !exists {
return s.errorResponse(req.ID, ErrCodeMethodNotFound,
fmt.Sprintf("Tool not found: %s", callParams.Name))
}
result, err := handler(callParams.Arguments)
if err != nil {
return s.errorResponse(req.ID, ErrCodeInternalError, err.Error())
}
return Response{ID: req.ID, Result: result}
default:
return s.errorResponse(req.ID, ErrCodeMethodNotFound,
fmt.Sprintf("Unknown method: %s", req.Method))
}
}
func (s *Server) errorResponse(id string, code int, message string) Response {
return Response{
ID: id,
Error: &Error{
Code: code,
Message: message,
},
}
}
// Serve handles stdio communication
func (s *Server) Serve(reader io.Reader, writer io.Writer) error {
decoder := json.NewDecoder(reader)
encoder := json.NewEncoder(writer)
log.Println("MCP File Operations Server started")
for {
var req Request
if err := decoder.Decode(&req); err != nil {
if err == io.EOF {
break
}
log.Printf("Decode error: %v", err)
continue
}
log.Printf("Request: %s - %s", req.ID, req.Method)
resp := s.HandleRequest(req)
if err := encoder.Encode(resp); err != nil {
log.Printf("Encode error: %v", err)
}
}
return nil
}
Step 7: Main Entry Point
// cmd/server/main.go
package main
import (
"log"
"os"
"mcp-file-tool/internal/mcp"
)
func main() {
server := mcp.NewServer()
// Serve over stdio (standard for MCP)
if err := server.Serve(os.Stdin, os.Stdout); err != nil {
log.Fatalf("Server error: %v", err)
}
}
MCP Request/Response Flow
Building and Testing
Build the Server
# Initialize module
go mod init mcp-file-tool
# Install dependencies
go get github.com/fsnotify/fsnotify
# Build
go build -o mcp-file-server ./cmd/server
# Or build for multiple platforms
GOOS=linux GOARCH=amd64 go build -o mcp-file-server-linux ./cmd/server
GOOS=darwin GOARCH=amd64 go build -o mcp-file-server-mac ./cmd/server
GOOS=windows GOARCH=amd64 go build -o mcp-file-server.exe ./cmd/server
Test with Examples
// test/integration_test.go
package test
import (
"encoding/json"
"strings"
"testing"
"mcp-file-tool/internal/mcp"
)
func TestToolsList(t *testing.T) {
server := mcp.NewServer()
req := mcp.Request{
ID: "1",
Method: "tools/list",
}
resp := server.HandleRequest(req)
if resp.Error != nil {
t.Fatalf("Error: %v", resp.Error)
}
// Verify we have all tools
result := resp.Result.(map[string]interface{})
tools := result["tools"].([]mcp.ToolDefinition)
if len(tools) != 5 {
t.Errorf("Expected 5 tools, got %d", len(tools))
}
}
func TestSearchFiles(t *testing.T) {
server := mcp.NewServer()
params := map[string]interface{}{
"pattern": "*.go",
"path": ".",
}
paramsJSON, _ := json.Marshal(params)
callParams := map[string]interface{}{
"name": "search_files",
"arguments": json.RawMessage(paramsJSON),
}
callParamsJSON, _ := json.Marshal(callParams)
req := mcp.Request{
ID: "2",
Method: "tools/call",
Params: json.RawMessage(callParamsJSON),
}
resp := server.HandleRequest(req)
if resp.Error != nil {
t.Fatalf("Error: %v", resp.Error)
}
t.Logf("Search result: %+v", resp.Result)
}
Manual Testing with JSON
# Start server
./mcp-file-server
# Send request (paste into stdin):
{"id":"1","method":"tools/list"}
# Expected response:
{"id":"1","result":{"tools":[...]}}
# Search files:
{"id":"2","method":"tools/call","params":{"name":"search_files","arguments":{"pattern":"*.go","path":"."}}}
Integration with AI Assistants
Configure in Claude Desktop
{
"mcpServers": {
"file-operations": {
"command": "/path/to/mcp-file-server",
"args": []
}
}
}
Using with OpenAI
import subprocess
import json
def call_mcp_tool(tool_name, params):
process = subprocess.Popen(
['./mcp-file-server'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True
)
request = {
"id": "1",
"method": "tools/call",
"params": {
"name": tool_name,
"arguments": params
}
}
process.stdin.write(json.dumps(request) + '\n')
process.stdin.flush()
response = process.stdout.readline()
return json.loads(response)
Performance Optimizations
1. Concurrent File Operations
// Process multiple files concurrently
func SearchFilesConcurrent(params FileSearchParams) (*SearchResult, error) {
results := make(chan FileInfo, 100)
errors := make(chan error, 1)
var wg sync.WaitGroup
// Worker pool for processing
for i := 0; i < runtime.NumCPU(); i++ {
wg.Add(1)
go worker(&wg, results, errors)
}
// ... search logic
go func() {
wg.Wait()
close(results)
}()
// Collect results
// ...
}
2. Caching for Repeated Searches
type CachedServer struct {
*Server
cache *lru.Cache
}
func (s *CachedServer) handleSearchFiles(params json.RawMessage) (interface{}, error) {
// Check cache
key := string(params)
if cached, ok := s.cache.Get(key); ok {
return cached, nil
}
// Call original handler
result, err := s.Server.handleSearchFiles(params)
if err == nil {
s.cache.Add(key, result)
}
return result, err
}
Production Considerations
-
Security:
- Validate all file paths (prevent path traversal)
- Set file size limits
- Restrict access to certain directories
- Sanitize regex patterns
-
Performance:
- Stream large files instead of loading into memory
- Use worker pools for concurrent operations
- Implement caching for frequently accessed files
- Add rate limiting
-
Reliability:
- Always create backups for modifications
- Use atomic writes (write to temp, then rename)
- Handle symlinks carefully
- Proper error messages
-
Monitoring:
- Log all file operations
- Track operation latencies
- Monitor memory usage
- Export metrics (Prometheus)
Conclusion
We’ve built a complete MCP server in Go that provides powerful file operations for AI assistants. This tool demonstrates:
- ✅ Full MCP Protocol Implementation: Standards-compliant server
- ✅ Comprehensive File Operations: Search, read, write, find/replace
- ✅ Production-Ready Code: Error handling, backups, validation
- ✅ High Performance: Efficient file I/O and concurrent processing
- ✅ Easy Integration: Works with any MCP-compatible AI assistant
The same architecture can be extended to support:
- Database operations
- API integrations
- Code analysis tools
- Build system integrations
- DevOps automation
Previous in series: Building a Real-Time File Monitor
MCP Documentation: modelcontextprotocol.io
Source code: Available on GitHub with complete implementation and tests.