Pular para o conteúdo principal
O arquivo mcp_servers.json define quais servidores MCP o ChatCLI deve conectar. Ele é carregado automaticamente no início da sessão.

Localização

LocalPrioridadeDescrição
~/.chatcli/mcp_servers.jsonPadrãoConfiguração global do usuário
$CHATCLI_MCP_CONFIGOverrideCaminho customizado via variável de ambiente
Flag --mcp-configMaiorOverride via flag do servidor
No modo client (TTY), basta criar o arquivo em ~/.chatcli/mcp_servers.json — o ChatCLI detecta automaticamente e inicializa os servidores sem precisar de variáveis de ambiente.

Auto-Enable (regras de detecção)

O ChatCLI inicializa o subsistema MCP automaticamente — manager + watcher de hot-reload — quando qualquer uma das condições abaixo é verdadeira:
CondiçãoComportamento
CHATCLI_MCP_ENABLED=trueOpt-in explícito; sempre habilita, mesmo se nada existir em disco
O arquivo mcp_servers.json existeHabilita e carrega imediatamente
O diretório pai existe (ex: ~/.chatcli/)Habilita o manager + watcher mesmo sem o arquivo, para que criar o arquivo depois dispare hot-reload sem reiniciar
A terceira regra é o que mantém o hot-reload funcional desde o primeiro uso. Se você abre o ChatCLI antes de ter um mcp_servers.json (cenário comum em primeira instalação), o watcher do ~/.chatcli/ já está rodando — basta criar o arquivo e os servidores sobem na hora, sem chatcli restart.

Hot-Reload e Tolerância a Erros

O watcher do mcp_servers.json reconcilia o estado vivo com o que está em disco usando fsnotify. Eventos de Create, Write, Rename e Remove são debounced em 400 ms para evitar reload mid-write quando editores reescrevem via rename.
Evento em discoResultado
Adicionar servidor novoInicia o servidor, descobre tools
Remover servidor da configPara o processo, drop das tools
Mudar command/args/envPara e reinicia com nova config
enabled: falseEquivale a remover (servidor para)
Arquivo deletadoPara todos os servidores
0 bytes ou JSON malformadoLoga warning, não aborta; corrija e salve para recarregar
Salvar um JSON inválido não derruba o ChatCLI: o manager continua registrado e o watcher segue ativo. Você corrige o arquivo no editor, salva, e o próximo evento dispara Reload normalmente. Útil quando você está editando ao vivo durante uma sessão.

Formato

{
  "mcpServers": [
    {
      // --- Core (sempre necessário) ---
      "name": "string",                // Nome único do servidor
      "transport": "stdio|sse|http",   // Tipo de transporte
      "command": "string",             // Comando para iniciar (stdio)
      "args": ["string"],              // Argumentos do comando (stdio)
      "env": {                         // Variáveis de ambiente (stdio)
        "KEY": "VALUE"
      },
      "url": "string",                 // URL do servidor (sse / http)
      "enabled": true,                 // Ativa/desativa sem remover
      "overrides": ["string"],         // Built-ins que este server substitui

      // --- Tier 1: campos tipados com efeito direto em runtime ---
      "description": "string",         // Mostrado em /mcp status sob o servidor
      "cwd": "string",                 // Working directory do processo (stdio)
      "autoApprove": ["string"],       // Tools que dispensam aprovação (audit-logged)
      "alwaysAllow": ["string"],       // Alias de autoApprove (Cline-compat)
      "disabledTools": ["string"],     // Tools escondidas do LLM
      "timeout": 60,                   // Timeout por RPC, em segundos

      // --- Tier 2: capacidades adicionais ---
      "initTimeout": 10,               // Timeout do handshake initialize, em segundos
      "headers": {                     // Headers HTTP custom (sse / http)
        "X-Foo": "${MY_TOKEN}"
      },
      "auth": {                        // Autenticação HTTP (bearer / basic / header)
        "type": "bearer",
        "token": "${MY_TOKEN}"
      },
      "enabledTools": ["string"], // Allowlist; precede disabledTools
      "tags": ["string"],         // Marcadores rendered em /mcp status
      "category": "string",       // Classificação curta (ex: "aws", "io")
      "trust": false,             // Auto-aprova TUDO (bypass total — usar com cautela)
      "channels": ["string"]      // Allow-list de MCP push-channels (vazio = aceita tudo)

      // Qualquer outra chave é preservada via round-trip mas ignorada por runtime
    }
  ]
}

Campos

Core (obrigatórios)

