Как избежать утечки данных между пользователями в MCP: используйте cachePartition для приватных ответов. Научитесь тестировать изоляцию кэша.
В спецификации MCP от 28 июля 2026 года кэш-подсказки (cache hints) определяют, как долго результат может считаться свежим, но не учитывают, кто именно запрашивает данные. Если Алиса нагревает общий кэш, а Боб затем использует тот же самый стор, неполный ключ кэша может вернуть Бобу результат Алисы. Это нарушение границы безопасности, которое стоит тестировать.
В этой статье мы разберем, как cacheScope и cachePartition помогают изолировать приватные ответы, и покажем рабочий пример на TypeScript, который воспроизводит утечку и исправляет её.
Спецификация MCP покрывает кэширование для discovery, списков инструментов и промптов, списков ресурсов, шаблонов ресурсов и чтения ресурсов. Значения cacheScope имеют разный смысл:
Для пользовательского каталога инструментов сервер может описать результат так:
const server = new McpServer({
name: "private-catalog",
version: "1.0.0"
}, {
cacheHints: {
"tools/list": {
ttlMs: 60_000,
cacheScope: "private"
}
}
});Эта подсказка сообщает о намерениях сервера, но не говорит общему кэшу, какой вызывающий сделал запрос. Ключ кэша уже должен включать метод и все параметры, которые могут изменить результат. Идентичность авторизации обычно передаётся вне параметров tools/list, поэтому клиенту нужна отдельная партиция для неё.
Опасная конфигурация выглядит просто: два контекста авторизации, один кэш ответов, и никакой партиции.
const sharedCache = new InMemoryResponseCacheStore();
function unpartitionedClient() {
return new Client({
name: "shared-gateway",
version: "1.0.0"
}, {
responseCacheStore: sharedCache
});
}Демонстрация использует два эндпоинта в одном процессе с одинаковой идентичностью сервера. Один эндпоинт отдаёт инструмент только для Алисы, другой — только для Боба. Эти эндпоинты имитируют разные результаты, которые реальный аутентифицированный сервер мог бы производить.
Алиса вызывает tools/list первой, помещая свой приватный результат в общий стор. Затем Боб вызывает тот же метод с теми же параметрами и идентичностью сервера. Без cachePartition поиск не различает контексты авторизации. Боб получает кэшированный список инструментов Алисы, а его эндпоинт не получает ни одного запроса tools/list. Официальное руководство по кэшированию TypeScript SDK предупреждает, что такая конфигурация может отдать одному пользователю приватное тело ответа другого.
Тест намеренно считается пройденным, когда воспроизводит небезопасный результат. Это делает видимым сбой без учётных данных, сетевого сервиса, модели или платного вызова API.
Решение — дать каждому контексту авторизации стабильную партицию кэша:
const sharedCache = new InMemoryResponseCacheStore();
function clientFor(cachePartition: string) {
return new Client({
name: "shared-gateway",
version: "1.0.0"
}, {
responseCacheStore: sharedCache,
cachePartition
});
}
const alice = clientFor("subject:alice");
const bob = clientFor("subject:bob");Теперь приватные записи Алисы хранятся отдельно от записей Боба. Тот же метод, параметры и идентичность сервера больше не приводят к одному и тому же месту в приватном кэше. Второй тест подтверждает, что оба эндпоинта получают по одному запросу, и каждый клиент видит только свои инструменты.
Я бы выводил партицию из стабильной, непрозрачной идентичности авторизации. Она должна включать все измерения, которые могут изменить видимость: тенант, субъект, роль или эффективную область. Сырой bearer-токен — плохая партиция: это секретный материал, и он может ротироваться, пока основной субъект остаётся тем же.
SDK обрабатывает публичные записи иначе: они остаются общими между партициями, потому что сервер явно объявил их безопасными для переиспользования между контекстами. Это сохраняет преимущество производительности, не ослабляя приватную изоляцию.
Мой чек-лист краток:
Рабочий пример на TypeScript включает небезопасное воспроизведение и исправление с партициями. Ограничения: cacheScope не является авторизацией. Он управляет переиспользованием кэша, но не предоставляет доступ. Сервер должен аутентифицировать вызывающего и авторизовать каждый некэшированный запрос.
TTL — это подсказка свежести, а не мгновенный механизм отзыва. Если права доступа меняются, ожидание истечения приватной записи может быть слишком медленным. Соответствующий клиент должен инвалидировать затронутые записи при получении соответствующего MCP-уведомления, а приложение всё равно нуждается в политике для изменений авторизации вне этого потока.
Другие границы протокола тоже важны. Многоэтапные повторные запросы с inputResponses или requestState не должны кэшироваться. Результат input_required неполный и не подлежит кэшированию. Пагинация кэшируется по одной странице за раз, использует ту же область между страницами и не гарантирует согласованность снимков.
Для процесса с одним субъектом и приватным неразделяемым стором партиция может добавить немного ценности. Для шлюзов, настольных хостов или сервисов, которые мультиплексируют пользователей через один кэш, я бы сделал изоляцию партиций регрессионным тестом, а не предположением конфигурации.
Знает ли ваш MCP-клиент, какому контексту авторизации принадлежит каждый приватный результат? Если нет — исправьте это сегодня.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →