> ## Documentation Index
> Fetch the complete documentation index at: https://prism.ntecdev.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# SPEC-044 v0.1: Channel-Based Signal Push — Wire Prism MCP Server as a Claude Code Channel for Real-Time Signal Delivery

> Channel-Based Signal Push — Wire Prism MCP Server as a Claude Code Channel for Real-Time Signal Delivery

<Info>**Status:** `draft` · **Version** `0.1` · Filed 2026-04-25</Info>

# SPEC-044: Channel-Based Signal Push — Wire Prism MCP Server as a Claude Code Channel for Real-Time Signal Delivery

## Version

0.1

## Status

draft

## Origin

Candi's high-level design doc ("Prism MCP Notification System") described a doorbell-not-delivery push architecture: the MCP server pushes a lightweight notification, Claude Code reacts by calling `prism_signals_pending` to drain the queue. The design was sound. **The push leg was never implemented.** Signals currently deliver only via startup drain (SPEC-037 §1) or verb-response piggyback (SPEC-037 §4). Idle agents — those waiting for user input with no active verb calls — go dark.

Claude Code v2.1.80 (March 2026) shipped **Channels** as a research preview: an official API for MCP servers to push events into a running session. This is exactly the mechanism Candi's design assumed existed. GitHub issue #36665 on the claude-code repo independently describes the same multi-agent coordination gap (synapt project, 4 agents going dark when idle). Channels was Anthropic's response.

This spec wires the existing Prism MCP server as a Claude Code channel, closing the signal delivery gap for idle agents without introducing new infrastructure.

## §1 — What a Channel Is

A channel is an MCP server that declares `capabilities.experimental['claude/channel']` in its `Server` constructor. Claude Code registers a notification listener for that server. When the server calls `mcp.notification({ method: 'notifications/claude/channel', params: { content, meta } })`, the event arrives in the model's context as a `<channel>` tag. The model sees it and can act on it.

Key properties from the official Channels reference (code.claude.com/docs/en/channels-reference):

* Runs as a stdio subprocess — **Prism MCP server already does this**
* Events arrive while the session is open — maps to our existing "online delivery" requirement
* Two-way channels can expose reply tools — we already have `prism_signal` as the reply mechanism
* `instructions` string goes into Claude's system prompt — tells the agent how to handle events
* Sender gating via allowlist — not needed for self-hosted Prism (the server IS the trusted source)

## §2 — Architecture

```
Signal sender                 Backend                 Redis pub/sub           Prism MCP Server         Claude Code session
(prism_signal verb)              │                        │                   (stdio subprocess)              │
       │                         │                        │                        │                          │
       ├────prism_signal────────►│                        │                        │                          │
       │                         ├──PUBLISH──────────────►│                        │                          │
       │                         │                        ├──subscriber thread────►│                          │
       │                         │                        │                        │                          │
       │                         │                        │               mcp.notification()                  │
       │                         │                        │               method: notifications/              │
       │                         │                        │               claude/channel                      │
       │                         │                        │                        ├─────<channel> tag───────►│
       │                         │                        │                        │                          │
       │                         │                        │                        │          Claude sees:     │
       │                         │                        │                        │          "signal arrived, │
       │                         │                        │                        │           drain queue"    │
       │                         │                        │                        │                          │
       │                         │                        │                        │     prism_signals_pending │
       │                         │                        │                        │◄─────────────────────────┤
       │                         │                        │                        │                          │
```

This is Candi's doorbell pattern implemented with the official API:

1. **Signal queued** — sender calls `prism_signal`, backend persists to `signal_queue` and PUBLISHes to Redis
2. **Subscriber receives** — existing Redis subscriber thread in `mcp/subscriber.py` dequeues the event
3. **Doorbell rings** — subscriber calls `mcp.notification()` with the channel method, pushing a lightweight notification into Claude Code's context
4. **Agent drains** — Claude Code sees the `<channel>` tag and calls `prism_signals_pending` (or any other verb, triggering piggyback drain)

