Ну чё, малютки, все уже прикрутили MCP-серверы к своему агенту, а что там внутри — знают единицы. Разберём протокол не «на пальцах», а по официальной спецификации: сырые JSON-RPC сообщения, четыре ревизии стандарта с 2024 по 2025 год, и что именно принесла последняя версия 2025-11-25 — включая свежую экспериментальную фичу Tasks, о которой ещё мало кто писал.
MCP — открытый протокол на JSON-RPC, который стандартизирует связь между LLM-приложением и внешними тулами/данными. Без него у тебя N агентов × M источников данных = N×M интеграций, каждая своя. С ним — N+M: каждый агент говорит на одном протоколе, каждый источник данных реализует его один раз. Та же идея, что у LSP для языковых серверов, только вместо IDE — LLM.
Зачем это вообще нужно
До MCP каждый агент, который хотел читать файлы, ходить в базу или дёргать API стороннего сервиса, писал под это свою интеграцию. Захотел добавить новый источник данных — переписывай код агента. Захотел использовать тот же источник в другом агенте — переписывай заново. Классическая проблема N×M: N агентов, M источников, N×M кусков клеевого кода.
MCP разрывает эту связь ровно так же, как в своё время Language Server Protocol разорвал связь между IDE и языками программирования: вместо того чтобы каждая IDE писала поддержку каждого языка, появился один протокол — язык реализует его один раз, и с ним работает любая IDE. MCP делает то же самое для LLM-агентов и источников контекста: сервер реализует протокол один раз, и его подключает любой MCP-совместимый клиент — Claude, свой агент на харнесе (я писал об этом отдельно), да хоть llama.cpp.
Архитектура: Host, Client, Server
Три роли, и путать их — типичная ошибка новичков:
Ключевой принцип дизайна, который часто упускают: сервер не должен видеть весь разговор и не должен видеть другие серверы. Host агрегирует контекст из разных клиентов, но каждый сервер получает только то, что нужно ему для работы. Это осознанная граница безопасности, а не недосмотр.
Серверы предоставляют три вида примитивов, клиенты — три других:
| Сторона | Примитив | Что это |
|---|---|---|
| Сервер | Tools | Функции, которые модель может вызвать |
| Сервер | Resources | Данные и контент для модели или пользователя |
| Сервер | Prompts | Шаблонные сообщения и готовые воркфлоу |
| Клиент | Sampling | Сервер может попросить клиента сходить к LLM самому |
| Клиент | Roots | Сервер узнаёт, в каких файловых/URI границах ему разрешено работать |
| Клиент | Elicitation | Сервер может запросить у пользователя дополнительные данные посреди работы |
Транспорт: как сообщения реально едут
Под капотом — JSON-RPC 2.0, везде в UTF-8. Спецификация определяет два стандартных транспорта:
stdio — клиент запускает сервер как дочерний процесс. Сообщения идут через stdin/stdout, построчно, без embedded-переносов строк. stderr сервер может использовать для любых логов — клиент не обязан считать это ошибкой. Самый простой вариант для локальных серверов: файлы, git, локальные тулы.
Streamable HTTP — сервер живёт отдельным процессом и обслуживает множество клиентов через один HTTP-эндпоинт, который принимает и POST, и GET. Опционально может стримить через Server-Sent Events. Этот транспорт заменил собой более ранний HTTP+SSE (об этом — в разделе версий).
Спецификация прямо требует: сервер обязан валидировать заголовок Origin на каждом входящем соединении и отвечать 403 Forbidden на невалидный, а локальный сервер должен биндиться на 127.0.0.1, а не на 0.0.0.0. Без этого сайт в браузере жертвы может через DNS rebinding обратиться к твоему локальному MCP-серверу от чужого домена.
У Streamable HTTP есть сессии (MCP-Session-Id в заголовке), resumability через Last-Event-ID для восстановления после разрыва, и обязательный с версии 2025-06-18 заголовок MCP-Protocol-Version на каждом запросе — сервер по нему понимает, с какой версией протокола говорит клиент.
Жизненный цикл сессии
Три фазы: Initialization (договорились о версии и возможностях) → Operation (обычная работа) → Shutdown (закрыли соединение). Инициализация — обязательно первое взаимодействие, и до её завершения ни одна сторона не шлёт ничего, кроме ping.
Смотри на реальном обмене сообщениями — от рукопожатия до первого вызова тула:
initialize до результата tools/call. Заголовки jsonrpc: "2.0" и id опущены для читаемости.{
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {
"roots": { "listChanged": true },
"sampling": {},
"elicitation": {}
},
"clientInfo": {
"name": "ExampleClient",
"version": "1.0.0"
}
}
}Обрати внимание на version negotiation: клиент шлёт свою последнюю поддерживаемую версию, и если сервер её не поддерживает — отвечает своей последней. Если клиент не поддерживает и версию сервера — он обязан отключиться. Никакого «подгоним на лету», версия одна на всю сессию, и с 2025-06-18 она же едет в заголовке MCP-Protocol-Version на каждом HTTP-запросе после инициализации.
Capabilities работают так же честно: если сервер не заявил tools.listChanged, он не имеет права слать уведомление о смене списка тулов — клиент на такое даже не подпишется. Заявленных, но не реализованных возможностей быть не должно.
Примитивы сервера: во что упирается модель
Tools — самый используемый примитив. Каждый тул — это name, description и inputSchema (обычная JSON Schema). Модель узнаёт о тулах через tools/list, вызывает через tools/call. Важный нюанс из спеки: результат может быть structured content (типизированный JSON в поле structuredContent, который сервер обязан провалидировать по заявленной outputSchema) или unstructured — текст, картинка, аудио, или resource_link — ссылка на ресурс, которую клиент может дозапросить отдельно.
{
"name": "get_weather_data",
"inputSchema": { "type": "object", "properties": { "location": { "type": "string" } }, "required": ["location"] },
"outputSchema": {
"type": "object",
"properties": { "temperature": { "type": "number" }, "conditions": { "type": "string" } },
"required": ["temperature", "conditions"]
}
}
Отдельно спецификация различает два вида ошибок тула. Protocol Errors — стандартная JSON-RPC ошибка: неизвестный тул, битый запрос. Tool Execution Errors — isError: true прямо в результате: невалидная дата, лимит API, бизнес-логика. Разница принципиальная: execution errors модель может использовать для самокоррекции и повторной попытки с другими аргументами, protocol errors — почти никогда.
Всё это — inputSchema, валидация, structured content — работает благодаря той же механике, что я разбирал в статье про structured outputs: constrained decoding гарантирует, что модель физически не сможет сгенерировать вызов тула, не проходящий по схеме.
Resources — данные, которые сервер отдаёт по URI: файл, строка в базе, страница документа. В отличие от тулов, ресурсы application-controlled или user-controlled, а не model-controlled — обычно их выбирает пользователь или сама логика хоста, а не модель по своей инициативе.
Prompts — готовые шаблоны сообщений, которые сервер предлагает пользователю: не модель их вызывает, а человек выбирает из меню («сделай code review», «резюмируй тред»).
Примитивы клиента: что сервер может попросить у хоста
Sampling — сервер, а не клиент, может инициировать обращение к LLM. Звучит странно, пока не увидишь пример: агент-оркестратор дергает MCP-сервер, а тот сам просит модель сформулировать промежуточный вывод — без того, чтобы городить собственную интеграцию с LLM-провайдером внутри сервера. Пользователь при этом обязан явно одобрить: будет ли сэмплинг вообще, какой промпт уйдёт, что сервер увидит в ответ.
Roots — сервер спрашивает клиента о границах: в какой директории или по каким URI ему разрешено работать. Не «дай мне доступ ко всей файловой системе», а «скажи, где мои границы».
Elicitation (с версии 2025-06-18) — сервер посреди выполнения запроса может попросить у пользователя дополнительные данные: недостающий параметр, подтверждение, выбор из списка. До этого серверу оставалось либо гадать, либо падать с ошибкой валидации.
История версий: четыре ревизии
MCP версионируется не semver’ом, а датой последнего обратно несовместимого изменения — YYYY-MM-DD. Если изменение обратно совместимо, номер версии не растёт вообще. На данный момент таких ревизий четыре:
Заметил забавную деталь в таймлайне? JSON-RPC batching добавили в марте 2025 и выпилили обратно в июне того же года — редкий случай, когда фичу в спецификации прожили одну версию и отменили. А вот elicitation, structured tool output и классификация серверов как OAuth Resource Server — те изменения, которые определили современный MCP таким, какой он есть.
Что нового в последней версии (2025-11-25)
Текущая ревизия принесла россыпь точечных улучшений — OpenID Connect Discovery поверх авторизации, иконки у тулов и ресурсов, incremental scope consent (сервер запрашивает доступ по мере надобности, а не всё разом), tool calling прямо внутри sampling-запроса, стандартизированный ElicitResult с поддержкой multi-select enum’ов. Но главное новое имя — Tasks, и оно пока экспериментальное.
Проблема, которую решают Tasks: что делать с запросом, который выполняется минутами или часами? Держать HTTP-соединение открытым — плохая идея: таймауты, разрывы, дорогая инфраструктура. Tasks превращают долгий запрос в durable state machine: вместо результата клиент сразу получает taskId, дальше поллит статус через tasks/get и забирает результат через tasks/result, когда тот готов.
Обрати внимание на статус input_required — задача не падает и не отменяется, если серверу вдруг понадобились данные от пользователя (та самая elicitation, но теперь внутри уже запущенной фоновой задачи). Это ровно тот сценарий, который ломал долгие агентные вызовы раньше: либо ты держишь соединение и надеешься, что не разорвётся, либо теряешь контекст на полпути.
Пока задача в статусе working, host может вернуть управление модели и дать ей заняться другими делами — сервер даже может подсказать промежуточный ответ через служебное поле _meta. Для агентного харнеса это готовый примитив параллелизма: запустил дорогой тул как задачу, не блокируясь пошёл разбирать остальные запросы.
Авторизация: как менялась по версиям
Отдельная большая тема, но эволюция показательна одной таблицей:
| Версия | Что с авторизацией |
|---|---|
| 2024-11-05 | Её нет вообще — только локальные доверенные серверы |
| 2025-03-26 | Полноценный фреймворк на OAuth 2.1 — MCP можно безопасно выставлять наружу |
| 2025-06-18 | Серверы формально классифицированы как OAuth Resource Server, добавлен discovery, обязательны Resource Indicators (RFC 8707) против кражи токенов |
| 2025-11-25 | OpenID Connect Discovery, incremental scope consent через WWW-Authenticate, OAuth Client ID Metadata Documents вместо ручной регистрации клиента |
Путь понятный: от «доверяем всему локальному» к полноценной модели с дискавери, скоупами и метаданными — протокол взрослел вместе с тем, как MCP-серверы стали разворачивать не только на localhost.
OAuth в MCP защищает канал между клиентом и сервером, но не то, что летит дальше — к самому LLM-провайдеру. Для этого нужен отдельный слой: я разбирал guardrails-llm-filter — прокси, который маскирует PII и секреты по пути к модели, прежде чем они вообще покинут твою инфраструктуру.
Как написать свой сервер
Минимальный сервер на Python SDK — один тул, stdio-транспорт:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-server")
@mcp.tool()
def get_weather(location: str) -> str:
"""Get current weather for a location."""
return f"{location}: 18°C, облачно"
if __name__ == "__main__":
mcp.run(transport="stdio")
Декоратор сам построит inputSchema из тайпхинтов и докстринг превратит в description — всё, что было в сыром JSON-RPC выше, SDK берёт на себя. Для Streamable HTTP меняется одна строка — transport="streamable-http".
Кто уже умеет
MCP поддерживают Claude (Desktop и API через MCP connector), большинство современных агентных харнесов, и — свежая новость — llama.cpp недавно завёз полноценную поддержку MCP прямо в llama-server, так что теперь и локальный опенсорсный стек может подключать те же серверы, что и облачные модели. Экосистема серверов (файлы, git, базы данных, поисковые системы, десятки сторонних сервисов) растёт быстрее, чем успевают выходить новые ревизии спеки.
TL;DR
MCP — редкий случай, когда за скучной аббревиатурой стоит действительно продуманный протокол: почитаешь спеку — и видно, что каждое ограничение (серверы не видят разговор, явное согласие на сэмплинг, обязательная валидация Origin) появилось не для галочки, а потому что кто-то уже наступил на эти грабли. 🫡

