Спайк: как 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": "<PROJECT>" <- РАВЕН корню проекта,
"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": "<PROJECT>" <- РАВЕН корню проекта,
"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 <OTHER-PROJECT>/.tausik/venv/Scripts/python.exe
<OTHER-PROJECT>/.mcp-custom/jira/server.py
--project <OTHER-PROJECT>Последняя строка — отдельный факт: ОДИН хост держит серверы, привязанные к РАЗНЫМ проектам, один относительным путём, другой абсолютным. Мульти-проектность существует на уровне ХОСТА, а не внутри процесса сервера.
Вывод по 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 строк протокола).
Что делать дальше
- Ничего срочного: резолв через конфиг работает, гарантия SEP-2577 — не менее 12 месяцев.
- Документацию поправить по факту: резолв держится на cwd, а
CLAUDE_PROJECT_DIRв процесс MCP не приходит. Строка${CLAUDE_PROJECT_DIR:-.}работает своим запасным вариантом. - Параметр проекта в инструментах заводить ТОЛЬКО под HTTP-транспорт, когда процесс перестанет быть привязан к окну. Сегодня он добавил бы 147 схем и ни одного нового ответа.