The notification content is deliberately minimal — signal type, sender identity, and a hint. Work payload is fetched via the existing drain mechanism. This keeps the push channel lightweight and makes correctness independent of push reliability (Candi's design choice #1).

## §3 — Implementation

### 3.1 Capability Declaration

In `mcp/server.py`, the MCP `Server` constructor gains the channel capability:

```python theme={null}
server = Server(
    { "name": "prism", "version": VERSION },
    {
        "capabilities": {
            "experimental": { "claude/channel": {} },
            "tools": {},
        },
        "instructions": (
            "Prism signal notifications arrive as <channel source=\"prism\" signal_type=\"...\" from=\"...\">. "
            "When you see one, call prism_signals_pending to drain the queue and act on the signals. "
            "Do not act on the notification content directly — it is a doorbell, not the delivery."
        ),
    },
)
```

### 3.2 Notification Emission

In `mcp/subscriber.py` (or the delivery strategy path), when a signal arrives via Redis pub/sub for the current session's identity:

```python theme={null}
async def _emit_channel_notification(self, signal: dict) -> None:
    """Push a doorbell notification into Claude Code via the Channels API."""
    await self._mcp_server.notification({
        "method": "notifications/claude/channel",
        "params": {
            "content": f"Signal from {signal['from_identity']}: {signal['signal_type']}. Call prism_signals_pending to read.",
            "meta": {
                "signal_type": signal["signal_type"],
                "from": signal["from_identity"],
                "signal_id": signal["signal_id"],
            },
        },
    })
```

### 3.3 Push Coalescing

At most one outstanding notification per drain cycle. When the subscriber receives multiple signals before the agent drains, only the first emits a notification. Subsequent signals are silently queued — the drain call will pick them all up. Implementation: a boolean `_notification_pending` flag on the subscriber, reset when `prism_signals_pending` or any piggyback drain fires.

### 3.4 Strategy Integration

The `ChannelsPushStrategy` in SPEC-034 §7.2 currently exists as a concept but was never wired. This spec replaces its implementation with the channel notification path above. The strategy's `deliver()` method calls `_emit_channel_notification()`. `supports_push()` returns `True`.

`PiggybackStrategy` (for Claude Desktop / non-channel surfaces) is unchanged — it remains the fallback for any surface that doesn't support channels.

### 3.5 Launcher Update

Claude Code must be started with the channel flag to enable push notifications. Two options:

**Option A — Development flag (research preview):**

```bash theme={null}
claude --dangerously-load-development-channels server:prism
```

**Option B — Allowlist approval (production):**
Submit Prism's channel to Anthropic's official marketplace for review. After approval, use `--channels plugin:prism@marketplace`.

For v0.1, Option A is acceptable. `bin/coder.sh` and `bin/coder.ps1` gain the flag when launching Claude Code.

### 3.6 Reconnect Reconciliation

Already handled. Startup drain (SPEC-037 §1) runs on every `prism_start`, catching any signals missed during disconnection. The channel notification is advisory; the queue is the source of truth (Candi's design choice #3).

## §4 — Claude Desktop Posture

Claude Desktop does not support the Channels API (it's a Claude Code feature). Desktop agents continue using `PiggybackStrategy` — signals deliver on the next verb call. No change needed. The `STRATEGY_MAP` (SPEC-034 §7.4) routes correctly:

```python theme={null}
STRATEGY_MAP = {
    "claude_code": ChannelsPushStrategy,    # uses channel notification
    "claude_desktop": PiggybackStrategy,    # unchanged
}
```

## §5 — Candi's Open Questions, Resolved

1. **Idle-push reliability**: Answered. Channels API delivers events while the session is open, including during idle. Empirically validated by the community (Telegram/Discord/Slack channel plugins all operate on idle sessions).

2. **Multi-session semantics**: Our signal routing already resolves to a specific identity. The channel notification is per-MCP-server-instance, which maps 1:1 to a Claude Code session. No ambiguity.

3. **Cursor failure modes**: Moot for the doorbell pattern. The notification carries no payload — it's a "check the queue" nudge. If the notification is lost, the next verb call's piggyback drain catches it. If the agent crashes between drain and processing, the signals remain in queue with `delivered_at = NULL` and re-deliver on next startup.

## §6 — Constraints

* **Research preview** — API surface may change. Channel registration syntax may evolve.
* **Requires claude.ai login** — API key auth is not supported for channels. We already use claude.ai login.
* **Custom channels need `--dangerously-load-development-channels`** — acceptable for our deployment; or apply for allowlist.
* **Events only arrive while session is open** — offline signals queue and deliver via startup drain (existing behavior, no regression).
* **Team/Enterprise must explicitly enable** — `channelsEnabled` managed setting. Not applicable to our personal install.

## §7 — Verification

1. **Idle delivery**: Donna's Claude Code session is idle (waiting for user input). Lola sends `prism_signal(to="Donna", signal_type="TaskAssigned", ...)`. Donna's session receives a `<channel>` tag within seconds. Donna calls `prism_signals_pending` and acts on the signal. Zero human relay.

2. **Push coalescing**: Send 3 signals to Donna in quick succession. Only 1 channel notification fires. Donna calls `prism_signals_pending` once and receives all 3.

3. **Reconnect reconciliation**: Send a signal while Donna's session is down. Start a new Donna session. Signal delivers via startup drain on `prism_start`. No channel notification needed.

4. **Piggyback fallback**: Lola (Desktop) receives a signal. No channel notification fires (Desktop doesn't support channels). Signal delivers on Lola's next verb call via piggyback. Existing behavior, no regression.

5. **Launcher integration**: `coder --as Donna` starts Claude Code with the channel flag. Verify `prism` appears in `/mcp` server list with channel capability registered.

## §8 — Decisions

* **Q1 (notification content)**: MINIMAL. The notification says "signal arrived, drain the queue" — not the full payload. Doorbell, not delivery. Keeps the push channel lightweight and avoids leaking potentially large payloads into the channel tag.

* **Q2 (coalescing strategy)**: ONE NOTIFICATION PER DRAIN CYCLE. Boolean flag, not a timer or count. Simple, correct, zero configuration.

* **Q3 (launcher flag)**: DEVELOPMENT FLAG FOR NOW. `--dangerously-load-development-channels server:prism` is acceptable during research preview. Migrate to allowlist when/if channels exits preview.

* **Q4 (two-way channel / reply tool)**: NOT NEEDED. The agent already has `prism_signal` as the reply mechanism. Exposing a separate channel reply tool would duplicate functionality. The channel is one-way: doorbell in, agent uses existing verbs to act and respond.

* **Q5 (permission relay)**: NOT IMPLEMENTED. The `claude/channel/permission` capability (v2.1.81+) allows forwarding tool approval prompts to remote devices. Interesting for mobile control scenarios but out of scope for v0.1 signal delivery.

## §9 — Files Changed

| File                              | Change   | What                                                                                  |
| --------------------------------- | -------- | ------------------------------------------------------------------------------------- |
| `mcp/server.py`                   | Modified | Add `claude/channel` capability + `instructions` to Server constructor                |
| `mcp/subscriber.py`               | Modified | Add `_emit_channel_notification()` method; call on signal receipt                     |
| `mcp/strategies/channels_push.py` | Modified | Wire `deliver()` to channel notification instead of stub                              |
| `bin/coder.sh`                    | Modified | Add `--dangerously-load-development-channels server:prism` when launching Claude Code |
| `bin/coder.ps1`                   | Modified | Same flag for Windows                                                                 |

No backend changes. No schema changes. No migrations. No new dependencies.

## §10 — Relationships

* **Implements** SPEC-034 §7.2 `ChannelsPushStrategy` — the strategy that was designed but never wired
* **Complements** SPEC-037 — piggyback drain remains the primary delivery for non-idle agents and non-channel surfaces; channel push covers the idle gap
* **Resolves** Candi's design doc open question #1 (idle-push reliability)
* **Informed by** SPEC-032 — Redis subscriber thread pattern reused for channel notification emission
* **Independent of** SPEC-028 — works with both Python and future TypeScript MCP server (Channels API is SDK-level, not language-specific)

## §11 — Authorship

* **Research + spec**: Lola (Claude Desktop, session e094097c) 2026-04-25
* **Design doc (Candi)**: prism-test.txt — high-level architecture that this spec implements
* **Steering**: Frank — confirmed the gap, directed research, approved implementation direction
