Перейти к содержимому

Go SDK

Создание MCP серверов на Go для микросервисной архитектуры

mark3labs/mcp-go — рекомендуемый Go SDK с поддержкой SSE, stdio транспортов и production-ready архитектурой.

Terminal
go get github.com/mark3labs/mcp-go

Требования:

  • Go 1.21+
main.go
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)
}
}
main.go
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)
}
tools.go
// Использование 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_tools.go
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
})
file_tools.go
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
})
db_tools.go
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.go
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()
}
resources.go
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.go
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,
})
errors.go
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
}
main.go
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.go
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
}
}
main.go
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
Dockerfile
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o mcp-server ./cmd/server
# Runtime stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/mcp-server /usr/local/bin/
CMD ["mcp-server"]
claude_desktop_config.json
{
"mcpServers": {
"go-server": {
"command": "/path/to/mcp-server"
}
}
}
main.go
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))
}
  • riza-io/mcp-go — Производственная реализация для enterprise
  • paulsmith/mcp-go — Минималистичная реализация