Start Debugging

cowork-terminal-mcp: доступ к терминалу хоста для Claude Cowork в одном MCP-сервере

cowork-terminal-mcp v0.4.1 связывает изолированную ВМ Claude Cowork с шеллом вашего хоста. Один инструмент, транспорт stdio, жёстко зафиксированный Git Bash на Windows.

Claude Cowork запускается внутри изолированной Linux-ВМ на вашей машине. Именно эта изоляция делает комфортным запуск Cowork в фоновом режиме без присмотра, но она же означает, что агент не может самостоятельно установить зависимости вашего проекта, выполнить сборку или сделать push коммита в репозиторий на хосте. Без моста агент останавливается на границе файловой системы ВМ. cowork-terminal-mcp v0.4.1 как раз и есть такой мост: узкоспециализированный MCP-сервер, который работает на хосте, предоставляет один инструмент (execute_command) и на этом останавливается. Всё это около 200 строк TypeScript, поставляется в npm как cowork-terminal-mcp.

Единственный инструмент, который предоставляет сервер

execute_command — это вся поверхность сервера. Его Zod-схема находится в src/tools/execute-command.ts и принимает четыре параметра:

ПараметрТипЗначение по умолчаниюОписание
commandstringобязательныйКоманда bash для выполнения
cwdstringдомашний каталогРабочий каталог (предпочтительнее, чем cd <path> &&)
timeoutnumber30000 мсСколько ждать до прерывания выполнения
envRecord<string, string>унаследованныеДополнительные переменные окружения поверх process.env

Возвращает JSON-объект с полями stdout, stderr, exitCode и timedOut. Вывод ограничен 1MB на поток, при достижении лимита добавляется суффикс [stdout truncated at 1MB] (или stderr).

Почему один инструмент? Потому что любой запрос вида «покажи список файлов», «запусти тесты» или «что говорит git status» сводится к команде шелла. Второй инструмент стал бы лишь чуть более тонкой обёрткой над тем же spawn. Каталог MCP остаётся компактным, модель не выбирает не тот инструмент, а поверхность атаки на хост остаётся тривиальной для аудита.

Подключение к Claude Cowork

Claude Cowork читает MCP-серверы из конфигурации Claude Desktop и пробрасывает их в свою изолированную ВМ. Файл конфигурации находится в одном из трёх мест:

Минимальная конфигурация:

{
  "mcpServers": {
    "cowork-terminal": {
      "command": "npx",
      "args": ["-y", "cowork-terminal-mcp"]
    }
  }
}

В Windows оберните команду в cmd /c, чтобы npx корректно разрешался (Claude Desktop запускает команды через PowerShell-совместимую обвязку, которая не всегда находит npm-shim’ы):

{
  "mcpServers": {
    "cowork-terminal": {
      "command": "cmd",
      "args": ["/c", "npx", "-y", "cowork-terminal-mcp"]
    }
  }
}

Для пользователей Claude Code CLI тот же сервер служит ещё и запасным выходом к терминалу хоста и регистрируется одной строкой:

claude mcp add cowork-terminal -- npx -y cowork-terminal-mcp

Единственное требование — bash. На macOS и Linux достаточно системного шелла. На Windows должен быть установлен Git for Windows, и сервер придерживается определённой позиции относительно того, какой bash.exe он готов принять, — это и есть следующий интересный момент.

Ловушка Git Bash на Windows

spawn("bash") на Windows выглядит безобидно и почти всегда даёт неверный результат. Порядок PATH в Windows ставит C:\Windows\System32 ближе к началу, и System32\bash.exe присутствует на большинстве современных установок Windows. Это не Git Bash, а лаунчер WSL. Когда MCP-сервер передаёт ему команду, она выполняется внутри Linux-ВМ, которая не видит файловую систему Windows так, как её видит хост, не может прочитать PATH Windows и не может выполнять .exe-файлы Windows. Видимый симптом получается забавный: dotnet --version возвращает «command not found», хотя SDK .NET явно установлен и присутствует в PATH. То же самое с node, npm, git и каждой нативной для Windows утилитой, к которой обращается агент.

cowork-terminal-mcp исправляет это на старте. resolveBashPath() на Windows полностью пропускает поиск по PATH и проходит фиксированный список мест установки Git Bash:

const candidates = [
  path.join(programFiles, "Git", "bin", "bash.exe"),
  path.join(programFiles, "Git", "usr", "bin", "bash.exe"),
  path.join(programFilesX86, "Git", "bin", "bash.exe"),
  path.join(programFilesX86, "Git", "usr", "bin", "bash.exe"),
  localAppData && path.join(localAppData, "Programs", "Git", "bin", "bash.exe"),
  localAppData && path.join(localAppData, "Programs", "Git", "usr", "bin", "bash.exe"),
];