CampoTipoObrigatórioDescrição
namestringIdentificador único. Aparece em logs e no prefixo [MCP:<name>] das tools
transportstring"stdio" (local), "sse" (HTTP+SSE — dois endpoints) ou "http" (Streamable HTTP da spec 2025-03-26 — endpoint único)
commandstringstdio ✅Comando para iniciar o processo MCP
argsstring[]Argumentos passados ao comando
envobjectVariáveis de ambiente do processo (com ${VAR} expansion)
urlstringsse / http ✅URL completa do endpoint MCP. Para http, a barra final é significativahttps://srv/mcp/ e https://srv/mcp são paths diferentes; o transport preserva exatamente o que está no JSON
enabledboolPadrão false. Permite desabilitar sem remover da config
overridesstring[]Plugins built-in que este server substitui. Conectado, os built-ins listados ficam escondidos do LLM; ao desconectar voltam automaticamente. Ex: ["@webfetch", "@websearch"]

Tier 1 — efeito direto em runtime

CampoTipoDescrição
descriptionstringMostrado em /mcp status sob a linha do servidor. Útil quando o nome do server é cifrado (prod-1, srv-a)
cwdstringWorking directory do processo (exec.Cmd.Dir). Aceita ${VAR} e leading ~/. Validado no spawn — caminho inexistente ou non-directory falha a conexão com erro acionável (ignorado em SSE)
autoApprovestring[]Lista de tool names que dispensam aprovação. Aceita "*" (toda tool do server). Hoje gera info-level audit log em cada chamada auto-aprovada; o helper já está pronto pro futuro gate de MCP no coder-mode. Veja Auto-aprovação
alwaysAllowstring[]Alias de autoApprove adotado pelo Cline. As duas listas se fundem num único set runtime — copy-paste de configs do Cline funciona sem renomear
disabledToolsstring[]Tool names escondidas tanto de /mcp tools quanto do prompt do LLM. Economia direta de tokens quando o servidor expõe 30+ tools mas o workflow só usa um punhado
timeoutint (segundos)Timeout por chamada RPC. Padrão 60. Cobre o pior caso de npx -y <pkg> cold-start

Tier 2 — capacidades adicionais

CampoTipoDescrição
initTimeoutint (segundos)Timeout do handshake MCP initialize e do endpoint-event SSE. Padrão 10. Útil pra servidores que carregam credenciais ou modelos no startup
headersobjectHeaders HTTP extra anexados a toda request HTTP do transport (SSE: GET do stream + POST das mensagens; http: cada POST JSON-RPC). Values passam por ${VAR} expansion. Ignorado em stdio
authobjectAutenticação para os transports HTTP (sse e http). Veja Autenticação HTTP. type aceita "bearer", "basic", "header". Empty type desabilita auth mesmo com outros campos preenchidos
enabledToolsstring[]Allowlist — quando não-vazio, apenas essas tools são expostas. Tem precedência sobre disabledTools
tagsstring[]Marcadores curtos rendered como #tag1 #tag2 no /mcp status
categorystringClassificação de um termo (ex: "aws", "database") rendered como [category] no /mcp status
trustboolAuto-aprova toda tool do server, sem consultar autoApprove. Emite warn-level no startup pra deixar a escolha visível em logs. Reserve para servers separadamente vetados
channelsstring[]Allow-list de MCP push-channels entregues pelo server. Vazio/omitido = aceita qualquer channel; com lista, só os channels literais nela passam pelo filtro (match exato — sem glob). "*" explícito = aceita tudo. Filtra no momento da recepção (antes do ring/persistência). Veja MCP Channels

Tier 3 — Catch-all (Extensions)

Qualquer chave JSON que não seja um dos campos acima é preservada verbatim quando o chatcli regrava o arquivo. Útil pra colar configs de outros clientes (AWS EKS MCP, Cline, Cursor) sem perder anotações vendor-specific. Importante:
  • As chaves extra são ignoradas pelo runtime do chatcli — só sobrevivem ao round-trip de save/load.
  • Não podem shadow um campo tipado: um JSON manual com Extensions["command"] nunca substitui o command real.
  • Quando promovermos uma chave (e.g. oauth2) pra campo tipado em uma release futura, o conteúdo já existente em Extensions migra automaticamente.

Variáveis de Ambiente (env)

O campo env aceita um objeto chave-valor que é mesclado com o ambiente do processo pai (os.Environ()). Duas regras importantes:

1. Herança do PATH e caches

