Поводом написания этой статьи послужил подслушанный диалог:
— А на чем у вас агенты написаны?
— У нас на 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: узнать, какие ресурсы доступны (например, weather, sights, hotels);
— 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)
содержимое (любое содержимое)
Цикл жизни ресурса состоит из запроса списка ресурсов, а затем чтения или подписки на него.

Для статических ресурсов (условно, «топ-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 обычно НЕ в MCP как таковом.
На замере 3 сценария: MCP (мой первый кривой), MCP (оптимизированный по правильному) и голый веб-сокет. Два типа запросов — поиск отелей и получение погоды, каждый по 100 раз, а потом все просуммируем.
Будем мерить: токены (input, output, total) и время отклика (среднее, мин, макс, P50, P95).


Две самые важные и эталонные картиночки про понимание насколько хорош MCP. Да, если с графика убрать левого претендента, то оптимизированная версия будет смотреться не так выигрышно относительно бейзлайна с голым вебсокетом, но это уже вполне допустимая плата за тот комфорт и возможности для построения систем, которое дает MCP.
Конечно, есть еще огромные пласты больших вопросов в виде обработки ошибок, безопасности, кэширования и много другого околопротокольного, но все это слишком бы усложнило и без того немалую статью.
Финальные вывод
MCP — крутой сокетный протокол, мне понравился. Это желание скрыть строгую типизацию привычных API за более абстрактной, которой не страшны даже серьезные изменения API налету, потому что MCP оперирует смыслами.
MCP имеет смысл для AI-first приложений, где LLMка служит основным интерфейсом. Если нужно вызвать несколько разных инструментов и сложные вещи можно свести к смыслам, то протокол очень хорош. Совсем неожиданный вывод, но протокол очень полезен для AI-агентов, агентных систем и соединения сложных LLM-кубиков между собой. Для других целей он может быть избыточен.
И MCP требует немного другого подхода к техническому проектированию и хорошего бизнес-понимания того, что мы вообще делаем и зачем здесь собрались. К сожалению, реализации этой новизны по старинке или гонка за универсальностью действительно могут превратить MCP в пожирателя токенов.
Но мы-то теперь знаем, что это не так! Теперь — с гарантией.
Спасибо!
Мой скромный тг-канальчик и другие статьи:
Sannyasin
Интересно )