Skip to main content
The mcp_servers.json file defines which MCP servers ChatCLI should connect to. It is loaded automatically at session startup.

Location

LocationPriorityDescription
~/.chatcli/mcp_servers.jsonDefaultUser’s global configuration
$CHATCLI_MCP_CONFIGOverrideCustom path via environment variable
--mcp-config flagHighestOverride via server flag
In client mode (TTY), simply create the file at ~/.chatcli/mcp_servers.json — ChatCLI auto-detects and initializes servers without needing environment variables.

Auto-Enable (detection rules)

ChatCLI initializes the MCP subsystem — manager + hot-reload watcher — automatically when any of the conditions below holds:
ConditionBehavior
CHATCLI_MCP_ENABLED=trueExplicit opt-in; always enables, even with nothing on disk
mcp_servers.json file existsEnables and loads immediately
Parent directory exists (e.g. ~/.chatcli/)Enables manager + watcher even without the file, so creating the file later triggers hot-reload without restart
The third rule keeps hot-reload working from first use. If you open ChatCLI before having an mcp_servers.json (typical first-install scenario), the watcher on ~/.chatcli/ is already running — just create the file and servers come up on the spot, no chatcli restart needed.

Hot-Reload and Error Tolerance

The mcp_servers.json watcher reconciles the live state with what’s on disk via fsnotify. Create, Write, Rename and Remove events are debounced at 400 ms to avoid mid-write reloads when editors rewrite via rename.
Event on diskResult
Add new serverStarts the server, discovers tools
Remove server from configStops the process, drops its tools
Change command/args/envStops and restarts with the new config
enabled: falseSame as removal (server stops)
File deletedAll servers stop
0 bytes or malformed JSONLogs a warning, does not abort; fix and save to reload
Saving an invalid JSON does not crash ChatCLI: the manager stays registered and the watcher keeps running. Fix the file in your editor, save it, and the next event triggers Reload normally. Useful when editing live during a session.

Format

{
  "mcpServers": [
    {
      // --- Core (always required) ---
      "name": "string",                // Unique server name
      "transport": "stdio|sse|http",   // Transport type
      "command": "string",             // Command to start (stdio)
      "args": ["string"],              // Command arguments (stdio)
      "env": {                         // Environment variables (stdio)
        "KEY": "VALUE"
      },
      "url": "string",                 // Server URL (sse / http)
      "enabled": true,                 // Enable/disable without removing
      "overrides": ["string"],         // Built-ins this server replaces

      // --- Tier 1: typed fields with direct runtime effect ---
      "description": "string",         // Shown under the server in /mcp status
      "cwd": "string",                 // Working directory for stdio process
      "autoApprove": ["string"],       // Tools that bypass the approval gate (audit-logged)
      "alwaysAllow": ["string"],       // Alias of autoApprove (Cline compat)
      "disabledTools": ["string"],     // Tools hidden from the LLM
      "timeout": 60,                   // Per-RPC timeout, in seconds

      // --- Tier 2: additional capabilities ---
      "initTimeout": 10,               // Timeout for the initialize handshake, in seconds
      "headers": {                     // Custom HTTP headers (sse / http)
        "X-Foo": "${MY_TOKEN}"
      },
      "auth": {                        // HTTP authentication (bearer / basic / header)
        "type": "bearer",
        "token": "${MY_TOKEN}"
      },
      "enabledTools": ["string"], // Allowlist; takes precedence over disabledTools
      "tags": ["string"],         // Markers rendered in /mcp status
      "category": "string",       // Short classification (e.g. "aws", "io")
      "trust": false,             // Auto-approves EVERY tool (full bypass — use with care)
      "channels": ["string"]      // Allow-list of MCP push-channels (empty = accept all)

      // Any other key is round-trip preserved but ignored at runtime
    }
  ]
}

Fields

Core (required)

