Недавно 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-сервером.