AmigaОбучение ИИ
Модуль 2 · Ключевые возможности MCP · урок 6 из 13

Roots

9 мин чтения▶ есть видеоверсия

Сервер, который читает файлы, должен знать, где ему можно читать. Хост это знает — у него открыт конкретный проект, — а сервер запускается как отдельный процесс и не знает ничего. Roots закрывают этот разрыв: клиент сообщает серверу список папок, с которыми имеет смысл работать. К концу урока вы будете понимать, что именно roots обещают, как их запросить и чем их заменяют в новых серверах.

Что такое root

Root («корень») — это URI папки или файла плюс необязательное имя для отображения:

json
{
  "uri": "file:///Users/anna/work/shop-frontend",
  "name": "shop-frontend"
}

В текущей спецификации uri обязан быть file://-адресом. Клиент может отдать один корень (открытый проект) или несколько — например, репозитории фронтенда и бэкенда, которые разработчик держит в одном окне.

Откуда клиент берёт этот список, спецификация не диктует. Обычно это открытая в редакторе рабочая область, папка, из которой запущен Claude Code, или явный выбор пользователя в настройках.

Что roots гарантируют и чего нет

Здесь важно быть точным, потому что название обманывает. Roots — это информация, а не ограничение. Спецификация прямо говорит: протокол не следит за тем, чтобы сервер оставался внутри корней. Сервер, который получил file:///Users/anna/work/shop-frontend, технически может прочитать /etc/passwd, и протокол ему не помешает.

Так что roots работают в две стороны как договорённость. Клиент обещает отдавать только те папки, к которым у пользователя есть права, и проверять URI на подмену пути. Сервер обещает уважать границы: проверять каждый путь, с которым работает, на вхождение в один из корней, и корректно обрабатывать ситуацию, когда корень исчез. Хороший сервер файловой системы делает это; плохой — нет, и roots ему не помеха.

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

Как это выглядит на проводе

Клиент, который поддерживает roots, объявляет возможность roots. Сервер запрашивает список методом roots/list; параметров у него нет. Ответ:

json
{
  "roots": [
    { "uri": "file:///Users/anna/work/shop-frontend", "name": "shop-frontend" },
    { "uri": "file:///Users/anna/work/shop-api", "name": "shop-api" }
  ]
}

Как и у sampling, способ доставки зависит от ревизии. В 2025-11-25 roots/list — запрос сервера к клиенту по обратному каналу. Клиент при этом мог объявить roots.listChanged и присылать уведомление notifications/roots/list_changed, когда пользователь открыл другую папку; сервер в ответ перезапрашивал список. В 2026-07-28 сервер не отправляет запросов, поэтому roots/list вкладывается в результат input_required точно так же, как sampling/createMessage в уроке «Sampling»: клиент отвечает списком в inputResponses и повторяет исходный вызов. Уведомление об изменении списка в новой ревизии исчезло — у сервера всё равно нет долгоживущего состояния, которое нужно было бы обновлять.

Если сервер попросит roots у клиента, который возможность не объявил, вызов инструмента завершится ошибкой -32021.

Roots в SDK

Механизм тот же, что и для sampling: резолвер возвращает маркер ListRoots(), а параметр инструмента получает ListRootsResult:

python
from pathlib import Path
from typing import Annotated
from urllib.parse import urlparse

from mcp.server import MCPServer
from mcp.server.mcpserver import ListRoots, Resolve
from mcp.types import ListRootsResult

mcp = MCPServer("project-search")


def workspace_roots() -> ListRoots:
    return ListRoots()


def root_paths(roots: ListRootsResult) -> list[Path]:
    """Превратить file://-URI в пути. Всё, что не file://, отбрасываем."""
    paths = []
    for root in roots.roots:
        parsed = urlparse(str(root.uri))
        if parsed.scheme == "file":
            paths.append(Path(parsed.path).resolve())
    return paths


@mcp.tool()
async def count_todos(
    roots: Annotated[ListRootsResult, Resolve(workspace_roots)],
) -> str:
    """Посчитать пометки TODO в исходниках рабочей области."""
    paths = root_paths(roots)
    if not paths:
        return "Клиент не сообщил рабочих папок."
    total = 0
    for base in paths:
        for file in base.rglob("*.py"):
            total += file.read_text(errors="ignore").count("TODO")
    return f"Найдено пометок TODO: {total} в {len(paths)} папках."

Резолвер workspace_roots не принимает аргументов — ему просто нечего спрашивать у инструмента. SDK выполняет его, получает список от клиента и подставляет результат. root.uri в SDK v2 — строка; мы разбираем её через urlparse и берём только file://.