FieldTypeRequiredDescription
namestringUnique identifier. Used in logs and the [MCP:<name>] tool prefix
transportstring"stdio" (local), "sse" (HTTP+SSE — two endpoints) or "http" (Streamable HTTP per the 2025-03-26 spec — single endpoint)
commandstringstdio ✅Command to start the MCP process
argsstring[]Arguments passed to the command
envobjectProcess environment variables (with ${VAR} expansion)
urlstringsse / http ✅Full URL of the MCP endpoint. For http, the trailing slash is significanthttps://srv/mcp/ and https://srv/mcp are different paths; the transport preserves exactly what is in the JSON
enabledboolDefault false. Disable without removing from config
overridesstring[]Built-in plugins this server replaces. Connected, the listed built-ins are hidden from the LLM; on disconnect, they come back automatically. E.g. ["@webfetch", "@websearch"]

Tier 1 — direct runtime effect

FieldTypeDescription
descriptionstringShown in /mcp status under the server line. Useful when server names are cryptic (prod-1, srv-a)
cwdstringWorking directory for the process (exec.Cmd.Dir). Accepts ${VAR} and leading ~/. Validated at spawn — a missing path or non-directory fails the connection with an actionable error (ignored for SSE)
autoApprovestring[]Tool names that bypass the approval gate. "*" matches every tool from this server. Today produces an info-level audit log on every auto-approved call; the helper is in place for the future MCP approval gate in coder mode. See Auto-approval
alwaysAllowstring[]Alias of autoApprove adopted by Cline. The two lists are merged into a single runtime set — copy-pasting a Cline config works without renaming
disabledToolsstring[]Tool names hidden from both /mcp tools and the LLM prompt. Direct token savings when a server exposes 30+ tools but the workflow only needs a handful
timeoutint (seconds)Per-call RPC timeout. Default 60. Covers the worst case of a npx -y <pkg> cold start

Tier 2 — additional capabilities

FieldTypeDescription
initTimeoutint (seconds)Timeout for the MCP initialize handshake and the SSE endpoint-event wait. Default 10. Useful for servers that bootstrap credentials or models on startup
headersobjectExtra HTTP headers attached to every HTTP request the transport makes (SSE: stream GET + message POSTs; http: each JSON-RPC POST). Values go through ${VAR} expansion. Ignored for stdio
authobjectAuthentication for the HTTP transports (sse and http). See HTTP Authentication. type accepts "bearer", "basic", "header". Empty type disables auth even when other fields are populated
enabledToolsstring[]Allowlist — when non-empty, only the listed tools are exposed. Takes precedence over disabledTools
tagsstring[]Short markers rendered as #tag1 #tag2 in /mcp status
categorystringSingle-word classification (e.g. "aws", "database") rendered as [category] in /mcp status
trustboolAuto-approves every tool from this server, no need to consult autoApprove. Emits a warn-level log at startup to keep the choice visible. Reserved for separately-vetted servers
channelsstring[]Allow-list of MCP push-channels delivered by the server. Empty/omitted = accept any channel; with a list, only literally matched channels pass the filter (exact match — no globs). Explicit "*" = accept all. Filtering happens at receive time (before the ring/persistence). See MCP Channels

Tier 3 — Catch-all (Extensions)

Any JSON key that isn’t one of the fields above is preserved verbatim when chatcli rewrites the file. Useful for pasting configs from other clients (AWS EKS MCP, Cline, Cursor) without losing vendor-specific annotations. Important:
  • Unknown keys are ignored by the chatcli runtime — they only survive the save/load round-trip.
  • They cannot shadow a typed field: a hand-edited JSON with Extensions["command"] will never override the real command.
  • When a key is promoted to a typed field in a future release (e.g. oauth2), the content already in Extensions migrates automatically.

Environment Variables (env)

The env field accepts a key-value object that is merged with the parent process environment (os.Environ()). Two important rules:

1. PATH and cache inheritance

The MCP process inherits ChatCLI’s full environment before the env overrides apply. Without that inheritance, launchers like npx, uvx, docker and pipx wouldn’t find their binaries or per-user caches.
{
  "name": "filesystem",
  "transport": "stdio",
  "command": "npx",
  "args": ["-y", "@anthropic/mcp-server-filesystem", "/workspace"],
  "env": {
    "FS_LOG_LEVEL": "debug"
  }
}
The child server receives PATH + HOME + FS_LOG_LEVEL=debug — not FS_LOG_LEVEL in isolation. If a key in env collides with the inherited environment, the env value wins (Unix semantics: last assignment in cmd.Env takes precedence).

