Documentation
¶
Overview ¶
Package channel implements the Claude Code "channels" MCP capability (`claude/channel`) for the peerbus cc adapter (--adapter=cc).
SCHEMA: DOCUMENTED. Every wire shape here mirrors the authoritative schema in CHANNELS_SCHEMA.md (Claude Code v2.1.80+, channels-reference); the typed mirror is in handshake_notes.go and is round-trip tested. No live capture was required.
── What this is ──
A stdio MCP server that is, additionally, a claude/channel: it advertises capabilities.experimental["claude/channel"]={} (registers Claude Code's notification listener so push-wake works) AND the standard tools={} capability (the bus.* reply tools). It is built ON TOP of internal/mcp — the SAME JSON-RPC core the generic adapter uses; internal/mcp was extended additively (ServerOption + Server.Notify) rather than forked. The JSON-RPC framing, dispatch, and tool plumbing are not reimplemented here.
Inbound (push-wake): a broker `deliver` (already HMAC-verified and deduped by the SHARED internal/adapter machinery — see internal/adapter/cc.go) is mapped to a JSON-RPC notification `notifications/claude/channel` with
params = { content: <single-line summary>,
meta: { from, source, msg_id, kind } }
emitted via mcp.Server.Notify (the additive server->client path). meta keys are identifier-safe (letters/digits/underscore only) per CHANNELS_SCHEMA.md §3 — keys with hyphens are silently dropped by Claude Code, so we use from / source / msg_id / kind. The content shape is a single line `📨 <kind> from <from>: "<decoded body>"` — flat by design because Claude Code's renderer collapses embedded newlines into spaces and then truncates with an ellipsis, so a multi-line banner is wasted vertical space. Claude Code's UI prefixes the notification with the MCP server name (rendered as `peerbus: <content>`), so the word "peerbus" inside the content would be duplicated noise — it is omitted. See formatInbound.
On every successful broker (re)register the cc adapter emits ONE system-kind notification (kind="system", content "📡 connected as <name>") so the consuming agent immediately knows its own bus name without an explicit bus.whoami round-trip — see AnnounceSelf. The push is gated on the MCP client having sent notifications/initialized: Claude Code silently drops claude/channel notifications received before the handshake completes (CHANNELS_SCHEMA.md §3), so a pre-handshake announce would never reach turn 1 of the session.
Outbound (reply path): standard MCP tools/list + tools/call exposing bus.send / bus.broadcast / bus.peers — the SAME tool surface and semantics as the generic adapter, served by the same internal/mcp tool plumbing over the SAME broker client + shared dedupe + HMAC (wired in internal/adapter/cc.go). The reply path is ordinary MCP tool calls; there is no special channel reply notification and no turn/correlation id (CHANNELS_SCHEMA.md §4).
Permission relay (notifications/claude/channel/permission) is DELIBERATELY NOT implemented: peerbus keeps escalation policy in the consuming agent's prompt, never in the bus. We therefore do not declare experimental["claude/channel/permission"].
Package channel implements the Claude Code "channels" MCP capability (`claude/channel`) for the peerbus cc adapter.
DOCUMENTED SCHEMA.
Every type in this file mirrors the authoritative `claude/channel` wire schema sourced from official Claude Code documentation (`channels-reference.md`, Claude Code v2.1.80+), recorded verbatim at the repo root in CHANNELS_SCHEMA.md and summarized in docs/spikes/claude-channel-handshake.md. No live capture was required; the earlier PROVISIONAL/BLOCKED status is rescinded.
These structs document the schema in Go and are round-trip tested. The live cc adapter (channel.go) builds the same frames directly; this file is the schema-of-record the tests pin to.
Index ¶
Constants ¶
const ChannelCapabilityKey = "claude/channel"
ChannelCapabilityKey is the experimental capability key the server advertises: capabilities.experimental["claude/channel"] = {} (DOCUMENTED, CHANNELS_SCHEMA.md §1). Its presence registers Claude Code's notification listener for the push method below.
const MCPProtocolVersion = "2025-06-18"
MCPProtocolVersion is the MCP protocol version string the cc adapter advertises (echoed from the client when present; this is the fallback).
const PushMethod = "notifications/claude/channel"
PushMethod is the JSON-RPC notification method the server emits to push-wake an idle session (DOCUMENTED, CHANNELS_SCHEMA.md §3).
Variables ¶
This section is empty.
Functions ¶
func UniqueName ¶
func UniqueName() string
UniqueName mints a friendly, lowercase peer name in the shape "<adjective>-<noun>-<3 base36>" (e.g. "wild-wasp-3kx"). It honours the PEERBUS_NAME environment variable verbatim when set (operator override), otherwise draws fresh entropy via crypto/rand.
The scheme replaces the older "cc-<hostname>-<pid>-<rand>" identifier — friendlier to read in logs / lists / Claude Code's <channel> tag while keeping a huge keyspace.
Types ¶
type Capabilities ¶
type Capabilities struct {
Experimental map[string]json.RawMessage `json:"experimental,omitempty"`
Tools json.RawMessage `json:"tools,omitempty"`
}
Capabilities is the MCP capabilities object. The channel capability lives under experimental["claude/channel"] as an empty object; tools is the standard MCP capability, present because the cc adapter exposes the bus.* reply tools (two-way channel). DOCUMENTED — CHANNELS_SCHEMA.md §1.
type ClientInfo ¶
ClientInfo is the MCP client identity block.
type Inbound ¶
Inbound is one already-HMAC-verified, already-deduped delivery the cc adapter pushes into the session. Source is the envelope `source` (e.g. "peer-bus" — the tag the consuming agent's prompt keys escalation off; peerbus itself has no such logic). Kind is the envelope `kind` ("msg" or "broadcast") so the channel layer can surface it as the <channel> XML kind attribute without re-decoding the body. Body is the opaque application JSON verbatim.
type InitializeParams ¶
type InitializeParams struct {
ProtocolVersion string `json:"protocolVersion"`
Capabilities Capabilities `json:"capabilities"`
ClientInfo ClientInfo `json:"clientInfo"`
}
InitializeParams is the `initialize` request params (client -> server).
type InitializeResult ¶
type InitializeResult struct {
ProtocolVersion string `json:"protocolVersion"`
Capabilities Capabilities `json:"capabilities"`
ServerInfo ServerInfo `json:"serverInfo"`
}
InitializeResult is the `initialize` result (server -> client). The server advertises experimental["claude/channel"]={} (so Claude treats it as a push-capable channel) and tools={} (so Claude discovers the bus.* reply tools). DOCUMENTED — CHANNELS_SCHEMA.md §1.
type OutboundBus ¶
type OutboundBus interface {
Send(ctx context.Context, to string, body json.RawMessage) error
Broadcast(ctx context.Context, body json.RawMessage) error
Peers(ctx context.Context) (self string, peers []string, err error)
}
OutboundBus is the broker-facing reply surface the bus.* tools delegate to. internal/adapter/cc.go implements it over the shared resuming broker client (HMAC sign + reconnect/resume) — the channel layer never touches the broker, HMAC, or dedupe itself.
Peers returns (self, peers, err): self is THIS adapter's bound peer name (so bus.peers can echo it back in the shaped {self, peers} result without a separate bus.whoami round-trip); peers is the broker registry sans the caller's own entry (filtered at the bus implementation — the broker returns the full registry including this peer, and exposing yourself in "peers" is confusing for the consuming agent).
type PushNotification ¶
type PushNotification struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params PushParams `json:"params"`
}
PushNotification is a full JSON-RPC notification frame that push-wakes an idle session. A notification has no `id`.
Method MUST equal PushMethod for the frame to be a valid push; the round-trip test treats a missing/empty Method as the malformed case.
type PushParams ¶
type PushParams struct {
Content string `json:"content"`
Meta map[string]string `json:"meta,omitempty"`
}
PushParams is the `params` of a `notifications/claude/channel` push (DOCUMENTED — CHANNELS_SCHEMA.md §3):
- Content (required): the event body, delivered as the text content of the injected <channel> XML tag.
- Meta (optional): each key/value becomes an XML attribute on the <channel> tag. ALL VALUES MUST BE STRINGS. Keys must be valid identifiers (letters, digits, underscores only); keys with hyphens or special characters are silently dropped by Claude Code.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is the cc-adapter MCP server: the internal/mcp JSON-RPC core configured as a claude/channel (experimental capability + Notify push path), with the bus.* tools delegating to an OutboundBus.
func NewServer ¶
NewServer builds the cc-adapter MCP server reading framed JSON-RPC from in and writing newline-delimited JSON-RPC to out. It advertises the claude/channel experimental capability and the standard tools capability, and serves bus.send/bus.broadcast/bus.peers over the supplied OutboundBus.
func (*Server) AnnounceSelf ¶ added in v0.3.0
AnnounceSelf emits a single system-kind claude/channel notification telling the session what peer name this adapter bound under. meta.kind is "system" so the consuming agent can ignore it from human-style escalation logic.
CALLER CONTRACT: this is the unconditional push primitive. The cc adapter MUST gate the call on Initialized() — Claude Code silently drops server-initiated notifications received before the client signals notifications/initialized (CHANNELS_SCHEMA.md §3), so a pre-handshake announce never reaches turn 1 of the session. See internal/adapter/cc.go's Run for the wiring.
func (*Server) Deliver ¶
Deliver maps one inbound broker delivery to a claude/channel push-wake notification and emits it (DOCUMENTED — CHANNELS_SCHEMA.md §3). content is a single-line human-readable summary of the inbound message (see formatInbound); meta carries identifier-safe string attributes (from / source / msg_id / kind) that Claude Code surfaces as <channel> XML attributes.
"kind" is "msg" for direct messages and "broadcast" for fan-outs; the channel layer takes the kind from the inbound envelope so the consuming agent's prompt can branch on it without re-parsing the body.
func (*Server) Initialized ¶ added in v0.3.0
func (s *Server) Initialized() <-chan struct{}
Initialized returns a channel that is closed once the MCP client has sent notifications/initialized (the handshake completion signal). The cc adapter's startup self-announce waits on this before pushing — see AnnounceSelf's caller contract.
type ServerInfo ¶
ServerInfo is the MCP server identity block.