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
Sources