2. ${VAR} / $VAR expansion

Values in env go through os.Expand against the parent environment before reaching the child. This lets you keep secrets out of the JSON — in shell variables, or in .env files loaded 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"
  }
}
If the shell has GH_TOKEN=ghp_xxx exported, the MCP server receives GITHUB_TOKEN=ghp_xxx. If the referenced variable doesn’t exist, the result is the empty string — same behavior as the shell. Lint your shell before assuming the token made it through.
Always prefer ${VAR} over literal values ("GITHUB_TOKEN": "ghp_xxx"). Plaintext tokens in JSON end up in git history, logs, backups and screenshots. The ${VAR} expansion is the idiomatic form — parity with Claude Desktop and OpenCode.
Expansion happens once at process spawn. If you change GH_TOKEN in the shell during a session, an MCP server already running keeps the old value. Use /mcp restart <name> to force re-resolution.
Secret typeWhere to put itHow to reference in JSON
API token (GitHub, Slack, Stripe)Shell env (~/.zshenv) or .env via direnv"TOKEN": "${GH_TOKEN}"
Connection stringShell env"DATABASE_URL": "${DATABASE_URL}"
DB passwordVault / 1Password CLI / passInject into shell before ChatCLI
Non-sensitive credential (e.g. project ID)Direct in JSON"GCP_PROJECT": "my-project-id"

Auto-approval and Trust

ChatCLI’s tool-execution pipeline emits an audit log for every MCP invocation that autoApprove, alwaysAllow or trust covers. The Manager.ShouldAutoApprove(toolName) helper consulted by the pipeline is already in place for the future interactive approval gate on the coder-mode MCP path.

autoApprove / alwaysAllow

Lists of tool names that bypass the approval gate. Accept "*" (any tool on the server). Names can be used with or without the mcp_ prefix — "read_file" and "mcp_read_file" both match.
{
  "name": "filesystem",
  "transport": "stdio",
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
  "enabled": true,
  "autoApprove": ["read_file", "list_directory"]
}
alwaysAllow is an alias popularized by Cline — chatcli folds the two lists into the same runtime set, so copy-pasting a Cline config works without renaming.

trust: true

Auto-approves every tool on the server, no need to list them one by one:
{
  "name": "my-trusted-tools",
  "transport": "stdio",
  "command": "/usr/local/bin/my-mcp",
  "enabled": true,
  "trust": true
}
trust: true is intended for servers the operator has separately vetted (audited code, immutable container, etc.). ChatCLI emits a warning at startup every time it connects with trust: true so the choice is visible in logs — a trust config committed by accident never goes silent.

Audit log

Every auto-approved call produces an info-level entry:
MCP tool auto-approved by config  tool=read_file coder_mode=true
Grep the chatcli log (CHATCLI_LOG_LEVEL=info or higher) to audit everything that took the bypass.

Tool filtering

When a server exposes dozens of tools but the workflow only uses a subset, hiding the rest saves tokens on every LLM turn.

disabledTools (blocklist)

{
  "name": "filesystem",
  "transport": "stdio",
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
  "enabled": true,
  "disabledTools": ["delete_file", "move_file"]
}
Accepts "*" (hides everything — useful to mute a server without removing the entry).

enabledTools (allowlist — takes precedence)

{
  "name": "filesystem",
  "transport": "stdio",
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
  "enabled": true,
  "enabledTools": ["read_file", "list_directory"]
}
When non-empty, only the listed tools are exposed. disabledTools on the same server is ignored. /mcp status shows the count of hidden tools (blocklist) or the total tool count when the allowlist is active.

Timeouts

Defaults cover the common case (npx -y cold start). Override when a server takes longer by design.

timeout (seconds)

Cap per RPC call. Default 60s. Applies to both tools/call and tools/list.
{
  "name": "slow-llm-proxy",
  "transport": "sse",
  "url": "https://proxy.internal/sse",
  "enabled": true,
  "timeout": 180
}

