Сегодня ядром данной статьи будет MCP — как мост между бекендом‑оберткой с LLM и внешними источниками, но при этом я также затрону смежные темы, чтобы картина была полной и не требовалось дополнительно гуглить.
Я постараюсь не давать устоявшиеся термины в контексте MCP, а также в процессе буду пояснять некоторые «базовые» термины, которые все как бы понимают — но нередко нет, чтобы мы все улавливали один и тот же контекст статьи.
Введение
MCP — это и спецификация (прим. нормативное описание того, как что‑то должно работать: термины, требования, форматы, правила совместимости) и протокол (прим. набор согласованных правил взаимодействия между участниками системы: какие сообщения можно посылать, в каком формате, в какой последовательности, какие ответы/ошибки ожидаются, как устроены состояния — handshake, запрос‑ответ и так далее).
MCP как протокол — это что происходит на «проводе/канале»:
какие JSON‑RPC методы существуют (например,
resources/list,resources/read,tools/callи тому подобное)какие поля и типы данных в запросах/ответах
какие правила последовательности: инициализация, capabilities, обработка ошибок, пагинация, стриминг (если поддерживается), закрытие соединения
какие есть официальные привязки к транспортам (stdio/HTTP и тому подобное) и их особенности
То есть протокол = поведение + обмен сообщениями.
MCP как спецификация — это «документ, который это всё фиксирует»:
формальное описание протокола (то, что выше)
общая модель сущностей (tools/resources/prompts и тому подобное)
определения схем данных/ошибок
требования совместимости/версирования
рекомендации по безопасности, примеры, пояснения
То есть спецификация = текст/стандарт, который описывает и нормирует протокол.
Границы применимости
MCP не является универсальной заменой REST/OpenAPI и не пытается описать произвольные веб‑API. MCP задаёт контракт, ориентированный на взаимодействие LLM: listing/reading контекстных ресурсов, вызовы инструментов, выдача готовых промптов, а также (в client features) запросы на sampling или roots при наличии поддержки.
MCP также не навязывает UI: спецификация неоднократно подчёркивает, что конкретные UX‑паттерны (как показывать списки, как подтверждать действия) — ответственность реализаций/хостов.
В основе протокольной части MCP лежит JSON‑RPC 2.0. Дальше раскрою понимание JSON‑RPC 2.0 (если уверены в своем знании — можно просто пропустить).
Пони��ание REST, RPC и JSON-RPС 2.0
REST
Начнем с REST, предполагая, что он уже известен всем. REST нам предлагает мыслить в контексте объектов, вот есть заказ (Order), с ним можно делать следующее:
Создать, Получить, Изменить и Удалить (тот самый CRUD). Отсюда и HTTP методы (границы для работы с объектом)
GET,POST,PUT/PATCH,DELETE;Обращаться к нему, через его имя:
/orders/123.
Основная идея: обращение к ресурсу (в URL), и работа с ним через заданные операции. Важно: REST старается держать ограниченный набор операций вокруг ресурсов, отсюда получаются следующее:
Плюсы: стандартизация, кеширование, совместимость с HTTP‑инструментами;
Минусы: нередко требуются обходы CRUD границ у ресурса и это порождает костыль (по идее временное, но часто — нет, решение проблемы, которое не вписывается в логику и структуру программы, но позволяет обойти ограничение).
RPC
Здесь скорее парадигма будет не про объект, а про функцию, то есть в первую очередь идёт не объект, а функция. К примеру, мы можем просто осуществить вызов calculatePrice(orderId), то есть обычно у тебя один URL, а уже внутри запроса ты пишешь что вызвать:'
{ "method": "orders.cancel", "params": { "id": 123 } }
Основная идея: все — это вызов метода/функции.
Все в контексте мира HTTP/CDN.
Почему у REST в плюсах есть кеширование? В REST обычно чтение происходит через
GET,HEADпо стабильному URL ресурса (GET /users/1), дляGET,HEADесть правила кеширования (Cache‑Control, ETag, Last‑Modified, etc) — это описано в стандарте HTTP caching.Почему RPC в некоторых кейсах не кешируется? RPC поверх HTTP часто выглядит как
POST /rpc(то есть один эндпоинт) и уже в теле{"method": "getUser", "params": {"id": 1}}, отсюда две проблемы для кеширования:
Метод POST, по умолчанию, считается unsafe (так как может менять состояние), поэтому промежуточные кеши ведут себя осторожно и не кешируют, а при успешных запросах даже обязаны инвалидировать кеш для затронутого URI.
Ключ кеша у shared HTTP caches обычно строится как URL+заголовки+метод, а тело туда не входит. И получается, что для CDN все запросы как одно и то же:
POST /rpc⇒ безопасно кэшировать сложно без дополнительной логики.
JSON-RPC 2.0
Это RPC протокол, где собственно все сообщения — JSON и есть стандартная обертка запроса/ответа, к тому же он ещё и transport‑agnostic (его можно гонять поверх HTTP, сокетов, stdio и т.д). Подробнее здесь.
Разберем Request, его поля:
jsonrpc: всегда"2.0"method: имя вызываемого метода (строка)params: аргументы (объект или массив) — опциональноid: идентификатор запроса (строка/число). Еслиidнет — это notification (ответ не ожидается)
{ "jsonrpc": "2.0", "id": 1, "method": "sum", "params": {"a": 1, "b": 2} }
А Response же выглядит так:
Успех:
resultОшибка:
error: { code, message, data? }(и нетresult). Стандартные коды ошибок (например-32601 Method not found,-32602 Invalid params) задаёт спецификация JSON‑RPC.idв ответе должен совпадать сidзапроса.
/ Успешный ответ { "jsonrpc": "2.0", "id": 1, "result": 3 } / Ошибка { "jsonrpc": "2.0", "id": 1, "error": { "code": -32602, "message": "Invalid params" } }
Почему
idв ответе должен совпадать сidзапроса? На уровне транспорта обычно один общий канал байтов, по которому летят несколько логических запросов и ответов. Транспорт сам по себе не гарантирует — «именно на X запрос придёт ответ», поэтому нуженidкак корреляционный ключ.В REST каждый запрос — это отдельное HTTP‑сообщение, а ответ структурно привязан к запросу на уровне протокола:
HTTP/1.1 без pipelining: запрос отправили → ответ пришёл → следующий.
HTTP/1.1 с pipelining (редко): ответы обязаны идти в том же порядке, что и запросы.
HTTP/2/HTTP/3: можно много запросов параллельно, ответы могут идти в любом порядке, но у HTTP/2/3 есть встроенный идентификатор потока (stream id), и стек HTTP сам правильно «склеивает» ответ с нужным запросом.
Как это связано с MCP? А в MCP операции вида resources/list, resources/read, resources/templates/list, completion/complete и тому подобное — это JSON‑RPC методы (та самая строка method, а параметры уходят в params).
Важно: MCP использует JSON‑RPC 2.0 как формат сообщений, но накладывает поверх него дополнительные ограничения, чтобы сессия была предсказуемой и проще дебажилась.
Любой request в MCP обязан иметь id (строка или число). Это значит, что request без id в MCP не используется — объект без id трактуется как notification (one‑way), на который запрещено отвечать.
В отличие от чистого JSON‑RPC, в MCP id не может быть null.
Идентификатор запроса не должен повторяться отправителем в рамках одной сессии: иначе корреляция ответов становится неоднозначной.
Эти детали кажутся мелочью, но они напрямую влияют на реализацию: таймауты, ретраи, параллелизм и сопоставление ответов с запросами обычно ломаются здесь.
Жизненный цикл MCP-сессии: initialize → initialized → работа → завершение
Чтобы дальше сущности не висели в воздухе, зафиксируем минимальный жизненный цикл любой MCP-сессии. Он обязателен и одинаков независимо от того, stdio это или HTTP-транспорт.
Клиент отправляет запрос
initialize, где сообщает:версию протокола (например, "2025-03-26"),
свои возможности (client capabilities: например, roots/sampling),
информацию о реализации (name/version).
Сервер отвечает на
initializeи возвращает:согласованную версию протокола,
свои возможности (server capabilities: tools/resources/prompts/logging/completions и т.д.),
serverInfo и (опционально) instructions.
После успешного initialize клиент обязан отправить notification
notifications/initialized. Это сигнал готовности: до него сервер не должен начинать нормальную работу (кроме ping/logging в оговорённых случа��х).Дальше начинается операционная фаза: list/read/call, подписки, нотификации и т.п.
Завершение — это закрытие underlying transport (закрыли stdin / закрыли HTTP-соединение). Отдельного shutdown method на уровне MCP не требуется.

