← MCP in Practice: Building Model Context Protocol Servers
02 / 07 · 50 min

Protocol Anatomy: Tools, Resources, Prompts, and JSON-RPC

JSON-RPC 2.0 frames, the session lifecycle, the three server primitives, and the two transports — stdio and Streamable HTTP.

Lesson content is currently available in Russian.

What you will learn

  • →Читать и писать кадры JSON-RPC 2.0: request, response, notification.
  • →Объяснять рукопожатие initialize и согласование capabilities.
  • →Различать tools, resources и prompts по тому, кто ими управляет, и выбирать транспорт под сценарий.

JSON-RPC 2.0 и жизненный цикл сессии

Весь MCP — это JSON-RPC 2.0 поверх транспорта. Кадров три вида: request (есть id, ждёт ответа), response (несёт result или error с тем же id) и notification (нет id, ответа не будет). Больше ничего в протоколе нет — любой сложный сценарий раскладывается на эти кадры.

Сессия начинается рукопожатием. Клиент отправляет initialize — сообщает версию протокола и свои возможности:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "roots": { "listChanged": true } },
    "clientInfo": { "name": "claude-code", "version": "2.0.0" }
  }
}

Сервер отвечает своей версией и capabilities — например { "tools": { "listChanged": true }, "resources": {} } — после чего клиент шлёт notification notifications/initialized, и сессия открыта. Смысл согласования capabilities в том, что обе стороны заранее знают, какие методы можно вызывать: если сервер не заявил prompts, клиент не будет спрашивать prompts/list.

Ошибки уровня протокола возвращаются стандартным объектом JSON-RPC: { "code": -32601, "message": "Method not found" }. Коды -32700 (parse error), -32600 (invalid request), -32602 (invalid params) стоит выучить — вы будете видеть их при отладке чаще, чем хотелось бы.

Tools: контракт вызова действия

Tools — примитив, которым управляет модель (model-controlled): именно LLM решает, когда вызвать инструмент. Клиент запрашивает список методом tools/list и получает определения. Каждое определение — это имя, описание для модели и JSON Schema аргументов:

{
  "name": "search_orders",
  "description": "Ищет заказы по статусу и лимиту. Возвращает не более limit записей.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "status": { "type": "string", "enum": ["new", "paid", "shipped"] },
      "limit": { "type": "number" }
    },
    "required": ["status"]
  }
}

Вызов — метод tools/call с именем и аргументами; ответ несёт массив content (текст, изображения, ссылки на ресурсы) и флаг isError:

{ "jsonrpc": "2.0", "id": 7, "method": "tools/call",
  "params": { "name": "search_orders", "arguments": { "status": "paid", "limit": 5 } } }
{ "jsonrpc": "2.0", "id": 7, "result": {
  "content": [{ "type": "text", "text": "[{\"id\":\"o_1\",\"total\":4200}]" }],
  "isError": false } }

Ключевая тонкость: ошибок два уровня. JSON-RPC error означает «вызов не состоялся» — метода нет, аргументы не прошли схему. А isError: true внутри result означает «инструмент выполнился и сообщил о неудаче» — например, внешний API вернул 404. Второй вариант модель видит и может отреагировать: поправить аргументы, попробовать другой путь, объяснить пользователю.

Resources и prompts: данные и шаблоны

Два оставшихся примитива отличаются от tools тем, кто решает их использовать.

Resources — application-controlled: приложение (host) само решает, какие данные подтянуть в контекст. Ресурс адресуется URI и читается без побочных эффектов — это «GET» мира MCP. Список отдаёт resources/list, чтение — resources/read:

{ "jsonrpc": "2.0", "id": 3, "method": "resources/read",
  "params": { "uri": "orders://o_1" } }
{ "jsonrpc": "2.0", "id": 3, "result": { "contents": [
  { "uri": "orders://o_1", "mimeType": "application/json",
    "text": "{\"id\":\"o_1\",\"status\":\"paid\"}" }
] } }

