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.