--dangerously-load-development-channels does not register bare server: channels (inbound notifications silently dropped)

Status Open
Maintainer reply None cached
Activity 8 comments · opened Jun 27, 2026

Summary

A .mcp.json MCP server that declares capabilities.experimental["claude/channel"] connects fine and its tools work (outbound reply succeeds), but inbound channel notifications are silently dropped when the session is launched with --dangerously-load-development-channels server:<name>.

Every session, --debug logs:

MCP server "myserver": Channel notifications skipped: server myserver not in --channels list for this session

This is the kind:"session" gate — the server has already passed the capability check, the tengu_harbor feature-flag gate, auth (claude.ai OAuth, confirmed logged in), and the org-policy gate. It fails purely because the dev-flag server was never registered into the session channel list.

Decompiling the binary (v2.1.195) shows the cause: the startup flag-assembly code parses the --dangerously-load-development-channels list but never registers it — it only registers the --channels list and then sends the dev-flag list to telemetry. The result is that --dangerously-load-development-channels is effectively a no-op for a bare server:<name> channel: outbound works, inbound is never delivered, and there is no working configuration on this version.

Environment

  • Claude Code v2.1.195 (current latest as of this report)
  • macOS, arm64
  • Authenticated via claude.ai OAuth (not an API key), logged in and confirmed
  • A third-party MCP server (declared in <project-root>/.mcp.json) that advertises the claude/channel capability. This class of server exists in the wild as community channel bridges (Slack/Telegram/Discord, etc.); the bug reproduces with any bare server: channel.

Repro steps

  1. Add a minimal stdio MCP server to <project-root>/.mcp.json that advertises the channel capability:

``json
{
"mcpServers": {
"myserver": {
"command": "node",
"args": ["./my-channel-server.js"]
}
}
}
``

The server, in its initialize response, declares:

``json
{
"capabilities": {
"experimental": { "claude/channel": {} }
}
}
``

and exposes an outbound reply tool.

  1. Launch Claude Code with the development-channels flag:

``
claude --dangerously-load-development-channels server:myserver --debug
``

  1. Observe:
  • The server connects, and its tools (including outbound reply) work normally.
  • No inbound channel notification (notifications/claude/channel) is ever delivered to the conversation.
  • The debug log shows the skip line every session (see Debug evidence).

Expected vs Actual

Expected: Passing --dangerously-load-development-channels server:myserver registers myserver as a development channel for the session, so inbound notifications/claude/channel messages are delivered to the conversation (this is the documented local-dev path for channels).

Actual: Inbound notifications are silently skipped every session. The server is never added to the session channel list, so the session gate never matches it. Outbound tools still work, which makes the failure look like a connectivity issue when it is actually a registration gap.

Debug evidence

With --dangerously-load-development-channels server:myserver:

MCP server "myserver": Channel notifications skipped: server myserver not in --channels list for this session

If you instead ALSO pass --channels server:myserver (so it does get registered), it clears the session gate but then fails the next gate, because the entry is registered as non-dev and a bare server: channel has no settings-based allowlist:

MCP server "myserver": Channel notifications skipped: server myserver is not on the approved channels allowlist (use --dangerously-load-development-channels for local dev)

...even though --dangerously-load-development-channels server:myserver is also present on the same command line — because that flag's bypass never takes effect (its entries were never registered as dev entries).

Root cause

From the de-minified v2.1.195 startup flag-assembly code. Both flags are parsed with the same tokenizer, but only --channels is registered into the session channel list; the dev-flag list is parsed and then used only for telemetry:

const channels    = opts.channels;                              // --channels
const devChannels = opts.dangerouslyLoadDevelopmentChannels;    // --dangerously-load-development-channels

let parsedChannels = [];
if (channels?.length) {
  parsedChannels = parseChannels(channels, "--channels");
  registerChannels(parsedChannels);                              // ← registered
}

let parsedDev;
if (!isNonInteractive && devChannels?.length) {
  parsedDev = parseChannels(devChannels, "--dangerously-load-development-channels"); // ← parsed...
}

if (parsedChannels.length || parsedDev?.length) {
  telemetry("tengu_mcp_channel_flags", { /* counts + plugin names only */ });        // ← ...only used here
}
// NOTE: there is no registerChannels(parsedDev) call — the dev-flag entries never enter the session channel list.

The session-gate matcher then looks the server up in the registered list and finds nothing:

function findEntry(name, list) {
  const parts = name.split(":");
  return list.find(e =>
    e.kind === "server"
      ? name === e.name
      : parts[0] === "plugin" && parts[1] === e.name);
}
// list never contains the dev-flag server → returns undefined → skip kind:"session"

The downstream allowlist gate confirms the consequence: when the entry is registered (via --channels) it is marked non-dev, so it fails the allowlist gate, and for a bare server: channel there is no settings-based allowlist (only allowedChannelPlugins exists, which is plugin-only). So neither path works.

Impact / why there is no workaround on this version

  • --dangerously-load-development-channels server:<name> is a no-op for inbound on bare server: channels — its parsed entries are never registered, so the session gate always skips.
  • Adding --channels server:<name> registers the entry but as non-dev, so it then fails the approved-allowlist gate.
  • There is no allowedChannelPlugins equivalent for bare server: channels (that setting is plugin-only), so the allowlist gate cannot be satisfied either.

Net: on v2.1.195 there is no working configuration to receive inbound channel notifications for a bare server: channel. The documented local-dev path is broken.

Suggested fix

Register the parsed dev-flag entries into the session channel list, marked as development entries so they bypass the approved-allowlist gate — i.e. add the missing call:

if (!isNonInteractive && devChannels?.length) {
  parsedDev = parseChannels(devChannels, "--dangerously-load-development-channels");
  registerChannels(parsedDev, { dev: true });   // ← currently missing
}

With the entries registered as dev, they clear the kind:"session" gate (now present in the list) and bypass the allowlist gate (marked dev), restoring the intended local-dev behavior.

Possibly related

  • #36503 — --channels plugin shows "Channels are not currently available" but inbound notifications are ignored (open; plugin-path variant of an inbound-drop).
  • #51845 — --channels flag never matches plugin server identifier, channel notifications always skipped (closed; same family of session-gate/identifier-matching failures, plugin-scoped).

View original on GitHub ↗

3 Comments

brenoperucchi · 2 months ago

Independent confirmation on Linux (not macOS-only) — still present in v2.1.197

Confirming this exact bug from a second, unrelated MCP server. The root-cause decomposition above matches what we see; adding two data points that may help triage:

1. It's not macOS-specific. This issue is labeled platform:macos, but we reproduce the identical failure on Linux x86_64 (Arch, kernel 7.0.9), Claude Code v2.1.197 (two patch releases after the reported v2.1.195), same claude.ai OAuth auth mode. The --dangerously-load-development-channels server:<name> registration gap is platform-independent — suggest widening the label.

2. Different consumer class, same failure. Our server (mcp-agent-relay) isn't a chat bridge — it's a job relay: one Claude session dispatches async work to another agent and expects a push (notifications/claude/channel) when the job reaches a terminal state. Outbound tools (dispatch/poll) work perfectly; the inbound channel push is silently dropped with the same debug line:

MCP server "agentrelay": Channel notifications skipped: server agentrelay not in --channels list for this session

Launched via claude --dangerously-load-development-channels server:agentrelay. Server declares capabilities.experimental["claude/channel"] and emits notifications/claude/channel on terminal transitions — never delivered.

3. Workaround for anyone blocked today. Since the push path is dead, we moved wake-up to a pull-side Stop hook: SessionStart seeds a baseline of already-seen jobs, and Stop/SubagentStop surface any new terminal transitions (with an optional bounded long-poll so the session waits for an in-flight job instead of settling early). No dev flag, no per-session dialog, works with both claude mcp add and plugin installs. Not as clean as a real push, but it's a functional substitute until the registration gap is fixed.

Happy to share a minimal relay repro if useful.

sankara-shakti · 1 month ago

Confirming for 2.1.205

Channel notifications never registered for server: entries, even with the documented flag combination

Summary

A channel MCP server (declared via .mcp.json, connected via --dangerously-load-development-channels server:<name> and --channels server:<name>) connects successfully, but notifications/claude/channel sent by that server are silently dropped. --debug mcp shows:

Channel notifications skipped: server <name> is not on the approved channels allowlist (use --dangerously-load-development-channels for local dev)

— even though --dangerously-load-development-channels was passed with that exact server name. This makes the channels feature (research preview) completely non-functional for any locally-developed server:-kind channel, as documented in https://code.claude.com/docs/en/channels-reference.

