Архитектура: хост, клиент, сервер
В прошлом уроке мы договорились о словах: хост, клиент, сервер. Теперь посмотрим, что между ними на самом деле летает. Это пригодится, когда сервер «не подключается» и нужно понять, где обрыв, — а ещё чтобы не бояться логов инспектора, который мы откроем через несколько уроков.
Ещё раз о трёх ролях
Хост — это приложение: Claude Code, приложение Claude, VS Code. Он создаёт по одному клиенту на каждый подключённый сервер, и каждый клиент держит выделенное соединение со своим сервером. Если в Claude Code подключены серверы трекера, GitLab и файловой системы, внутри работают три клиента, и они ничего не знают друг о друге.
Сервер — программа, которая отвечает на запросы клиента. Где она запущена, неважно: локальный сервер стартует как дочерний процесс хоста прямо на вашем ноутбуке, удалённый живёт где-то в облаке и обслуживает много клиентов сразу. С точки зрения протокола это один и тот же сервер.
Два слоя: данные и транспорт
Протокол делится на два слоя, и это разделение стоит запомнить.
Слой данных описывает, что говорят друг другу клиент и сервер: формат сообщений, набор методов, примитивы (инструменты, ресурсы, промпты), уведомления. Он один для всех.
Транспортный слой описывает, как доставить сообщение: через какой канал, как разделять сообщения, как авторизоваться. Транспортов два:
- stdio — клиент запускает сервер как дочерний процесс и пишет ему в стандартный ввод, а читает из стандартного вывода. Никакой сети, никаких портов. Так работают почти все локальные серверы.
- Streamable HTTP — клиент отправляет сообщения HTTP-запросами
POST, а сервер может отвечать потоком (Server-Sent Events). Так работают удалённые серверы: авторизация обычными заголовками или OAuth.
Один и тот же сервер можно запустить в любом транспорте, не меняя логику. В нашем учебном проекте будет stdio, а Streamable HTTP подробно разбирает курс «MCP: продвинутые темы».
Из stdio следует правило, которое ломает больше серверов, чем любая другая ошибка: сервер на stdio не должен ничего печатать в стандартный вывод. Вывод — это канал протокола. Один print("отладка") — и клиент получает мусор вместо JSON. Логи пишите в стандартный поток ошибок, через модуль logging.
JSON-RPC: язык слоя данных
Все сообщения — это JSON-RPC 2.0, простой формат вызова удалённых методов. Есть три вида сообщений: запрос (с id, ждёт ответа), ответ (с тем же id) и уведомление (без id, ответа не ждёт).
Вот как клиент спрашивает у сервера список инструментов:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}И что сервер отвечает (сокращённо):
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "read_doc",
"description": "Прочитать документ проекта целиком.",
"inputSchema": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "Имя файла, например brief.md" }
},
"required": ["name"]
}
}
]
}
}Обратите внимание на inputSchema. Это обычная JSON Schema, и именно её хост покажет модели, чтобы та знала, с какими аргументами вызывать инструмент. Когда мы будем писать сервер на Python, эту схему за нас соберёт SDK из аннотаций типов.
Вызов инструмента — метод tools/call:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "read_doc",
"arguments": { "name": "brief.md" }
}
}Ответ содержит массив content — список блоков (текст, картинка, встроенный ресурс), которые хост передаст модели как результат.
Названия методов устроены единообразно: tools/list и tools/call, resources/list и resources/read, prompts/list и prompts/get. Увидев в логе resources/read, вы уже знаете, что происходит.
Как клиент узнаёт, что умеет сервер
Прежде чем что-то вызывать, клиенту нужно понять, какие примитивы поддерживает сервер: только инструменты или ещё ресурсы и промпты, умеет ли он присылать уведомления. Это называется обнаружением возможностей (capabilities).
В актуальной версии протокола (2026-07-28) каждый запрос несёт в поле _meta версию протокола, имя клиента и его возможности, а клиент может отправить запрос server/discover, чтобы получить от сервера его имя, поддерживаемые версии и список возможностей. В более ранних версиях это делалось рукопожатием initialize в начале соединения. Python SDK скрывает разницу: и в том, и в другом случае после подключения у клиента есть готовый объект с возможностями сервера.
Для нашего сервера ответ будет выглядеть примерно так:
{
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": true, "listChanged": true },
"prompts": { "listChanged": true }
}
}listChanged: true означает, что сервер умеет сообщать, когда его список инструментов (или ресурсов, промптов) изменился.
Уведомления
Иногда серверу нужно что-то сказать без запроса: «список инструментов изменился», «ресурс обновился», «задача выполнена на сорок процентов». Для этого есть уведомления — сообщения без id. Хост, получив notifications/tools/list_changed, заново запрашивает tools/list и обновляет то, что видит модель.
Уведомления не гарантированы: если соединение переподключилось, часть может потеряться, поэтому хосты всё равно периодически перечитывают списки. В актуальной версии протокола клиент подписывается на нужные типы уведомлений явно, запросом subscriptions/listen. Подробнее — в уроке «Уведомления: логи и прогресс» продвинутого курса.
Собираем картину
Тестировщик пишет в Claude Code: «Прочитай бриф и скажи, какие риски не учтены». Что происходит:
- При старте Claude Code запустил наш сервер как дочерний процесс (транспорт stdio) и запросил его возможности и список инструментов.
- Модель получила описание
read_docвместе с запросом пользователя и решила вызвать инструмент с аргументомbrief.md. - Хост спросил у пользователя подтверждение, затем клиент отправил
tools/callчерез стандартный ввод процесса. - Сервер вернул
contentс текстом брифа, клиент передал его модели как результат вызова. - Модель написала ответ.
Слой данных здесь — методы и JSON, транспорт — стандартный ввод-вывод процесса. Поменяйте транспорт на HTTP, и шаги останутся теми же.
Попробуйте сами
10–15 мин на рабочем месте- Напишите от руки JSON-RPC-запрос
tools/callдля инструмента «создать задачу в трекере» с полями «заголовок» и «приоритет». Потом — ответ сервера с одним текстовым блоком. Сверьте с примерами выше. - Возьмите любой MCP-сервер, который уже подключён у вас в Claude Code, и по его настройкам определите транспорт: если в конфигурации есть
command, это stdio; еслиurl— Streamable HTTP. - Подумайте, какой транспорт нужен серверу «документы проекта», если документы лежат у каждого на ноутбуке, и какой — если они в общей вики компании.
Коротко
- Хост создаёт по клиенту на каждый сервер; клиенты друг о друге не знают.
- Два слоя: слой данных (JSON-RPC, методы, примитивы) и транспорт (stdio или Streamable HTTP).
- stdio: сервер — дочерний процесс, общение через стандартный ввод-вывод. Ничего не печатать в stdout.
- Методы называются единообразно:
tools/list,tools/call,resources/read,prompts/get. - Клиент узнаёт возможности сервера при подключении; уведомления сообщают об изменениях.
Видеоверсия
Сценарий озвучки · 437 слов, ≈ 3 мин
В прошлый раз мы назвали три роли: хост, клиент и сервер. Сегодня посмотрим, что между ними на самом деле летает. Это пригодится, когда сервер «не подключается» и нужно понять, где обрыв.
Напомню роли. Хост — приложение: Claude Code, приложение Claude, редактор. На каждый подключённый сервер хост создаёт отдельного клиента, и клиенты ничего не знают друг о друге. Сервер — программа, которая отвечает на запросы. Где она запущена — на вашем ноутбуке или в облаке — для протокола неважно.
Протокол делится на два слоя. Слой данных описывает, что именно говорят друг другу клиент и сервер: формат сообщений, набор методов, примитивы. Транспортный слой описывает, как доставить сообщение. Транспортов два. Первый — стандартный ввод-вывод: клиент запускает сервер как дочерний процесс и пишет ему в стандартный ввод, а читает из стандартного вывода. Никакой сети. Так работают почти все локальные серверы. Второй — Streamable HTTP: сообщения летят обычными эйч-ти-ти-пи-запросами, а сервер может отвечать потоком. Так работают удалённые серверы.
Из первого транспорта следует правило, которое ломает больше серверов, чем любая другая ошибка. Сервер на стандартном вводе-выводе не должен ничего печатать в стандартный вывод. Вывод — это канал протокола. Одна отладочная печать — и клиент получает мусор вместо джейсона. Логи пишите в поток ошибок.
Теперь слой данных. Все сообщения — это JSON-RPC, простой формат вызова удалённых методов. Запрос с номером, ответ с тем же номером, и уведомление без номера, которое ответа не ждёт. Названия методов устроены единообразно: «tools list» — дай список инструментов, «tools call» — вызови инструмент, «resources read» — прочитай ресурс, «prompts get» — дай промпт. В ответе на список инструментов сервер присылает для каждого имя, описание и схему аргументов. Именно эту схему хост покажет модели, чтобы та знала, как вызывать инструмент.
Прежде чем что-то вызывать, клиент выясняет, что умеет сервер: только инструменты, или ещё ресурсы и промпты, умеет ли присылать уведомления. Это называется обнаружением возможностей. В актуальной версии протокола каждый запрос несёт версию и возможности клиента, а сервер описывает себя в ответ на отдельный запрос. В старых версиях это было рукопожатие в начале соединения. Python SDK разницу прячет.
Уведомления — это способ сервера сказать что-то без запроса: список инструментов изменился, ресурс обновился, задача выполнена наполовину. Хост, получив такое, перечитывает список и обновляет то, что видит модель.
Соберём картину. Тестировщик пишет в Claude Code: «Прочитай бриф и скажи, какие риски не учтены». При старте хост уже запустил наш сервер и узнал, что у него есть инструмент «прочитать документ». Модель решает его вызвать. Хост спрашивает подтверждение у человека, клиент отправляет вызов в стандартный ввод процесса, сервер возвращает текст брифа, модель пишет ответ. Слой данных — методы и джейсон, транспорт — ввод-вывод процесса. Поменяйте транспорт на эйч-ти-ти-пи, и шаги останутся теми же.
В следующем уроке разберём подробнее три примитива: инструменты, ресурсы и промпты, — и кто из участников какой примитив контролирует.