O processo MCP herda todo o ambiente do ChatCLI antes de aplicar os valores de env. Sem essa herança, launchers como npx, uvx, docker e pipx não encontrariam seus binários nem seus caches por usuário.
{
  "name": "filesystem",
  "transport": "stdio",
  "command": "npx",
  "args": ["-y", "@anthropic/mcp-server-filesystem", "/workspace"],
  "env": {
    "FS_LOG_LEVEL": "debug"
  }
}
Aqui o servidor filho recebe PATH + HOME + FS_LOG_LEVEL=debug — não apenas FS_LOG_LEVEL isolado. Se a chave do env colidir com o ambiente herdado, o valor do env ganha (semântica do Unix: última atribuição no cmd.Env prevalece).

2. Expansão ${VAR} / $VAR

Valores em env passam por os.Expand contra o ambiente pai antes de ir pro processo filho. Isso permite manter secrets fora do JSON — em variáveis de shell, ou em arquivos .env carregados via source/direnv/mise/asdf.
{
  "name": "github",
  "transport": "stdio",
  "command": "npx",
  "args": ["-y", "@anthropic/mcp-server-github"],
  "env": {
    "GITHUB_TOKEN": "${GH_TOKEN}",
    "DEBUG": "$NODE_DEBUG"
  }
}
Se o shell tem GH_TOKEN=ghp_xxx exportado, o servidor MCP recebe GITHUB_TOKEN=ghp_xxx. Se a variável referenciada não existe, o resultado é string vazia — mesmo comportamento do shell. Faça lint do seu shell antes de assumir que o token chegou.
Use sempre ${VAR} em vez de literais ("GITHUB_TOKEN": "ghp_xxx"). Tokens em plaintext no JSON acabam em git history, em logs, em backups e em screenshots. A expansão ${VAR} é a forma idiomática — paridade com Claude Desktop e OpenCode.
A expansão acontece uma vez no spawn do processo. Se você mudar GH_TOKEN no shell durante uma sessão, o servidor MCP que já estava rodando continua com o valor antigo. Use /mcp restart <nome> para forçar a re-resolução.

Padrões recomendados

Tipo de secretOnde colocarComo referenciar no JSON
Token de API (GitHub, Slack, Stripe)Shell env (~/.zshenv) ou .env via direnv"TOKEN": "${GH_TOKEN}"
Connection stringShell env"DATABASE_URL": "${DATABASE_URL}"
Senha de DBVault / 1Password CLI / passInject no shell antes do ChatCLI
Credencial não sensível (ex: project ID)Direto no JSON"GCP_PROJECT": "my-project-id"

Auto-aprovação e Trust

ChatCLI’s tool-execution pipeline tem um audit-log que registra toda invocação MCP coberta por autoApprove, alwaysAllow ou trust. O helper Manager.ShouldAutoApprove(toolName) consultado pelo pipeline já está pronto pro futuro gate de aprovação interativa no coder mode.

autoApprove / alwaysAllow

Listas de tool names que dispensam aprovação. Aceitam "*" (qualquer tool do server). Os nomes podem ser usados com ou sem o prefixo mcp_"read_file" e "mcp_read_file" casam.
{
  "name": "filesystem",
  "transport": "stdio",
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
  "enabled": true,
  "autoApprove": ["read_file", "list_directory"]
}
alwaysAllow é alias popularizado pelo Cline — chatcli funde as duas listas no mesmo set runtime, então copy-paste de configs do Cline funciona sem renomear.

trust: true

Auto-aprova toda tool do server, sem precisar listar uma por uma:
{
  "name": "my-trusted-tools",
  "transport": "stdio",
  "command": "/usr/local/bin/my-mcp",
  "enabled": true,
  "trust": true
}
trust: true é destinado a servidores que o operador separadamente verificou (código auditado, container imutável, etc.). ChatCLI emite um warning no startup toda vez que conecta com trust: true pra que a escolha fique visível em logs — uma config trust commitada por engano não passa silenciosa.

Audit log

Toda chamada auto-aprovada gera um entry info-level:
MCP tool auto-approved by config  tool=read_file coder_mode=true
Grep no log do chatcli (CHATCLI_LOG_LEVEL=info ou maior) para auditar tudo que passou pelo bypass.

Filtragem de tools

Quando um servidor expõe dezenas de tools mas o workflow só usa um subconjunto, esconder o resto economiza tokens em todo turno do LLM.

disabledTools (blocklist)

{
  "name": "filesystem",
  "transport": "stdio",
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
  "enabled": true,
  "disabledTools": ["delete_file", "move_file"]
}
Aceita "*" (esconde tudo — útil pra desabilitar um server sem remover o entry).

enabledTools (allowlist — precede blocklist)

{
  "name": "filesystem",
  "transport": "stdio",
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
  "enabled": true,
  "enabledTools": ["read_file", "list_directory"]
}
Quando não-vazio, apenas as tools listadas são expostas. disabledTools no mesmo server é ignorado. /mcp status mostra a contagem de tools escondidas (blocklist) ou conta total quando a allowlist está ativa.

