Поводом написания этой статьи послужил подслушанный диалог:
— А на чем у вас агенты написаны?
— У нас на MCP!
Для меня MCP всегда был просто протоколом, то есть именно способом отправки и обработки запросов. А когда я слушал выступления или читал некоторые статьи о том, как плох/хорош MCP, меня не покидало ощущение чего-то странного. Но я все же решил, что это от незнания и я чего-то не понимаю. А когда не понимаешь, но очень хочешь понимать, то самый лучший способ — это взять и разобраться.
Именно это предлагаю и сделать в статье, а также замерить MCP, чтобы ответить на вечный вопрос: сколько сжирает MCP, подключать ли его вообще или и так сойдет?
В качестве реальной задачки попробуем взять такую интересную вещь как «подбор направления для отпуска». Допустим, мы хотим на чего-то такое, чтобы ух, но при этом погода была хорошая и билеты на наши даты подешевле. Когда-то 10 лет назад я пытался решить такое детерминировано, но сегодня это отличная задача для LLM-based систем, которым для принятия решения достаточно подложить в контекст актульные свежие данные.
LLMки неплохо знают где музеи, а где море, они неплохо знают где плюс-минус какая кухня, но вот погоду или цены на отели на наши даты без внешних источников они по своей архитектуре знать не могут. И именно это мы попробуем обыграть.
У меня есть хобби-сайтец про путешествия, на котором есть то самое обогащение, которое нужно по условию задачи. И именно его мы сделаем через REST и через MCP и замерим одно с другим, — если такое вообще честно замерить. Но мы точно попробуем.
И так, у нас есть LangGraph, за которым стоит chatgpt-4o-mini. Соберем агента, которому дадим два инструмента — по нужным датам в переданной локации сходить за погодой и за ценами на отели. А за основу возьмем прекрасный жаркий город — Бангкок.
Первый пример данных, если это интересно
# Погода
{
"Bangkok": {
"2025-10-13": {
"temperature": 32,
"feels_like": 38,
"humidity": 75,
"condition": "Partly cloudy",
"wind_speed": 12,
"wind_direction": "SW",
"precipitation_chance": 30,
"uv_index": 9,
"visibility": 10,
"pressure": 1012,
"updated_at": "2025-10-13T08:00:00Z"
},
"2025-10-14": {
"temperature": 31,
"feels_like": 37,
"humidity": 78,
"condition": "Thunderstorms",
"wind_speed": 15,
"wind_direction": "SW",
"precipitation_chance": 80,
"uv_index": 7,
"visibility": 8,
"pressure": 1011,
"updated_at": "2025-10-14T08:00:00Z"
},
.....
}
# Отели
{
"name": "Mandarin Oriental, Bangkok",
"category": "luxury",
"avgPriceRubPerNight_estimate": { "low": 30000, "high": 70000 },
"currency": "RUB",
"lat": 13.7225,
"lon": 100.5100,
"address": "48 Soi Charoenkrung 40, Bang Rak, Bangkok 10500, Thailand",
"source": "https://www.booking.com/hotel/th/mandarin-oriental-bangkok.html"
},
{
"name": "The Peninsula Bangkok",
"category": "luxury",
"avgPriceRubPerNight_estimate": { "low": 22000, "high": 50000 },
"currency": "RUB",
"lat": 13.7281,
"lon": 100.5062,
"address": "333 Charoennakorn Road, Khlong San, Bangkok 10600, Thailand",
"source": "https://www.booking.com/hotel/th/the-peninsula-bangkok.html"
},
MCP-сервер глобально состоит из двух частей: протокольная обвязка и сама логика реализации вызываемых методов. Вот чтобы логику реализации отделить от эксперимента, мы сделаем ее сразу статичной, то есть это будет json-файл сразу с данными, который нам надо прочитать и передать по запросу.
Вводная про MCP
MCP (Model Context Protocol) — это попытка стандартизировать способ, как LLM получает и обновляет контекст. Протокол придуман для задачи обогащения контекста — чтобы не сразу все складывать в огромный синхронный контекст, а запрашивать (и обмениваться) данными по мере необходимости.
Под капотом все довольно понятно: MCP — это JSON-RPC 2.0 поверх транспорта (stdio, HTTP с SSE (Server-Sent Events или кастомный транспорт типа WebSocket), который идеально подходит для задач подкидывания данных по мере появления).
Каждое взаимодействие идёт через стандартные JSON-RPC вызовы:
— initialize: установить соединение и согласовать протоколы;
— listResources: узнать, какие ресурсы доступны (например, git, tasks, metrics);
— getResource: получить данные из ресурса;
— subscribe: подписаться на обновления (events);
— notify: отправить уведомление без ожидания ответа.
Помимо них есть tools/list, tools/call, prompts/list, prompts/get, resources/templates, но про них будет позже.
Пример базового обмена:
# Запрос от клиента
{
"jsonrpc": "2.0",
"id": 1,
"method": "getResource",
"params": { "resource": "hello" }
}
# Ответ
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"message": "Привет от MCP!",
"timestamp": "2025-10-13T20:25:03.921Z"
}
}
Ну вот как бы и все, как будто бы без магии. Все остальное уже регулировка деталей: контракты и конвенции (например, какие типы ресурсов бывают, какие метаданные можно запрашивать и так далее).
Пишем первый сервер и UI-тулер
Ок, первый подход к снаряду — пишем максимально простой hello world MCP-сервер:
Первый hello world код, в котором ничего интересного
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
import uvicorn
import json
from datetime import datetime
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
await ws.accept()
try:
while True:
data = await ws.receive_text()
try:
req = json.loads(data)
except Exception:
continue
method = req.get("method")
req_id = req.get("id")
if method == "initialize":
await ws.send_text(json.dumps({
"jsonrpc": "2.0",
"id": req_id,
"result": {"resources": [{"name": "hello", "schema": "HelloSchema"}]}
}))
continue
if method == "getResource" and req.get("params", {}).get("resource") == "hello":
await ws.send_text(json.dumps({
"jsonrpc": "2.0",
"id": req_id,
"result": {"message": "Привет от MCP (py)", "ts": datetime.utcnow().isoformat()}
}))
continue
if method == "subscribe" and req.get("params", {}).get("resource"):
await ws.send_text(json.dumps({"jsonrpc": "2.0", "id": req_id, "result": {"subscribed": True}}))
await ws.send_text(json.dumps({
"jsonrpc": "2.0",
"method": "resourceChanged",
"params": {"resource": req["params"]["resource"], "data": {"updated": True}}
}))
continue
await ws.send_text(json.dumps({
"jsonrpc": "2.0",
"id": req_id,
"error": {"code": -32601, "message": "Method not found"}
}))
except WebSocketDisconnect:
print("Клиент отключился")
if __name__ == "__main__":
uvicorn.run("server:app", host="0.0.0.0", port=3000, reload=True)
И под его тестирование я собрал простенький UI на React+TypeScript.
И тут я понял, что этого UI достаточно, чтобы раскопать все необходимое про MCP, нам и агент-то никакой не нужен. Ведь по сути «агент» это (если очень-очень огрубить) для MCP просто посылалка запросов, а здесь я все могу сэмулировать сам. Ок, значит без агента, просто нужно сэмулировать реальный кейс и посмотреть что там внутри.
Ок, тогда допилим наш код MCP-сервера и UI так, что в нем поддерживались все доступные методы.

Мм, кажется все выглядит вполне прилично? Где-то в этот момент я понял, что сравниваться с REST напрямую не очень правильно, потому что реализация MCP не является проблемой и оверхедом, а оверхедом является (или не является) флоу данных внутри протокола.
Протокол MCP оперирует базовым понятием «ресурса». Ресурс (Resource) в MCP — это любой источник данных, к которому можно получить доступ, это как бы базовая единица смысла. Если проводить аналогию с REST, то «ресурс» это любой метод, к которому мы можем обратиться, что-то из него достать или подписаться на его обновления.
Есть еще промпты (здесь понятно) и инструменты (tools) — в контексте MCP это атомарные операции на ресурсами, а по факту вызов чего угодно извне.
У ресурса есть:
-
уникальный ид (hotel://Grand Palace Hotel, weather://Bangkok/2025-10-13 и так далее)
-
MIME Type (application/json, text/markdown, и другие привычные)
-
метаданные (uri, name, description, mimeType)
-
содержимое (любое содержимое)
Цикл жизни ресурса состоит из запроса списка ресурсов, а затем чтения или подписки на него.
Для статических ресурсов (условно, топ-10 что посмотреть может не меняться годами) подписка не нужна, а вот для частоизменяемых ресурсов типа цены или погода (как у нас) — самое то.
Вот так примерно внутри выглядит подписка:
Давайте посмотрим на мои ресурсы: всего лишь 20 отелей и один город, а уже огого сколько контекста? А если городов будет 500, а отелей по 10000 в каждом? Получается, MCP это пожиратель токенов?
Ну, и да и нет. Глобально, при прямом запросе «найди мне отель в Бангкоке меньше 10000 рублей» нам нужно передать все отели как ресурсы, затем отфильтровать их на принимающей стороне и да, такое простой запрос съест огромное количество токенов.
К сожалению, миф о жуткой неэффективности и забиваемом контексте MCP вырос отсюда, так как часто в погоне за галочкой о внедрении MCP в проект все делается не очень оптимально, потому что MCP это не совсем привычный API и его достаточно специфично надо готовить. И да — в такой реализации оно жрет просто огромное количество токенов. Плохой дизайн, неправильное понимание цели протокола, неправильная гранулярность данных и вот это все в стране невыученных уроков — в таком виде MCP действительно сжирает токены как не в себя. Это причина номер один.
Вторая причина — MCP задумывался как универсальная смысловая абстракция над кучей сложных штук — базы данных, сложные сервисы и так далее. И именно универсальность, то есть, желание чтобы к сервису мог подключиться кто угодно с какой угодно задачей, порождает повышенную избыточность. Потому что ты только сделал tools/list сложного сервиса, а тебе уже прилетела вся их документация на 50к токенов.
Когда я готовился к статье, я прочитал кейс о том, как оптимизировали MCP для крупного парка развлечений, который подается как большой успех. И читая кейс я испытывал странные чувства, как будто бы от того, что это MCP и это про AI все вдруг забыли как вообще проектируются системы, и что «давай вывалим все что есть и пусть оно там само разберется» не работает хорошо.
Вот первоначально что вываливалось в их MCP:
Данные по каждому аттракциону
{
"attractions": [{
"id": 12345,
"name": "Space Mountain",
"park": "Magic Kingdom",
"land": "Tomorrowland",
"type": "roller_coaster",
"status": "OPERATING",
"wait_time": 45,
"fast_pass_available": true,
"height_requirement": "44 inches",
"duration": "2 minutes 30 seconds",
"description": "Indoor roller coaster in complete darkness with space theme",
"historical_wait_times": [30,35,40,45,50,55,60,45,40,35],
"accessibility": {
"wheelchair_accessible": false,
"assistive_listening": true
},
"seasonal_schedule": {"summer": "9:00-23:00"},
"metadata": {
"created_at": "2024-01-01T00:00:00Z",
"data_source": "official_api",
"confidence_score": 0.98
}
}]
}
По факту собрав статистичку, разработчики поняли, что практически 90% обращений к MCP это просто запросы на время работы аттракционов и запросы на время ожидания в очереди, после чего всю систему и данные правильно нарезали на разные сущности, выделили самое используемое и убрали избыточные данные, перегружавшие весь контекст.
Загрузка данных по требованию, разделение инструментов, передача только нужных инструментов для запроса, выделение ключевых паттернов использования и компрессия json (historical_wait_times -> hwt) — все это прекрасно работает для ускорения MCP.
Конкретно та ситуация благополучно, но многие-то в своих MCP так не сделали.
И как вывод: чем конкретнее будет наш кейс применения, чем лучше и точечнее будет адаптирован MCP, тем он будет эффективнее. А в остальном — ну как по мне, та это прикольная абстракция обертка над сокетами.
А что с этим делать?
MCP придумали в тех же стенах, где придумали одну из лучших LLM мира, поэтому решения есть.
Resource Templates
Посмотрим еще разок мой скрин. Сюда в отдельные ресурсы попали все отели, хотя по факту — это один ресурс, за которым сотни отелей. Это как раз та самая кривая реализация MCP, которую делать не надо.

Такие ресурсы надо объединять в шаблоны, это делается на уровне кода.
Код шаблонов
{
"uriTemplate": "hotel://{city}/{hotel_id}",
"name": "Hotel information",
"parameters": {
"city": {"type": "string"},
"hotel_id": {"type": "string"}
}
}
Инструменты (Tools) вместо ресурсов
Наиболее эффективное решение это все операции выносить в инструменты. Надо найти больше 1 сущности? Это должен быть инструмент.
Метаданные
Иногда все более-менее понятно по метаданным и можно пробовать вынести туда самое ключевое, но это все сработает на маленьком количестве ресурсов.
Самый-самый главный вывод: tools для операций, а resource — только для разового чтения, когда нам нужно читать (или подписываться) именно на него.
Итоговые замеры
Я обещал сделать замеры, но в процессе как обычно все свелось к тому, что мерить напрямую особо нечего. Но я все же решил сделать несколько больших графиков, чтобы подчеркнуть основную идею. Все эти цифры абсолютно неповторимы, потому что моя задача и реализация точно не такая как ваша, но главную идею, ради которой все было написано, они передают хорошо. Проблема обычно НЕ в MCP как таковом.
3 сценария: MCP (мой первый кривой), MCP (оптимизированный по правильному) и голый веб-сокет. Два типа запросов — поиск отелей и получение погоды, каждый по 100 раз, а потом все просуммируем.
Будем мерить: токены (input, output, total) и время отклика (среднее, мин, макс, P50, P95).
Две самые важные и эталонные картиночки про понимание насколько хорош MCP.
Конечно, есть еще огромные пласты вопросов в виде обработки ошибок, безопасности, кэширования и много другого околопротокольного, но все это слишком бы усложнило и без того немалую статью.
Финальные вывод
MCP — крутой сокетный протокол, мне понравился. Это желание скрыть строгую типизацию REST-style API за более абстрактной, которой не страшны даже серьезные изменения API налету, потому что MCP оперирует смыслами.
MCP имеет смысл для AI-first приложений, где LLMка служит основным интерфейсом. Если нужно вызвать несколько разных инструментов и сложные вещи можно свести к смыслам, то протокол очень хорош. Совсем неожиданный вывод, но протокол очень полезен для AI-агентов, агентных систем и соединения сложных LLM-кубиков между собой. Для других целей он может быть избыточен.
И MCP требует немного другого подхода к техническому проектированию, хорошего бизнес-понимания того, что мы вообще делаем. К сожалению, реализации по старинке или гонка за универсальностью действительно могут превратить MCP в пожирателя токенов.
Но это не так! Теперь — с гарантией.
Спасибо!
Мой скромный тг-канальчик и другие статьи:
Автор: antipov_dmitry