Для семейств однотипных ресурсов есть resource templates — URI-шаблоны вида orders://{orderId} по RFC 6570, которые клиент раскрывает сам.

Prompts — user-controlled: пользователь явно выбирает шаблон, обычно через slash-команду в интерфейсе. Метод prompts/get принимает аргументы и возвращает готовые сообщения для модели. Так сервер поставляет проверенные сценарии работы со своим доменом: «сделай недельный отчёт по заказам», «разбери жалобу клиента по шаблону».

Практическое правило выбора примитива: побочные эффекты или вычисления → tool; чистые данные для контекста → resource; заготовленный сценарий, который запускает человек → prompt. Один сервер обычно комбинирует все три.

Транспорты: stdio и Streamable HTTP

Кадры JSON-RPC нужно как-то доставлять. Спецификация определяет два транспорта.

stdio — host сам запускает процесс сервера и пишет кадры в его stdin, а читает из stdout, по одному JSON-сообщению на строку. Это транспорт по умолчанию для локальных серверов: нулевая сетевая настройка, секреты передаются через переменные окружения процесса, жизненный цикл управляется клиентом. Отсюда железное правило, о которое разбивается каждый второй новичок: stdout принадлежит протоколу. Один случайный console.log — и клиент получает мусор вместо кадра, парсер падает, соединение рвётся. Все логи — только в stderr.

Streamable HTTP (ревизия 2025-03-26, заменил устаревший HTTP+SSE) — сервер поднимает один endpoint, обычно /mcp. Клиент шлёт POST с JSON-RPC-кадром; сервер отвечает либо одиночным JSON, либо открывает SSE-поток — это позволяет слать прогресс и уведомления во время долгих операций. Сессия идентифицируется заголовком Mcp-Session-Id, который сервер выдаёт при initialize, а клиент возвращает в каждом запросе. Транспорт для удалённых серверов: один процесс обслуживает много пользователей, работает авторизация OAuth, стандартное масштабирование за балансировщиком.

Выбор прост. Локальный инструмент для разработчика (доступ к файлам, git, локальной БД) — stdio. Продукт-как-сервис, мультитенантность, централизованные обновления — Streamable HTTP. TypeScript SDK позволяет собрать один сервер и подключить к нему любой из транспортов — логика не меняется.

Practice

Сессия на бумаге

Не запуская код, выпишите полную последовательность кадров JSON-RPC для сценария: клиент подключается к серверу заказов, получает список инструментов, вызывает search_orders с невалидным статусом (ошибка схемы), затем с валидным (успех), читает ресурс orders://o_1 и корректно завершает сессию. Сверьте каждый кадр со спецификацией.

Deliverable

Текстовый файл с 8–10 валидными JSON-RPC-кадрами: initialize и initialized, tools/list, две попытки tools/call (ошибка -32602 и успех), resources/read — с корректными id и структурой.

Checkpoint

Почему у tools/call ошибки существуют на двух разных уровнях и чем ответ с JSON-RPC error отличается от result с isError: true?

  • ✓ JSON-RPC error описан как ошибка протокола: метод не найден, аргументы не прошли схему — вызов не состоялся.
  • ✓ isError: true описан как результат состоявшегося вызова: инструмент выполнился и сообщил о неудаче доменного уровня.
  • ✓ Указано, что содержимое result с isError видит модель и может скорректировать своё поведение.

Check yourself

Каким методом открывается MCP-сессия?

01Каким методом открывается MCP-сессия?

Что обязательно входит в определение инструмента в ответе tools/list?

02Что обязательно входит в определение инструмента в ответе tools/list?

Чем notification отличается от request в JSON-RPC 2.0?

03Чем notification отличается от request в JSON-RPC 2.0?

Кто управляет каждым примитивом MCP?

04Кто управляет каждым примитивом MCP?

Почему console.log ломает stdio-сервер?

05Почему console.log ломает stdio-сервер?

Sources

Protocol Anatomy: Tools, Resources, Prompts, and JSON-RPC · MCP in Practice: Building Model Context Protocol Servers · VORCL