DeepDigest
Habr / Машинное обучение · · ~4 мин

Как работает взаимодействие модели и MCP-сервера: разбор на примере Claude

Автор разобрал взаимодействие модели Claude и MCP-сервера, выявив особенности передачи данных и управления поведением агента. Обнаружены нюансы отладки, из‑за которых сложно понять, какие данные на самом деле обрабатывает модель. Представлен инструмент mcpsnoop, позволяющий отслеживать трафик между клиентом и сервером без влияния на работу системы.

LLM
Как работает взаимодействие модели и MCP-сервера: разбор на примере Claude

Недавно Claude уверенно пересказал большой документ, хотя прочитал только его начало. Автор подключил mcp-server-fetch и попросил агента извлечь из документа несколько фрагментов. Ответ получился складным и уверенным. Проанализировав трафик JSON-RPC между клиентом и сервером, автор выяснил, что ответ вернулся обрезанным на 6000 символах — при этом сервер пометил его как успешный. В самом конце ответа сервер добавил для модели инструкцию: Content truncated. Call the fetch tool with a start_index of 6000 to get more content. Второго вызова в потоке не было — модель ответила только по первой части.

При разборе сессии обнаружилось нечто более важное: в описании инструмента fetch есть указание, которое пользователь не видит, а модель читает и следует ему. Речь о том, что инструмент теперь предоставляет доступ в интернет и позволяет получать актуальную информацию. Через такие описания работают атаки класса tool poisoning — когда модель получает указания, которые пользователь не давал.

Значительная часть того, что управляет поведением агента, — это текст, который сервер адресует модели, а не пользователю. Канал — единственное место, где видно, что модель прочитала на самом деле.

У MCP есть официальный Inspector. Он подключается к серверу как отдельный клиент и показывает то, что отправляет серверу сам Inspector, а не то, что в живой сессии отправляет настоящий клиент (Claude Desktop, Cursor, Codex и т. д.). Настоящий клиент — это работающая модель, которая сама решает, какой инструмент вызвать и с какими аргументами. Воспроизвести её поведение, вызывая инструменты вручную, не получится.

Логи и отладчик на стороне сервера тоже не дают полной картины: им видна лишь часть данных — только те запросы, которые дошли до сервера.

В основе MCP лежит JSON-RPC 2.0, чаще всего поверх stdio (клиент запускает сервер как процесс и общается с ним через его stdin и stdout), реже — поверх streamable HTTP. Сообщения передаются построчным JSON. Сессия начинается с хендшейка: клиент отправляет серверу initialize со своими capabilities, а сервер отвечает своими.

{
 "jsonrpc": "2.0",
 "id": 0,
 "method": "initialize",
 "params": {
 "protocolVersion": "2025-11-25",
 "capabilities": {
 "roots": {},
 "elicitation": {}
 },
 "clientInfo": {
 "name": "claude-code"
 }
 }
}

Затем идут tools/list, tools/call и остальное:

{
 "jsonrpc": "2.0",
 "id": 2,
 "method": "tools/call",
 "params": {
 "name": "fetch",
 "arguments": {
 "url": "https://raw.githubusercontent.com/torvalds/linux/master/MAINTAINERS"
 }
 }
}

Есть три момента, которые легко упустить при отладке:

1. Когда инструмент падает, сервер обычно отвечает успехом, а факт ошибки прячет в «isError»: true. Если смотреть только на поле error протокола, такие падения легко пропустить.

2. Сервер тоже отправляет запросы клиенту: sampling, elicitation и другие. Например, server-filesystem при старте сам запрашивает у клиента roots/list.

3. Сервер отправляет уведомления без идентификатора и ответа не ждёт. В обычной отладке они теряются, а в потоке видны.

Идея — встроиться в реальный канал, ничего не сломав. Прозрачный прокси между клиентом и сервером — mcpsnoop — совмещает две роли в одном исполняемом файле.

Первая роль: прозрачный прокси. Клиент запускает mcpsnoop вместо сервера, а тот уже поднимает настоящий сервер и передаёт stdio между ними без изменений. В конфигурации клиента меняется одна строка.

// было
{ "command": "node", "args": ["build/index.js"] }
// стало
{ "command": "mcpsnoop", "args": ["--", "node", "build/index.js"] }

Всё, что идёт после --, — это обычная команда запуска сервера.

Вторая роль: хаб и TUI. Он собирает трафик со всех таких посредников и показывает его в реальном времени.

Наблюдение не должно влиять на трафик, поэтому посредник намеренно примитивен: он не разбирает данные, а пересылает байты как есть и отдельно отдаёт копию каждого кадра. Вся логика вынесена в хаб — он сопоставляет запрос с ответом по идентификатору, замеряет длительность каждого вызова, отмечает ошибки и зависания.

Клиент поднимает посредника, когда сочтёт нужным, а интерфейс можно открыть в любой момент. Чтобы порядок не имел значения, посредник пишет каждый кадр сразу в две стороны — в живой поток и в журнал на диске. Интерфейс при старте подхватывает и историю, и новые события.

Интерфейс построен на Bubble Tea, навигация целиком с клавиатуры. Есть отдельный экран с тем, что стороны отправили при хендшейке, и фильтр потока. Пойманный некорректный вызов можно прогнать заново на свежей копии сервера, не перезапуская клиент.

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

mcpsnoop demo

Попробовать инструмент можно так:

go install github.com/kerlenton/mcpsnoop/cmd/mcpsnoop@latest

Или через Homebrew:

brew tap kerlenton/mcpsnoop
brew install mcpsnoop

Последние версии Homebrew с осторожностью относятся к сторонним tap. Если установка отклоняется, нужно подтвердить доверие к tap и повторить её:

brew trust kerlenton/mcpsnoop
brew install mcpsnoop

Также можно скачать готовый исполняемый файл со страницы релизов. Для streamable HTTP mcpsnoop умеет работать обратным прокси:

mcpsnoop http --target http://localhost:3000/mcp --listen :7000

Инструмент можно попробовать уже сейчас с любым клиентом и MCP-сервером.

// поделиться Telegram VK