initTimeout (seconds)

Cap on the MCP initialize handshake — and, in SSE, on the endpoint-event wait. Default 10s. Bump for servers that load credentials, perform an auth handshake, or set up the environment at startup:
{
  "name": "self-hosted-bedrock",
  "transport": "sse",
  "url": "https://bedrock-proxy.internal/sse",
  "enabled": true,
  "initTimeout": 60
}
SSE: the http.Client.Timeout is set to max(timeout, initTimeout) so a short timeout does not kill the SSE GET (which stays open for the lifetime of the connection).

Working directory (cwd) — stdio

{
  "name": "project-fs",
  "transport": "stdio",
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-filesystem"],
  "enabled": true,
  "cwd": "${HOME}/repos/my-project"
}
  • Accepts ${VAR} / $VAR expansion (same lookup as env).
  • Accepts leading ~/ expanded against HOME.
  • Validated at spawn: a missing path or non-directory fails the connection with an actionable error instead of silently inheriting chatcli’s CWD.
  • Empty = inherits chatcli’s CWD (legacy behavior).
Ignored for SSE.

HTTP headers and authentication — sse / http

Both HTTP transports (sse and http) share the same custom-header and typed-auth machinery.

headers

Extra headers attached to every HTTP request the transport makes:
  • sse: GET on the stream + POSTs of JSON-RPC messages.
  • http: every JSON-RPC POST to the Streamable HTTP endpoint.
{
  "name": "internal-mcp",
  "transport": "http",
  "url": "https://mcp.internal/mcp/",
  "enabled": true,
  "headers": {
    "X-Tenant-Id": "acme",
    "X-Trace-Id": "${TRACE_ID}"
  }
}
Values go through ${VAR} expansion (same lookup as env).

HTTP Authentication (auth)

Typed block with three modes:

bearer

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

basic

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

header (custom)

{
  "auth": {
    "type": "header",
    "header": "X-API-Key",
    "token": "${MY_TOKEN}"
  }
}
Produces X-API-Key: <token>. When header is omitted, the default is X-API-Key.
Safety rails: empty type is no-op even with token/username populated (so a half-completed config does not leak credentials). A token whose env var is undefined suppresses the header entirely instead of sending an empty Authorization: Bearer (which would pass a naive auth-presence check on the server).
Authorization/custom headers are re-applied on every request, so rotating the env var at runtime makes the next call carry the fresh token — no /mcp restart needed.

Metadata (description, tags, category)

Cosmetic fields rendered in /mcp status so it’s easy to tell servers apart when many are connected:
{
  "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"]
}
Each output line is opt-in: configs without any of these fields render exactly like before v1.115.

Channel subscriptions (channels)

