Skip to content

Спайк: как MCP-сервер узнаёт проект, и какова launch-модель (gmcp-spike-roots) ​

Статус: выполнен, смена #277, 2026-09-29. Хост пробы — Claude Code в расширении VS Code на Windows 11, claude.exe под Code.exe.

Премиса переписана в смене #121: спека MCP от 2026-07-28 (SEP-2577) депрекирует roots, поэтому первичный предмет спайка — три НЕ-депрекированных механизма резолва проекта, а roots остаются переходным путём. Депрекация annotation-only с гарантией не менее 12 месяцев.

Пути машины в логах ниже заменены на <PROJECT> и <OTHER-PROJECT>: абсолютный путь разработчика — класс утечки, который гейт публичного снапшота ловит по делу (память #710). Заменены только ПРЕФИКСЫ; доказательство несёт форма, а не буква диска — в логе по-прежнему видно, что cwd равен корню проекта, что --project у одного сервера относительный, а у другого абсолютный и указывает на ДРУГОЙ проект, и что CLAUDE_PROJECT_DIR пуст.

Итог одной таблицей ​

МеханизмПоддержан?Чем доказано
Параметр тулаДа, на уровне протоколапроба ниже: сервер получил абсолютный путь дословно
Resource URIДа хостом, но не решает задачуresources/list и resources/read отвечают; хост несёт для них отдельные инструменты
Конфиг сервера (argv + cwd)Да, и это то, что работает в продевсе живые серверы подняты с --project, cwd = корень проекта
session.list_roots()НЕ ПРОВЕРЕНтребует регистрации нового сервера и перезапуска хоста — внутри смены недостижимо; записан dead_end

Launch-модель: один stdio-процесс на окно, не общий. Значит multi-tenant кэш НЕ нужен: достаточно one-root-per-process.

Проба 1 — три механизма на пробном сервере ​

Пробный сервер и клиент живут в scratchpad, не в дереве (AC-6). Клиент — обычный mcp.client.stdio, то есть проверяется ПРОТОКОЛЬНАЯ сторона, а не хост.

== initialize ==
server: gmcp-probe 0
server capabilities: experimental=None logging=None prompts=None resources=None tools=None
                     completions=None tasks=None

== tools/list ==
  whoami ['project']

== tools/call whoami {project: <absolute>} ==
{
  "tool_parameter_received": "<PROJECT>",
  "argv_project": ".",
  "cwd": "&lt;PROJECT&gt;"   <- РАВЕН корню проекта,
  "CLAUDE_PROJECT_DIR": null,
  "WORKSPACE_FOLDER_PATHS": null
}

== resources/list ==
  tausik://project/root - Project root as this server resolved it

== resources/read tausik://project/root ==
{
  "tool_parameter_received": null,
  "argv_project": ".",
  "cwd": "&lt;PROJECT&gt;"   <- РАВЕН корню проекта,
  "CLAUDE_PROJECT_DIR": null,
  "WORKSPACE_FOLDER_PATHS": null
}

ОШИБКА ПРОБЫ, записанная как результат (AC-5). На завершении сервер упал:

LookupError: <ContextVar name='request_ctx' at 0x000001F9B25C4720>
  File ".../scratchpad/probe_server.py", line 87, in main
    print("client capabilities:", server.request_context, file=sys.stderr)

Это дефект самой пробы, а не протокола: request_context существует только внутри обработки запроса, а печать стояла ПОСЛЕ server.run. Отказ произошёл после всех четырёх обменов, поэтому логи выше действительны. Побочный вывод, который стоит того: возможности КЛИЕНТА так, снаружи запроса, не прочитать — чтобы узнать, объявил ли хост roots, нужен обработчик внутри сессии.

Что из этого следует по каждому механизму ​

Параметр тула — работает, но в TAUSIK его нет и добавить его молча нельзя. Сервер получил путь дословно. При этом наши серверы отвергают необъявленный аргумент по имени (reject_unknown_arguments), а из 147 инструментов ни один не объявляет параметр проекта: проверено на tausik_status, схема несёт только compact и verbose. То есть механизм доступен, но это работа, а не свойство.

Resource URI отвечает, но задачи не решает, и это важнее его поддержки. Ресурс ПУБЛИКУЕТ сервер. Чтобы опубликовать корень проекта, сервер обязан его уже знать — круг замкнут. Resource URI годится, чтобы сервер СООБЩИЛ хосту, какой проект он обслуживает, и не годится, чтобы УЗНАТЬ его.

Конфиг сервера — рабочий механизм, и он уже в проде. .mcp.json объявляет ${CLAUDE_PROJECT_DIR:-.}, а проба показывает CLAUDE_PROJECT_DIR: null: хост не экспортирует эту переменную в процесс MCP. Резолв фактически делает запасной . плюс cwd, который хост ставит в корень воркспейса. Это работает без участия пользователя, но опирается на cwd, а не на объявленную переменную, — и документация, называющая переменную, описывает не то, что происходит.

Проба 2 — хост поддерживает ресурсы ​

Вызов инструмента хоста, не нашего сервера:

ListMcpResourcesTool()
-> No resources found. MCP servers may still provide tools even if they have no resources.

Чистый пустой ответ, а не «capability не поддержана»: хост несёт ListMcpResourcesTool и ReadMcpResourceTool как штатные инструменты. Пусто потому, что подключённые серверы ничего не публикуют — codebase-rag/rag_server.py:99 возвращает [] по построению.

Проба 3 — launch-модель, замер по живым процессам ​

claude.exe hosts parented by a VSCode window: 7
tausik-project launcher processes:            6
distinct parent PIDs of those launchers:      6
parents are claude.exe: claude,claude,claude,claude,claude,claude

Шесть серверов, шесть РАЗНЫХ родителей, каждый — свой claude.exe. Общего процесса нет. Седьмое окно своего сервера не подняло: у него другой проект, и это согласуется с моделью, а не противоречит ей.

Командные строки живых серверов (фрагмент):

PID=30780 PPID=32228 ./.tausik/venv/Scripts/python.exe ./.claude/mcp/project/server.py --project .
PID=20792 PPID=28760 ./.tausik/venv/Scripts/python.exe ./.claude/mcp/project/server.py --project .
PID=26784 PPID=47372 &lt;OTHER-PROJECT&gt;/.tausik/venv/Scripts/python.exe
                     &lt;OTHER-PROJECT&gt;/.mcp-custom/jira/server.py
                     --project &lt;OTHER-PROJECT&gt;

Последняя строка — отдельный факт: ОДИН хост держит серверы, привязанные к РАЗНЫМ проектам, один относительным путём, другой абсолютным. Мульти-проектность существует на уровне ХОСТА, а не внутри процесса сервера.

Вывод по AC-2: multi-tenant кэш не нужен. Процесс обслуживает один корень, потому что хост поднимает его на окно и пинит --project при спавне. Сервер уже делает os.chdir(args.project) и держит одну ProjectService; менять тут нечего.

Побочно: stateless-ядро играет на руку ​

Новая спека убрала initialize-хендшейк (SEP-2575) и заголовок Mcp-Session-Id (SEP-2567). Для будущего HTTP-сервера это значит, что любой запрос может прийти в любой инстанс, — и тогда параметр тула становится единственным механизмом из трёх, который переживёт отсутствие процесса-на-проект. Сегодня этого не требуется: транспорт stdio, модель — процесс на окно.

Границы этой пробы ​

  • Хостовая сторона параметра тула и resource URI не проверена end-to-end. Проба говорит, что протокол это умеет и что хост УМЕЕТ ресурсы; она не говорит, что Claude Code передаст корень воркспейса сам. Для этого нужен новый сервер в конфиге и перезапуск хоста.
  • session.list_roots() не вызывался. По той же причине. Записан dead_end, чтобы следующий подход не считал молчание ответом.
  • В MCP-треде проекта ноль обращений к депрекируемому API — совпадает с замером l26-mcp-deprecation-audit (19 файлов, три сервера, 0 строк протокола).

Что делать дальше ​

  1. Ничего срочного: резолв через конфиг работает, гарантия SEP-2577 — не менее 12 месяцев.
  2. Документацию поправить по факту: резолв держится на cwd, а CLAUDE_PROJECT_DIR в процесс MCP не приходит. Строка ${CLAUDE_PROJECT_DIR:-.} работает своим запасным вариантом.
  3. Параметр проекта в инструментах заводить ТОЛЬКО под HTTP-транспорт, когда процесс перестанет быть привязан к окну. Сегодня он добавил бы 147 схем и ни одного нового ответа.