Разработка ИИ-агентов · Модуль 4 · Урок 4.2
Разработка MCP-сервера на Go: инструменты и ресурсы
Из чего состоит сервер
MCP-сервер — это программа, которая принимает JSON-RPC сообщения по выбранному транспорту и отвечает на них. Минимально нужно обработать: initialize (рукопожатие и объявление возможностей), tools/list (отдать описания инструментов), tools/call (выполнить инструмент и вернуть результат). Если выставляете ресурсы — добавьте resources/list и resources/read.
На практике это удобно писать на официальном Go SDK для MCP: он берёт на себя JSON-RPC, рукопожатие и транспорты, а вы лишь регистрируете инструменты и их обработчики. Имена пакетов и API SDK версионно-зависимы — сверяйтесь с репозиторием. Но чтобы понять, что внутри, полезно один раз увидеть «голую» реализацию на stdlib.
Инструмент сервера = схема + обработчик
Каждый инструмент на сервере — это пара: описание (имя, текст, inputSchema в JSON Schema) и обработчик — Go-функция, которая принимает разобранные аргументы и возвращает результат. Описание уходит клиенту в ответ на tools/list; обработчик вызывается на tools/call.
Правила те же, что в Модуле 2: ясное имя-глагол, точное описание «что/когда», строгая схема с required и enum. Разница лишь в том, что теперь инструмент живёт в отдельном процессе и доступен любому хосту. Результат tools/call обычно возвращается как массив контента (чаще всего текстовый блок) — клиент передаст его модели.
Ресурсы и аккуратность сервера
Ресурсы выставляют данные только для чтения по URI: resources/list отдаёт каталог (URI + метаданные), resources/read — содержимое. Это удобно для документов, конфигов, записей — хост подтянет их как контекст, не выполняя действий.
Несколько правил продакшн-сервера: валидируйте входные аргументы (не доверяйте клиенту), ошибки возвращайте как корректный JSON-RPC error, а не «падайте»; для stdio помните, что stdout занят протоколом — любые логи пишите в stderr, иначе сломаете обмен сообщениями; делайте обработчики идемпотентными там, где возможно. Сервер — это публичный API: к надёжности и безопасности относитесь соответственно.
// Учебный каркас: читаем JSON-RPC построчно из stdin, отвечаем в stdout.
// В реальном проекте берите официальный MCP Go SDK — он делает это за вас.
type rpcReq struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
}
func main() {
in := bufio.NewScanner(os.Stdin)
in.Buffer(make([]byte, 0, 64*1024), 1<<20)
out := json.NewEncoder(os.Stdout)
for in.Scan() {
var req rpcReq
if err := json.Unmarshal(in.Bytes(), &req); err != nil {
continue
}
switch req.Method {
case "initialize":
_ = out.Encode(result(req.ID, map[string]any{
"protocolVersion": "2025-06-18", // сверьтесь с актуальной спекой
"capabilities": map[string]any{"tools": map[string]any{}},
"serverInfo": map[string]any{"name": "demo", "version": "0.1.0"},
}))
case "tools/list":
_ = out.Encode(result(req.ID, map[string]any{"tools": toolDefs()}))
case "tools/call":
_ = out.Encode(result(req.ID, callTool(req.Params)))
}
}
// ВАЖНО: логи — только в stderr; stdout занят протоколом.
}func callTool(params json.RawMessage) map[string]any {
var p struct {
Name string `json:"name"`
Arguments json.RawMessage `json:"arguments"`
}
_ = json.Unmarshal(params, &p)
switch p.Name {
case "get_weather":
var a struct{ City string `json:"city"` }
if err := json.Unmarshal(p.Arguments, &a); err != nil || a.City == "" {
return textResult("ошибка: нужен непустой city", true) // isError=true
}
return textResult("В городе "+a.City+": +18°C, ясно", false)
default:
return textResult("неизвестный инструмент: "+p.Name, true)
}
}
// content — массив блоков; для текста это {type:"text", text:"..."}.
func textResult(s string, isErr bool) map[string]any {
return map[string]any{
"content": []map[string]any{{"type": "text", "text": s}},
"isError": isErr,
}
}Anti-patterns
| Анти-паттерн | Почему плохо | Как правильно |
|---|---|---|
| Писать в stdout логи при stdio-транспорте | Логи ломают JSON-RPC обмен | Все логи — в stderr; stdout только для протокола |
| Не валидировать аргументы из tools/call | Кривой ввод роняет сервер | Проверять аргументы; ошибку отдавать как результат с isError/JSON-RPC error |
| Реализовывать JSON-RPC и транспорт вручную в проде | Много кода и краевых случаев | Использовать официальный MCP Go SDK; ручная реализация — для понимания |
| Слабые описания/схемы инструментов сервера | Клиентская модель ошибается с вызовами | Те же правила, что в Модуле 2: имя-глагол, чёткое описание, строгая схема |
Практическое задание
- Поднимите минимальный stdio MCP-сервер, отвечающий на
initializeиtools/list. - Добавьте один инструмент с
inputSchemaи обработчикомtools/call, возвращающим текстовый контент. - Сделайте валидацию аргументов и возврат ошибки как результата с
isError, а не паникой. - Убедитесь, что логи идут в stderr, и проверьте обмен, отправив сообщения вручную.
- (Доп.) Перепишите сервер на официальном MCP Go SDK и сравните объём кода.
Проверка знаний
Почему при stdio-транспорте нельзя писать логи в stdout?
Верный ответ: B
B верно. В stdio MCP клиент читает JSON-RPC из stdout сервера; любой лишний текст там нарушает протокол. Логи пишут в stderr.
Что составляет инструмент на MCP-сервере?
Верный ответ: B
B верно. Инструмент = описание/схема (для tools/list) + обработчик (для tools/call). URI (D) относится к ресурсам.
Зачем на практике использовать официальный MCP Go SDK вместо ручной реализации?
Верный ответ: B
B верно. SDK снимает рутину протокола и транспорта. Ручная реализация полезна для понимания, но в проде дороже в поддержке. A и D неверны: вручную тоже можно, просто хлопотнее.