mcp_servers.json file defines which MCP servers ChatCLI should connect to. It is loaded automatically at session startup.
Location
~/.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: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
Themcp_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.
Format
Fields
Core (required)
Tier 1 — direct runtime effect
Tier 2 — additional capabilities
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 realcommand. - When a key is promoted to a typed field in a future release (e.g.
oauth2), the content already inExtensionsmigrates 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 theenv overrides apply. Without that inheritance, launchers like npx, uvx, docker and pipx wouldn’t find their binaries or per-user caches.
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.
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.
${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.Recommended patterns
Auto-approval and Trust
ChatCLI’s tool-execution pipeline emits an audit log for every MCP invocation thatautoApprove, 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.
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:
Audit log
Every auto-approved call produces an info-level entry: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)
"*" (hides everything — useful to mute a server without removing the entry).
enabledTools (allowlist — takes precedence)
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.
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:
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
- Accepts
${VAR}/$VARexpansion (same lookup asenv). - Accepts leading
~/expanded againstHOME. - 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).
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.
${VAR} expansion (same lookup as env).
HTTP Authentication (auth)
Typed block with three modes:
bearer
Authorization: Bearer <token>.
basic
Authorization: Basic base64(user:pass).
header (custom)
X-API-Key: <token>. When header is omitted, the default is X-API-Key.
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).OAuth 2.1 (interactive, no config)
The threeauth modes above are for pre-minted tokens. When a remote server instead challenges with 401 + an OAuth flow (e.g. the AWS MCP server), you do not configure auth at all — ChatCLI discovers the authorization server, registers a client dynamically, runs a PKCE browser flow, and stores/refreshes the token in the encrypted auth store automatically. Authorize with /mcp login <name> or let the agent call @mcp-login. See OAuth 2.1 authorization. Optional env var: CHATCLI_MCP_OAUTH_PORT (loopback callback port, default 8765).
Metadata (description, tags, category)
Cosmetic fields rendered in /mcp status so it’s easy to tell servers apart when many are connected:
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.
"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./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
/channel rules reload.
Schema
Fields
Modes — summary
Validation
Parse failures reject the entire file (atomic apply — either all rules land, or nothing changes). Onreload with an error, ChatCLI keeps the previous rules active and shows the error in the output.
Typical errors:
Ready-to-use examples
Passive CI watcher
Prod alerts with confirmation
Autonomous canary deploys
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
- stdio (Local)
- SSE (HTTP+SSE)
- http (Streamable HTTP)
Testing with curl
Useful for isolating proxy / CA / handshake issues before involving ChatCLI. The wire shape is the same thehttp transport emits:
-iprints response headers (for theMcp-Session-Id).-Ndisables buffering — if the server replies with SSE, you see events arriving in real time rather than waiting for the body to close.
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
Protocol
ChatCLI speaks MCP Protocol v2024-11-05 (advertised oninitialize) 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).
Tool Naming
mcp_ prefix is added automatically to avoid collisions with native tools (plugins, @coder, @webfetch, etc.).Popular MCP Servers
Troubleshooting
Server won't connect
Server won't connect
- The command exists and is in PATH (
which npx) - Config file is at
~/.chatcli/mcp_servers.json enabledistrue- Use
/mcp statusto see connection errors - Use
/mcp logs <name>to see the server’s recent stderr (npm 404, panic, missing executable) - Check logs with
CHATCLI_LOG_LEVEL=debug
The MCP server does not see the token / connection string
The MCP server does not see the token / connection string
- Variable not exported in the shell —
${GH_TOKEN}in the config expands to empty ifGH_TOKENisn’t inos.Environ(). Confirm withecho $GH_TOKENbefore starting ChatCLI. - 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.
/mcp logs <name> — MCP servers usually log “missing token” / 401 to stderr.Hot-reload doesn't fire when I create mcp_servers.json
Hot-reload doesn't fire when I create mcp_servers.json
~/.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.I edited the JSON and it has a syntax error — did I lose everything?
I edited the JSON and it has a syntax error — did I lose everything?
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.Tools not showing up
Tools not showing up
- The server may not support
tools/list - Use
/mcp toolsto list discovered tools - Restart with
/mcp restart
Tool execution timeout
Tool execution timeout
npx -y cold start). If a specific server takes longer:- Bump
timeout(seconds) on that server’s config only — see Timeouts. - If failure is during the handshake (not the call), bump
initTimeoutseparately. - For long-running servers (LLM proxies, slow agents), prefer SSE — the stream stays open while the server processes.
An MCP tool was auto-approved without me confirming
An MCP tool was auto-approved without me confirming
autoApproveoralwaysAllowin the config lists the tool name (or"*") — see Auto-approval.trust: trueon the server is a full bypass. Every auto-approved call emits an info-level logMCP tool auto-approved by config tool=<name>. Grep the chatcli log to audit.- To remove the bypass, edit the JSON (hot-reload reapplies) or run
/mcp reload.
SSE server returned 401/403
SSE server returned 401/403
- Confirm the env var referenced by
auth.token/headersis exported in the shell.${UNSET_VAR}expands to an empty string and ChatCLI suppresses the entire header instead of sendingAuthorization: Bearer(empty) — so the server reports 401 as if no auth was attempted. - Rotated the token in the shell? Headers are re-applied on every request, no
/mcp restartneeded. But confirm the shell that started ChatCLI has the new variable (export, source, etc.). - For
type: "header"with a custom name, confirm the server expects exactly the configured name (ChatCLI sends literally what’s inauth.header).
HTTP transport: -32600 / 'Not Acceptable: Client must Accept text/event-stream'
HTTP transport: -32600 / 'Not Acceptable: Client must Accept text/event-stream'
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.HTTP transport: ~10s timeout on initialize, but curl works
HTTP transport: ~10s timeout on initialize, but curl works
- Corporate proxy not exported. Curl reads
~/.curlrc(withproxy=...), Go does not. ExportHTTPS_PROXY,HTTP_PROXYandNO_PROXYin the shell that runs ChatCLI — ourhttp.Clientuseshttp.DefaultTransport, which honors those three variables natively. - Missing trailing slash in
url. FastMCP / Starlette / FastAPI servers 307-redirect/mcpto/mcp/. Go follows the 307 on POST, but proxies in the path frequently break the redirect (body or headers dropped). Configureurlwith the slash exactly as the server answers 200 OK in curl. - 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.pempointing to the same bundle curl uses.
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).An MCP tool does not show up in /mcp tools
An MCP tool does not show up in /mcp tools
disabledToolslists its name — remove it or flip toenabledTools.enabledToolsis populated but the tool isn’t in the allowlist — add it or clearenabledToolsto return to the default “all visible”./mcp statusshows the hidden-tool count (blocklist).
Override not working — LLM still sees the built-in
Override not working — LLM still sees the built-in
- The name in
overridesexactly matches the plugin name, including the@(e.g.,"@webfetch", not"webfetch") - The MCP server is connected (
/mcp status) - The server’s
enabledfield istrue - If the server crashed and reconnected, the override resumes automatically on the next conversation turn
Built-in disappeared but the MCP server has no equivalent tool
Built-in disappeared but the MCP server has no equivalent tool
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.I want to force the built-in even with MCP active
I want to force the built-in even with MCP active
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.