Primary Role
Connect channel plugins (Discord, Slack, WhatsApp, etc.) with the MCP tool system via conversation binding.
Bridging channel-specific plugins with MCP tool capabilities and conversation binding
The channel bridge connects channel adapters (Discord, Slack, WhatsApp, and 20+ more) with the MCP tool system. It resolves which plugin owns a given channel, binds the plugin’s identity to the gateway request scope, and ensures that channel-scoped tools are available to agents processing messages from that channel.
Conversation Binding Channel Scoping Plugin Identity Gateway Integration
When a message arrives from Discord or Slack, the agent needs to know which plugin owns that channel and what tools that plugin provides. The channel bridge is the routing layer — it maps each incoming message to a plugin identity and ensures the agent has the right tools for the job.
Think of a large hotel (the platform) with multiple concierge desks (channel plugins) — one for business guests (Slack), one for leisure guests (Discord), one for international guests (WhatsApp). When a guest (message) walks in, the lobby receptionist (channel bridge) identifies which concierge desk handles their booking, puts on the right name badge (plugin identity binding), and gives them the right set of room keys (channel-scoped tools). This way, the concierge can only access rooms and services relevant to their guests.
Connect channel plugins (Discord, Slack, WhatsApp, etc.) with the MCP tool system via conversation binding.
Channel plugin registrations, conversation context, gateway request scope.
How to bind plugin identity to gateway requests, when to allow gateway subagent binding, how channel plugins expose tools.
Channel-scoped tools available to agents processing messages from specific channels.
flowchart TD
subgraph Channels["Channel Plugins"]
Discord["Discord Plugin"]
Slack["Slack Plugin"]
WhatsApp["WhatsApp Plugin"]
More["20+ more"]
end
subgraph Binding["Conversation Binding"]
ConvBind["conversation-binding.ts"]
ChannelIDs["channel-plugin-ids.ts"]
Scope["gateway-request-scope.ts"]
end
subgraph Bridge["Tool Bridge"]
PluginRT["Plugin Runtime"]
ToolFactory["Tool Factory"]
AgentTools["Agent Tool Set"]
end
Discord --> ConvBind
Slack --> ConvBind
WhatsApp --> ConvBind
More --> ConvBind
ConvBind --> ChannelIDs
ChannelIDs --> Scope
Scope --> PluginRT
PluginRT --> ToolFactory
ToolFactory --> AgentTools
Each channel plugin registers itself during loading. When a message arrives, the conversation binding layer identifies the owning plugin, binds its identity to the gateway request scope, and passes context through the tool factory pipeline so the agent receives the correct channel-scoped tools.
Channel bridging operates in three stages: registration, identity resolution, and scope binding.
Channel plugins register via api.registerChannel(channelPlugin) during plugin loading. This stores a mapping from channel type identifiers to the plugin that handles them. Each channel plugin declares its supported channel types and any tools it provides.
// channel-plugin-ids.ts — registration mapping
const channelPluginMap = new Map<string, string>();
export function registerChannelPlugin(
channelType: string,
pluginId: string,
): void {
if (channelPluginMap.has(channelType)) {
throw new Error(
`Channel type "${channelType}" already claimed by plugin "${channelPluginMap.get(channelType)}"`,
);
}
channelPluginMap.set(channelType, pluginId);
}
export function getPluginIdForChannel(channelType: string): string | undefined {
return channelPluginMap.get(channelType);
}
When a message arrives, conversation-binding.ts resolves which channel plugin handles it by looking up the channel type in the channelPluginMap. The resolved pluginId is then passed to the scope binding layer.
// conversation-binding.ts — identity resolution
export async function resolveConversationBinding(
message: IncomingMessage,
): Promise<ConversationBinding> {
const channelType = message.channelType;
const pluginId = getPluginIdForChannel(channelType);
if (!pluginId) {
throw new Error(`No plugin registered for channel type: "${channelType}"`);
}
return {
pluginId,
channelType,
conversationId: message.conversationId,
metadata: message.channelMetadata,
};
}
withPluginRuntimePluginIdScope() in gateway-request-scope.ts binds the plugin’s identity to the gateway request. This scoping ensures that all tool calls made during agent execution carry the correct plugin identity, so MCP servers declared by the channel plugin’s bundle are accessible through the bridge.
// gateway-request-scope.ts — scope binding
export function withPluginRuntimePluginIdScope<T>(
pluginId: string,
fn: () => T,
): T {
const previousScope = currentPluginScope;
currentPluginScope = pluginId;
try {
return fn();
} finally {
currentPluginScope = previousScope;
}
}
sequenceDiagram participant Chan as Channel Adapter participant Bind as conversation-binding.ts participant IDs as channel-plugin-ids.ts participant Scope as gateway-request-scope.ts participant RT as Plugin Runtime participant Agent as Agent Chan->>Bind: Incoming message Bind->>IDs: Resolve plugin ID for channel IDs-->>Bind: pluginId Bind->>Scope: withPluginRuntimePluginIdScope(pluginId) Scope-->>RT: Scoped runtime context RT->>Agent: Provide channel-scoped tools Agent->>Agent: Execute with tools
The sequence above traces a single incoming message from a channel adapter through the full binding pipeline. The plugin identity is resolved once per message, and the scope is held for the duration of agent execution.
Channel ID to plugin ID mapping — the channelPluginMap is populated during plugin loading. Each channel type (e.g., "discord", "slack") maps to exactly one plugin ID. Lookups are O(1) via Map.get().
const pluginId = channelPluginMap.get(message.channelType);
if (!pluginId) {
throw new Error(`No plugin registered for channel: "${message.channelType}"`);
}
Gateway request scope binding — withPluginRuntimePluginIdScope() sets the active plugin identity for the duration of the callback. This is a synchronous context manager pattern — the scope is restored in the finally block even if the callback throws.
const result = withPluginRuntimePluginIdScope(pluginId, () => {
// All tool calls inside this scope carry pluginId
return resolvePluginTools({ context, allowlist, existingToolNames });
});
Conversation binding resolution — the onConversationBindingResolved event fires after successful binding, allowing other subsystems (logging, analytics, rate limiting) to react to the resolved channel context.
const binding = await resolveConversationBinding(message);
eventBus.emit('onConversationBindingResolved', {
pluginId: binding.pluginId,
channelType: binding.channelType,
conversationId: binding.conversationId,
});
| Scenario | Behavior | Code Location |
|---|---|---|
| Unknown channel ID | Error thrown: No plugin registered for channel type; message is rejected at the binding layer | conversation-binding.ts |
| Plugin not enabled for channel | Channel type is not in the map (plugin didn’t register); same error as unknown channel ID | channel-plugin-ids.ts |
| Gateway subagent mode without binding | Falls back to gatewaySubagentPluginId from context; scope is set directly without channel lookup | gateway-request-scope.ts |
| Multiple plugins claim same channel | Second registration throws an error during plugin loading; the first plugin wins | registerChannelPlugin() |
src/plugins/
├── conversation-binding.ts ← Resolves incoming messages to plugin identity
├── channel-plugin-ids.ts ← Channel type → plugin ID mapping registry
├── gateway-request-scope.ts ← withPluginRuntimePluginIdScope() context manager
├── plugin-runtime.ts ← Plugin runtime that invokes tool factories in scope
└── channels/
├── discord.ts ← Discord channel adapter plugin
├── slack.ts ← Slack channel adapter plugin
├── whatsapp.ts ← WhatsApp channel adapter plugin
└── ... ← 20+ additional channel adapters