Транспорты MCP: stdio и Streamable HTTP
MCP определяет стандартные транспорты, которые переносят JSON‑RPC сообщения:
stdio
Клиент запускает MCP‑сервер как subprocess и общается через stdin/stdout.Практический плюс: минимальная поверхность атаки и простая локальная интеграция. Практический минус: это IPC внутри машины — удобнее для desktop/локальных инструментов, хуже для удалённых серверов.
Важная деталь реализации: сообщения разделяются переводом строки (newline‑delimited), и сообщение не должно содержать «встроенных» переносов строки — иначе парсер ломает границы сообщений.Streamable HTTP
Это современный стандартный HTTP‑транспорт MCP. Клиент отправляет каждое JSON‑RPC сообщение отдельным POST на единый MCP endpoint (например, /mcp). Сервер может (опционально) использовать SSE, чтобы отправлять поток сообщений от сервера к клиенту (но концептуально это всё ещё MCP поверх HTTP).
Важные security‑грабли для Streamable HTTP:сервер обязан валидировать Origin, иначе возможны атаки DNS rebinding на локально запущенные MCP‑серверы;
при локальном запуске серверу лучше слушать только localhost (127.0.0.1), а не 0.0.0.0;
для удалённого доступа почти всегда нужна аутентификация.
Если держать в голове эти свойства транспорта, дальше проще понимать: где сессия, почему появляются нотификации, и почему безопасность MCP — это не только про tool calling, но и про сам канал связи.
Далее разберем основные сущности и их взаимодействия.
Сущности и потоки
Взаимодействие идет через три уровня:
Host: «LLM application», в которой живёт пользовательский опыт и которая содержит в себе MCP clients;
Client: «коннектор» внутри host, который поддерживает 1:1 соединение с конкретным MCP server (один экземпляр MCP‑клиента держит одно изолированное соединение/сессию ровно с одним конкретным MCP‑сервером. Хост может поднять много таких клиентов — по одному на каждый сервер, чтобы сохранить изоляцию и независимый lifecycle соединений );
Server: процесс/сервис, предоставляющий инструменты, ресурсы и промпты.

