Пожиратель токенов (или нет): анатомия протокола MCP для ИИ-агентов

в 16:21, , рубрики: Anthropic, llm, MCP, model context protocol, protocol, ии-агенты, мультиагентные системы, язковые модели

Поводом написания этой статьи послужил подслушанный диалог:

— А на чем у вас агенты написаны?

— У нас на MCP!

Для меня MCP всегда был просто протоколом, то есть именно способом отправки и обработки запросов. А когда я слушал выступления или читал некоторые статьи о том, как плох/хорош 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

Первая версию UI для тестирования MCP

И тут я понял, что этого UI достаточно, чтобы раскопать все необходимое про MCP, нам и агент-то никакой не нужен. Ведь по сути «агент» это (если очень-очень огрубить) для MCP просто посылалка запросов, а здесь я все могу сэмулировать сам. Ок, значит без агента, просто нужно сэмулировать реальный кейс и посмотреть что там внутри.

Простой обменник запросами по протоколу MCP

Простой обменник запросами по протоколу MCP

Ок, тогда допилим наш код MCP-сервера и UI так, что в нем поддерживались все доступные методы.

Пожиратель токенов (или нет): анатомия протокола MCP для ИИ-агентов - 4

Мм, кажется все выглядит вполне прилично? Где-то в этот момент я понял, что сравниваться с 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, которую делать не надо.

Пожиратель токенов (или нет): анатомия протокола MCP для ИИ-агентов - 7

Такие ресурсы надо объединять в шаблоны, это делается на уровне кода.

Код шаблонов
{
  "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 и голого веб-сокета
Замеры токенов двух разных MCP и голого веб-сокета

Замеры токенов двух разных MCP и голого веб-сокета

Две самые важные и эталонные картиночки про понимание насколько хорош MCP.

Конечно, есть еще огромные пласты вопросов в виде обработки ошибок, безопасности, кэширования и много другого околопротокольного, но все это слишком бы усложнило и без того немалую статью.

Финальные вывод

MCP — крутой сокетный протокол, мне понравился. Это желание скрыть строгую типизацию REST-style API за более абстрактной, которой не страшны даже серьезные изменения API налету, потому что MCP оперирует смыслами.

MCP имеет смысл для AI-first приложений, где LLMка служит основным интерфейсом. Если нужно вызвать несколько разных инструментов и сложные вещи можно свести к смыслам, то протокол очень хорош. Совсем неожиданный вывод, но протокол очень полезен для AI-агентов, агентных систем и соединения сложных LLM-кубиков между собой. Для других целей он может быть избыточен.

И MCP требует немного другого подхода к техническому проектированию, хорошего бизнес-понимания того, что мы вообще делаем. К сожалению, реализации по старинке или гонка за универсальностью действительно могут превратить MCP в пожирателя токенов.

Но это не так! Теперь — с гарантией.

Спасибо!

Мой скромный тг-канальчик и другие статьи:

Автор: antipov_dmitry

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js