Personal AI assistant for Jack's Discord server. Powered by Claude (Anthropic
API) and fronted as a Discord bot. Designed to be portable, containerized, and
extensible — MCP tool servers can plug in via handler_bot/mcp_client.py.
Discord <-> handler_discord <-> handler_bot <-> Anthropic API (Claude)
|
+----> MCP servers (none wired today)
handler_bot/— Stateless Claude agent layer.agent.pyruns the async messages loop with prompt caching;mcp_client.pyis the multi-server MCP foundation. No conversation state lives here.handler_discord/— Discord layer.bot.pylistens for mentions/DMs;history.pyfetches recent channel messages and converts them to the Anthropic format (multi-speaker[Name]:prefix);chunker.pysplits long replies to fit the 2000-char message cap.scripts/run.py— Entry point. Wires everything together.
The bot is stateless: every turn it fetches the last N messages from the
Discord channel and passes them to Claude. Discord is the source of truth, so
the bot is restart-safe and can't drift out of sync. The system prompt is
prompt-cached (top-level cache_control) so per-turn cost stays low.
- Python 3.11+
- An Anthropic API key — https://console.anthropic.com/
- A Discord application + bot token
- Go to https://discord.com/developers/applications and create a new app.
- In Bot, click Reset Token to get the bot token (this goes in
.envasDISCORD_BOT_TOKEN). - Enable Message Content Intent under Privileged Gateway Intents — the bot needs this to read message text.
- In OAuth2 → URL Generator, select scopes
botandapplications.commands. For bot permissions pick:View Channels,Send Messages,Read Message History. (AddSend Messages in Threadsif you want thread support.) - Open the generated URL to invite the bot to your server.
make venv # creates .venv and installs the package
cp .env.template .env # then edit .env and fill in the secrets
make run # starts the botMention the bot in a channel (@Handler what's up?) or DM it directly.
cp .env.template .env # fill in secrets
make docker-up # build image + start container in background
make docker-logs # tail logs
make docker-down # stopThe container runs as a non-root user; no ports are exposed (the bot dials out to Discord over WebSocket).
All config lives in .env. See .env.template for the full list. The most
relevant knobs:
| Variable | Default | Notes |
|---|---|---|
ANTHROPIC_API_KEY |
— | Required |
DISCORD_BOT_TOKEN |
— | Required |
CLAUDE_MODEL |
claude-haiku-4-5 |
Or claude-sonnet-4-6 / claude-opus-4-7 |
CLAUDE_MAX_TOKENS |
8000 |
Per-response output cap |
CLAUDE_EFFORT |
high |
low / medium / high / xhigh / max |
HANDLER_HISTORY_FETCH_LIMIT |
30 |
Discord messages fetched as context per turn |
ALLOWED_USER_IDS |
(any) | Comma-separated; empty = anyone |
ALLOWED_GUILD_IDS |
(any) | Comma-separated; empty = any server |
Production runs on a Mac mini at home (zakia-server). A self-hosted GitHub
Actions runner on that host watches main; every push triggers
.github/workflows/deploy.yml which
rsyncs the new code to ~/handler-one-bot/, refreshes deps, and
restarts the bot. The bot's .env and .venv/ stay on the host and are
never touched by the workflow.
Replicating the runner on a new host: see docs/RUNNER_SETUP.md.
- Bot layer + Discord layer + Claude integration
- Self-hosted runner + auto-deploy on push
- Web search + web fetch (Anthropic server-side tools)
- Slash commands
handler-one-discord-bot/
├── handler_bot/ # Stateless Claude agent layer
│ ├── agent.py # Async messages loop, prompt caching, tool dispatch
│ ├── mcp_client.py # Multi-server MCP foundation
│ ├── prompt.py # System prompt
│ └── config.py # pydantic-settings env loader
├── handler_discord/ # Discord layer
│ ├── bot.py # discord.py client, mention/DM handling
│ ├── history.py # Fetches channel history, formats for Anthropic
│ └── chunker.py # 2000-char chunking with code-fence repair
├── scripts/run.py # Entry point
├── docs/
│ ├── architecture.md # Design notes
│ └── RUNNER_SETUP.md # Self-hosted runner installation guide
├── .github/workflows/
│ └── deploy.yml # Auto-deploy to zakia-server on push to main
├── Dockerfile # Multi-stage build, non-root runtime
├── docker-compose.yml # Local dev orchestration
├── GNUmakefile # make venv / run / docker-up / docker-logs
├── .env.template # Copy to .env and fill in
└── pyproject.toml # Dependencies + tool config