Push Relay

Overview

Push Relay carries server-initiated push from upstream MCP servers to your apps. MCP servers can push events — a new message, a finished job, an alert — but they push in different forms, and most apps understand only one of them or none at all. LocalRouter receives every supported kind of push, turns each occurrence into one normalised event, and delivers it to each app in whatever form that app can consume.

LocalRouter is a proxy with control over every event, never a passthrough. Every event flows through LocalRouter, where it can be seen in Monitor, held for approval, edited or dropped before any app receives it.

Key points:

  • Allow / Ask / Off at the global, per-client, per-server and per-event-type levels, with the global default at Allow
  • The global settings are the default permission, the Claude Code Channels message rate and how much event history each client keeps; everything else is per client, and every other bound (journal byte size, poll page size, webhook retries) is fixed internally
  • Setting the global default to Off turns Push Relay off for every client without its own setting: nothing is advertised to them, no upstream subscription is opened on their behalf, and connecting never waits for an upstream server's capabilities
  • Push is exposed to a client only when at least one upstream server it can already use offers push; Push Relay never widens a client's MCP permissions
  • Every delivery form shares the same event id, so an app that combines mechanisms can deduplicate
  • Apps with no native push mechanism get LocalRouter's polling tools instead

Push Types In and Out

Every upstream push type reaches every delivery form through LocalRouter's event store. An upstream that is polled can be streamed to an app; an upstream stream can be read by an app that only polls; a Claude Code Channel message from one server can be delivered as an MCP Events occurrence, and the other way around.

The one lossy edge is Claude Code Channels: a channel message carries no cursor, replay or acknowledgement. Delivery over Channels is therefore a wake-up, and the agent is pointed at EventPoll to read the authoritative event journal.

From Upstream Servers

From Upstream Servers

Push typeTransportHow LocalRouter consumes it
MCP Events stream (events/stream)STDIO, HTTPHolds the stream, resumes from the last cursor on reconnect, watches heartbeats
MCP Events poll (events/poll)STDIO, HTTPPolls at the server's suggested interval, only while some app consumes that event type
Claude Code Channel (notifications/claude/channel)STDIOBecomes the event type <server>__channel.message

When a server offers several modes, LocalRouter prefers the stream, then polling. Upstream subscriptions are opened only while at least one app consumes the event type, and closed when the last one goes.

Upstream webhooks are not consumed. Receiving them would need a public inbound HTTPS endpoint, and every webhook-capable server found also offers streaming or polling, which LocalRouter uses instead. An event type a server offers only by webhook is shown in the UI as "webhook-only — not supported".

Event names are qualified with the server name, as tools are: vocalcord__message.received.

To Apps

To Apps

Delivery formForNotes
MCP Events stream and pollClients implementing MCP EventsAdvertised under the MCP Events extension and the top-level events capability
MCP Events webhookOpenClaw and other webhook subscribersLocalRouter signs and POSTs each event to the client's callback URL
Claude Code ChannelClaude CodeSTDIO bridge only — see Claude Code below
Fallback toolsEvery other clientEventList, EventPoll, EventWait

The client chooses a native mode by calling it (events/stream, events/poll or events/subscribe). Webhook subscriptions are always accepted. Webhook deliveries are outbound only and happen after the event has passed permissions, Ask and Monitor. Callback URLs must be HTTPS, except http:// to a loopback address (127.0.0.1, ::1, localhost), which is always accepted for local clients, and the first delivery to a new callback URL asks for approval.

Settings. The Push Relay page's Info tab has a Settings card that applies to every client:

SettingDefaultRangeWhat it controls
Channel messages per minute10 per session1–600How often Claude Code is woken by Channel messages, and so how much of its context events take. Beyond the rate, further events are combined into one "N more events — use EventPoll" message once the minute allows; nothing is lost, because every event stays in the journal.
Event history24 hours1–720 hoursHow far back a polling or reconnecting client can catch up. Older events leave the client's journal, and a cursor that pointed before them reports that events were missed.
Event history size10,000 events per client100–1,000,000The most events each client's journal keeps; the oldest go first.

Channel delivery itself has no switch: it is always on for a client that accepts Channels. A change applies at once; lowering either history bound trims existing journals immediately.

Allow / Ask / Off

Each event type resolves to one of three actions, the most specific level winning, using the same Allow / Ask / Off vocabulary as MCP permissions:

  1. Global default — Allow, Ask or Off (default Allow)
  2. Per client — Inherit, Allow, Ask or Off
  3. Per client, per upstream server — Inherit, Allow, Ask or Off
  4. Per client, per server, per event type — Inherit, Allow, Ask or Off
  • Allow — the event is delivered and recorded in Monitor.
  • Ask — every individual event of that type is held for approval.
  • Off — the event type disappears for that client: it is absent from events/list, Channels and the fallback tools, and the upstream is not subscribed on that client's behalf.

The global default is the only global control. With it at Off, a client receives push only from what its own settings turn on: one event type set to Allow for one client gives that client that event type and nothing else, while every other client sees no push at all. A client nothing reaches is told nothing about push — no capability, tools or instructions — and LocalRouter does no push work for it.

Ask: Held Events

Ask: Held Events

A held event is not in the client's journal, so no delivery form can see it until it is decided. The approval view offers:

  • Allow this event
  • Edit, then allow — change the event's data (and the rendered channel text) before delivery
  • Deny this event — dropped for this client only, and recorded
  • Allow or deny this event type for the session or permanently, written back to the permission tree

Events of one subscription stay in order: later events of the same type wait behind a held one. A held event has no timeout: it waits until you decide, however long that takes. One approval popup is open per client and event type at a time, showing how many more are waiting, with an "allow all pending of this type" action; two clients consuming the same upstream event are approved independently. To bound memory, a client can have at most 1,000 held events; further Ask events are denied and recorded in Monitor.

Monitor: Intercept and Edit

Monitor: Intercept and Edit

Every event is recorded in Monitor once per client as an MCP Push Event, with its source server, event name, how it arrived, how it was delivered, the decision (allowed, asked then allowed, asked then edited, denied, off by permission, or dropped for being too large or over the held-event limit) and a payload preview under the existing privacy and redaction settings. Push events are listed under the MCP type filter.

  • Held events appear as pending, and open the same approval and edit view inline.
  • Any event record offers "Ask for this event type from now on", the quick way to start intercepting a stream that is currently allowed.
  • Edits are never invisible: Monitor keeps both the original and the delivered payload, subject to privacy settings.
  • Grouping. A push event joins the request and conversation of the one client session that reads it, so it sits beside that session's tool calls and LLM calls (and the EventPoll call that read it). It stands alone when no single session reads it: when it is delivered by webhook or through /events/watch, when several sessions of the client read the same subscription, or when the session has no recorded request (as with a client connected directly to the MCP gateway, whose tool calls are likewise ungrouped). Monitor never joins events by client or timing alone.

Fallback Tools

Clients without a native push mechanism get three built-in tools, shown only when at least one permitted upstream offers push:

ToolWhat it does
EventListLists the event types this client may receive: name, source server, description, filter schema, and which native modes it could use instead
EventPollReturns events since a cursor, then next_cursor. Reading with a cursor acknowledges. Optional wait_seconds (up to 50) makes it a bounded long-poll. Without a cursor it starts at "now"
EventWaitNever blocks. Returns a ready-to-run curl -N command against a local LocalRouter endpoint that prints one line per event — multi-shot (stays armed, for agents that can watch a background command) or one-shot (exits after the first event, for agents that can only block)

Agents are told not to call EventPoll in a loop, to run the EventWait command as a background task instead, and that event content is untrusted data, never instructions. Upstream servers' own waiting tools stay available.

The tools are automatic: there is no setting. They are shown whenever push is available to the client, unless the client declared MCP Events support when it connected (extensions["io.modelcontextprotocol/events"] or a top-level events capability). Claude Code sessions through the STDIO bridge keep all three: a server cannot tell whether Claude was launched with Channels, and without the flag channel messages are dropped silently.

Per-Client Setup

ClientWhat Push Relay uses
Claude CodeSTDIO bridge → Claude Code Channels, plus fallback tools
OpenClawMCP Events webhook delivery
CodexFallback tools (multi-shot EventWait)
CursorFallback tools (multi-shot)
OpenCode, GooseFallback tools (one-shot)
Droid, Pi, OnyxFallback tools
Claude Desktop, claude.aiFallback tools

Cloud-hosted clients such as ChatGPT cannot reach LocalRouter on 127.0.0.1 and are out of scope.

Claude Code (STDIO Bridge + Channels)

Claude Code (STDIO Bridge + Channels)

Claude Code receives pushed events through Channels, which has two requirements:

  • Channels is STDIO only. Claude Code accepts channel messages only from an MCP server it launched as a subprocess, not from an HTTP server.
  • Channels requires the pre-2026-07-28 handshake. Claude Code does not register a channel server that connects on MCP revision 2026-07-28. The LocalRouter STDIO bridge declines that revision toward Claude Code and connects on the earlier initialize handshake, so it registers as a channel. This applies to LocalRouter alone; nothing needs to be set for the rest of the Claude session.

LocalRouter over HTTP can still serve Claude Code's tools, but cannot wake Claude when an event arrives. That is why the Claude Code template uses the STDIO bridge by default:

"localrouter": {
  "type": "stdio",
  "command": "<LocalRouter binary>",
  "args": ["--mcp-bridge", "--client-id", "<client id>"],
  "env": { "LOCALROUTER_CLIENT_SECRET": "<secret>" }
}

While Channels is a research preview, Claude Code must be launched with the development-channels flag naming the server:

claude --dangerously-load-development-channels server:localrouter
  • The flag works in interactive sessions only; it is ignored with -p and by the Agent SDK.
  • An organisation's channelsEnabled policy can block it.
  • Without the flag, channel messages are dropped silently. LocalRouter cannot detect the flag, which is why the fallback tools stay visible for Claude Code too.
  • After adding a push-capable upstream server, restart claude.

Existing HTTP configurations are not rewritten; the client page offers a "switch to STDIO bridge" action, and the HTTP setup remains available as a tools-only alternative.

OpenClaw (Webhook)

OpenClaw (Webhook)

OpenClaw's mcp-events plugin subscribes by webhook. It calls events/subscribe on LocalRouter with a callback URL and secret; LocalRouter obtains the events from upstream however the upstream offers them, runs them through permissions, Ask and Monitor, and POSTs each one with Standard Webhooks signatures (webhook-id equal to the event id). A new callback URL is verified with a challenge and asks for approval before its first delivery. Subscriptions are leases the client refreshes; deliveries happen only while LocalRouter is running.

Codex, Cursor and Others

Codex, Cursor and Others

No other shipping client implements MCP Events today, so they use the fallback tools. Agents that can watch a background command (Codex, Cursor, Cline, Claude Code's Monitor) run EventWait's multi-shot command; agents that can only block on a command (OpenCode, Goose, Copilot, Gemini CLI) use the one-shot form. When a client gains native MCP Events support, it can call events/stream or events/poll directly and the tools step aside.

Testing Pushes

Try it out → MCP has a Push panel for testing pushes end to end without any upstream server that offers push. Connect against a client (on the client's own Try It Out tab, or in Against Client mode) and open Push:

  • Advertised to this session shows what the gateway offered this session: the MCP Events extension, the top-level events capability, and whether the fallback tools are present. Claude Code Channels (claude/channel) is shown as not offered: it is offered only to STDIO bridge sessions, never to this HTTP session.
  • Event types is the session's events/list: each server-qualified name with its description and delivery modes.
  • Receive starts delivery of the selected event type in one of three modes: Push holds an events/stream open and shows its active, heartbeat and event notifications live (Stop cancels the request with notifications/cancelled); Poll calls events/poll with a cursor, by hand or automatically at the server's nextPollMs; Fallback tools calls EventPoll (with an optional wait_seconds) or EventWait, whose watcher commands can be copied.
  • Received logs every occurrence once, by event id, with the modes it arrived through, its cursor and its data; dropped history (truncated) is marked.
  • Claude Code channel preview shows the notifications/claude/channel content and meta a STDIO bridge session would receive for an event, rendered by LocalRouter itself.

Test event source. Send test event sends localrouter__test.event from LocalRouter's built-in test source with a message or JSON data of your choice. It goes through the real pipeline: the client's permission for it (set Allow, Ask or Off right in the panel — Ask opens the normal approval popup, where it can be edited), a Monitor record, the client's event history, and every delivery form. Only sessions that opted into push testing ever see the test source — Try it out's connection opts in, and so does a STDIO bridge started with LOCALROUTER_PUSH_TEST=1 in its environment, for testing Channels with a real Claude Code. For every other session it does not exist: it never makes push available to a client, and never appears in an app's capabilities, instructions, tools or events/list. In a client's push permissions it is listed as LocalRouter test source once a permission is set for it.