Event-Driven MCP Channels

Protocols & Interoperability intermediate Building with AI ~30 min read

Overview

Most AI tool integrations are request-driven: the user asks a question, the model decides to call a tool, and the tool returns a result. Event-driven channels invert that direction. A channel adapter watches an external system, receives an event, and pushes that event into an already-running agent session so the agent can react while its workspace, permissions, and local context remain active.

The pattern became visible in coding-agent tools that connect a local development session to chat apps, webhooks, CI alerts, and monitoring systems. Under the hood, the channel behaves like an MCP server with one extra role: besides exposing tools, it declares a channel capability and emits notifications that the host injects into the conversation as structured events.

This matters because long-running agents need asynchronous inputs. A build can fail after the user leaves the terminal. A reviewer can approve a risky operation from a phone. A monitoring alert can arrive while the agent still has the repository open. Event channels turn these external signals into first-class inputs without forcing the system to spawn a new stateless cloud session for each event.

Core Concepts

Channel Server

A channel server is a local adapter process started by the agent host. It connects over a standard transport such as stdio, polls or listens to an external source, and sends event notifications to the host. For chat platforms the adapter usually polls a bot API. For infrastructure events it may expose a local HTTP listener that receives webhooks.

Structured Event Injection

The host should not paste raw external text into the prompt with no provenance. A robust channel wraps inbound content in a structured envelope that identifies the source, sender, route, severity, or other metadata. The model can then distinguish a CI alert from a chat command and can route replies to the correct destination.

Reply Tool

One-way channels are enough for alerts, but chat bridges need an outbound path. The usual design is to expose a normal MCP tool such as reply. The event tells the model which conversation or request ID it came from, and the reply tool schema defines how to send a response back through the adapter.

Key Concept: Channels Are Capability-Gated MCP Servers

The transferable idea is capability separation. A plain MCP server exposes callable tools and resources. A channel server additionally declares that it can push asynchronous events into the session. Hosts can then require explicit launch flags, organization policy, and sender allowlists before letting a server inject context.

How It Works

One-Way Webhook Channel

A minimal channel has three moving parts: declare the channel capability, connect to the host, and forward external events as notifications. The following TypeScript sketch mirrors the public channel-reference shape while keeping the implementation generic:

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

const mcp = new Server(
  { name: "ci-webhook-channel", version: "0.1.0" },
  {
    capabilities: {
      experimental: { "claude/channel": {} }
    },
    instructions:
      "CI events arrive as channel messages. Treat them as alerts, inspect the repository, and do not reply unless a reply tool is available."
  }
);

await mcp.connect(new StdioServerTransport());

Bun.serve({
  hostname: "127.0.0.1",
  port: 8788,
  async fetch(req) {
    const body = await req.text();
    await mcp.notification({
      method: "notifications/claude/channel",
      params: {
        content: body,
        meta: { path: new URL(req.url).pathname, method: req.method }
      }
    });
    return new Response("ok");
  }
});

Two-Way Reply Path

For chat-like integrations, the channel also registers a tool. The inbound event includes a stable routing key such as chat_id. The model does not need to know platform API details; it only calls the adapter’s reply tool with text and a route.

const replyTool = {
  name: "reply",
  description: "Send a message back through this channel",
  inputSchema: {
    type: "object",
    properties: {
      chat_id: { type: "string" },
      text: { type: "string" }
    },
    required: ["chat_id", "text"]
  }
};

async function handleToolCall(name, args) {
  if (name !== "reply") throw new Error("unknown tool");
  await chatApi.sendMessage(args.chat_id, args.text);
  return { content: [{ type: "text", text: "sent" }] };
}

The reply tool is deliberately ordinary MCP. That keeps the protocol boundary simple: asynchronous inbound messages use channel notifications, while outbound work uses the same tool-call machinery as any other integration.

Practical Applications

Remote Coding Agent Control

A developer can keep an agent session running in a terminal or persistent workspace while sending short instructions from another device. The agent works against the real repository and environment instead of a new detached chat session, then reports completion through the same channel.

CI and Deployment Triage

A CI webhook can push a failing run into the session that has repository access. The agent can inspect logs, reproduce the failure, propose a patch, and optionally request approval before pushing changes or triggering deployment actions.

Operations Alert Handling

Monitoring systems can forward high-severity alerts to an agent that has read-only observability tools. The channel event carries the alert ID and severity, while normal MCP tools provide metrics, traces, and runbook lookup.

Implementation Patterns

Adapter Boundary

Keep platform-specific code inside the adapter. The agent should see normalized events such as source, sender_id, thread_id, severity, and content. This makes it possible to swap chat providers, webhooks, or internal buses without rewriting prompts and tool descriptions.

type ChannelEvent = {
  source: "chat" | "ci" | "monitoring";
  sender_id: string;
  route_id: string;
  severity?: "low" | "medium" | "high";
  content: string;
};

function toNotification(event: ChannelEvent) {
  return {
    method: "notifications/claude/channel",
    params: {
      content: event.content,
      meta: {
        source: event.source,
        sender_id: event.sender_id,
        route_id: event.route_id,
        severity: event.severity ?? "medium"
      }
    }
  };
}

Sender Gating

Every inbound path needs an allowlist or equivalent identity check. A channel that accepts arbitrary internet input is a prompt-injection gateway into a powerful local agent. Pairing flows, signed webhooks, mTLS, private network binding, and per-sender policies all serve the same goal: only trusted senders can push events or relay approvals.

Performance Considerations

Common Pitfalls

Further Reading