When an MCP server emits push notifications (JSON-RPC messages without id), ChatCLI forwards them to the MCP Channels ring — a durable structure that feeds the system prompt, the inbox banner, and the trigger engine. The channels field on the server config is an allow-list of channel names: only literally listed channels are accepted for that server. Useful when a server is chatty across multiple categories (e.g. emits both ci-pipeline and deploys/staging and metrics/raw) but your workflow only cares about a slice.
{
  "name": "prom-alerts",
  "transport": "sse",
  "url": "https://prom-alerts.internal/sse",
  "enabled": true,
  "channels": ["alerts/critical", "alerts/error"]
}
ConfigurationBehavior
Omitted or []Accepts every channel the server emits
["alerts/critical"]Only alerts/critical is accepted; alerts/info or metrics/* are dropped (debug log)
["*"] (explicit)Equivalent to omitted — passes everything
[" alerts ", ""]Whitespace is trimmed, empty strings ignored; equivalent to ["alerts"]
Matching is literal (no glob support here). "channels": ["alerts"] does not match "alerts/critical". Use the exact channel name the server emits. Globs (alerts/*) only exist in trigger rules (triggers.json), which run after this filter.
Filtering happens at receive time — before the in-memory ring and before persistence. Rejected channels do not consume the ring budget and do not appear in /channel list. Works across all three transports: sse (via stream), http (via GET listener) and stdio (via notifications on stdout). See MCP Channels for what happens after the filter.

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

Separate file from mcp_servers.json, opt-in, describes how ChatCLI should react to channel events. Without this file, channels work only as a passive inbox. With it, you define rules that fire banners, yes/no prompts or autonomous agent runs.

Location

~/.chatcli/mcp/triggers.json
Auto-loaded on startup when MCP is enabled. Manual reload: /channel rules reload.

Schema

{
  "rules": [
    {
      "name": "string",            // unique, required
      "server": "string",          // exact match against server name; empty = any
      "channel": "string",         // literal, "*" or prefix-glob ("alerts/*")
      "contentRegex": "string",    // Go regexp applied to Content
      "mode": "notify|confirm|auto",
      "prompt": "string",          // template with {{content}} {{channel}} {{server}} {{seq}} {{timestamp}}
      "tools": ["string"],         // whitelist (required when mode=auto)
      "rateLimit": "duration",     // Go format: "5m", "30s", "1h"
      "dedupWindow": "duration"
    }
  ]
}

Fields

FieldTypeRequiredDescription
namestringUnique rule identifier (shows in /channel rules and in logs)
serverstringExact match against the MCP server name. Empty = any; "*" = any (explicit)
channelstringMatch on the message channel. Supports literal ("ci-pipeline"), wildcard ("*") and prefix-glob ("alerts/*" matches alerts/critical, alerts/info, etc.)
contentRegexstringGo regexp applied to the notification Content. Validated at parse — invalid regex rejects the entire file
modestring"notify" (default), "confirm", "auto". Case-sensitive
promptstringrequired for confirm and autoTemplate sent to the agent when the rule fires. Vars: {{content}}, {{channel}}, {{server}}, {{seq}}, {{timestamp}}. Empty → ChatCLI uses a default "Investigate this <server>/<channel> event: <content>" (valid only in notify)
toolsstring[]required for autoWhitelist of tools the agent can invoke. In mode: auto, validation rejects rules without tools — protection against an autonomous trigger with unfettered tool access
rateLimitdurationPer-rule cap: after a fire, ignore matches within this window. Go time.Duration format: "5m", "30s", "1h". Empty = no limit
dedupWindowdurationDedup by (rule, content prefix): same rule + same prefix (up to 256 chars) within the window becomes a no-op. Empty = no dedup

Modes — summary

ModeWhen action happensSurpriseUse case
notifyNever — registers in banner onlyZeroPassive inbox; user investigates on demand
confirmWhen user runs /channel confirm <id>LowUser-in-the-loop; important alert that deserves human confirmation
autoAt the next prompt tick (drained automatically)High — explicit opt-inStandardized and safe tasks (smoke checks, sanity checks)

Validation

Parse failures reject the entire file (atomic apply — either all rules land, or nothing changes). On reload with an error, ChatCLI keeps the previous rules active and shows the error in the output. Typical errors:
ErrorCauseFix
rule #N (""): rule name is requiredMissing or empty nameEvery rule needs a unique name
mode "auto" requires a non-empty tools whitelistmode: "auto" without toolsAdd "tools": [...] or switch to confirm
mode "confirm" requires a prompt templatemode: "confirm" without promptAdd "prompt": "..."
invalid contentRegex: error parsing regexpInvalid regexUse https://regex101.com flavor “Golang” to debug
invalid mode "Notify"Case-sensitiveUse lowercase: notify/confirm/auto
duplicate rule name "x"Two entries with the same nameNames must be unique per file
invalid rateLimit "5min"Invalid Go formatUse 5m, 30s, 1h, 500ms

Ready-to-use examples

Passive CI watcher

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

Prod alerts with confirmation

{
  "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"
    }
  ]
}

Autonomous canary deploys

{
  "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"
    }
  ]
}

Ring persistence

Separate from rules, ChatCLI keeps a durable JSONL file at ~/.chatcli/mcp/channels.jsonl with every received message. Rotates at 10 MiB to channels.jsonl.1 (one backup). Automatic replay on boot (last 200 messages). Full details: MCP Channels — Persistence.

Examples by Transport

Local servers communicating via stdin/stdout with JSON-RPC 2.0:
{
  "mcpServers": [
    {
      "name": "filesystem",
      "transport": "stdio",
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-server-filesystem", "/home/user/projects"],
      "enabled": true
    }
  ]
}
ChatCLI manages the process lifecycle: starts on init, kills on shutdown. Content-Length framing (LSP-style) is used for communication.

Testing with curl

Useful for isolating proxy / CA / handshake issues before involving ChatCLI. The wire shape is the same the http transport emits:
URL='https://your-server/mcp/'   # keep the trailing slash exactly as your config

# 1) initialize handshake — capture the Mcp-Session-Id from the 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 the session for tools/list (substitute <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"}'
Key flags:
  • -i prints response headers (for the Mcp-Session-Id).
  • -N disables buffering — if the server replies with SSE, you see events arriving in real time rather than waiting for the body to close.
If curl works but ChatCLI hangs for 10s, it’s almost always a corporate proxy that isn’t exported (Go honors HTTPS_PROXY/HTTP_PROXY/NO_PROXY from the shell — curl reads ~/.curlrc, Go does not) or an internal CA missing from the trust store Go sees (SSL_CERT_FILE=/path/corp-ca.pem resolves it).

Full Examples

Multiple Servers

{
  "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", "postgresql://localhost:5432/mydb"],
      "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"]
    }
  ]
}
Never commit tokens or passwords in the config file. Use the ${VAR} syntax (documented above in Environment Variables) so secrets come from the shell — never from the JSON.

Protocol

ChatCLI speaks MCP Protocol v2024-11-05 (advertised on initialize) over three transports:
  • stdio — JSON-RPC 2.0 over the child process’s stdin/stdout (newline-delimited).
  • sse — HTTP+SSE shape from the original spec (GET /sse + POST /messages).
  • http — Streamable HTTP from the 2025-03-26 spec revision (single POST to the configured endpoint).
Supported methods:
MethodDirectionDescription
initializeClient → ServerInitial handshake with version and capabilities
notifications/initializedClient → ServerConfirms initialization
tools/listClient → ServerDiscovers available tools
tools/callClient → ServerExecutes a tool with arguments

Tool Naming

Original name on server:  read_file
Name in ChatCLI:          mcp_read_file
Description:              [MCP:filesystem] Reads a file from the filesystem
The mcp_ prefix is added automatically to avoid collisions with native tools (plugins, @coder, @webfetch, etc.).

ServerDescriptionInstallation
@anthropic/mcp-server-filesystemFile system accessnpx -y @anthropic/mcp-server-filesystem /path
@anthropic/mcp-server-githubGitHub integrationnpx -y @anthropic/mcp-server-github
@anthropic/mcp-server-postgresPostgreSQL queriesnpx -y @anthropic/mcp-server-postgres
@anthropic/mcp-server-slackSlack integrationnpx -y @anthropic/mcp-server-slack
@anthropic/mcp-server-brave-searchWeb search via Bravenpx -y @anthropic/mcp-server-brave-search
@anthropic/mcp-server-memoryPersistent memorynpx -y @anthropic/mcp-server-memory
Check modelcontextprotocol.io for the complete list of available MCP servers.

Troubleshooting

Check:
  1. The command exists and is in PATH (which npx)
  2. Config file is at ~/.chatcli/mcp_servers.json
  3. enabled is true
  4. Use /mcp status to see connection errors
  5. Use /mcp logs <name> to see the server’s recent stderr (npm 404, panic, missing executable)
  6. Check logs with CHATCLI_LOG_LEVEL=debug
The cause is almost always one of two things:
  1. Variable not exported in the shell${GH_TOKEN} in the config expands to empty if GH_TOKEN isn’t in os.Environ(). Confirm with echo $GH_TOKEN before starting ChatCLI.
  2. Token rotated mid-session — expansion happens at spawn. After exporting the new variable, run /mcp restart <name> to force a re-spawn with the updated environment.
To inspect what the server actually received, open /mcp logs <name> — MCP servers usually log “missing token” / 401 to stderr.
With the hot-reload fixes, the watcher starts when the parent directory exists — even without the file. If you never created ~/.chatcli/, there is no watcher.Fix: create the directory once (mkdir -p ~/.chatcli) and restart ChatCLI. From then on, creating/editing mcp_servers.json triggers reload automatically.To force a manual reload at any time: /mcp reload.
No. When LoadConfig fails (0 bytes, invalid JSON), ChatCLI logs a warning but keeps the manager + watcher running. Fix the JSON in your editor, save it, and the next event triggers Reload normally — no session restart needed.
  1. The server may not support tools/list
  2. Use /mcp tools to list discovered tools
  3. Restart with /mcp restart
Default per-call timeout is 60s (covers npx -y cold start). If a specific server takes longer:
  1. Bump timeout (seconds) on that server’s config only — see Timeouts.
  2. If failure is during the handshake (not the call), bump initTimeout separately.
  3. For long-running servers (LLM proxies, slow agents), prefer SSE — the stream stays open while the server processes.
Check:
  1. autoApprove or alwaysAllow in the config lists the tool name (or "*") — see Auto-approval.
  2. trust: true on the server is a full bypass. Every auto-approved call emits an info-level log MCP tool auto-approved by config tool=<name>. Grep the chatcli log to audit.
  3. To remove the bypass, edit the JSON (hot-reload reapplies) or run /mcp reload.
  1. Confirm the env var referenced by auth.token/headers is exported in the shell. ${UNSET_VAR} expands to an empty string and ChatCLI suppresses the entire header instead of sending Authorization: Bearer (empty) — so the server reports 401 as if no auth was attempted.
  2. Rotated the token in the shell? Headers are re-applied on every request, no /mcp restart needed. But confirm the shell that started ChatCLI has the new variable (export, source, etc.).
  3. For type: "header" with a custom name, confirm the server expects exactly the configured name (ChatCLI sends literally what’s in auth.header).
Returned by Streamable HTTP servers that check the first media type in Accept as the client’s preference (FastMCP, some MCP gateways). ChatCLI sends Accept: text/event-stream, application/json in that exact order since the first release with transport: "http" — so this error only appears if you override Accept via headers in the config. Remove the Accept entry from headers and the transport falls back to the default.
Almost always one of three causes (in probability order):
  1. Corporate proxy not exported. Curl reads ~/.curlrc (with proxy=...), Go does not. Export HTTPS_PROXY, HTTP_PROXY and NO_PROXY in the shell that runs ChatCLI — our http.Client uses http.DefaultTransport, which honors those three variables natively.
  2. Missing trailing slash in url. FastMCP / Starlette / FastAPI servers 307-redirect /mcp to /mcp/. Go follows the 307 on POST, but proxies in the path frequently break the redirect (body or headers dropped). Configure url with the slash exactly as the server answers 200 OK in curl.
  3. Corporate CA outside the trust store Go sees. On macOS especially, CAs added via a mobile-config profile are sometimes not picked up. Export SSL_CERT_FILE=/etc/ssl/certs/corp-ca.pem pointing to the same bundle curl uses.
To distinguish, bump initTimeout to 60 or 120 in the config — if it goes from “hangs at 10s” to “responds in 30s”, it’s a slow proxy; if it keeps hanging, it’s (1) or (2).
Likely hidden by config:
  1. disabledTools lists its name — remove it or flip to enabledTools.
  2. enabledTools is populated but the tool isn’t in the allowlist — add it or clear enabledTools to return to the default “all visible”.
  3. /mcp status shows the hidden-tool count (blocklist).
Check:
  1. The name in overrides exactly matches the plugin name, including the @ (e.g., "@webfetch", not "webfetch")
  2. The MCP server is connected (/mcp status)
  3. The server’s enabled field is true
  4. If the server crashed and reconnected, the override resumes automatically on the next conversation turn
The overrides field only hides the built-in — it does not validate whether the MCP server actually provides an equivalent tool. If you set "overrides": ["@webfetch"] but the MCP server has no web_fetch tool, the LLM simply won’t have that capability. Remove the override or add the tool to the MCP server.
Remove the built-in from the MCP server’s overrides list. Both tools (MCP and built-in) will be visible simultaneously and the LLM will choose based on each tool’s name and description.