Skip to main content
MCP Channels is the ChatCLI push notification system. MCP servers send proactive messages (alerts, CI events, deploys, external webhooks) and ChatCLI stores them in a durable ring, injects them into the next chat / agent / coder turns, and โ€” optionally โ€” triggers the agent automatically via a rules engine with three operating modes.
Channels work across all three MCP transports supported by ChatCLI:
  • sse (classic HTTP+SSE) โ€” persistent listener on the GET /sse stream.
  • http (Streamable HTTP, MCP spec 2025-03-26) โ€” opt-in listener via GET on the configured endpoint.
  • stdio โ€” any JSON-RPC message without id emitted by the child process is treated as a notification.
Delivery is passive by default (a discreet banner on the next prompt) and reactive by explicit opt-in (confirm or auto rules in ~/.chatcli/mcp/triggers.json).

Architecture at a glance

Each box is a real source file (cli/mcp/channels.go, cli/mcp/triggers/triggers.go, cli/channel_triggers.go, cli/agent_system_prompt.go). The split is deliberate: ChannelManager is process-local and independent of the CLI; the trigger engine is a separate package without a reverse dependency; the CLI plugs in via OnMessage.

How a message reaches the agent

1

Server emits notification

MCP server sends a JSON-RPC message without id (a notification in the spec). Can be over the SSE stream, the GET of Streamable HTTP, or a stdout line in stdio.
2

Transport extracts and routes

The matching transport parses, identifies it as a notification (id absent) and calls ChannelManager.ProcessSSENotification. The method becomes the channel: notifications/ci-pipeline โ†’ channel ci-pipeline; channel/message with params.channel โ†’ channel params.channel.
3

Subscription filter

If the server config has channels: [...] and the channel is not listed, the message is dropped (debug log). Empty list means โ€œaccept everythingโ€.
4

Persist + ring + unread

Message gets a monotonic seq, lands in the in-memory ring (FIFO, default 200), is append-only written to ~/.chatcli/mcp/channels.jsonl, and the unread counter increments.
5

Fan-out to subscribers

Each registered OnMessage callback receives a copy. The CLI registers two: the trigger engine (evaluates rules) and the banner renderer (prepares the UI).
6

System prompt โ€” next turn

On the next chat/agent/coder turn, the 5 most recent messages from the ring are injected as a block into the system prompt (uncached, so the cached prefix is not invalidated).
7

Reactive trigger (optional)

If any rule matched, the engine emits an Action that lands in one of three queues: notify (discreet banner), confirm (yes/no prompt), auto (runs the agent at the next prompt tick).

Persistence

Guarantees

  • Durable across sessions: messages received while ChatCLI was closed are visible on the next boot (up to the load limit).
  • Append-only: we never rewrite the file. A process crash mid-write leaves at worst one truncated line โ€” the loader skips line-by-line, so a single corrupt line never poisons the rest.
  • Automatic rotation: when the file reaches 10 MiB, it is renamed to .1 and a fresh file is opened. We keep one historical file โ€” channels are telemetry, not forensic audit log.
  • Best-effort writes: if the write fails (disk full, permissions), ChatCLI logs a warning once, marks persistence as disabled only for that session, and keeps serving the in-memory ring.

Boot โ€” replay

On startup, ChatCLI reads the last 200 lines combined from channels.jsonl.1 + channels.jsonl in chronological order. Replayed messages:
  • enter the ring;
  • do not count as unread (they were already seen);
  • preserve their original seq (the internal counter is advanced past the highest observed).

How to configure

Persistence is on by default when HOME is resolvable. In containerized environments without HOME (rare), ChannelManager falls back to in-memory-only and logs an info on startup. There is no flag to โ€œdisable persistenceโ€ โ€” to wipe the state, delete the file manually (rm ~/.chatcli/mcp/channels.jsonl*).

Per-server filter โ€” channels

The optional channels field on the MCP server config is an allow-list that decides which channels from that server are accepted into the ring.
Rules:
  • Empty/omitted โ†’ accepts every channel the server emits.
  • Explicit "*" โ†’ equivalent to empty.
  • List โ†’ only literally listed channels pass; others are dropped (debug log).
  • Whitespace in entries is trimmed, so " alerts " matches "alerts".