Тут, в целом, должно быть понятно, давайте перейдем к сущностям в самом MCP. Будем отталкиваться от рассмотрения типового потока, который представляет :
Выбор источника внешних возможностей
Хост понимает, что ему нужно выйти за пределы локального контекста (данные/действия/шаблоны), выбирает подходящий MCP‑сервер(а). Решение обратиться может быть на основе эвристик, конфига, выбора юзера или подсказке от LLM.Установка сессии и описание того, что сервер умеет
Хост подключается к серверу и получает каталог «возможностей»:что можно прочитать (контекст/артефакты);
что можно выполнить (действия);
какие есть готовые шаблоны взаимодействия (подсказки/промпты);
(иногда) возможность обратного запроса к LLM через хост.
Планирование использования возможностей
LLM (и/или пользователь) предлагает, какие возможности нужны, а хост решает и применяет политику: что разрешено, что спросить у пользователя, что логировать, что ограничить.Получение внешней информации и/или выполнение действий
Хост: подтягивает нужные данные как контекст (файлы/документы/записи/и тому подобное) и/или запускает действия (получить ответ сервиса, что‑то посчитать, сделать запрос, создать объект и тому подобное) и получает результаты в удобном формате.Сборка рабочего контекста
Хост упаковывает полученное в контекст для размышления (фрагменты текста, структурированные данные, вложения) и формирует запрос к LLM.Рассуждение и итерация
LLM отвечает на основе собранного контекста. Если в процессе выясняется, что нужно ещё — цикл повторяется: снова план → получить/выполнить → обновить контекст → продолжить.(Опционально) Обратный ход
Иногда сервер не только отдаёт/делает, но и просит хост получить у LLM кусок генерации/решения и хост решает, можно ли это выполнять.

