Start Debugging

cowork-terminal-mcp: acesso ao terminal do host para Claude Cowork em um único servidor MCP

cowork-terminal-mcp v0.4.1 conecta a VM isolada do Claude Cowork à shell do seu host. Uma ferramenta, transporte stdio, Git Bash fixado por caminho absoluto no Windows.

Claude Cowork roda dentro de uma VM Linux isolada na sua máquina. Esse isolamento é o que torna confortável deixar o Cowork rodando sem supervisão, mas também significa que o agente não consegue instalar as dependências do seu projeto, compilar seu código ou fazer push de um commit no repositório do host por conta própria. Sem uma ponte, o agente para no limite do sistema de arquivos da VM. cowork-terminal-mcp v0.4.1 é essa ponte: um servidor MCP de propósito único que roda no host, expõe uma única ferramenta (execute_command) e nada mais. No total são cerca de 200 linhas de TypeScript e ele é distribuído no npm como cowork-terminal-mcp.

A única ferramenta que o servidor expõe

execute_command é toda a superfície. Seu schema Zod fica em src/tools/execute-command.ts e aceita quatro parâmetros:

ParâmetroTipoPadrãoDescrição
commandstringobrigatórioO comando bash a ser executado
cwdstringdiretório homeDiretório de trabalho (prefira-o em vez de cd <path> &&)
timeoutnumber30000 msPor quanto tempo esperar antes de abortar a execução
envRecord<string, string>herdadoVariáveis de ambiente extras sobrepostas a process.env

Retorna um objeto JSON com stdout, stderr, exitCode e timedOut. A saída é limitada a 1MB por stream, com um sufixo [stdout truncated at 1MB] (ou stderr) quando o limite é atingido.

Por que uma única ferramenta? Porque toda solicitação de “liste os arquivos”, “rode os testes” ou “o que diz o git status” se reduz a um comando de shell. Uma segunda ferramenta seria apenas um wrapper mais fino sobre o mesmo spawn. O catálogo MCP fica pequeno, o modelo não escolhe a ferramenta errada e a superfície de ataque do host fica trivial de auditar.

Como conectar ao Claude Cowork

Claude Cowork lê servidores MCP da configuração do Claude Desktop e os encaminha para sua VM isolada. O arquivo de configuração fica em um de três lugares:

A configuração mínima:

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

No Windows, envolva o comando em cmd /c para que npx resolva corretamente (Claude Desktop dispara comandos por meio de uma camada compatível com PowerShell que nem sempre encontra os shims do npm):

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

Para usuários do Claude Code CLI, o mesmo servidor também funciona como uma rota de fuga até o terminal do host e é registrado em uma linha:

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

O único pré-requisito é o bash. No macOS e no Linux a shell do sistema basta. No Windows é preciso ter o Git for Windows instalado, e o servidor é opinativo sobre qual bash.exe ele aceita, que é o próximo ponto interessante.

A armadilha do Git Bash no Windows

spawn("bash") no Windows parece inocente e quase sempre está errado. A ordem do PATH do Windows coloca C:\Windows\System32 perto do início, e System32\bash.exe existe na maioria das instalações modernas do Windows. Esse binário é o launcher do WSL. Quando o servidor MCP entrega um comando para ele, o comando roda dentro de uma VM Linux que não enxerga o sistema de arquivos do Windows como o host enxerga, não consegue ler o PATH do Windows e não consegue executar arquivos .exe do Windows. O sintoma visível é curioso: dotnet --version retorna “command not found” mesmo com o SDK do .NET claramente instalado e no PATH. O mesmo vale para node, npm, git e cada ferramenta nativa do Windows que o agente tenta usar.

cowork-terminal-mcp resolve isso na inicialização. resolveBashPath() ignora completamente a busca no PATH no Windows e percorre uma lista fixa de locais de instalação do 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"),
];

O primeiro candidato que existsSync confirmar vence, e o caminho absoluto resolvido é o que vai para spawn. Se nenhum existir, o servidor lança uma exceção no carregamento do módulo com um erro que lista cada caminho verificado e aponta para https://git-scm.com/download/win. Não há fallback para o bash do System32 e não há degradação silenciosa.