Побеждает первый кандидат, который подтверждает existsSync, и именно с этим разрешённым абсолютным путём вызывается spawn. Если ни один не найден, сервер на этапе загрузки модуля бросает исключение с сообщением, в котором перечислены все проверенные пути и указана ссылка https://git-scm.com/download/win. Никакого фолбэка на bash из System32 и никакой тихой деградации.

Более широкий вывод: на Windows «доверять PATH» — выстрел в ногу всякий раз, когда важно поведение конкретного бинарника. Разрешайте по абсолютному пути или громко падайте. Эта правка вышла именно в v0.4.1, потому что пользователи наблюдали, как агент настаивает на отсутствии dotnet на машинах, где тот был очевидно установлен.

Тайм-ауты, ограничения вывода и правило одного шелла

В исполнителе встречаются ещё три решения, и все они продуманные.

AbortController вместо тайм-аута на уровне шелла. Когда команда превышает свой timeout, сервер не оборачивает вызов bash в timeout 30s .... Он вызывает abortController.abort(), что Node.js преобразует в завершение процесса. Дочерний процесс генерирует событие error, у которого name равен AbortError; обработчик очищает таймер, и инструмент резолвится с exitCode: null и timedOut: true:

const timer = setTimeout(() => {
  abortController.abort();
}, options.timeout);

child.on("error", (error) => {
  clearTimeout(timer);
  if (error.name === "AbortError") {
    resolve({ stdout, stderr, exitCode: null, timedOut: true });
  } else {
    reject(error);
  }
});

Так механика тайм-аута остаётся вне строки команды пользователя и работает одинаково на Windows и Unix.

Лимит 1MB на поток, встроенный. stdout и stderr накапливаются в строках JavaScript, но каждое событие data проверяется условием length < MAX_OUTPUT_SIZE (1 048 576 байт). При достижении лимита дополнительные данные отбрасываются и устанавливается флаг. Итоговая строка результата получает суффикс [stdout truncated at 1MB]. Это цена буферизации вместо стриминга: модель получает чистый структурированный результат, но tail -f some.log — не та задача, для которой создан этот сервер. Типичный npm test или dotnet build помещается без проблем.

Шелл — это bash, и точка. В v0.3.0 был параметр shell, позволявший модели выбирать cmd на Windows. v0.4.0 его удалила. Причина зарыта в CHANGELOG: правила двойных кавычек cmd.exe молча обрезают многострочные строки на первом переводе строки, поэтому тела heredoc, которые модель отправляла через cmd, схлопывались до первой строки. Модель полагала, что команда отработала с тем телом, которое она составила; bash на другой стороне был с этим не согласен. Убрать выбор оказалось дешевле, чем учить модель всегда выбирать bash. По той же причине описание инструмента (в src/tools/execute-command.ts) активно подталкивает модель к heredoc’ам:

gh pr create --title "My PR" --body "$(cat <<'EOF'
## Summary

- First item
- Second item
EOF
)"

Символы \n в JSON-строке command декодируются в настоящие переводы строк до того, как их увидит bash, а дальше всё делает heredoc-семантика самого bash.

Без PTY, по дизайну

Дочерний процесс запускается с stdio: ["ignore", "pipe", "pipe"], без псевдотерминала. Нет способа подключиться к работающему prompt, нет сигнализации ширины терминала, нет согласования цвета по умолчанию. Для команд сборки, установки пакетов, git и запуска тестов этого вполне достаточно; модель получает чистый вывод без ANSI-escape-последовательностей в качестве шума. Для vim, top, lldb или любого REPL, который ожидает интерактивный TTY, этот инструмент не подходит. Сервер и не пытается его имитировать.

Такой компромисс выбран сознательно. MCP-сервер на основе PTY потребовал бы стриминга, протокола частичного вывода и интерактивной семантики ввода-вывода, которую сам MCP сейчас плохо моделирует. cowork-terminal-mcp остаётся в той области, где разовая команда действительно укладывается в протокол.

Когда этот мост — правильный

cowork-terminal-mcp мал намеренно. Один инструмент, только stdio, громко падающее разрешение bash, продуманные ограничения вывода, без выбора шелла, без PTY. Если вы запускаете Claude Cowork на Windows и хотите, чтобы он действительно мог что-то выполнять на хосте, это и есть мост, благодаря которому граница sandbox перестаёт мешать. Если вы уже пользуетесь Claude Code CLI, это дешёвая дополнительная возможность, которую полезно держать зарегистрированной на тот день, когда какой-то workflow выйдет за пределы встроенного инструмента Bash модели. Исходный код и issues — на github.com/marius-bughiu/cowork-terminal-mcp; пакет в npm — cowork-terminal-mcp.

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад