
Когда вы подключаете MCP-сервер к Claude Desktop или другому агентному клиенту, вы доверяете ему не один запрос, а цепочку операций с доступом к файловой системе, базе данных или внешнему API. Проблема в том, что спецификация Model Context Protocol версии 2025-03-26 не требует от сервера очищать состояние между сессиями. Это означает, что данные из одного разговора могут «перетечь» в следующий — и вы об этом не узнаете, пока не проверите целенаправленно.
Речь не о гипотетической уязвимости. В официальном репозитории MCP-серверов на GitHub есть примеры, где сервер хранит временные файлы, кеш запросов или пул соединений с базой данных без привязки к идентификатору сессии. Если ваш агент работает с разными пользователями или проектами через один сервер, пересечение контекстов — вопрос времени.
Эта статья — не общий обзор безопасности MCP, а конкретный чеклист для разработчика, который хочет убедиться, что его сервер не сливает контекст. Сценарии, метрики и примеры кода — всё воспроизводится локально за час.
Почему утечка контекста в MCP — не баг, а особенность спецификации
MCP построен на модели «клиент — сервер», где каждый запрос — это JSON-RPC сообщение с методом (tools/call, resources/read, prompts/get) и параметрами. Спецификация определяет транспорт (stdio или SSE), формат сообщений и типы ресурсов, но ничего не говорит про управление состоянием между сессиями.
Когда сервер принимает новое соединение через stdio, он запускает новый процесс — это даёт естественную изоляцию. Но если сервер работает через SSE (Server-Sent Events) или использует общий пул соединений к базе данных, границы между сессиями размываются. Типичные сценарии:
- Кеш ресурсов: сервер запоминает результат чтения файла или запроса к API и возвращает его следующему клиенту без повторного обращения к источнику.
- Общие переменные: в Python SDK глобальные переменные модуля живут всё время работы сервера. Если вы случайно сохранили туда данные пользователя, они будут доступны всем сессиям.
- Пулы соединений: SQLite или Postgres MCP-сервер может держать одно соединение для всех сессий, и пока вы не переключите базу данных явно, следующий клиент увидит те же таблицы.
В документации Anthropic к MCP подчёркивается, что сервер должен быть stateless между запросами, но это рекомендация, а не требование валидации. Ни один официальный инструмент не проверяет изоляцию контекста при запуске сервера.
Сценарий 1: пересечение файлового пространства между сессиями
Самый простой для воспроизведения сценарий. Допустим, у вас есть MCP-сервер, который читает файлы из директории проекта. Первая сессия запрашивает `/projects/alpha/config.json`, вторая — `/projects/beta/config.json`. Если сервер кеширует пути или содержимое, вторая сессия может получить данные первой.
Как проверить локально
Запустите MCP-сервер в режиме SSE (или через stdio с одним процессом на несколько запросов).
2. Подключитесь к нему из двух разных клиентов (или двух экземпляров Claude Desktop с разными профилями).
3. В первой сессии запишите файл с уникальным содержимым через инструмент `filesystem/write`.
4. Во второй сессии попросите прочитать тот же файл — сервер не должен вернуть его, если файл лежит вне разрешённой директории второй сессии.
Что должно произойти: сервер либо возвращает ошибку доступа, либо возвращает пустой результат. Если он отдаёт содержимое — утечка.
Пример проверки в Python с использованием MCP Python SDK
python
import asyncio
from mcp import ClientSession, StdioServerParameters
async def test_context_isolation(server_command: list[str]):
# Первая сессия
params1 = StdioServerParameters(command=server_command[0], args=server_command[1:])
async with ClientSession(params1) as session1:
await session1.initialize()
# Записываем уникальный маркер
await session1.call_tool(«write_file», {«path»: «/tmp/test_marker_1.txt», «content»: «SESSION_1_DATA»})
# Вторая сессия — новый процесс, но тот же сервер
params2 = StdioServerParameters(command=server_command[0], args=server_command[1:])
async with ClientSession(params2) as session2:
await session2.initialize()
result = await session2.call_tool(«read_file», {«path»: «/tmp/test_marker_1.txt»})
# Если сервер возвращает содержимое — утечка
assert result.content[0].text != «SESSION_1_DATA», «CONTEXT_LEAK_DETECTED»
Этот тест ловит только самый грубый случай. Более тонкая утечка — когда сервер не возвращает файл напрямую, но его кеш влияет на скорость ответа во второй сессии. Для этого замеряйте время первого и второго запроса: если второй значительно быстрее, вероятно, сработал кеш.
Сценарий 2: общий пул соединений к базе данных
SQLite MCP-серверы особенно уязвимы, потому что SQLite по умолчанию работает с одним файлом базы данных. Если сервер держит постоянное соединение, а клиенты переключают базу данных через аргументы инструмента, возможна утечка.
Воспроизведение
Запустите MCP-сервер для SQLite (например, из официального репозитория `modelcontextprotocol/servers`).
2. В первой сессии создайте таблицу `secrets` и вставьте строку.
3. Во второй сессии попросите выполнить `SELECT * FROM secrets` — сервер не должен видеть эту таблицу, если вторая сессия использует другую базу данных.
Ограничение метода: этот тест работает только если сервер поддерживает переключение базы данных через параметры инструмента. Если сервер жёстко привязан к одному файлу при запуске, утечка невозможна — но тогда он просто не подходит для мультитенантной работы.
Метрика для CI: после каждого теста проверяйте, что количество таблиц в базе данных второй сессии совпадает с ожидаемым пустым состоянием. Если там появилась таблица из первой сессии — изоляция нарушена.
Сценарий 3: утечка через глобальное состояние в Python SDK
MCP Python SDK позволяет определять инструменты через декораторы. Если внутри функции-обработчика вы используете глобальную переменную или замыкание с изменяемым состоянием, это состояние будет общим для всех сессий одного процесса.
Пример уязвимого сервера
python
from mcp.server import Server
request_counter = 0 # глобальная переменная
@server.list_tools()
async def handle_list_tools():
global request_counter
request_counter += 1
# … возврат списка инструментов
Каждый вызов `list_tools` увеличивает счётчик. Если первый клиент сделал 5 запросов, второй клиент увидит счётчик 6, а не 0. В продакшене такая ситуация маловероятна, но если в глобальном состоянии хранятся не счётчики, а пользовательские данные — утечка гарантирована.
Проверка: обнуляйте все глобальные переменные между тестовыми сессиями или, лучше, вообще не используйте глобальное состояние в сервере. Для CI добавьте линтер, который запрещает глобальные переменные mutable-типов в файлах с инструментами MCP.
Сценарий 4: пересечение через SSE-транспорт
SSE (Server-Sent Events) — это постоянное соединение, через которое сервер может отправлять клиенту события без дополнительного запроса. Если сервер использует SSE и не привязывает события к конкретной сессии, все подключённые клиенты получат одни и те же уведомления.
Пример: MCP-сервер для мониторинга файловой системы отправляет событие при изменении файла. Если сервер не фильтрует события по подписке клиента, клиент A получит уведомление об изменении в директории клиента B.
Воспроизведение
Запустите MCP-сервер с SSE-транспортом.
2. Подключите два клиента.
3. Клиент A подписывается на изменения в `/path/A`, клиент B — в `/path/B`.
4. Измените файл в `/path/B`.
5. Проверьте, получил ли клиент A это событие.
Что делать, если утечка найдена: переходите на stdio-транспорт для изоляции процессов или добавляйте в сервер проверку `session_id` перед отправкой события. Спецификация MCP пока не стандартизирует механизм подписок с фильтрацией, поэтому ответственность полностью на разработчике сервера.
Метрики изоляции контекста для CI
Чтобы проверки не были разовыми, заложите их в конвейер. Вот минимальный набор метрик, которые должен измерять ваш CI при сборке MCP-сервера:
| Метрика | Что измеряет | Порог срабатывания |
|---|---|---|
| `context_leak_file_read` | Чтение файла, записанного другой сессией | Любое успешное чтение = утечка |
| `context_leak_db_table` | Обнаружение таблицы, созданной другой сессией | Любое успешное обнаружение = утечка |
| `context_leak_global_var` | Изменение глобальной переменной между сессиями | Изменение > 0 = утечка |
| `context_leak_sse_event` | Получение события, не предназначенного для этой сессии | Любое получение = утечка |
| `response_time_ratio` | Отношение времени ответа второго запроса к первому | < 0.5 может указывать на кеш |
Последняя метрика — косвенная. Если второй запрос выполняется вдвое быстрее первого, это не обязательно утечка (возможно, сервер прогрел кеш базы данных), но это повод проверить вручную.
Ограничения такого подхода
Все описанные тесты проверяют только изоляцию между последовательными сессиями. Они не ловят:
- Утечку внутри одной сессии: если агент делает несколько запросов подряд, и второй запрос получает данные от первого (например, через неочищенные временные файлы). Это сложнее воспроизвести, но потенциально опаснее — данные могут пересекаться внутри одного разговора.
- Утечку через побочные каналы: время ответа, размер сообщения, порядок полей в JSON — всё это может нести информацию о предыдущих запросах. Доказать утечку через timing attack в CI практически невозможно без специализированных инструментов.
- Утечку на стороне клиента: сам клиент (Claude Desktop, продолжение) может кешировать ответы сервера и показывать их в новом разговоре. Это проблема клиента, а не сервера, но для пользователя результат одинаков — данные из одного чата появляются в другом.
Что делать, если вы нашли утечку
Если хотя бы один тест из чеклиста провалился, у вас три варианта:
Перейти на stdio-транспорт — каждый запрос запускает новый процесс, изоляция гарантируется операционной системой. Минус: выше latency и потребление памяти.
2. Добавить session_id — передавать идентификатор сессии в каждом запросе (через параметры инструмента или через заголовок) и привязывать все состояния к этому ID. Это требует изменений в сервере, но сохраняет производительность SSE.
3. Использовать отдельные серверы для разных контекстов — запускать экземпляр MCP-сервера на каждый проект или пользователя. Самый надёжный, но и самый ресурсоёмкий вариант.
Ни один из вариантов не гарантирует полной изоляции, если сервер написан с глобальным состоянием. Поэтому первый шаг — аудит кода на наличие глобальных переменных, кешей и общих пулов соединений. Чеклист выше — только индикатор, а не замена code review.
Перед тем как подключать новый MCP-сервер к продакшен-агенту, проверьте его этим чеклистом. Если сервер не проходит хотя бы один тест, либо фиксите, либо не используйте в мультитенантном режиме. Спецификация MCP не защитит вас от утечки — только ваши тесты.