A lição mais ampla: no Windows, “confiar no PATH” é um tiro no pé sempre que o comportamento de um binário específico importa. Resolva por caminho absoluto ou falhe ruidosamente. A correção saiu na v0.4.1 explicitamente porque havia usuários vendo o agente insistir que dotnet estava faltando em máquinas onde claramente estava instalado.

Timeouts, limites de saída e a regra de uma única shell

No executor aparecem mais três escolhas, todas deliberadas.

AbortController em vez de um timeout de shell. Quando um comando excede seu timeout, o servidor não envolve a invocação do bash em timeout 30s .... Ele chama abortController.abort(), o que o Node.js traduz em matar o processo. O filho emite um evento error cujo name é AbortError, o handler limpa o timer e a ferramenta resolve com exitCode: null e 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);
  }
});

Isso mantém a maquinaria do timeout fora da string de comando do usuário e se comporta de forma idêntica no Windows e no Unix.

Limite de 1MB, por stream, embutido. stdout e stderr são acumulados em strings do JavaScript, mas cada evento data é condicionado a length < MAX_OUTPUT_SIZE (1.048.576 bytes). Quando o limite é atingido, dados adicionais são descartados e uma flag é ativada. A string de resultado final ganha o sufixo [stdout truncated at 1MB]. Esse é o custo de fazer buffer em vez de streaming: o modelo recebe um resultado estruturado e limpo, mas tail -f some.log não é um caso de uso para o qual esse servidor foi feito. Um npm test ou dotnet build típico cabe tranquilamente.

A shell é bash, ponto final. v0.3.0 tinha um parâmetro shell que deixava o modelo escolher cmd no Windows. v0.4.0 removeu. A razão está enterrada no CHANGELOG: as regras de aspas duplas do cmd.exe truncam silenciosamente strings multilinha na primeira quebra de linha, então os corpos de heredoc que o modelo enviava através do cmd colapsavam para a primeira linha. O modelo achava que o comando havia rodado com o corpo que ele construiu; o bash do outro lado discordava. Remover a opção saiu mais barato do que ensinar o modelo a sempre escolher bash. Também é por isso que a descrição da ferramenta (em src/tools/execute-command.ts) empurra ativamente o modelo para usar heredocs:

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

- First item
- Second item
EOF
)"

Os caracteres \n na string command do JSON são decodificados em quebras de linha reais antes do bash vê-las, e a semântica de heredoc do bash dá conta do resto.

Sem PTY, por design

O processo filho é disparado com stdio: ["ignore", "pipe", "pipe"], sem pseudo-terminal. Não há como anexar a um prompt em execução, não há sinalização de largura de terminal, não há negociação de cor por padrão. Para comandos de build, instalação de pacotes, git e execução de testes, isso está ótimo; o modelo recebe uma saída limpa sem escapes ANSI atrapalhando. Para vim, top, lldb ou qualquer REPL que espere uma TTY interativa, essa é a ferramenta errada. O servidor não tenta fingir uma.

Essa concessão é deliberada. Um servidor MCP baseado em PTY precisaria de streaming, de um protocolo de saída parcial e de semântica de E/S interativa que o próprio MCP ainda não modela bem. cowork-terminal-mcp permanece dentro do limite onde a execução de comandos one-shot realmente encaixa no protocolo.

Quando essa é a ponte certa

cowork-terminal-mcp é pequeno de propósito. Uma ferramenta, só stdio, resolução de bash que falha alto, limites de saída deliberados, sem escolha de shell, sem PTY. Se você roda Claude Cowork no Windows e quer que ele de fato execute coisas no host, essa é a ponte que faz o limite do sandbox parar de incomodar. Se você já usa o Claude Code CLI, é um recurso extra barato de manter registrado para o dia em que um workflow precisar sair da ferramenta Bash embutida do modelo. O código-fonte e as issues estão em github.com/marius-bughiu/cowork-terminal-mcp; o pacote está no npm em cowork-terminal-mcp.

Comments

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

< Voltar