Skip to main content
Status: draft · Version 0.1 · Filed 2026-04-25

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:
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:
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):
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:
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 enablechannelsEnabled 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

FileChangeWhat
mcp/server.pyModifiedAdd claude/channel capability + instructions to Server constructor
mcp/subscriber.pyModifiedAdd _emit_channel_notification() method; call on signal receipt
mcp/strategies/channels_push.pyModifiedWire deliver() to channel notification instead of stub
bin/coder.shModifiedAdd --dangerously-load-development-channels server:prism when launching Claude Code
bin/coder.ps1ModifiedSame 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
Last modified on April 27, 2026