|
18 | 18 | - **Session-Based:** maintain multiple work sessions and contexts per project |
19 | 19 | - **LSP-Enhanced:** Crush uses LSPs for additional context, just like you do |
20 | 20 | - **Extensible:** add capabilities via MCPs (`http`, `stdio`, and `sse`) |
| 21 | +- **Channel-Ready:** MCP servers can push real-time events into your session via [Claude Channels](https://code.claude.com/docs/en/channels-reference) — CI failures, webhooks, and more, acting on them without you typing a thing |
21 | 22 | - **Works Everywhere:** first-class support in every terminal on macOS, Linux, Windows (PowerShell and WSL), Android, FreeBSD, OpenBSD, and NetBSD |
22 | 23 | - **Industrial Grade:** built on the Charm ecosystem, powering 25k+ applications, from leading open source projects to business-critical infrastructure |
23 | 24 |
|
@@ -367,11 +368,137 @@ which do expand. |
367 | 368 | "headers": { |
368 | 369 | "API-Key": "$(echo $API_KEY)" |
369 | 370 | } |
| 371 | + }, |
| 372 | + "signal": { |
| 373 | + "type": "stdio", |
| 374 | + "command": "uv", |
| 375 | + "args": [ |
| 376 | + "run", |
| 377 | + "--directory", |
| 378 | + "/path/to/signal-mcp", |
| 379 | + "python", |
| 380 | + "signal_mcp/main.py", |
| 381 | + "--user-id", |
| 382 | + "+15551234567", |
| 383 | + "--channel" |
| 384 | + ] |
| 385 | + } |
| 386 | + } |
| 387 | +} |
| 388 | +``` |
| 389 | + |
| 390 | +#### Channels (experimental) |
| 391 | + |
| 392 | +An MCP server can also act as a **channel**: instead of only exposing tools it |
| 393 | +calls, it *pushes* events straight into your session so Crush reacts to things |
| 394 | +happening outside the terminal — a webhook, a CI failure, a chat message. See |
| 395 | +the [channels reference](https://code.claude.com/docs/en/channels-reference) |
| 396 | +for the protocol. |
| 397 | + |
| 398 | +A server becomes a channel by declaring the `claude/channel` capability in its |
| 399 | +`initialize` result (`capabilities.experimental["claude/channel"] = {}`) and |
| 400 | +emitting `notifications/claude/channel` events with a `content` body and an |
| 401 | +optional `meta` map. Crush injects each event into the active session as a |
| 402 | +`<channel>` element: |
| 403 | + |
| 404 | +```text |
| 405 | +<channel source="webhook" severity="high" run_id="1234"> |
| 406 | +build failed on main: https://ci.example.com/run/1234 |
| 407 | +</channel> |
| 408 | +``` |
| 409 | + |
| 410 | +Listing a channel server in `mcp` is **not** enough to enable it — pushing is |
| 411 | +gated behind an explicit per-session opt-in, so a server present in config |
| 412 | +stays silent until you ask for it: |
| 413 | + |
| 414 | +```bash |
| 415 | +# Enable one or more configured MCP servers as channels for this session. |
| 416 | +crush --channels server:webhook |
| 417 | +crush --channels server:webhook --channels server:signal |
| 418 | +``` |
| 419 | + |
| 420 | +The `source` attribute is always the (trusted) server name. Payloads are |
| 421 | +untrusted, server-initiated input: Crush validates their structure, caps the |
| 422 | +body and attribute sizes, restricts `meta` keys to identifiers |
| 423 | +(`[A-Za-z0-9_]`), escapes all content so a payload cannot break out of the |
| 424 | +`<channel>` element or forge attributes, and drops malformed payloads. A |
| 425 | +server that has not been opted in via `--channels`, or that never declared the |
| 426 | +capability, cannot inject anything. |
| 427 | + |
| 428 | +Channel delivery works in the default in-process `crush` and against a shared |
| 429 | +`crush serve` backend. In-process, an event routes into the session you have |
| 430 | +open, or starts one if none is open, so it is never dropped. Against a |
| 431 | +`crush serve` backend the server routes each event exactly once — into the |
| 432 | +session an attached client is viewing (the most recently updated one when |
| 433 | +clients are viewing different sessions), otherwise into the workspace's most |
| 434 | +recent session, creating one only when none exists. That holds even with no |
| 435 | +clients connected, so a headless server still processes channel pushes; |
| 436 | +attached clients see the injected turn arrive through the normal event |
| 437 | +stream. Servers that are live channels are marked `channel` in the MCP list |
| 438 | +so you can confirm the opt-in took effect. |
| 439 | + |
| 440 | +**Two-way channels.** A channel can also be interactive. Because a channel is a |
| 441 | +regular MCP server, any tool it exposes (a `reply` tool, say) is available to |
| 442 | +the agent through the normal MCP tool path — nothing channel-specific is |
| 443 | +required. Declare `tools` in the server's capabilities, register the tool, and |
| 444 | +use the server's `instructions` string (injected into the system prompt) to |
| 445 | +tell Crush when to call it and which `<channel>` attribute to pass back (like a |
| 446 | +`chat_id`). |
| 447 | + |
| 448 | +For example, [Signal MCP](https://github.com/joestump/signal-mcp) lets Crush |
| 449 | +send and receive Signal messages through a |
| 450 | +[signal-cli](https://github.com/AsamK/signal-cli) daemon: |
| 451 | + |
| 452 | +1. **Start the daemon:** `signal-cli -a +15551234567 daemon --tcp 127.0.0.1:7583 --receive-mode on-start --no-receive-stdout` |
| 453 | +2. **Add to `crush.json`:** Use the example in the [MCPs](#mcps) section above. |
| 454 | +3. **Launch with channel:** `crush --channels server:signal` |
| 455 | + |
| 456 | +Incoming messages arrive as `<channel>` tags; use the `send_message_to_user` tool to reply. |
| 457 | + |
| 458 | +#### Channel reply routing |
| 459 | + |
| 460 | +By default a channel-originated turn only produces terminal output, so a |
| 461 | +person messaging you on Signal never sees the answer unless the model decides |
| 462 | +to call a send tool itself. Adding a `channel_reply` block to a channel |
| 463 | +server's MCP config makes the routing deterministic: when a turn that |
| 464 | +originated from that channel finishes without the model having replied through |
| 465 | +the channel, Crush sends the final assistant response back through the |
| 466 | +configured tool — to the sender for direct messages, or to the group for group |
| 467 | +messages. |
| 468 | + |
| 469 | +```json |
| 470 | +{ |
| 471 | + "mcp": { |
| 472 | + "signal": { |
| 473 | + "type": "stdio", |
| 474 | + "command": "uv", |
| 475 | + "args": ["run", "signal_mcp/main.py", "--operator", "+15551234567", "--channel"], |
| 476 | + "channel_reply": { |
| 477 | + "user": { "tool": "send_message_to_user", "target_param": "user_id" }, |
| 478 | + "group": { "tool": "send_message_to_group", "target_param": "group_id" }, |
| 479 | + "suppress_tools": ["send"] |
| 480 | + } |
370 | 481 | } |
371 | 482 | } |
372 | 483 | } |
373 | 484 | ``` |
374 | 485 |
|
| 486 | +How it routes: |
| 487 | + |
| 488 | +- **Group pushes** (meta carries `group`) go through the `group` route; |
| 489 | + **direct pushes** (meta carries `sender`) go through the `user` route. |
| 490 | + `target_meta` overrides which meta attribute supplies the target; |
| 491 | + `message_param` (default `message`) names the tool argument that receives |
| 492 | + the reply text. |
| 493 | +- If the model already called a route tool — or any tool listed in |
| 494 | + `suppress_tools` — during the turn, the automatic reply is skipped, so |
| 495 | + richer model-driven replies aren't duplicated. |
| 496 | +- Local (non-channel) turns and channels without a `channel_reply` block are |
| 497 | + unaffected. |
| 498 | + |
| 499 | +The same shape works for any messaging channel (Discord, Slack, …): point the |
| 500 | +routes at that server's send tools and the matching meta attributes. |
| 501 | + |
375 | 502 | ### Hooks |
376 | 503 |
|
377 | 504 | Crush has preliminary support for hooks. For details, see |
|
0 commit comments