Старый способ — await ctx.session.list_roots() — сохранился для совместимости, но выдаёт MCPDeprecationWarning и работает только на соединениях с клиентами ревизии 2025-11-25.

Проверка путей

Если инструмент принимает путь от модели, а не берёт его из roots, проверка вхождения обязательна:

python
def inside_roots(path: Path, roots: list[Path]) -> bool:
    resolved = path.resolve()
    return any(resolved == r or r in resolved.parents for r in roots)

resolve() здесь не для красоты: он раскрывает символические ссылки и .., без него shop-frontend/../../etc пройдёт проверку по строковому префиксу. Такую функцию стоит вызывать в каждом инструменте, который получает путь снаружи, и возвращать понятную ошибку, а не читать файл.

Статус: устарело

Roots помечены устаревшими в ревизии 2026-07-28 (SEP-2577), с тем же горизонтом, что sampling и логирование: остаются в спецификации минимум до июля 2027 года. Рекомендованная замена — три варианта на выбор. Передавать папку аргументом инструмента: модель знает, над каким проектом работает пользователь, и может передать путь явно. Использовать URI ресурсов, которые сервер сам публикует. Или задавать разрешённые папки в конфигурации сервера при запуске — например, аргументом командной строки в конфигурации хоста, как это делает эталонный сервер файловой системы.

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

Попробуйте сами

10–15 мин на рабочем месте
  1. Посмотрите, какие серверы в вашем хосте работают с файлами, и выясните из их документации, откуда они узнают разрешённые папки: из roots, из аргументов запуска или из конфигурации.
  2. Напишите функцию inside_roots из урока и проверьте её на трёх путях: внутри корня, с .. наружу и через символическую ссылку, ведущую наружу. Последний случай особенно показателен.
  3. Прочитайте раздел Roots спецификации и найдите, что обязан делать клиент перед тем, как отдать корень серверу.

Коротко

  • Root — file://-URI папки или файла с необязательным именем; клиент отдаёт список запросом roots/list.
  • Roots — информация, а не ограничение: протокол не мешает серверу выходить за их пределы; сервер обязан проверять пути сам.
  • В 2025-11-25 roots/list — запрос сервера по обратному каналу; в 2026-07-28 — вложен в результат input_required (MRTR).
  • В SDK v2: резолвер возвращает ListRoots(), инструмент получает ListRootsResult со списком roots.
  • Проверка вхождения пути — через resolve(), а не строковый префикс.
  • Возможность устарела с 2026-07-28; замена — аргумент инструмента, URI ресурса или конфигурация сервера.

Видеоверсия

Сценарий озвучки · 463 слова, ≈ 4 мин

Сервер, который читает файлы, должен знать, где ему можно читать. Хост это знает: у него открыт конкретный проект. А сервер запускается отдельным процессом и не знает ничего. Roots, или корни, закрывают этот разрыв: клиент сообщает серверу список папок, с которыми имеет смысл работать.

Что такое корень. Это адрес папки или файла в виде file-URI плюс необязательное имя для отображения. Клиент может отдать один корень, например открытый проект, или несколько — репозитории фронтенда и бэкенда, которые разработчик держит в одном окне. Откуда клиент берёт список, спецификация не диктует: обычно это открытая рабочая область, папка, из которой запущен Claude Code, или выбор пользователя.

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

Как это выглядит на проводе. Клиент объявляет возможность roots. Сервер запрашивает список методом roots/list без параметров и получает массив корней. Способ доставки зависит от ревизии, и здесь всё так же, как с sampling. В старой ревизии это запрос сервера к клиенту по обратному каналу, и клиент мог присылать уведомление, что список изменился. В новой ревизии сервер не отправляет запросов, поэтому запрос корней вкладывается в результат «нужен ввод», клиент отвечает списком и повторяет вызов инструмента. Уведомление об изменении списка в новой ревизии исчезло.

В Python SDK механизм тот же, что для sampling. Пишете резолвер, который возвращает маркер ListRoots, и объявляете параметр инструмента как зависимость от него. SDK получает список от клиента и подставляет результат. В примере из урока инструмент считает пометки TODO во всех питоновских файлах рабочих папок: превращает адреса в пути, берёт только file-адреса и обходит их.

Отдельно про проверку путей. Если инструмент принимает путь от модели, обязательно проверьте, что он лежит внутри одного из корней. И проверяйте не по строковому префиксу, а после раскрытия символических ссылок и переходов на уровень выше. Иначе путь «проект, две точки, две точки, etc» пройдёт проверку по префиксу и выведет наружу.

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

В следующем уроке построим сервер, который получает корни от клиента, и проверим, что он не выходит за их пределы.

Отметка хранится только в вашем браузере