The original design had Claude Code return a JSON array of tool calls via StructuredOutput. This worked, but was limiting — Claude had to batch all tool calls into a single response, couldn't chain results, and complex JSON parsing was error-prone.
MCP solves all of these. Claude Code makes HTTP calls to a local server during its turn, gets results back, and can chain tool calls naturally. Within a single turn, the bot can send multiple messages, check the database, generate an image, and react to a message — all as separate MCP calls interleaved with reasoning. The harness runs the MCP server, so tool execution is still controlled by Rust code.
The MCP server binds to a random localhost port. The port is passed to Claude Code via --mcp-config. Tools are pre-authorized with --allowedTools mcp__claudir-tools so CC does not prompt for permission on each call.
When Claude Code makes MCP calls, it does not write to stdout. The worker thread tracks liveness via a last_activity_at timestamp that updates on each stdout JSON line. During a long MCP tool call (image generation can take 10+ seconds), this timestamp goes stale, and the health monitor might conclude CC is unresponsive.
Solution: shared AtomicU64 heartbeat timestamp, passed to both the MCP server and Claude Code. Every MCP tool call updates it:
fn touch_heartbeat(&self) {
if let Some(ref ts) = self.config.last_activity_at {
ts.store(now_secs, Ordering::SeqCst);
}
}Now the health monitor sees activity regardless of whether CC is writing to stdout or making MCP calls.
Messaging: send_message, edit_message, delete_message, send_voice, send_animation, send_poll, stop_poll, add_reaction. These wrap the Telegram Bot API via the TelegramApi trait, enabling testing with mock implementations.
User/Group Management: mute_user, ban_user, kick_user, unban_user, get_chat_admins, get_members, get_user_info.
Database: query (SQL SELECT only — enforced server-side). Returns max 100 rows with text truncated to 2000 chars.
Memory System: create_memory, read_memory, edit_memory, list_memories, search_memories, delete_memory. Persistent file storage that survives across sessions.
Reminders: set_reminder, list_reminders, cancel_reminder, edit_reminder. Stored in SQLite with support for one-time, cron-based periodic, and token-threshold triggers.
Focus/Mute: mute_chat, unmute_chat, mute_dms, unmute_dms, switch_focus, get_focus_state, peek_chat. Controls which chats the bot pays attention to.
Media Generation: gen_image (Gemini API), render_html (headless browser to PNG), preview_image.
Geocoding/Maps: yandex_geocode, yandex_map (static map generation).
Research: page_search (semantic search over chat history via PageIndex server).
Billing: send_invoice, credit_stars, get_star_balance, get_star_transactions.
Meta/Admin: now, pause, report_issue, reset_session, restart_harness, get_heartbeats, set_chat_alias.
The memory system provides file-based persistent storage — user notes, reflections, skill definitions, configuration. The MemoryStore struct manages a memories/ directory with security-hardened path resolution:
pub fn resolve_path(&self, relative_path: &str) -> Result<PathBuf> {
// Reject path traversal components (..)
// Reject absolute paths
// Reject symlinks in path (prevents symlink-based traversal)
self.reject_symlinks_in_path(&full_path)?;
// Validate canonical path is within allowed directories
self.validate_path_within_allowed_dirs(parent)?;
// ONLY NOW create directories
}The security checks happen in a specific order: path component validation, symlink rejection, canonical path validation, and only then directory creation. This prevents an attack where a symlink placed in the path redirects create_dir_all outside the memories directory.
Edit operations require prior read — tracked via canonical paths in a HashSet<String>. This prevents blind edits and ensures the bot sees the current state before modifying.
Send message rate limiter: 20 messages per 60 seconds per chat, tracked per-chat.
Sensitive keyword filter: Messages to public chats are checked against a word-boundary regex. Private groups bypass this filter.
Username confirmation: Before sending messages to public chats, all @username mentions are checked against an allow-list. Unknown usernames trigger a confirmation dialog — the tool returns a prompt asking the bot whether the mention reveals private information, and the bot must explicitly confirm via confirm_send before the message is delivered. This prevents accidental doxxing while still allowing legitimate username mentions.
SSRF protection: preview_image resolves URLs and checks against private IP ranges before fetching. This prevents the bot from being tricked into fetching internal services.
Telegram retry with fallback: When send_message fails because the replied-to message was deleted, the tool automatically retries without reply_to_message_id.
The entire MCP layer exists because of a fundamental security constraint: Claude Code must not have arbitrary code execution when processing untrusted user input. Every tool call goes through Rust code that validates parameters, enforces rate limits, checks permissions, and sanitizes output. The model gets a well-defined API surface — not a shell.
This is the core architectural insight: trust the model to decide what to do, but verify and execute how in trusted Rust code.