Собственно все разбираемые дальше сущности будут олицетворением того, что было описано выше и, по сути, будут представлены технические детали реализации в MCP. Я также буду ссылаться к представленному потоку, чтобы вы могли понять какую роль та или иная сущность играет в взаимодействии, ссылка будет в формате «поток-2.1».
Resources - контекстные данные
Это данные/контент, который сервер делает доступным клиенту, чтобы клиент мог использовать это как контекст для LLM‑взаимодействий: файлы, схемы БД, документы приложения и так далее.
Каждый resource уникально идентифицируется URI (uri: string). Это не обязательно веб‑адрес: URI может быть file://..., git://... или кастомной схемой (например db://...) — MCP это допускает.
Список доступных ресурсов можно получить через обращение JSON‑RPC метод "resources/list" и он поддерживает cursor‑pagination (cursor → nextCursor).
Замечание про capabilities и realtime:
Поддержка ресурсов объявляется сервером в capabilities (в ответе на initialize). Там же сервер явно указывает, умеет ли он:
слать нотификацию о смене списка (
listChanged);поддерживать подписки на изменения конкретного ресурса (
subscribe).
Если включён listChanged, сервер шлёт точную нотификацию:
notifications/resources/list_changedи клиент обычно реагирует пере-вычитыванием каталога черезresources/list.Если включён subscribe, клиент подписывается на конкретный
uriчерезresources/subscribe, а сервер присылаетnotifications/resources/updated {uri}.
Ещё деталь, которую полезно знать при UI/UX: resources и content-блоки могут иметь annotations (подсказки клиенту) — например audience (user/assistant), priority и lastModified. Это не “логика протокола”, но сильно влияет на то, как удобно показывать ресурсы в интерфейсе.
Про cursor‑pagination — в первом запросе клиент либо не передает cursor, либо ставит cursor: null, сервер возвращает первую страницу (resources: [...]) и, если есть продолжение, кладёт в ответ nextCursor — это непрозрачный токен (буквально — продолжить отсюда); дальше клиент делает следующий запрос тем же методом, но уже с params: {"cursor": "<nextCursor из прошлого ответа>"}, и снова получает очередную порцию ресурсов и новый nextCursor; цикл повторяется, пока сервер не перестанет присылать nextCursor (или пришлёт null) — это означает, что страниц больше нет. «поток-2.1»
{ "jsonrpc": "2.0", "id": 1, "method": "resources/list", "params": { "cursor": "optional-cursor-value" } }
{ "jsonrpc": "2.0", "id": 1, "result": { "resources": [ { "uri": "file:///project/src/main.rs", "name": "main.rs", "title": "Rust Software Application Main File", "description": "Primary application entry point", "mimeType": "text/x-rust" } ], "nextCursor": "next-page-cursor" } }
MIME type (media type) — стандартный идентификатор формата данных вроде:
text/plain
application/json
text/markdown
image/png
application/pdfОн нужен, чтобы получатель понимал формат и как его обрабатывать. (В HTTP это то же самое поле
Content-Type.) Подробнее здесь.В MCP у ресурсов и их содержимого есть опциональный
mimeType, чтобы клиент мог: корректно отобразить (markdown vs plain text), понять что это бинарный файл (png/pdf) или выбрать обработчик/рендерер.
Чтение ресурса — метод "resources/read" с параметром uri. Ответ содержит contents[], где каждый элемент может быть: text (для текстового контента) или blob (base64 для бинарного) с опциональным mimeType. «поток-4»
{ "jsonrpc": "2.0", "id": 2, "method": "resources/read", "params": { "uri": "file:///project/src/main.rs" } }
{ "jsonrpc": "2.0", "id": 2, "result": { "contents": [ { "uri": "file:///project/src/main.rs", "mimeType": "text/x-rust", "text": "fn main() {\n println!(\"Hello world!\");\n}" } ] } }
Resource templates
Это способ описать динамические/параметризованные ресурсы через uriTemplate (URI Template — это строка с выражениями {...}, которые расширяются значениями переменных), чтобы клиент мог конструировать валидные uri под конкретные значения параметров.
Шаблоны также перечисляются через метод "resources/templates/list" и он тоже может быть пагинируемым. «поток-2.3»
{ "jsonrpc": "2.0", "id": 3, "method": "resources/templates/list", "params": { "cursor": "optional-cursor-value" } }
{ "jsonrpc": "2.0", "id": 3, "result": { "resourceTemplates": [ { "uriTemplate": "file:///{path}", "name": "Project Files", "title": "📁 Project Files", "description": "Access files in the project directory", "mimeType": "application/octet-stream" } ], "nextCursor": "next-page-cursor" } }
Чтобы подставлять валидные аргументы в шаблоны в MCP есть completion API — метод completion/complete, который возвращает подсказки автодополнения аргументов: для prompts (пока их не разбирали) и для наших resource URI templates (в ссылке ref/resource). «поток-2.3»
Идея: пользователь набирает аргумент, а клиент показывает варианты:
{ "jsonrpc": "2.0", "id": 1, "method": "completion/complete", "params": { "ref": { "type": "ref/resource", "uri": "file:///{path}" }, "argument": { "name": "path", "value": "src/ma" } } }
{ "jsonrpc": "2.0", "id": 1, "result": { "completion": { "values": ["src/main.rs", "src/main_test.rs"], "total": 10, "hasMore": true } } }
Другие сущности (Prompts, Tools)
Prompts. Если resource/template отвечает на вопрос «какой контент можно прочитать/подставить в контекст?», то prompt отвечает на вопрос «какую заготовку диалога/инструкций можно использовать?».
как и
resources/list, у prompts естьprompts/list(тоже с cursor‑pagination). «поток-2.1»
{ "jsonrpc": "2.0", "id": 1, "method": "prompts/list", "params": { "cursor": "optional-cursor-value" } }
{ "jsonrpc": "2.0", "id": 1, "result": { "prompts": [ { "name": "code_review", "title": "Request Code Review", "description": "Asks the LLM to analyze code quality and suggest improvements", "arguments": [ { "name": "code", "description": "The code to review", "required": true } ] } ], "nextCursor": "next-page-cursor" } }
аналогично
resources/read(uri)(получить содержимое), у prompts естьprompts/get(name, arguments)— в ответ сервер отдаёт готовый набор сообщений (messages), который хост может напрямую вставить в диалог с LLM. «поток-4»
{ "jsonrpc": "2.0", "id": 2, "method": "prompts/get", "params": { "name": "code_review", "arguments": { "code": "def hello():\n print('world')" } } }
{ "jsonrpc": "2.0", "id": 2, "result": { "description": "Code review prompt", "messages": [ { "role": "user", "content": { "type": "text", "text": "Please review this Python code:\n def hello():\n print('world')" } } ] } }
prompts в MCP описаны как user‑controlled (идея: пользователь явно выбирает prompt, например как slash‑команду).
Tools. Если resource/prompts— это читать, то tool — это про выполнить.
получение
tools/list(тоже с cursor‑pagination). У tool естьinputSchema(JSON Schema) — это ключевое отличие от resources/prompts, потому что tool — это функция с формальным интерфейсом. «поток-2.2»
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": { "cursor": "optional-cursor-value" } }
{ "jsonrpc": "2.0", "id": 1, "result": { "tools": [ { "name": "get_weather", "title": "Weather Information Provider", "description": "Get current weather information for a location", "inputSchema": { "type": "object", "properties": { "location": { "type": "string", "description": "City name or zip code" } }, "required": ["location"] } } ], "nextCursor": "next-page-cursor" } }
использование идет через
tools/call(name, arguments)— сервер выполняет операцию и возвращает результат как набор content‑блоков (текст, изображение, аудио, ссылки на ресурсы и так далее). «поток-4»
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "get_weather", "arguments": { "location": "New York" } } }
{ "jsonrpc": "2.0", "id": 2, "result": { "content": [ { "type": "text", "text": "Current weather in New York:\n Temperature: 72°F\n Conditions: Partly cloudy" } ], "isError": false } }
tools описаны как model‑controlled (модель может выбирать и инициировать вызов), при этом спецификация отдельно подчёркивает human‑in‑the‑loop (подробнее о данном концепте здесь) как рекомендуемую практику для безопасности
Подписки
В MCP есть два разных механизма реального времени.
Первый это «список изменился» — уведомление, после которого клиент сам перечитывает каталог. Это относится к resources / prompts / tools.
На handshake сервер объявляет capability
listChangedдля соответствующей подсистемы.Когда сервер решает, что каталог изменился (добавились/исчезли/изменились элементы), он отправляет JSON‑RPC notification (без
id):notifications/*/list_changedКлиентская реакция почти всегда одна: обновить кеш каталога, то есть снова вызвать
*/list. В доках по архитектуре это прямо описывается как типичная реакция наnotifications/tools/list_changed.
Второе же «изменился конкретный ресурс» — настоящая подписка на объект (только для resources). Это относится только к resources (не к prompts/tools).
Сервер объявляет capability
resources: { subscribe: true }.Клиент подписывается на конкретный
uri:resources/subscribe { uri }Когда содержимое ресурса меняется, сервер шлёт notification:
notifications/resources/updated { uri }Клиент обычно делает lazy load: перечитывает ресурс через
resources/read(uri)только когда это реально нужно (или обновляет кеш сразу — зависит от стратегии).Чтобы перестать слушать изменения:
resources/unsubscribe { uri }
В итоге, типичная система взаимодействия будет выглядеть так:

