Go SDK
Создание MCP серверов на Go для микросервисной архитектуры
mark3labs/mcp-go — рекомендуемый Go SDK с поддержкой SSE, stdio транспортов и production-ready архитектурой.
Установка
Заголовок раздела «Установка»go get github.com/mark3labs/mcp-goТребования:
- Go 1.21+
Базовый сервер
Заголовок раздела «Базовый сервер»package main
import ( "context" "log"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server")
func main() { s := server.NewMCPServer( "my-server", "1.0.0", server.WithDescription("MCP сервер на Go"), )
if err := server.ServeStdio(s); err != nil { log.Fatal(err) }}Инструменты (Tools)
Заголовок раздела «Инструменты (Tools)»Базовый инструмент
Заголовок раздела «Базовый инструмент»package main
import ( "context" "fmt"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server")
func main() { s := server.NewMCPServer("calculator", "1.0.0")
// Регистрация инструмента через AddTool addTool := mcp.NewTool("add", mcp.WithDescription("Сложение двух чисел"), mcp.WithNumber("a", mcp.Required(), mcp.Description("Первое число")), mcp.WithNumber("b", mcp.Required(), mcp.Description("Второе число")), )
s.AddTool(addTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { a := req.Params.Arguments["a"].(float64) b := req.Params.Arguments["b"].(float64)
return mcp.NewToolResultText(fmt.Sprintf("Результат: %f", a+b)), nil })
server.ServeStdio(s)}Функциональный стиль
Заголовок раздела «Функциональный стиль»// Использование NewToolWithRawSchema для полного контроляdivideTool := mcp.NewToolWithRawSchema("divide", "Деление чисел", map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "a": map[string]interface{}{"type": "number", "description": "Делимое"}, "b": map[string]interface{}{"type": "number", "description": "Делитель"}, }, "required": []string{"a", "b"}, },)
s.AddTool(divideTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { a := req.Params.Arguments["a"].(float64) b := req.Params.Arguments["b"].(float64)
if b == 0 { return nil, fmt.Errorf("деление на ноль") }
return mcp.NewToolResultText(fmt.Sprintf("Результат: %f", a/b)), nil})Инструмент с HTTP запросами
Заголовок раздела «Инструмент с HTTP запросами»import ( "io" "net/http")
fetchTool := mcp.NewTool("fetch_url", mcp.WithDescription("Загрузка содержимого URL"), mcp.WithString("url", mcp.Required(), mcp.Description("URL для загрузки")),)
s.AddTool(fetchTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { url := req.Params.Arguments["url"].(string)
resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) if err != nil { return nil, err }
return mcp.NewToolResultText(string(body)), nil})Инструмент с файловой системой
Заголовок раздела «Инструмент с файловой системой»import ( "os" "path/filepath" "strings")
readFileTool := mcp.NewTool("read_file", mcp.WithDescription("Чтение файла"), mcp.WithString("path", mcp.Required(), mcp.Description("Путь к файлу")),)
s.AddTool(readFileTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { path := req.Params.Arguments["path"].(string)
// Проверка безопасности cleanPath := filepath.Clean(path) if !strings.HasPrefix(cleanPath, "/allowed/") { return nil, fmt.Errorf("доступ запрещён") }
content, err := os.ReadFile(cleanPath) if err != nil { return nil, err }
return mcp.NewToolResultText(string(content)), nil})Инструмент с базой данных
Заголовок раздела «Инструмент с базой данных»import ( "database/sql" _ "github.com/lib/pq")
type QueryParams struct { SQL string `json:"sql"`}
type DBTool struct { db *sql.DB}
func NewDBTool(connStr string) (*DBTool, error) { db, err := sql.Open("postgres", connStr) if err != nil { return nil, err } return &DBTool{db: db}, nil}
func (t *DBTool) Query(ctx context.Context, params json.RawMessage) ([]mcp.Content, error) { var p QueryParams if err := json.Unmarshal(params, &p); err != nil { return nil, err }
rows, err := t.db.QueryContext(ctx, p.SQL) if err != nil { return nil, err } defer rows.Close()
var results []map[string]interface{} columns, _ := rows.Columns()
for rows.Next() { values := make([]interface{}, len(columns)) pointers := make([]interface{}, len(columns)) for i := range values { pointers[i] = &values[i] }
rows.Scan(pointers...)
row := make(map[string]interface{}) for i, col := range columns { row[col] = values[i] } results = append(results, row) }
output, _ := json.MarshalIndent(results, "", " ") return []mcp.Content{ mcp.TextContent(string(output)), }, nil}Ресурсы (Resources)
Заголовок раздела «Ресурсы (Resources)»func configResource(ctx context.Context, uri string) (mcp.ResourceContent, error) { config := map[string]interface{}{ "version": "1.0.0", "environment": "production", }
data, _ := json.Marshal(config) return mcp.ResourceContent{ URI: uri, MimeType: "application/json", Text: string(data), }, nil}
func main() { server := mcp.NewServer("config-server", "1.0.0")
server.AddResource(mcp.Resource{ URI: "config://app", Name: "App Configuration", Description: "Конфигурация приложения", MimeType: "application/json", Handler: configResource, })
server.Run()}Динамический ресурс
Заголовок раздела «Динамический ресурс»func fileResource(ctx context.Context, uri string) (mcp.ResourceContent, error) { // Извлечение пути из URI path := strings.TrimPrefix(uri, "file://")
content, err := os.ReadFile(path) if err != nil { return mcp.ResourceContent{}, err }
return mcp.ResourceContent{ URI: uri, MimeType: "text/plain", Text: string(content), }, nil}
server.AddResource(mcp.Resource{ URI: "file://{path}", Name: "File Reader", Description: "Чтение файлов", Handler: fileResource,})Промпты (Prompts)
Заголовок раздела «Промпты (Prompts)»type CodeReviewParams struct { Language string `json:"language"` Code string `json:"code"`}
func codeReviewPrompt(ctx context.Context, params json.RawMessage) ([]mcp.Message, error) { var p CodeReviewParams if err := json.Unmarshal(params, &p); err != nil { return nil, err }
prompt := fmt.Sprintf(`Проведи код-ревью следующего %s кода:
%s%s%s%s
Обрати внимание на:1. Потенциальные баги2. Производительность3. Безопасность4. Best practices`, p.Language, "```", p.Language, p.Code, "```")
return []mcp.Message{ {Role: "user", Content: mcp.TextContent(prompt)}, }, nil}
server.AddPrompt(mcp.Prompt{ Name: "code_review", Description: "Промпт для код-ревью", Schema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "language": map[string]interface{}{"type": "string"}, "code": map[string]interface{}{"type": "string"}, }, "required": []string{"language", "code"}, }, Handler: codeReviewPrompt,})Обработка ошибок
Заголовок раздела «Обработка ошибок»import "github.com/paulsmith/mcp-go/errors"
func safeTool(ctx context.Context, params json.RawMessage) ([]mcp.Content, error) { var p Params if err := json.Unmarshal(params, &p); err != nil { return nil, errors.InvalidParams("Неверный формат параметров") }
if !isAllowed(p.Path) { return nil, errors.Forbidden("Доступ запрещён") }
data, err := loadData(p.Path) if err != nil { if os.IsNotExist(err) { return nil, errors.NotFound("Ресурс не найден") } return nil, errors.Internal("Внутренняя ошибка") }
return []mcp.Content{mcp.TextContent(data)}, nil}Логирование
Заголовок раздела «Логирование»import ( "log/slog" "os")
func main() { // Настройка структурированного логирования logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ Level: slog.LevelInfo, })) slog.SetDefault(logger)
server := mcp.NewServer("logged-server", "1.0.0") server.SetLogger(logger)
server.Run()}Middleware
Заголовок раздела «Middleware»func loggingMiddleware(next mcp.ToolHandler) mcp.ToolHandler { return func(ctx context.Context, params json.RawMessage) ([]mcp.Content, error) { start := time.Now() slog.Info("Tool called", "params", string(params))
result, err := next(ctx, params)
duration := time.Since(start) if err != nil { slog.Error("Tool failed", "error", err, "duration", duration) } else { slog.Info("Tool completed", "duration", duration) }
return result, err }}Graceful Shutdown
Заголовок раздела «Graceful Shutdown»func main() { server := mcp.NewServer("graceful-server", "1.0.0")
// Настройка инструментов...
// Graceful shutdown ctx, cancel := context.WithCancel(context.Background()) defer cancel()
sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() { <-sigChan slog.Info("Получен сигнал завершения") cancel() }()
if err := server.RunWithContext(ctx); err != nil && err != context.Canceled { log.Fatal(err) }}Структура проекта
Заголовок раздела «Структура проекта»my-mcp-server/├── cmd/│ └── server/│ └── main.go├── internal/│ ├── tools/│ │ ├── calculator.go│ │ └── filesystem.go│ ├── resources/│ │ └── config.go│ └── prompts/│ └── templates.go├── go.mod├── go.sum├── Dockerfile└── README.md# Build stageFROM golang:1.21-alpine AS builderWORKDIR /appCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 go build -o mcp-server ./cmd/server
# Runtime stageFROM alpine:latestRUN apk --no-cache add ca-certificatesCOPY --from=builder /app/mcp-server /usr/local/bin/CMD ["mcp-server"]Конфигурация Claude Desktop
Заголовок раздела «Конфигурация Claude Desktop»{ "mcpServers": { "go-server": { "command": "/path/to/mcp-server" } }}SSE транспорт
Заголовок раздела «SSE транспорт»package main
import ( "log" "net/http"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server")
func main() { s := server.NewMCPServer("sse-server", "1.0.0")
// Добавление инструментов...
// SSE сервер sseServer := server.NewSSEServer(s)
http.HandleFunc("/sse", sseServer.HandleSSE) http.HandleFunc("/message", sseServer.HandleMessage)
log.Println("SSE сервер запущен на :3000") log.Fatal(http.ListenAndServe(":3000", nil))}Альтернативные SDK
Заголовок раздела «Альтернативные SDK»- riza-io/mcp-go — Производственная реализация для enterprise
- paulsmith/mcp-go — Минималистичная реализация