Привет! Кажется, все вокруг уже повайбкодили, потыкали агентов и упёрлись в одну и ту же стену: из коробки модель не знает про вашу систему ничего. Она не видит ваши логи, вашу базу, вашу Графану. Можно сколько угодно просить Claude "глянь, что там с продом" - доступов у него нет, и максимум, что он сделает, это уверенно нафантазирует.
Собственно, сегодня доступы выдаём: разбираемся, что такое MCP, и пишем свой сервер на Go.
В чём суть?
Помните зоопарк зарядок до USB-C? С интеграциями LLM было ровно так же: у каждого вендора свои плагины и свои форматы тулов, и написанное под одну модель не работало с другой. Продолжалось это, пока Anthropic не выкатили MCP - единый протокол, по которому любая модель подключается к любому инструменту. Стандарт быстро подхватили все: OpenAI, Google, Cursor и остальные.
Механика до безобразия простая. MCP-сервер - это обычный сервис, который по JSON-RPC (через stdio или HTTP) отдаёт клиенту список своих тулов с JSON-схемами. Считайте, свагер, только читает его не фронтендер, а нейронка. Дальше клиент (Claude, Cursor, да кто угодно) сам решает, когда и какой тул дёрнуть, чтобы ответить на ваш вопрос. Вам остаётся написать сами тулы - то есть обычный гошный код, который вы пишете каждый день(Или нейронка пишет за вас).
Пишем свой
У протокола есть официальный Go SDK, который Anthropic пилит вместе с Google, недавно доехала стабильная версия. Из приятного: схемы тулов генерятся из обычных структур с тегами, руками JSON Schema писать не надо - за это уже подумали.
Соберём сервер pg-doctor с тулом "покажи, какие таблицы распухли".
Разберём по шагам.
Шаг 1. Описываем контракт тула
Начинаем со структур входа и выхода:
type In struct {
Limit int json:"limit" jsonschema:"сколько таблиц вернуть"
}
type Out struct {
Tables []string json:"tables" jsonschema:"таблицы и их размеры"
}
Обычные структуры с тегами, но тег jsonschema тут - самое важное место во всём сервере. SDK сгенерит из них JSON Schema, и именно её увидит модель, когда будет решать, что это за тул и какие параметры ему передать. Чем понятнее описание - тем реже нейронка промахивается с вызовом.
Шаг 2. Поднимаем сервер
server := mcp.NewServer(&mcp.Implementation{
Name: "pg-doctor",
Version: "v1.0.0",
}, nil)
Implementation - это просто визитка: имя и версия, которыми сервер представится клиенту при рукопожатии. Вторым аргументом можно передать опции, но для нашего проекта хватит nil.
Шаг 3. Пишем сам тул
А теперь заметьте, из чего состоит тул. Это обычная гошная функция с контекстом:
func topTables(
ctx context.Context,
req *mcp.CallToolRequest,
in In,
) (*mcp.CallToolResult, Out, error) {
query := `
SELECT relname || ': ' ||
pg_size_pretty(pg_total_relation_size(relid))
FROM pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT $1`
rows, err := db.QueryContext(ctx, query, in.Limit)
if err != nil {
return nil, Out{}, err
}
defer rows.Close()
var out Out
for rows.Next() {
var t string
rows.Scan(&t)
out.Tables = append(out.Tables, t)
}
return nil, out, rows.Err()
}
Никакой магии: SDK сам распарсит аргументы от модели в нашу структуру In, сам сериализует Out обратно. Внутри - самый обычный database/sql, каким вы ходите в базу каждый день. Description при этом работает так же, как теги из первого шага: по нему модель понимает, когда этот тул вообще уместно дёргать.
Осталось зарегистрировать функцию как тул - одной строкой:
gomcp.AddTool(server, &mcp.Tool{
Name: "top_tables",
Description: "топ самых больших таблиц в базе",
}, topTables)
Description работает так же, как теги из первого шага: по нему модель понимает, когда этот тул вообще уместно дёргать.
Шаг 4. Подключаем транспорт
ctx := context.Background()
err := server.Run(ctx, &mcp.StdioTransport{})
if err != nil {
log.Fatal(err)
}