Авторизация в MCP (важно для HTTP-транспорта)
Авторизация в MCP — опциональная часть стандарта, но для HTTP‑сценариев она быстро становится обязательной практикой: иначе любой, кто достучался до MCP ручки, может вызвать tools и прочитать resources.
Ключевая идея: для HTTP‑транспорта MCP описывает авторизацию на базе OAuth‑подхода (под множество сценариев — от user‑based до service‑to‑service). Типичный сигнал «нужна авторизация» выглядит так: сервер отвечает HTTP 401 Unauthorized, после чего клиент инициирует авторизационный flow и начинает прикладывать access token в каждом HTTP‑запросе.
Важно то, что этот блок относится именно к HTTP‑based транспорту. Для stdio обычно работают другие модели (например, секреты/учётки из окружения), потому что канал локальный и контролируется хостом.
Почему именно MCP
Чтобы ответить на данный вопрос надо прежде всего разобраться, а почему не нашлось подходящего из существующих решений до появления MCP. Связано это с тем, что интеграции между LLM‑приложениями и внешними системами чаще строились как набор точечных, вендорных или продуктовых решений: каждый хост/модель/платформа имели свой формат tool calling, свои плагины или свой слой коннекторов. Это работало, но плохо масштабировалось: один и тот же источник данных или действие приходилось переупаковывать под разные экосистемы.
И как обычно это бывает, для устранения существующих минусов придумывают универсальное решение. Именно так и поступила компания Anthropic, которая представила MCP (анонс был 25 ноября 2024), который закрывает разрыв как универсальный и открытый протокол взаимодействия LLM‑приложений с внешними системами: не только tool calling, но и стандартизировано подключать контекст и capabilities серверов через единый клиент‑серверный контракт.
Итоги
Что мы зафиксировали:
MCP — это контракт между LLM-приложением и внешним миром, оформленный как JSON-RPC протокол с чётким lifecycle (initialize/initialized).
Основные server features раскладываются на: читать (resources), выполнять (tools) и шаблонизировать взаимодействия (prompts), плюс утилиты вроде completion.
Реализация упирается не только в методы протокола, но и в транспорт: stdio проще и безопаснее для локальных интеграций, Streamable HTTP — база для удалённых серверов, но требует безопасности и (часто) авторизации.
Что дальше логично разобрать отдельно:
Client features (roots/sampling/elicitation) и реальные “server→client” флоу.
Практика безопасности: human-in-the-loop, политика подтверждений, минимальные привилегии и аудит.
Реальный пример end-to-end (сессия, листинг, вызов tool, подписки) и как это тестировать.
Если будет интерес — продолжу серией, но уже с упором на практику реализации и типовые ловушки продакшена.
На этом все. Спасибо за внимание! Надеюсь данный материал действительно был полезен для вас. В качестве основного источника знаний и примеров непосредственно использовалась официальная документация, которая находится здесь.