Environment

  • Claude Code: 2.1.205 (also reproduced on 2.1.204 before an auto-update)
  • Node: v20.20.2
  • OS: Linux, 6.17.0-35-generic (Ubuntu 24.04)
  • @modelcontextprotocol/sdk: 1.29.0

Minimal repro

This is Anthropic's own "webhook receiver" example from the channels reference docs, trimmed to the minimum needed:

// repro.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const mcp = new Server(
  { name: "repro", version: "0.0.1" },
  {
    capabilities: { experimental: { "claude/channel": {} } },
    instructions: "Events from this channel arrive as <channel source=\"repro\" ...>.",
  },
);

await mcp.connect(new StdioServerTransport());
console.error("[repro] connected over stdio");

setTimeout(() => {
  mcp.notification({
    method: "notifications/claude/channel",
    params: { content: "hello from the repro channel" },
  });
  console.error("[repro] pushed a channel notification");
}, 3000);
// .mcp.json, in the same directory
{
  "mcpServers": {
    "repro": { "command": "npx", "args": ["tsx", "repro.ts"] }
  }
}

Run:

claude --channels server:repro --dangerously-load-development-channels server:repro --debug mcp --debug-file /tmp/repro-debug.txt

Accept the "New MCP server found" prompt if shown. Wait a few seconds, then check the debug log:

grep "repro" /tmp/repro-debug.txt

Actual result

[DEBUG] MCP server "repro": Successfully connected (transport: stdio) in 348ms
[DEBUG] MCP server "repro": Connection established with capabilities: {"hasTools":false,...}
[DEBUG] MCP server "repro": Channel notifications skipped: server repro is not on the approved channels allowlist (use --dangerously-load-development-channels for local dev)

The <channel source="repro" ...> tag never appears in the session — Claude never sees the pushed notification.

Expected result

Per the channels reference:

claude --dangerously-load-development-channels plugin:yourplugin@yourmarketplace / claude --dangerously-load-development-channels server:webhook

should be sufficient to load a local development channel and have its notifications delivered. Instead, the server-kind entry is treated as not dev-approved even when explicitly passed to --dangerously-load-development-channels.

Notes from investigating further

  • Passing --dangerously-load-development-channels server:repro alone (no --channels) produces a different skip reason: server repro not in --channels list for this session — meaning the dev flag alone never adds the entry to the session's effective allowed-channels list at all.
  • Passing both flags with the same server:repro value (the combination the docs suggest) produces the "not on the approved channels allowlist" skip shown above — i.e. an entry is found, but it's not marked as dev-approved.
  • This is not caused by an unanswered confirmation dialog: reproduced identically in (a) a fully interactive session with every prompt manually approved by a human, and (b) a non-interactive session with the project pre-trusted and the MCP server pre-approved via enabledMcpjsonServers in ~/.claude.json (no prompts pending at all). Same skip message in both.
  • Reading the bundled CLI source (minified, node_modules-style build), the relevant lookup is roughly list.find(entry => matches(name, entry)), and entries appear to be appended to the effective list separately for --channels and --dangerously-load-development-channels. If both flags produce a separate entry for the same server name, and the non-dev entry happens to be checked/found first, the dev flag's entry is effectively never consulted. This is a guess based on reading obfuscated output, not confirmed against source — but it would fully explain the observed behavior.
  • Everything downstream of channel-notification delivery works correctly — this isn't a broader MCP or connection problem. A real HTTP-triggered agent workflow built against this channel correctly reaches its own request-handling code, and the only missing link is Claude Code delivering the notification in the first place.

Impact

The channels research-preview feature is unusable for any server:-kind (i.e. non-plugin, locally developed) channel on this build, despite following the documented setup exactly. This blocks building and testing custom channel integrations entirely, since even the canonical docs example doesn't work as written.

Ashr4f · 1 month ago

Same behavior with the plugin: form on 2.1.220 (Windows 11). A marketplace-installed channel plugin launched with --dangerously-load-development-channels plugin:<name>@<marketplace> connects, its tools work, outbound reply works, but inbound notifications are dropped with "Channel notifications skipped: server plugin:<name>:<server> not in --channels list for this session". The plain --channels flag warns the plugin is not on the approved allowlist and points at this dev flag, and allowedChannelPlugins in managed settings changes nothing. So the flag currently isn't an escape hatch for either the server: or the plugin: form. Filed #82571 for the broader allowlist question.

Showing cached comments. Read the full discussion on GitHub ↗