| name | channel-connected-artifacts |
| description | Use when an interactive artifact needs to communicate directly with Claude Code — sending pinned comments automatically, triggering MCP queries, or updating the UI without manual copy-paste. Triggers include "artifact talks to Claude", "live data in HTML", "connect to PostHog/Stripe", "skip the export-paste step", or building a custom internal dashboard powered by Claude as backend. |
Channel-Connected Artifacts
Overview
Wire a Bun interactive artifact directly to Claude Code via the Claude Code channels feature — the same mechanism used for Telegram, Discord, and iMessage. The artifact becomes a frontend; Claude Code becomes the backend.
No more export-paste-wait. Click in the UI, leave a pinned comment, Claude immediately receives it, queries your MCP servers if needed, edits the artifact, and the browser hot-reloads.
A channel is just an MCP server with one extra capability flag — claude/channel. Claude Code already spawns MCP servers and already holds a bidirectional JSON-RPC pipe over stdio with each one. A channel reuses that pipe to push events into the running agent.
Core architecture:
Browser (artifact.html)
│ WebSocket ws://localhost:3000/__channel
▼
Bun server (server.ts — an MCP server using @modelcontextprotocol/sdk)
│ MCP stdio — mcp.notification("notifications/claude/channel", ...)
▼
Claude Code agent (sees <channel source="..." ...>)
│ edits artifact.html, queries MCP servers if needed
▼
Browser reloads with updated content
REQUIRED PREREQUISITE: Understand interactive-bun-artifacts (Layer 2) first. Layer 3 builds on it.
Authoritative docs: https://code.claude.com/docs/en/channels-reference
When to Use
- Artifact depends on live data from MCP servers (PostHog, Stripe, databases)
- You want pinned comments to automatically trigger Claude without copy-paste
- The artifact is approaching internal tool or product territory
- You need Claude to run bash, fetch files, or query APIs in response to UI actions
- You are building a custom dashboard and want Claude as the data/logic layer
Do NOT use when:
- No MCP servers or external data are involved (Layer 2 suffices)
- One or two rounds of changes is all you need
- The team isn't comfortable with the
--dangerously-load-development-channels flag
How Claude Code Channels Work
Channels are in research preview; they require Claude Code v2.1.80+ and a claude.ai login. During the preview, an approved allowlist (Telegram, Discord, iMessage, fakechat) auto-registers. Anything you build yourself needs the development flag:
claude --dangerously-load-development-channels server:channel-name
server:channel-name matches an entry in .mcp.json. Claude Code spawns that MCP server on startup and registers a listener for its notifications/claude/channel events.
Architecture in Detail
┌──────────────────────────────────────────────────────┐
│ Browser: artifact.html │
│ - Dashboard / design / explainer │
│ - Pin Comment UI on each element │
│ - Sends pinned comments over WebSocket │
└─────────────────┬────────────────────────────────────┘
│ WebSocket (JSON payload)
┌─────────────────▼────────────────────────────────────┐
│ server.ts (Bun + MCP SDK) │
│ - Bun.serve: HTTP + WebSocket on localhost:3000 │
│ - MCP Server: declares claude/channel capability │
│ - On WS message: mcp.notification( │
│ 'notifications/claude/channel', │
│ { content, meta }) │
└─────────────────┬────────────────────────────────────┘
│ JSON-RPC over stdio
┌─────────────────▼────────────────────────────────────┐
│ Claude Code agent │
│ - Receives <channel source="..." ...>event</channel>│
│ - Has access to: MCP servers, Bash, codebase │
│ - Interprets + fetches data + rewrites artifact.html│
└─────────────────┬────────────────────────────────────┘
│ file write
┌─────────────────▼────────────────────────────────────┐
│ Bun server: serves artifact.html no-store │
│ - Browser reload picks up new content │
└──────────────────────────────────────────────────────┘
One process, two transports, bridged in server.ts: Bun's HTTP/WebSocket server faces the browser, and the MCP SDK's StdioServerTransport faces Claude Code.
Setup Prompt: Channel-Connected Artifact
Use this prompt to have Claude build the full setup:
Using my data from [MCP server 1, e.g. PostHog] and [MCP server 2, e.g. Stripe],
create a channel-connected Bun artifact so that:
1. The artifact shows [describe what to visualize — customer journey,
funnel, dashboard, etc.]
2. I can click any element and leave a pinned comment.
3. The comment goes directly to you (Claude Code) via the channel — I should
NOT have to copy-paste.
4. You query the relevant MCP servers and update the artifact; the browser
hot-reloads automatically.
Implement the channel as a single MCP server using the official
@modelcontextprotocol/sdk — NO custom "channel plugin" interface,
NO @anthropic/claude-code-channels import (that package does not exist).
Required files:
- [name]/server.ts (Bun HTTP + WebSocket + MCP SDK)
- [name]/artifact.html (the dashboard with Pin Comment UI)
- [name]/package.json (depends on @modelcontextprotocol/sdk)
- .mcp.json (registers the server)
Give me the exact run command.
Complete Example: Customer Journey Dashboard
Scenario (from Ray's video): Explore customer journey data from PostHog and Stripe, visualize it, and ask follow-up questions by clicking on sections.
Step 1 — MCP data-source servers in .mcp.json:
{
"mcpServers": {
"posthog": {
"command": "npx",
"args": ["-y", "@posthog/mcp-server"],
"env": { "POSTHOG_API_KEY": "phx_..." }
},
"stripe": {
"command": "npx",
"args": ["-y", "@stripe/mcp-server"],
"env": { "STRIPE_SECRET_KEY": "sk_live_..." }
},
"customer-journey": {
"command": "bun"
The last entry registers the channel server alongside the data-source MCPs.
Step 2 — Prompt Claude using the setup prompt above.
Step 3 — Claude generates:
customer-journey/
server.ts — Bun + MCP SDK channel server
artifact.html — funnel dashboard with Pin Comment UI
package.json — declares @modelcontextprotocol/sdk dep
.mcp.json — registers customer-journey server
Step 4 — Install and launch:
cd customer-journey && bun install
claude --dangerously-load-development-channels server:customer-journey
Claude Code spawns server.ts automatically. The Bun HTTP listener starts on port 3000.
Step 5 — Open http://localhost:3000 and pin a comment on a funnel stage:
"Can you expand on this? Show me a Sankey diagram of where trial users go — are they churning or downgrading?"
Step 6 — In the Claude Code terminal, the event arrives as:
<channel source="customer-journey" stage="Trial → Paid">
Can you expand on this? Show me a Sankey diagram of where trial users go — are they churning or downgrading?
</channel>
Claude queries PostHog and Stripe (via their MCP servers), edits artifact.html to add the Sankey section, the browser reloads.
Step 7 — Iterate:
"Remove the revenue card."
"Add tabs for Acquisition / Activation / Retention / Revenue."
Each pin arrives as a <channel> event; Claude decides whether it's a data request or a UI change and acts accordingly.
The server.ts Pattern (Correct, MCP-SDK-Based)
Claude should generate this — shown here for debugging reference. This is the officially documented pattern from https://code.claude.com/docs/en/channels-reference:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const mcp = new Server(
{ name: "customer-journey", version: "1.0.0" },
{
capabilities: {
experimental: { "claude/channel": {} },
},
instructions:
"You receive pinned comments from the customer-journey dashboard. " +
"Events arrive as <channel source=\"customer-journey\" stage=\"...\">. " +
"When one arrives, update customer-journey/artifact.html; the browser " +
"hot-reloads on file save.",
},
);
await mcp.connect(new StdioServerTransport());
const server = Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/") {
(.(.. + ), {
: { : , : },
});
}
(url. === && server.(req)) {
;
}
(, { : });
},
: {
() {
text = raw === ? raw : raw.();
{ element, comment } = .(text);
mcp.({
: ,
: {
: comment,
: { : element },
},
});
ws.(.({ : }));
},
},
});
Key rules this pattern encodes:
- Use the official SDK.
@modelcontextprotocol/sdk is published on npm. Do NOT import from @anthropic/claude-code-channels — that package does not exist.
- The channel is the MCP server. There is no separate "channel plugin" file with
onMessage/onResponse. That pattern was a hallucination. The entire channel is one Server instance.
- Declare the capability.
capabilities.experimental["claude/channel"] = {} is what tells Claude Code this is a channel.
- Emit via
mcp.notification. The method is exactly notifications/claude/channel. Params: content (string — tag body) and meta (record of identifier-keyed strings — tag attributes).
- stdout is MCP-only. The SDK writes JSON-RPC to stdout. Any stray
console.log corrupts the stream. Use console.error for logs.
- One process. Bun's HTTP server and the MCP stdio transport run in the same process. Claude Code spawns it; don't run it in a separate terminal.
Required Files
| File | Purpose |
|---|
[name]/server.ts | Bun server + MCP channel server (the code above) |
[name]/artifact.html | Dashboard UI with Pin Comment WebSocket client |
[name]/package.json | Declares @modelcontextprotocol/sdk dependency |
.mcp.json | Registers [name] so Claude Code spawns it |
Minimal package.json:
{
"name": "[name]-channel",
"type": "module",
"private": true,
"dependencies": { "@modelcontextprotocol/sdk": "^1.0.0" }
}
Two-Way Channels (Optional)
The example above is one-way: browser → Claude, with the response coming back as a file edit + hot reload. If you need Claude to send messages back over the channel (e.g. a chat bridge), expose an MCP tool named reply with a standard inputSchema — the docs' "Expose a reply tool" section covers it. Most dashboard/artifact channels don't need this; the file-write + hot-reload loop is the feedback.
Complete Example: Internal Analytics Dashboard
Scenario: A persistent dashboard for SaaS metrics.
Build a channel-connected Bun artifact that serves as my daily analytics dashboard.
Data sources (via MCP data servers):
- PostHog: DAU, feature flag adoption, funnel completion rates
- Stripe: MRR, churn rate, new trials today, upgrades
Layout:
- Top row: 4 KPI cards (DAU, MRR, Churn %, Trials)
- Middle: Funnel visualization (signup → activation → paid)
- Bottom: Recent events feed (last 20 significant events)
Channel behavior:
- Clicking any KPI card and leaving a comment triggers a deep-dive query
- Clicking a funnel stage asks "what happened here?"
- Clicking the events feed asks Claude to correlate events with Stripe data
Architecture: single MCP server using @modelcontextprotocol/sdk, with Bun
HTTP + WebSocket. No separate channel plugin file.
Create:
analytics-dashboard/server.ts
analytics-dashboard/artifact.html
analytics-dashboard/package.json
update .mcp.json
Give me the run command.
Complete Example: Codebase Explorer
Scenario: Visually explore your codebase architecture with Claude as the query engine.
Create a channel-connected artifact that shows my codebase architecture.
- Visualize: major directories, key files, dependency relationships
- When I click a module and leave a comment like "explain this",
Claude reads the actual files and updates the artifact with an
explanation panel
- When I comment "show callers of X", Claude greps the codebase and
adds a callsite diagram
- Use filesystem (bash) + codebase context — no external MCP needed
Channel: single MCP server (server.ts) using @modelcontextprotocol/sdk.
Evolving the Artifact Into a Product
Once the channel artifact has a form factor you like:
The customer-journey dashboard feels like a real product. Can you:
1. Polish the UI — proper header, navigation, consistent spacing
2. Add basic auth (Bun middleware)
3. Make the layout responsive for 1440px and 1280px widths
4. Export a standalone version (artifact-standalone.html) that doesn't
need the channel — static with last-fetched data
5. Write a brief README for this internal tool
For wider distribution, channels can be packaged as plugins and published to a marketplace — /plugin install drops the --dangerously-load-development-channels flag entirely. See the plugins docs when you're ready.
Two Jobs the Artifact Does Simultaneously
At Layer 3 the artifact plays two roles at once:
-
Decision Capture — Records what you notice, want changed, or need explained.
Example: "Not enough users reach the pricing section — maybe move it up."
-
Conversational Control Surface — Becomes a live interface for steering Claude.
Example: Claude acts on "can you move up the pricing section" by editing both the artifact and (if you ask) the actual codebase.
Neither the browser nor the server branches on message type — every pin becomes a <channel> event with identical shape. Claude is the sole interpreter.
Tips
- Use the official SDK. Never invent a channel plugin interface.
@modelcontextprotocol/sdk is the only import you need. See https://www.npmjs.com/package/@modelcontextprotocol/sdk.
- Run
bun install first. The SDK is a real dependency.
- Don't start the server manually. Claude Code spawns it via
.mcp.json. A second instance collides on port 3000 and its stdio goes nowhere.
- Use meta for structured attributes.
meta: { stage: "Landing Page" } → <channel source="..." stage="Landing Page">.... Meta keys must be identifiers (letters/digits/underscores); hyphens etc. are silently dropped.
- Keep
server.ts thin. No business logic, no API calls — Claude (with its MCP tools) does the work.
- Log to stderr, not stdout. stdout is reserved for MCP JSON-RPC.
Common Mistakes
| Mistake | Fix |
|---|
Importing @anthropic/claude-code-channels | That package does not exist. Use @modelcontextprotocol/sdk. |
Hand-rolling the MCP protocol (manual JSON-RPC framing, initialize handshake) | Use the SDK's Server + StdioServerTransport. The SDK handles all of that. |
Separate channels/[name].ts plugin with onMessage/onResponse | Not a real pattern. The channel is the MCP server. One file: [name]/server.ts. |
| Channel messages not appearing | Verify the flag: --dangerously-load-development-channels server:[name]. Confirm [name] matches your .mcp.json entry. |
| Channel stays disconnected | Run /mcp in Claude Code. "Failed to connect" usually means an import error — check ~/.claude/debug/<session-id>.txt. |
| Hot reload not triggering | Artifact must be served with Cache-Control: no-store, or the browser caches stale HTML. |
| Port 3000 already in use | An old server instance is lingering; lsof -i :3000 and kill the PID before restart. |
| stdout corruption / silent disconnects | Something console.loged. Audit — all logs must go to console.error. |
@agent tag forgotten | Use @claude-code-guide or your configured agent for the right context. |
See Also