Timeouts

Padrões cobrem o caso comum (npx -y cold start). Override quando o servidor demora mais que isso por desenho.

timeout (segundos)

Cap por chamada RPC. Padrão 60s. Aplica-se tanto a tools/call quanto a tools/list.
{
  "name": "slow-llm-proxy",
  "transport": "sse",
  "url": "https://proxy.internal/sse",
  "enabled": true,
  "timeout": 180
}

initTimeout (segundos)

Cap do handshake MCP initialize — e, em SSE, do endpoint-event wait. Padrão 10s. Bump pra servidores que carregam credenciais, fazem auth-handshake ou montam o ambiente no startup:
{
  "name": "self-hosted-bedrock",
  "transport": "sse",
  "url": "https://bedrock-proxy.internal/sse",
  "enabled": true,
  "initTimeout": 60
}
SSE: o http.Client.Timeout é setado para max(timeout, initTimeout) pra que um timeout curto não mate o GET de SSE (que fica aberto pelo tempo da conexão).

Working directory (cwd) — stdio

{
  "name": "project-fs",
  "transport": "stdio",
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-filesystem"],
  "enabled": true,
  "cwd": "${HOME}/repos/my-project"
}
  • Aceita ${VAR} / $VAR expansion (mesmo lookup do env).
  • Aceita leading ~/ expandido contra HOME.
  • Validado no spawn: caminho inexistente ou non-directory faz a conexão falhar com erro acionável em vez de silenciosamente herdar o CWD do chatcli.
  • Vazio = herda o CWD do chatcli (comportamento legado).
Ignorado em SSE.

Headers HTTP e autenticação — sse / http

Os dois transports HTTP (sse e http) compartilham o mesmo mecanismo de headers customizados e autenticação tipada.

headers

Headers extra anexados a toda request HTTP do transport:
  • sse: GET do stream + POST de mensagens JSON-RPC.
  • http: cada POST JSON-RPC ao endpoint Streamable HTTP.
{
  "name": "internal-mcp",
  "transport": "http",
  "url": "https://mcp.internal/mcp/",
  "enabled": true,
  "headers": {
    "X-Tenant-Id": "acme",
    "X-Trace-Id": "${TRACE_ID}"
  }
}
Values passam por ${VAR} expansion (mesmo lookup do env).

Autenticação HTTP (auth)

Bloco tipado com três modos:

bearer

{
  "auth": {
    "type": "bearer",
    "token": "${MY_TOKEN}"
  }
}
Resulta em Authorization: Bearer <token>.

basic

{
  "auth": {
    "type": "basic",
    "username": "${MY_USER}",
    "password": "${MY_PASS}"
  }
}
Resulta em Authorization: Basic base64(user:pass).

header (custom)

{
  "auth": {
    "type": "header",
    "header": "X-API-Key",
    "token": "${MY_TOKEN}"
  }
}
Resulta em X-API-Key: <token>. Quando header é omitido, o default é X-API-Key.
Salvaguardas: empty type é no-op mesmo com token/username populados (evita meio-config vazando credencial). Token cuja env var não existe suprime o header inteiro em vez de mandar Authorization: Bearer vazio (que passaria por um auth check ingênuo no server).
Authorization/headers customizados são re-aplicados a cada request, então rotacionar a env var em runtime já produz a próxima chamada com o token novo — sem /mcp restart.

Metadados (description, tags, category)

Campos cosméticos rendered em /mcp status pra facilitar identificar quem é quem quando há muitos servers:
{
  "name": "prod-aws",
  "transport": "sse",
  "url": "https://prod-aws-mcp.internal/sse",
  "enabled": true,
  "description": "Read-only AWS account access via EKS MCP",
  "category": "aws",
  "tags": ["prod", "readonly"]
}
Cada linha de output é opt-in: configs sem nenhum desses campos renderizam exatamente como antes da v1.115.

Channel subscriptions (channels)

