Welcome to Chapter 4!
In the previous chapter, Connection Lifecycle Management, we learned how to keep the "phone lines" open between our application and the servers. We handled connections dropping and reconnecting automatically.
Now that the line is open, we have a new challenge. What if we aren't sitting at our computer? What if we are talking to Claude via Slack, Discord, or Telegram?
This chapter covers Channel Notifications & Permissions. Think of this as a "Message Bridge" with a strict "Security Guard". It allows external apps to talk to Claude, but it ensures no one can trick Claude into doing something dangerous without your explicit permission.
Imagine you are using a Telegram bot connected to Claude to manage your cloud servers.
We need a system that:
yes tbxkq) to approve dangerous actions, so scripts cannot guess the answer.We will break this down into two parts: Inbound Notifications (The Bridge) and Permission Handshakes (The Guard).
Usually, Claude talks only when you talk to it. But a "Channel" (like a Slack bot) might receive a message from a colleague at any time. The Bridge wraps this message in a special format so Claude knows, "Hey, this came from Slack, not the user typing directly."
When a tool needs permission, we don't just ask "Yes/No". We generate a unique 5-letter code (e.g., abcde).
If a hacker sends "yes", it fails. If they guess "yes qwert", it fails. They must know the exact code shown to you.
Here is how a message travels from a chat app to Claude, and how a permission is verified.
Let's look at the code that powers this security system.
Not every server is allowed to interrupt you. We check a strict "Allowlist" before letting a server act as a channel.
This logic lives in channelNotification.ts.
// channelNotification.ts
export function gateChannelServer(
serverName: string,
capabilities: ServerCapabilities,
// ...
): ChannelGateResult {
// 1. Does the server claim to be a channel?
if (!capabilities?.experimental?.['claude/channel']) {
return { action: 'skip', reason: 'No capability declared' };
}
// 2. Is the user logged in securely?
if (!getClaudeAIOAuthTokens()?.accessToken) {
return { action: 'skip', reason: 'Requires auth' };
}
// 3. Is this server in the user's allowlist?
// ... checks allowlist ...
return { action: 'register' };
}
Explanation: Before listening to a server, we ensure it has the right capabilities (claude/channel) and that the user is authenticated. This prevents random tools from spamming your chat window.
When a message comes in, we wrap it in XML tags. This helps Claude distinguish between "System Instructions", "User Input", and "Channel Messages".
// channelNotification.ts
export function wrapChannelMessage(
serverName: string,
content: string,
meta?: Record<string, string>,
): string {
// Create attributes like source="discord"
const attrs = Object.entries(meta ?? {})
.map(([k, v]) => ` ${k}="${escapeXmlAttr(v)}"`)
.join('');
// Wrap the content
return `<${CHANNEL_TAG} source="${serverName}"${attrs}>\n${content}\n</${CHANNEL_TAG}>`;
}
Explanation: If a message comes from Discord, it gets transformed into <channel source="discord">Hello</channel>. This gives Claude context about where the text originated.
This is the core of the security system. We generate a short, random-looking ID for every permission request.
This logic is in channelPermissions.ts.
// channelPermissions.ts
export function shortRequestId(toolUseID: string): string {
// 1. Create a candidate ID based on the tool usage
let candidate = hashToId(toolUseID);
// 2. Safety Check: Ensure the random letters don't spell bad words
for (let salt = 0; salt < 10; salt++) {
if (!ID_AVOID_SUBSTRINGS.some(bad => candidate.includes(bad))) {
return candidate; // Safe to use!
}
// Try again with a different salt
candidate = hashToId(`${toolUseID}:${salt}`);
}
return candidate;
}
Explanation: We turn the tool request ID into a 5-letter code (like tbxkq). Crucially, we check against a blocklist (ID_AVOID_SUBSTRINGS) to ensure the random code doesn't accidentally spell something offensive before sending it to your phone.
When the user replies "yes tbxkq", the server sends a structured event back to the MCP app. We need to match that ID to the pending request.
// channelPermissions.ts
export function createChannelPermissionCallbacks() {
const pending = new Map<string, (response: Response) => void>();
return {
// 1. Verification Logic
resolve(requestId, behavior, fromServer) {
const key = requestId.toLowerCase();
const resolver = pending.get(key);
if (!resolver) return false; // ID not found or expired
pending.delete(key); // Remove it so it can't be reused
resolver({ behavior, fromServer }); // Unlock the tool!
return true;
}
}
}
Explanation: This acts like a ticket booth.
pending box. In this chapter, we learned:
<channel> tags for context.tbxkq) to prevent spoofing and ensure the human actually approved the specific action.Now that we can securely talk to external channels, we need to ensure that the data passing through these channels is clean and that the servers identify themselves correctly.
Next Chapter: Normalization & Identification Utilities
Generated by Code IQ