Если вы читали или смотрели видео про MCP, то наверняка сталкивались с таким комментарием:
«Спасибо, ещё одна статья, из которой я ничего не понял».
Аналогии и пояснения вроде «MCP — это как USB Type-C» или «MCP — это Tools, Resources и Prompts» лично мне не добавляли понимания. Поэтому я решил подробно изучить данную технологию и написать статью, где будет понятно, достоверно и применимо. Без магии. Попутно я прошёл обучение у Anthropic (ссылки дам, сертификаты выдают, курсы бесплатные, правда на английском).
Я ставил себе цель ответить на вопросы:
-
Что такое MCP и как он связан с ИИ?
-
Как чат GPT (большая языковая модель, LLM) может вызвать какой-то инструмент (tool)?
-
Модель же языковая, т.е. она умеет говорить, рассуждать, отвечать, но никак не делать. Как LLM может читать файлы, вызывать программы, открывать интернет сайты, вызывать внешние API?
Для программистов, кто в теме и уже использовал MCP-сервера, т.е. знает серверные примитивы: tools, resources и prompts, возможно будут интересны клиентские примитивы: sampling, roots, elicitation. Они звучат загадочно и трудно переводимы. И чтобы вас заинтриговать: MCP-сервер благодаря sampling может «сжигать» ваши LLM-токены для выполнения своих серверных задач. А благодаря roots получать доступ к файлам на вашем компьютере.
Итак, поехали.
🔄 Механика диалога: OpenAI-совместимый API в деталях
Люди общаются с чатами GPT (Qwen, Deepseek, GigaChat, Grok и другими) через чат-бот. А программы получаются доступ к таким моделям через API. Причем в индустрии уже есть стандарт, он называется OpenAI-совместимый API. Вы платите, получается ключ (длинную строку символов) и можете программно задавать вопросы и получать ответы от LLM в формате JSON.
💡 Лайфхак: как общаться бесплатно с разными моделями. Такую возможность предоставляет console.groq.com. Не спутайте: название сайта похоже на модель Grok от Илона Маска, но это другой сервис.
Но мы отвлеклись. Диалог с моделью через OpenAI-совместимый API выглядит примерно так:
🎭 Сценарий: «Погода на Мальдивах»
Системный промпт (role: system):
«Ты создан для помощи людям в том, как им одеваться в путешествия в зависимости от погоды. В твоём распоряжении есть инструмент
get_weatherс параметрами: дата начала путешествия, дата конца, страна, населённый пункт.»
Пользователь спрашивает (role: user):
«Я еду завтра на Мальдивы. Брать ли зонтик?»
Модель могла бы ответить (role: assistant):
«Зачем вам зонтик? На Мальдивах всегда прекрасная погода. Даже если промокнете — настроение это вам не испортит.»
Здесь не было вызвано никаких инструментов. ИИ решил, что знает ответ независимо от погоды.
Но! Модель могла бы поступить как в еврейском анекдоте — ответить вопросом на вопрос. Примерно так:
«Уважаемая программа, я отвечу пользователю, если вы:
Вызовете ваш инструмент
get_weather(где я уже подставила нужные даты, страну и город).Вернёте мне ответ от
get_weatherв чат, тогда я соберу финальную рекомендацию.»
Вы уже заметили три стандартные роли: system, user, assistant. А знали ли вы, что в стандарте OpenAI API есть четвёртая роль — tool? Именно через неё MCP «сшивается» с диалогом модели.
🔹 Шаг 1: Запрос от пользователя (вместе с системным промтом)
Ваша программа (назовём её Host) формирует запрос к модели. Обратите внимание: мы передаём не только вопрос пользователя, но и список доступных инструментов в поле tools(в данном случает единственный инструмент get_weather).
POST /v1/chat/completions
{
"model": "google/gemma-4-26b-a4b-it",
"messages": [
{
"role": "system",
"content": "Ты подсказываешь людям как им одеваться в путешествие в зависимости от погоды. В твоем распоряжении есть инструмент get_weather с параметрами (дата начала путешествия, дата конца, страна, населенный пункт)."
},
{
"role": "user",
"content": "Я еду завтра (1 мая 2026 года) на Мальдивы на недельку. Брать ли зонтик :-)"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Получает прогноз погоды для указанного города. Возвращает температуру, вероятность осадков, давление и силу ветра.",
"parameters": {
"type": "object",
"properties": {
"country": {"type": "string", "description": "Country name, e.g. Maldives"},
"city": {"type": "string", "description": "City name, e.g. Malé"},
"start_date": {"type": "string", "format": "date", "description": "Start date in YYYY-MM-DD"},
"end_date": {"type": "string", "format": "date", "description": "End date in YYYY-MM-DD"}
},
"required": ["country", "city", "start_date", "end_date"]
}
}
}
]
}
💡 Обратите внимание:
tools— это не сам MCP-запрос. Это просто описание, которое вы «скармливаете» модели, чтобы она знала: «Эй, если нужно узнать погоду — вот как можно попросить».
🔹 Ответ от LLM «вопросом на вопрос»
Модель понимает: у неё нет актуальных данных о погоде на Мальдивах. Она не выдумывает ответ. Вместо этого она делегирует задачу вам, т.е. просит вызвать get_weather с уже подставленными параметрами.
... начало пропущено ...
{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "chatcmpl-tool-abac20964c6ac906",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{"city": "Malé", "country": "Maldives", "end_date": "2026-05-08", "start_date": "2026-05-01"}"
}
}
]
},
"finish_reason": "tool_calls"
}
... конец пропущен ...
Полный текст JSON здесь:
{
"id": "chatcmpl-9e9710ae64b15c96",
"object": "chat.completion",
"created": 1777024800,
"model": "google/gemma-4-26b-a4b-it",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "chatcmpl-tool-abac20964c6ac906",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{"city": "Malé", "country": "Maldives", "end_date": "2026-05-08", "start_date": "2026-05-01"}"
}
}
]
},
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 275,
"completion_tokens": 57,
"total_tokens": 332
}
}
Обратите внимание:
-
✅поле
content: null, т.е. модель не генерирует текст для отображения пользователю, а запрашивает инструмент. Или текст в полеcontent, или вызов инструментов в массивеtool_calls. Либо-либо, но не вместе. -
✅
finish_reason: "tool_calls"— это сигнал вашему коду: «Стоп. Не показывай ответ пользователю. Сначала выполни инструмент». (Все коды возврата, кромеfinish_reasonразберем ниже). -
✅
arguments— это строка, содержащая валидный JSON. ИИ мозг сформулировал готовые параметры и имя MCP-инструмента, в котором нуждается. Двойные кавычки"уже экранированы. Одинарные кавычки здесь сломают парсинг.
🎭 Это и есть интеллект. Не в выполнении запроса. А в понимании:
Что у модели нет данных «в голове»
Что есть инструмент
get_weather, который может помочьКакие параметры нужны (
country,city,start_date)Как извлечь их из вопроса пользователя («завтра» →
2026-05-01, «Мальдивы» →country: "Maldives")
Большая языковая модель не «вызывает» MCP-сервер напрямую. Она говорит вашей программе: «Эй, я не могу ответить, но знаю, кто может. Сходи, спроси вот так, а результат верни мне — я соберу финальный ответ».
🔹 Шаг 2: Ваш код выполняет MCP-запрос (а не LLM-модель!)
Теперь в дело вступает ваш код (Host). Он видит finish_reason: "tool_calls" и делает:
# 1. Парсим вызов инструмента
tool_call = response.choices[0].message.tool_calls[0]
tool_name = tool_call.function.name # "get_weather"
args = json.loads(tool_call.function.arguments) # {"country": "Maldives", ...}
# 2. Вызываем MCP-сервер (это НЕ запрос к LLM!)
result = await mcp_client.call_tool(name=tool_name, arguments=args)
# 3. Получаем :
# result = "Malé, Maldives: 31°C, 20% chance of rain"
⚠️ Важно: MCP-сервер в простейшем случае — это просто программа. Сервер не «думает», это не ИИ. Программа получила параметры, сделала запрос к погодному API (или кэшу), вернула строку с ответом. Всё.
MCP-клиент (ваша программа) общается с MCP-сервером посредством обмена JSON сообщениями по стандарту JSON-RPC 2.0.
Транспорта для доставки сообщений два:
-
stdio— стандартные потоки ввода-вывода (stdin,stdout,stderr). Используется, когда MCP-сервер и MCP-клиент установлены на одном компьютере. -
Streamable HTTP / HTTPS — универсальный способ для общения по сети.
Ответ от MCP-сервера приходит примерно в таком виде:
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"content": [
{
"type": "text",
"text": "Malé, Maldives: 31°C, 20% chance of rain, wind 12 km/h"
}
],
"isError": false
}
}
💡 Обратите внимание: внутри
content[].textлежит обычная строка. Именно её вы потом «упакуете» вrole: "tool"для отправки обратно в LLM. MCP-сервер не знает проassistant,userилиfinish_reason. Он просто отдаёт данные в своём формате.
🔹 Шаг 3: Второй запрос к LLM (с результатом инструмента)
Теперь вы повторно отправляете полную историю диалога, включая результат выполнения инструмента. Обратите внимание на новую роль tool и обязательный tool_call_id:,
... Полный повтор беседы ...
{
"role": "tool", // <- Четвертая роль стандарта OpenAI API
"tool_call_id": "chatcmpl-tool-abac20964c6ac906", // <- Связка с вопросом выше
"content": "Malé, Maldives: 31°C, 20% chance of rain, wind 12 km/h"
}
... Описание инструментов, которые модель имеет права использовать ...
Полный JSON без сокращений здесь:
POST /v1/chat/completions
{
"model": "google/gemma-4-26b-a4b-it",
"messages": [
{
"role": "system",
"content": "Ты создан для помощи людям в том, как им одеваться в путешествия в зависимости от погоды. В твоем распоряжении есть инструмент get_weather с параметрами (дата начала путешествия, дата конца, страна, населенный пункт)."
},
{
"role": "user",
"content": "Я еду завтра на Мальдивы. Брать ли зонтик?"
},
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "chatcmpl-tool-abac20964c6ac906",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{"city": "Malé", "country": "Maldives", "start_date": "2026-05-01", "end_date": "2026-05-08"}"
}
}
]
},
{
"role": "tool", // <- Четвертая роль стандарта OpenAI API
"tool_call_id": "chatcmpl-tool-abac20964c6ac906", // <- Связка с вопросом выше
"content": "Malé, Maldives: 31°C, 20% chance of rain, wind 12 km/h"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Получает прогноз погоды для указанного города. Возвращает температуру, вероятность осадков, давление и силу ветра.",
"parameters": {
"type": "object",
"properties": {
"country": {"type": "string", "description": "Country name, e.g. Maldives"},
"city": {"type": "string", "description": "City name, e.g. Malé"},
"start_date": {"type": "string", "format": "date", "description": "Start date in YYYY-MM-DD"},
"end_date": {"type": "string", "format": "date", "description": "End date in YYYY-MM-DD"}
},
"required": ["country", "city", "start_date", "end_date"]
}
}
}
]
}
Почему передаем в role: "tool"и зачем tool_call_id ?
-
✅ Чтобы модель понимала: это не новый вопрос от человека, а ответ на её же запрос
-
✅ Чтобы связать результат с конкретным вызовом через
tool_call_id -
✅ Чтобы при параллельных вызовах (3 инструмента сразу) модель не запуталась, какой результат к какому запросу относится
🔹 Финальный ответ от LLM
Теперь у модели есть всё: вопрос пользователя + актуальные данные о погоде. Она генерирует человеческий ответ:
{
"choices": [{
"message": {
"role": "assistant",
"content": "На Мальдивах завтра 31°C, вероятность дождя 20%. Зонтик можно не брать, но лёгкий дождевик не помешает. Хорошего отдыха! ☀️",
"tool_calls": null
},
"finish_reason": "stop"
}]
}
Полный JSON итогового ответа от LLM здесь:
{
"id": "chatcmpl-906e5202d20eeb3d",
"object": "chat.completion",
"created": 1777026960,
"model": "google/gemma-4-26b-a4b-it",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "На Мальдивах завтра 31°C, вероятность дождя 20%. Зонтик можно не брать, но лёгкий дождевик не помешает. Хорошего отдыха! ☀️",
"tool_calls": []
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 350,
"completion_tokens": 94,
"total_tokens": 444
}
}
Пояснения:
-
✅
finish_reason: "stop"это сигнал: модель закончила, можно показывать ответ пользователю. -
✅
contentсодержит финальный, человеческий текст. -
✅
tool_calls: [] или null— больше инструментов не нужно.📋 Все стандартные значения finish_reason в OpenAI-совместимом API
Значение
Когда появляется
stopМодель “штатно” сгенерировала завершающий токен. Можно “курсор” для ввода следующего ответа показывать пользователю
tool_callsМодель сгенерировала требования, чтобы ей вызвали один или несколько инструментов и написали ответы
lengthДостигнут лимит
max_tokensили общий лимит контекстаcontent_filterСработала система модерации (запрещённый контент, токсичность и т.п.)
📦 Примитивы MCP-сервера (Tools, Resources, Prompts)
Я подробно описал как работает Tools. Статья уже получается длинной, поэтому только упомяну два других примитива.
|
Примитив |
Аналогия в программировании |
Кто владелец |
Зачем нужен |
|---|---|---|---|
|
Tools |
Процедуры/Функции (Write + Read) |
Модель решает, когда вызвать |
Действия: запрос к БД, отправка email, расчёт. Могут менять состояние. |
|
Resources |
Read-only данные |
Приложение (Host) подкидывает в контекст |
Файлы, конфиги, записи БД. Доступ по URI: |
|
Prompts |
Переиспользуемые шаблоны |
Пользователь / UI |
Готовые сценарии для человека: «Ревью кода», «Планирование отпуска». Не для автоматизации. |
💡 Важный нюанс: LLM умеет работать только с Tools. Ресурсы и промпты полезны для вашей программы, но LLM напрямую их не видит. Если хотите, чтобы ИИ сам решал, когда читать файл — оберните
resources/readв Tool. Тогда модель сможет запросить его черезtool_calls.
🌑 Тёмная сторона протокола (о чём молчат туториалы)
MCP — двусторонний протокол. Сервер может не только отдавать данные, но и запрашивать ресурсы у вашего клиента.
В таблице перечислены возможности, которые предоставляет MCP-клиент:
|
Команда |
Что делает |
Почему стоит напрячься |
|---|---|---|
|
|
Сервер просит вашу LLM что-то сгенерировать |
Может «съесть» ваши токены. Публичный сервер погоды может использовать вашу модель для саммаризации логов, а платить будете вы. |
|
|
Сервер запрашивает подтверждение или данные у пользователя |
«Подтвердите перевод на счёт X» или «Введите номер карты». |
|
|
Сервер шлёт логи и прогресс выполнения |
Похоже на режим |
|
|
Вы даёте серверу доступ к локальным папкам |
|
🔌 Итак, что такое MCP
MCP — это описание формата, протокол для обмена JSONами. MCP состоит всего из двух слоёв:
-
Транспорт —
HTTP/HTTPS(по сети) илиSTDIO(локально, как subprocess). -
Данные —
JSON-RPC 2.0. Строгая договорённость о том, как упаковывать запросы, ответы и уведомления. В статье я практически это не затронул.
Какую проблему это решает?
До MCP каждый разработчик ИИ-агента писал свой велосипед: парсил Swagger, хардкодил эндпоинты, вручную валидировал параметры.
MCP заменяет это само-описанием. Вместо чтения документации ИИ-агент (MCP-клиент) просто спрашивает у MCP-сервера: {"method": "tools/list"}. И получает список инструментов и их параметром. Причем ответ в виде JSON понятен и человеку и модели.
Таким образом:
-
🧠 LLM — это мозг. Понимает задачу, принимает решение, когда и какой инструмент вызвать и с какими параметрами.
-
✋ Ваш код (Host) — это руки. Выполняет реальные запросы к MCP-серверам, получает данные, возвращает их модели.
-
⚙️ MCP-сервер — это программа. Скрипт, API, БД, файловая система и т.д. Он не AI. Он просто ждёт вызова и отдаёт результат.
MCP — это стандартизированный способ для вашего кода получать инструменты и данные, чтобы «кормить» ими LLM через обычный OpenAI-совместимый API.
Никакой магии. LLM не вызывает серверы. Серверы не думают. Ваш код — единственный мост.
-
finish_reason: "tool_calls"— сигнал делегации. -
role: "tool"— канал возврата данных. -
content: null— правило чистого разделения.
Продолжение следует …
Спасибо, что дочитали. Уверен вы смогли прояснить для себя что же такое MCP.
Если по комментариям будет виден интерес в подробном пояснении:
-
Перечня команд MCP (
tools/list,resource/get, … ) -
Форматов MCP-сообщений (
jsonrpc,params,result) -
Других примитивов, кроме
tools(resources,prompts,sampling) -
Как написать свой MCP-сервер или MCP-клиент на Python.
-
Как отлаживать с помощью
mcp inspector(почти аналог Postman и Swagger, но для MCP)
, то я с удовольствием напишу следующую часть статьи по вашим интересам.
Подписывайтесь на телеграмм @APIMAGIC — буду публиковать обновления и отвечать на вопросы.
P.S. Обещанные ссылки на курс MCP от Anthropic (с бесплатной сертификацией) и полезные видео:
Автор: Dmitry_Barovik