Quando um servidor MCP emite push notifications (JSON-RPC messages sem id), o ChatCLI as encaminha pro MCP Channels ring — uma estrutura durável que alimenta o system prompt, o banner de inbox e o trigger engine. O campo channels na config do servidor é um allow-list de channel names: só os channels literalmente listados são aceitos para esse servidor. Útil quando um servidor é tagarela em múltiplas categorias (ex: emite tanto ci-pipeline quanto deploys/staging quanto metrics/raw) mas seu workflow só se importa com uma fatia.
{
  "name": "prom-alerts",
  "transport": "sse",
  "url": "https://prom-alerts.internal/sse",
  "enabled": true,
  "channels": ["alerts/critical", "alerts/error"]
}
ConfiguraçãoComportamento
Omitido ou []Aceita todos os channels que o server emitir
["alerts/critical"]alerts/critical é aceito; alerts/info ou metrics/* são descartados (debug log)
["*"] (explícito)Equivale a omitido — passa tudo
[" alerts ", ""]Whitespace é trimmado, strings vazias ignoradas; equivale a ["alerts"]
Match é literal (não há glob aqui). "channels": ["alerts"] não pega "alerts/critical". Use o nome exato do channel que o servidor emite. Os globs (alerts/*) existem só nas rules de trigger (triggers.json), que rodam depois desse filtro.
O filtro acontece na entrada — antes do ring em memória e antes da persistência. Channels rejeitados não consomem o budget do ring nem aparecem em /channel list. Funciona com os três transports: sse (via stream), http (via listener GET) e stdio (via notifications no stdout). Veja MCP Channels pra detalhes do que acontece depois do filtro.

Trigger rules (~/.chatcli/mcp/triggers.json)

Arquivo separado do mcp_servers.json, opt-in, que descreve como o ChatCLI deve reagir a channel events. Sem esse arquivo, channels funcionam só como inbox passivo. Com ele, você define rules que disparam banners, perguntas yes/no ou execuções automáticas do agent.

Localização

~/.chatcli/mcp/triggers.json
Carregado automaticamente no startup quando o MCP está habilitado. Reload manual: /channel rules reload.

Schema

{
  "rules": [
    {
      "name": "string",            // único, obrigatório
      "server": "string",          // match exato no nome do server; vazio = qualquer
      "channel": "string",         // literal, "*" ou prefix-glob ("alerts/*")
      "contentRegex": "string",    // Go regexp aplicado em Content
      "mode": "notify|confirm|auto",
      "prompt": "string",          // template com {{content}} {{channel}} {{server}} {{seq}} {{timestamp}}
      "tools": ["string"],         // whitelist (obrigatório quando mode=auto)
      "rateLimit": "duration",     // formato Go: "5m", "30s", "1h"
      "dedupWindow": "duration"
    }
  ]
}

Campos

CampoTipoObrigatórioDescrição
namestringIdentificador único da rule (aparece em /channel rules e em logs)
serverstringMatch exato no nome do servidor MCP. Vazio = qualquer; "*" = qualquer (explícito)
channelstringMatch no channel da mensagem. Suporta literal ("ci-pipeline"), wildcard ("*") e prefix-glob ("alerts/*" pega alerts/critical, alerts/info, etc.)
contentRegexstringRegex Go aplicada sobre o Content da notification. Validada no parse — regex inválido rejeita o arquivo inteiro
modestring"notify" (default), "confirm", "auto". Case-sensitive
promptstringobrigatório em confirm e autoTemplate do prompt enviado ao agent quando a rule dispara. Vars: {{content}}, {{channel}}, {{server}}, {{seq}}, {{timestamp}}. Quando vazio, ChatCLI usa um default "Investigate this <server>/<channel> event: <content>" (válido apenas em notify)
toolsstring[]obrigatório em autoWhitelist de tools que o agent pode invocar. Em mode: auto, validação rejeita rule sem tools — proteção contra trigger autônomo sem floor de tools
rateLimitdurationCap por rule: após uma fire, ignora matches dentro dessa janela. Formato time.Duration do Go: "5m", "30s", "1h". Vazio = sem limite
dedupWindowdurationDedup por (rule, content prefix): mesma rule + mesmo prefixo (até 256 chars) dentro da janela é no-op. Vazio = sem dedup

Modos — resumo

ModoQuando ação ocorreSurpresaCaso de uso
notifyNunca — só registra no bannerZeroInbox passivo; user investiga sob demanda
confirmQuando user responde /channel confirm <id>BaixaUser-in-the-loop; alerta importante que merece confirmação humana
autoNo próximo prompt tick (drained automaticamente)Alta — opt-in explícitoTasks padronizadas e seguras (smoke checks, sanity checks)

Validação

Falhas no parse rejeitam o arquivo inteiro (atomic apply — ou todas as rules entram, ou nada muda). Em reload com erro, o ChatCLI mantém as rules anteriores ativas e mostra o erro na saída. Erros típicos:
ErroCausaFix
rule #N (""): rule name is requiredFalta name ou está vazioToda rule precisa de um name único
mode "auto" requires a non-empty tools whitelistmode: "auto" sem toolsAdicione "tools": [...] ou troque pra confirm
mode "confirm" requires a prompt templatemode: "confirm" sem promptAdicione "prompt": "..."
invalid contentRegex: error parsing regexpRegex inválidoUse https://regex101.com flavor “Golang” pra depurar
invalid mode "Notify"Case-sensitiveUse notify/confirm/auto em lowercase
duplicate rule name "x"Dois entries com mesmo nameNames únicos por arquivo
invalid rateLimit "5min"Formato Go inválidoUse 5m, 30s, 1h, 500ms

Exemplos prontos

CI watcher passivo

{
  "rules": [
    {
      "name": "ci-failures",
      "server": "ci-monitor",
      "channel": "ci-pipeline",
      "contentRegex": "(?i)fail|broken|red",
      "mode": "notify",
      "rateLimit": "30s",
      "dedupWindow": "1m"
    }
  ]
}

Prod alerts com confirmação

{
  "rules": [
    {
      "name": "prod-pages",
      "server": "prom-alerts",
      "channel": "alerts/critical",
      "mode": "confirm",
      "prompt": "Critical alert on prod:\n\n{{content}}\n\nInvestigate?",
      "rateLimit": "5m",
      "dedupWindow": "2m"
    }
  ]
}

Canary deploys autônomos

{
  "rules": [
    {
      "name": "canary-sanity-check",
      "server": "deploy-tracker",
      "channel": "deploys/canary/*",
      "mode": "auto",
      "prompt": "Canary deploy started: {{content}}.\nRun canary smoke checks and report.",
      "tools": ["kubectl_get", "kubectl_logs", "http_request"],
      "rateLimit": "1m",
      "dedupWindow": "30s"
    }
  ]
}

Persistência do ring

Separado das rules, o ChatCLI mantém um arquivo JSONL durável em ~/.chatcli/mcp/channels.jsonl com todas as mensagens recebidas. Rotaciona aos 10 MiB pra channels.jsonl.1 (um backup). Replay automático no boot (até 200 últimas mensagens). Detalhes completos: MCP Channels — Persistência.

Exemplos por Transporte

Servidores locais que se comunicam via stdin/stdout com JSON-RPC 2.0:
{
  "mcpServers": [
    {
      "name": "filesystem",
      "transport": "stdio",
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-server-filesystem", "/home/user/projects"],
      "enabled": true
    }
  ]
}
O ChatCLI gerencia o ciclo de vida do processo: inicia no startup, mata no shutdown. Content-Length framing (LSP-style) é usado na comunicação.

Testando com curl

Útil pra isolar problemas de proxy/CA/handshake antes de envolver o ChatCLI. O wire é o mesmo que o transport http envia:
URL='https://seu-servidor/mcp/'   # mantenha a barra final exatamente como sua config

# 1) Handshake initialize — capture o Mcp-Session-Id do response
curl -sS -i -N "$URL" \
  -H 'Content-Type: application/json' \
  -H 'Accept: text/event-stream, application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
        "protocolVersion":"2024-11-05","capabilities":{},
        "clientInfo":{"name":"curl-test","version":"1"}}}'

# 2) Reuse a sessão no tools/list (substitua <SID>)
curl -sS -i -N "$URL" \
  -H 'Content-Type: application/json' \
  -H 'Accept: text/event-stream, application/json' \
  -H 'Mcp-Session-Id: <SID>' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
Flags importantes:
  • -i mostra response headers (pro Mcp-Session-Id).
  • -N desativa buffering — se o servidor responder SSE, você vê os eventos chegando em tempo real em vez de esperar fechar.
Se o curl funciona mas o ChatCLI dá timeout de 10s, é quase sempre proxy corporativo não exportado (Go respeita HTTPS_PROXY/HTTP_PROXY/NO_PROXY no shell — curl lê ~/.curlrc e o Go não) ou CA interna ausente do trust store que o Go enxerga (SSL_CERT_FILE=/path/corp-ca.pem resolve).

Exemplos Completos

Múltiplos Servidores

{
  "mcpServers": [
    {
      "name": "filesystem",
      "transport": "stdio",
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-server-filesystem", "/workspace"],
      "enabled": true
    },
    {
      "name": "github",
      "transport": "stdio",
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-server-github"],
      "env": {
        "GITHUB_TOKEN": "${GH_TOKEN}"
      },
      "enabled": true
    },
    {
      "name": "postgres",
      "transport": "stdio",
      "command": "/usr/local/bin/mcp-postgres",
      "args": ["--connection-string", "${POSTGRES_DSN}"],
      "env": {
        "PGPASSWORD": "${POSTGRES_PASSWORD}"
      },
      "enabled": true
    },
    {
      "name": "web-search",
      "transport": "sse",
      "url": "http://localhost:8080/sse",
      "enabled": true,
      "overrides": ["@webfetch", "@websearch"]
    },
    {
      "name": "deepwiki",
      "transport": "http",
      "url": "https://mcp.deepwiki.com/mcp/",
      "enabled": true,
      "auth": {
        "type": "bearer",
        "token": "${DEEPWIKI_TOKEN}"
      }
    },
    {
      "name": "slack",
      "transport": "stdio",
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-server-slack"],
      "env": {
        "SLACK_BOT_TOKEN": "${SLACK_BOT_TOKEN}"
      },
      "enabled": false
    },
    {
      "name": "prom-alerts",
      "transport": "sse",
      "url": "https://prom-alerts.internal/sse",
      "enabled": true,
      "channels": ["alerts/critical", "alerts/error"],
      "auth": {
        "type": "bearer",
        "token": "${PROM_ALERTS_TOKEN}"
      },
      "description": "Prometheus AlertManager bridge",
      "category": "observability",
      "tags": ["prod"]
    }
  ]
}
Nunca commite tokens ou senhas no arquivo. Use a sintaxe ${VAR} (documentada acima em Variáveis de Ambiente) para que o secret venha do shell — nunca do JSON.

Servidor Desabilitado

Defina "enabled": false para manter a configuração sem conectar:
{
  "name": "experimental",
  "transport": "stdio",
  "command": "my-experimental-mcp",
  "enabled": false
}

Protocolo

O ChatCLI fala MCP Protocol v2024-11-05 (anunciado no initialize) sobre três transports:
  • stdio — JSON-RPC 2.0 via stdin/stdout do processo filho (newline-delimited).
  • sse — modelo HTTP+SSE da spec original (GET /sse + POST /messages).
  • http — Streamable HTTP da revisão 2025-03-26 (POST único ao endpoint configurado).
Métodos suportados:
MétodoDireçãoDescrição
initializeClient → ServerHandshake inicial com versão e capabilities
notifications/initializedClient → ServerConfirma inicialização
tools/listClient → ServerDescobre ferramentas disponíveis
tools/callClient → ServerExecuta uma ferramenta com argumentos

Nomeação de Tools

Nome original no servidor:  read_file
Nome no ChatCLI:           mcp_read_file
Descrição:                 [MCP:filesystem] Reads a file from the filesystem
O prefixo mcp_ é adicionado automaticamente para evitar colisões com ferramentas nativas (plugins, @coder, @webfetch, etc.).

Servidores MCP Populares

ServidorDescriçãoInstalação
@anthropic/mcp-server-filesystemAcesso ao sistema de arquivosnpx -y @anthropic/mcp-server-filesystem /path
@anthropic/mcp-server-githubIntegração com GitHubnpx -y @anthropic/mcp-server-github
@anthropic/mcp-server-postgresQueries PostgreSQLnpx -y @anthropic/mcp-server-postgres
@anthropic/mcp-server-slackIntegração com Slacknpx -y @anthropic/mcp-server-slack
@anthropic/mcp-server-brave-searchBusca web via Bravenpx -y @anthropic/mcp-server-brave-search
@anthropic/mcp-server-memoryMemória persistentenpx -y @anthropic/mcp-server-memory
Consulte modelcontextprotocol.io para a lista completa de servidores MCP.

Troubleshooting

Verifique:
  1. O comando existe e está no PATH (which npx)
  2. O arquivo de config está em ~/.chatcli/mcp_servers.json
  3. enabled está true
  4. Use /mcp status para ver erros de conexão
  5. Use /mcp logs <nome> para ver as últimas linhas de stderr do servidor (npm 404, panic, missing executable)
  6. Verifique logs com CHATCLI_LOG_LEVEL=debug
A causa quase sempre é uma das duas:
  1. Variável não exportada no shell${GH_TOKEN} na config expande para vazio se GH_TOKEN não está em os.Environ(). Confirme com echo $GH_TOKEN antes de iniciar o ChatCLI.
  2. Mudou o token mid-sessão — a expansão acontece no spawn. Após exportar a variável nova, rode /mcp restart <nome> para forçar o re-spawn com o ambiente atualizado.
Para inspecionar o que o servidor recebeu, abra /mcp logs <nome> — servidores MCP costumam logar “missing token” / 401 em stderr.
A partir das fixes de hot-reload, o watcher é iniciado quando o diretório pai existe — mesmo sem o arquivo. Se você nunca criou ~/.chatcli/, não há watcher.Solução: crie o diretório uma vez (mkdir -p ~/.chatcli) e reinicie o ChatCLI. Daí em diante, criar/editar mcp_servers.json dispara reload automaticamente.Para forçar um reload manual a qualquer momento: /mcp reload.
Não. Quando o LoadConfig falha (0 bytes, JSON inválido), o ChatCLI loga um warning mas mantém o manager + watcher rodando. Você corrige o JSON no editor, salva, e o próximo evento dispara Reload normalmente — sem reiniciar a sessão.
  1. O servidor pode não suportar tools/list
  2. Use /mcp tools para listar tools descobertas
  3. Reinicie com /mcp restart
O timeout padrão é 60s por chamada RPC (cobre cold-start de npx -y). Se um servidor específico demora mais:
  1. Suba o timeout (segundos) só pra esse server na config — veja Timeouts.
  2. Se a falha é no handshake (não na chamada), bumpe initTimeout separadamente.
  3. Para servers de longa duração (proxies LLM, agentes lentos), prefira SSE — o stream fica aberto enquanto o server processa.
Verifique:
  1. autoApprove ou alwaysAllow no config tem o nome dela (ou "*") — veja Auto-aprovação.
  2. trust: true no server faz bypass total. Toda chamada auto-aprovada gera log info MCP tool auto-approved by config tool=<nome>. Grep no log do chatcli pra auditar.
  3. Para remover o bypass, edite o JSON (hot-reload reaplica) ou rode /mcp reload.
  1. Confira que a env var referenciada por auth.token/headers está exportada no shell. ${UNSET_VAR} expande para string vazia e ChatCLI suprime o header inteiro em vez de mandar Authorization: Bearer (vazio) — então o server reporta 401 como se nem auth tivesse vindo.
  2. Rotacionou o token no shell? Headers são re-aplicados a cada request, não precisa de /mcp restart. Mas confirme que o shell que iniciou o ChatCLI tem a nova variável (export, source, etc.).
  3. Para type: "header" com nome custom, confirme que o server espera exatamente o nome configurado (ChatCLI envia literalmente o que está em auth.header).
Erro retornado por servidores Streamable HTTP que checam o primeiro media type do Accept como preferência do cliente (FastMCP, alguns gateways MCP). ChatCLI envia Accept: text/event-stream, application/json exatamente nessa ordem desde a primeira release com transport: "http" — então esse erro só aparece se você sobrescrever Accept via headers na config. Remova a entrada Accept dos headers e o transport assume o padrão.
Quase sempre é uma de três causas (em ordem de probabilidade):
  1. Proxy corporativo não exportado. Curl lê ~/.curlrc (com proxy=...), Go não. Exporte HTTPS_PROXY, HTTP_PROXY e NO_PROXY no shell que roda o ChatCLI — nosso http.Client usa http.DefaultTransport que respeita essas três variáveis nativamente.
  2. Barra final faltando na url. Servidores FastMCP / Starlette / FastAPI 307-redirecionam /mcp para /mcp/. O Go segue o 307 com POST, mas proxies no caminho frequentemente quebram o redirect (body ou headers descartados). Configure url com a barra exatamente como o servidor responde 200 OK no curl.
  3. CA corporativa fora do trust store que o Go enxerga. Em macOS especialmente, CAs adicionadas via mobile-config às vezes não são pegadas. Exporte SSL_CERT_FILE=/etc/ssl/certs/corp-ca.pem apontando pra mesma bundle que o curl usa.
Pra distinguir entre essas causas, bumpe initTimeout para 60 ou 120 na config — se passa de “hang em 10s” para “responde em 30s”, é proxy lento; se continua hangando, é (1) ou (2).
Provavelmente foi escondida por config:
  1. disabledTools lista o nome dela — remova ou inverta para enabledTools.
  2. enabledTools está populado mas a tool não está na allowlist — adicione ou esvazie enabledTools para voltar ao default “tudo visível”.
  3. /mcp status mostra a contagem de tools escondidas (blocklist).
Verifique:
  1. O nome no overrides está exatamente como o nome do plugin, incluindo o @ (ex: "@webfetch", não "webfetch")
  2. O servidor MCP está connected (/mcp status)
  3. O campo enabled do server está true
  4. Se o server caiu e reconectou, o override volta automaticamente no próximo turno de conversa
O campo overrides apenas esconde o built-in — não valida se o MCP server realmente oferece uma ferramenta equivalente. Se você colocar "overrides": ["@webfetch"] mas o MCP server não tem web_fetch, o LLM simplesmente não terá essa capacidade. Remova o override ou adicione a ferramenta ao MCP server.
Remova o built-in da lista overrides do servidor MCP. Ambas as ferramentas (MCP e built-in) ficarão visíveis simultaneamente e o LLM escolherá baseado no nome e descrição de cada uma.