MCP-сервер и ловушка рабочей директории: когда `.env` и audit-log «уезжают» из репозитория

Соло-разработчик, который подключает свой MCP-сервер к desktop-клиенту или агенту, часто задаёт в конфиге только command и args — без поля cwd. Процесс стартует с непредсказуемой рабочей директории, и «голые» относительные пути в server.py начинают резолвиться мимо репозитория: credentials из .env не подхватываются, audit-log пишется в чужую папку — без единой ошибки в логе. В разборе кейса автор MCP-сервера показывает, как один такой баг он уже закрыл — и почему точечный фикс не спасает соседнюю строку с тем же антипаттерном.
Почему cwd критичен для MCP как subprocess
Протокол MCP рассчитан на то, что клиент поднимает сервер отдельным процессом. В типичном сценарии — тот же, что описан для Claude Desktop в key_facts.md репозитория автора — в JSON попадают имя команды и аргументы, а рабочая директория subprocess не задаётся явно.
Отсюда следствие для агентного стека: любой путь вида ".env" или "logs/article_updates.jsonl" привязан не к файлу сервера, а к тому, откуда клиент или оболочка запустили Python. Перезапуск desktop-приложения, другая вкладка терминала, контейнер — и один и тот же MCP-tool начинает читать и писать в разные каталоги.
Исключений процесс не бросает: open() и os.makedirs(..., exist_ok=True) отрабатывают «успешно», просто не там, где вы ждёте.
Для соло-разработчика с собственным MCP-сервером это не косметика: агент вызывает tools, а побочные эффекты — загрузка секретов, журнал аудита — тихо расходятся по диску.
Первый фикс: .env через __file__
В функции load_env() автор заменил литерал ".env" на путь относительно расположения server.py:
os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
Причина сбоя была ровно в CWD: без привязки к __file__ файл окружения искался от текущей директории процесса, а MCP-клиент её не контролирует. После патча credentials читаются из корня репозитория независимо от того, откуда стартовал subprocess.
Это базовый приём для любой MCP-обвязки на Python: если сервер — отдельный файл в репо, пути к конфигам и данным должны якориться на __file__, а не на надежду, что клиент запустит вас из нужной папки.
Второй путь: audit-log tool update_article
Примерно на девятнадцать строк ниже того же исправления в server.py оставался второй литерал:
_ARTICLE_UPDATE_LOG = "logs/article_updates.jsonl"
Его использует _log_article_update() — запись в журнал перед или после перезаписи статьи через MCP-tool update_article. Задача лога — оставить след, если агент или скрипт передал неверный article_id или «галлюцинированное» значение: не затереть live-пост молча.
Тот же класс бага: при open(..., "a") файл создаётся относительно текущего cwd. Автор воспроизвёл это без полного MCP-стека — в sandbox не было пакета mcp, поэтому тело _log_article_update гоняли standalone с os.chdir() во временную папку после tempfile.mkdtemp().
С неисправленным литералом лог появлялся в scratch/logs/, а не в logs/ репозитория. Два реальных вызова из разных cwd писали бы в две несвязанные директории.
Паттерн исправления — тот же, что для .env:
_ARTICLE_UPDATE_LOG = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "logs", "article_updates.jsonl"
)
После os.chdir(scratch) лог снова попадает в logs/ проекта. Заголовок исходного поста отражал момент обнаружения («второй путь остался неисправленным»); в тексте автор доводит оба антипаттерна до одного решения.
Урок для vibe coding: не останавливаться на одной строке
Разброс путей по рабочей директории обнуляет смысл журнала аудита: цель — громче сигналить об ошибке записи, а при голых относительных путях сбой тише — файлы уезжают, исключений нет.
Автор формулирует практический вывод для своего репозитория my-git-manager:
- исправление одной строки не заставляет искать тот же паттерн в остальном файле, даже если он виден рядом;
- после любого бага с путями в MCP-обвязке — пройти файл на другие голые строковые литералы как пути ФС;
- зафиксировать правило в
docs/project_notes/bugs.md(автор планирует занести туда grep-чеклист).
Для разработчика, который собирает MCP-tools под агента, это чеклист на один вечер: открыть server.py, найти все строковые литералы-пути, перевести их на os.path.join(dirname(abspath(__file__)), …), перезапустить клиент из другой директории и убедиться, что .env и логи остаются в репо.
Полный перечень MCP-tools в посте не раскрыт — верифицированы update_article и контекст audit-log. Упоминаний Cursor или VS Code в источнике нет; риск универсален для любого клиента, который стартует сервер subprocess без cwd.
Источники
- Пост @enjoy_kumawat на Dev.to (2 августа 2026): My MCP Server Fixed a CWD-Relative Path Bug Once. A Second Hardcoded Relative Path Sat Two Functions Below It, Unfixed. — дата доступа: 2026-08-02 UTC.