Filtering happens at receive time (before the ring and persistence), so a noisy server emitting 20 channels when you only care about 2 doesnโ€™t pollute anything.
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, which run after this filter.

Auto-injection into the system prompt

On every turn (chat, agent, coder), the 5 most recent ring messages are added to the system prompt as an extra block:

Important guarantees

  • No cache hint: this block is volatile and intentionally lives outside the Anthropic cached prefix. Putting something that changes every turn inside the cache would trash the entire KV cache โ€” worse than not caching at all.
  • Same content in chat / agent / coder: the system-prompt builders for all three modes were unified to include the block.
  • Empty ring โ†’ block omitted: zero overhead when you have never received anything.

Why only 5?

Calibrated for balance: enough to give recent context (CI running + hottest alert) without using too many tokens when the user has multiple chatty servers. If you need more history for a specific turn, use /channel inject (which injects the most recent 10).

Reactive Triggers (rules engine)

This is the opt-in part. When you want ChatCLI to react to events (e.g., โ€œif CI failed, ask the agent to investigateโ€; โ€œif thereโ€™s a critical alert, run the agentโ€), define rules in ~/.chatcli/mcp/triggers.json.

Rule schema

Field reference:

Matching โ€” practical examples

Matches exactly the alerts/critical channel on any server.
Matches alerts/critical, alerts/warning, alerts/info โ€” any sub-channel under alerts/. Does not match errors/critical (different prefix).
Triple condition: server prom-alerts, channel alerts/*, and content matching the regex. All must be satisfied (AND).

Modes

mode: notify (default โ€” zero surprise)

When the rule matches:
  1. Immediately: a toast prints on stderr with the rule name and a content preview.
  2. The next time you type (next prompt tick), a banner shows above the prompt with the item in the inbox.
  3. Nothing runs. You decide whether to act.

mode: confirm (user-in-the-loop)

When the rule matches:
  1. Stderr toast + prompt in the banner: โ€œrun /channel confirm <id> yes or /channel confirm <id> noโ€.
  2. Nothing runs until the user answers.
  3. /channel confirm <id> yes (or just /channel confirm <id>, defaulting to yes) fires the agent with the rule template.
  4. /channel confirm <id> no discards the action.
Confirm actions expire after 30 min โ€” if you donโ€™t reply, they leave the queue so state doesnโ€™t grow indefinitely.

mode: auto (autonomous โ€” strong opt-in)

Requires tools whitelist (validation rejects auto rules without tools). When the rule matches:
  1. Stderr toast informing the trigger is queued.
  2. At the next prompt tick (after draining park resumes and before processing user input), the agent runs automatically with the rule template.
  3. Execution is rendered inside an AUTO-AGENT envelope box โ€” visually distinct from a normal turn response.
  4. Esc aborts as in any agent execution.

Guard-rails for auto

  • tools whitelist required โ€” startup validation rejects an auto rule without tools.
  • rateLimit and dedupWindow recommended โ€” without them, a noisy server can spawn dozens of turns per minute.
  • Esc always aborts, regardless of whether the agent was triggered by user or rule.
  • /channel pause shuts everything off globally โ€” use before a sensitive operation to avoid interference.

Rules configuration

Root schema:
Ready-to-use examples:

CI watcher (notify only โ€” safe default)

Prod alerts โ†’ confirm

Canary deploys โ†’ auto (with tight whitelist)

Validation

Schema failures reject the entire file (atomic apply โ€” either every rule lands, or nothing changes). Common errors: Runtime reload: /channel rules reload.

/channel commands

All subcommands support autocomplete (Tab after /channel ), and completion is multi-level: clear offers --all, rules offers reload, confirm completes the live pending IDs then the yes/no decision, and run completes recent message seqs. The command palette (bare /channel) shares the same source.

Examples

Listing

Filtering

Manually running a message

Inspecting rules


@channels โ€” the agent reads the inbox

Everything above is user-driven. In agent/coder mode the model gets its own read-mostly window into the same inbox through the @channels builtin, so pushed events can steer a task mid-flight without waiting for the user to paste them.
Pending confirm actions are never exposed to the model. They gate real side effects (agent runs on a rule template) and stay a human decision via /channel confirm <id>. The agent can read and acknowledge the inbox; it cannot approve actions on the userโ€™s behalf. list/unread are read-only; ack only resets attention counters.
Smart unread. Protocol notifications the client already handled itself โ€” most notably dynamic tool-list refreshes โ€” are recorded in the ring for audit but do not inflate the unread counter. An inbox that refilled itself from machine-to-machine chatter could never be drained; only events that actually need attention count.

Auto-injection vs /channel inject โ€” when to use which

Auto-injection is silent and continuous โ€” as long as the ring has content. /channel inject is an explicit action that adds a system message to history (sticks until the next compaction). /channel run is the โ€œinvestigate nowโ€ path without a registered rule.

Reconnection and fault tolerance

Server-side sessions on Streamable HTTP: when the server emits Mcp-Session-Id on initialize, the transport echoes the header on every request (including the listener GET). Reconnection preserves the session automatically.

Use cases

CI/CD

notify rule on ci-pipeline. You see failures in the banner; /channel run <seq> launches the agent to investigate with tools.

Prod alerts (Prometheus/Datadog)

confirm rule on alerts/critical with a prompt template. Each alert becomes an โ€œinvestigate?โ€ question you answer on demand.

Canary deploys

auto rule with a tight whitelist (kubectl_*, http_request). Every canary kicks off smoke checks automatically.

External webhooks (GitHub/Jira/Slack)

A custom MCP server translates webhooks into notifications. ChatCLI pulls from the ring on next turns or triggers dedicated rules.

Limits and trade-offs

These numbers are not configurable today. They may be parametrized in a future release โ€” open an issue if you hit any of them.

Troubleshooting

Expected behavior if you only have notify rules or no rules at all. notify is passive by design. For action:
  • Add a confirm or auto rule to ~/.chatcli/mcp/triggers.json and run /channel rules reload.
  • Or run /channel run <seq> manually against the message.
May be a listener disabled. The Streamable HTTP listener is opt-in by spec โ€” check:
  1. /mcp logs <name> โ€” if you see server returned 405 on GET <url>, 404, or 501, the server does not implement push and the listener stopped cleanly.
  2. Otherwise, check that the server is emitting notifications in JSON-RPC format without id. Non-JSON data: content is also captured (lands in the raw channel), but doesnโ€™t trigger rules tied to specific channels.
In order:
  1. /channel rules โ€” confirm the rule exists and is active.
  2. Was /channel pause run? /channel resume reactivates.
  3. Matching: does the server/channel/contentRegex actually match? Remember that channel: "alerts" does NOT match "alerts/critical" โ€” use "alerts/*".
  4. rateLimit or dedupWindow โ€” a recent fire may be suppressing.
Yes, with the session closed: rm ~/.chatcli/mcp/channels.jsonl*. On next boot, the ring starts empty.At runtime, rotation automatically cuts at 10 MiB โ€” you can reach ~20 MiB total (active + .1) before the next cut.
/channel rules reload. If reload fails (invalid schema), the error appears in the response and ChatCLI keeps the previous rules active โ€” no โ€œno rulesโ€ state.If the error is a regex, paste it into https://regex101.com flavor โ€œGolangโ€ to isolate.
Symptom: agent investigates, does something that triggers another notification, which triggers the rule again. Fix:
  1. Significantly bump rateLimit (e.g., "15m").
  2. Refine the contentRegex to only catch the pathological case.
  3. /channel pause while you investigate the config offline.
Use /channel inject โ€” puts the last 10 as a system message in history. Persists in history until the next compaction.To view without injecting into the LLM: /channel list shows up to 20.
Check the ruleโ€™s tools whitelist. In auto, only listed tools can be invoked without extra approval; tools outside the whitelist follow the agentโ€™s normal approval flow โ€” which may be your intent or not. Refine the whitelist in triggers.json.

Next steps

MCP Integration

Configure MCP servers โ€” foundation for channels.

MCP Config

Complete reference of mcp_servers.json (including the channels field).

Hooks System

Lifecycle hooks โ€” complement channels for internal events.

Command Reference

Full slash command list, including /channel.