Разработка ИИ-агентов · Модуль 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: к надёжности и безопасности относитесь соответственно.

Минимальный stdio MCP-сервер на stdlib: каркас цикла JSON-RPC
// Учебный каркас: читаем 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 занят протоколом.
}
Обработчик tools/call: разбор аргументов и текстовый результат
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?

  • A stdout медленнее stderr
  • B stdout занят протоколом JSON-RPC — посторонний вывод ломает обмен сообщениями
  • C Go запрещает писать в stdout из библиотек
  • D Логи вообще не нужны

Что составляет инструмент на MCP-сервере?

  • A Только имя
  • B Описание со схемой входа (inputSchema) плюс обработчик, выполняющий действие на tools/call
  • C Готовый ответ модели
  • D URI ресурса

Зачем на практике использовать официальный MCP Go SDK вместо ручной реализации?

  • A Ручная реализация невозможна
  • B SDK берёт на себя JSON-RPC, рукопожатие и транспорты, оставляя вам регистрацию инструментов; меньше кода и краевых случаев
  • C SDK делает модель умнее
  • D Без SDK сервер не пройдёт initialize