channel

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 20, 2026 License: MIT Imports: 7 Imported by: 0

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

View Source
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.

View Source
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).

View Source
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

type ClientInfo struct {
	Name    string `json:"name"`
	Version string `json:"version"`
}

ClientInfo is the MCP client identity block.

type Inbound

type Inbound struct {
	ID     string
	From   string
	Source string
	Kind   string
	Body   json.RawMessage
}

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

func NewServer(ob OutboundBus, in io.Reader, w io.Writer) *Server

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

func (s *Server) AnnounceSelf(self string)

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

func (s *Server) Deliver(in Inbound)

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.

func (*Server) Serve

func (s *Server) Serve(ctx context.Context) error

Serve runs the JSON-RPC read/dispatch loop until ctx is cancelled or stdin closes (the cc adapter's lifecycle is bound to its stdio session).

type ServerInfo

type ServerInfo struct {
	Name    string `json:"name"`
	Version string `json:"version"`
}

ServerInfo is the MCP server identity block.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL