Build a Discord Bot With Claude Tool-Use: End-to-End Python Tutorial (May 2026)
Register a Discord app, wire discord.py to Claude tool-use with web-search and calculator tools, deploy on Railway's free trial — full Python walkthrough.
Image: Anthropic platform docs — Tool use with Claude overview (platform.claude.com), used for editorial coverage of the API surface discussed below.
TL;DR
This tutorial walks through a working Discord bot that answers
/ask slash commands by routing the question through Claude with
two tools wired in: a server-side web_search tool that Anthropic
runs 1 , and a client-side calculator tool that
the bot executes locally. The stack is Python 3.11+, discord.py
2.x for the Discord client, the official anthropic SDK, and
Railway’s free trial for hosting 2 . Total
walk-through time runs roughly 45 minutes for a developer
comfortable with pip and git.
Per Anthropic’s tool-use overview, client tools follow a loop:
Claude responds with stop_reason: "tool_use", your code executes
the call, you send back a tool_result content block, and Claude
either calls another tool or returns a final text answer 3 .
Server tools (like web_search) run on Anthropic’s
infrastructure, so the response already contains the tool result
without a round-trip through your code 1 .
What you’ll need
- Python 3.11 or newer with
pipavailable on PATH. - A Discord account and a server you control (creating a personal test server is free).
- An Anthropic API key (
console.anthropic.com); a fresh account ships with prepaid trial credit visible on the billing page. - A Railway account for hosting; Railway’s published trial grants a one-time $5 credit, expiring 30 days after signup, with no credit card required 2 . After the trial, the Free plan provides $1 of credit per month and does not roll over 4 .
Step 1 — Register the Discord application
Open the Discord Developer Portal at
discord.com/developers/applications and click New
Application. Name it claude-tool-bot (or anything you’ll
recognise) and accept the developer terms.
From the application page, two surfaces matter:
- Bot tab — reveal the token (you’ll only see it once; copy it). Newly created apps ship with a bot user enabled by default 5 .
- OAuth2 → URL Generator — tick
botandapplications.commandsscopes. Under bot permissions, tick Send Messages and Use Slash Commands. Copy the generated URL and open it; Discord prompts you to invite the bot to a server you have Manage Server permission on.
Image: Discord Developer Docs — Building your first Discord Bot quick-start (docs.discord.com), used for editorial coverage of the registration flow described.
Slash commands do not need the Message Content privileged intent — per Discord’s quick-start, the recommendation for new bots is to use slash commands precisely because they avoid that gating review 5 . Leave all three privileged intents off for this build.
Step 2 — Project scaffolding
Create the project and install dependencies:
mkdir claude-discord-bot && cd claude-discord-bot
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install "discord.py>=2.4" "anthropic>=0.40" python-dotenv
pip freeze > requirements.txt
python-dotenv reads a local .env file into os.environ at
startup 6 , which keeps secrets out of source
control.
Create .env in the project root:
DISCORD_TOKEN=your_discord_bot_token_here
ANTHROPIC_API_KEY=sk-ant-your-key-here
TEST_GUILD_ID=your_test_server_id_here
The TEST_GUILD_ID is the numeric ID of your test server —
right-click the server icon in Discord with Developer Mode on
(Settings → Advanced → Developer Mode) and Copy Server ID.
Guild-scoped command syncs propagate in seconds; global syncs can
take up to an hour to appear across all servers 7 .
Add .env to .gitignore immediately. Tokens committed to public
GitHub repositories are scraped by bots within
minutes 5 .
Step 3 — Define the two tools
Create tools.py. The calculator is a client tool the bot
executes locally; the web_search slot points at Anthropic’s
server-side tool, so the bot does not need its own search
provider 1 :
import ast
import operator as op
# Allow-listed AST nodes — never use eval() on model output.
_OPS = {
ast.Add: op.add,
ast.Sub: op.sub,
ast.Mult: op.mul,
ast.Div: op.truediv,
ast.Pow: op.pow,
ast.USub: op.neg,
ast.UAdd: op.pos,
ast.Mod: op.mod,
}
def safe_eval(node):
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp):
return _OPS[type(node.op)](safe_eval(node.left), safe_eval(node.right))
if isinstance(node, ast.UnaryOp):
return _OPS[type(node.op)](safe_eval(node.operand))
raise ValueError(f"Disallowed expression: {ast.dump(node)}")
def calculator(expression: str) -> str:
"""Evaluate a basic arithmetic expression and return the result."""
try:
tree = ast.parse(expression, mode="eval")
result = safe_eval(tree.body)
return str(result)
except Exception as exc:
return f"calculator error: {exc}"
CLAUDE_TOOLS = [
{
"type": "web_search_20260209",
"name": "web_search",
},
{
"name": "calculator",
"description": (
"Evaluate a basic arithmetic expression with +, -, *, /, **, %, "
"and unary minus. Returns the numeric result as a string."
),
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "A numeric expression, e.g. '(12 * 7) + 3'.",
}
},
"required": ["expression"],
},
},
]
def dispatch_client_tool(name: str, tool_input: dict) -> str:
if name == "calculator":
return calculator(tool_input.get("expression", ""))
return f"tool '{name}' is not implemented"
Two things to note. First, safe_eval walks the AST instead of
calling eval() — never pass model-generated strings to eval
even inside a sandboxed bot. Second, the web_search entry uses
the web_search_20260209 type identifier published in Anthropic’s
overview snippet 1 ; check the current version
string on the live docs when you build, since Anthropic versions
server tools by date.
Image: Anthropic platform docs — Define tools (platform.claude.com), used for editorial coverage of the input_schema and tool definition format described.
Step 4 — The Claude conversation loop
Create claude_client.py. The function below handles the
agentic loop: send the user prompt, inspect the response for
tool_use blocks, execute any client tools, append a
tool_result content block, and loop until Claude returns plain
text with stop_reason: "end_turn" 3 .
import os
import anthropic
from tools import CLAUDE_TOOLS, dispatch_client_tool
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
MODEL = "claude-opus-4-7"
MAX_TURNS = 6 # safety cap on the tool loop
SYSTEM_PROMPT = (
"You are a helpful assistant in a Discord channel. "
"Use the web_search tool when the user asks about current events, "
"prices, releases, or any fact you are not confident is stable. "
"Use the calculator tool for any non-trivial arithmetic. "
"Keep final answers under 1500 characters so they fit in one Discord message."
)
def ask_claude(question: str) -> str:
messages = [{"role": "user", "content": question}]
for _ in range(MAX_TURNS):
response = client.messages.create(
model=MODEL,
max_tokens=1024,
system=SYSTEM_PROMPT,
tools=CLAUDE_TOOLS,
messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
text_blocks = [b.text for b in response.content if b.type == "text"]
return "\n".join(text_blocks) or "(no text returned)"
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
# Server tools (web_search) are executed by Anthropic — skip them here.
if block.name == "web_search":
continue
result = dispatch_client_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
if not tool_results:
# Only server-tool blocks fired; let Claude continue with the server result.
continue
messages.append({"role": "user", "content": tool_results})
return "Hit the maximum tool-use turns without a final answer."
The claude-opus-4-7 model identifier matches Anthropic’s current
tool-use overview Python snippet 1 . Swap to
claude-haiku-4-5 if you want a cheaper, faster bot for casual
server use; both share the same tool-use system-prompt token
footprint per Anthropic’s pricing table 8 .
Step 5 — Wire it into discord.py
Create bot.py. The app_commands tree is the modern slash
command surface; tree.sync(guild=...) propagates commands to a
specific test server within seconds, which is what you want during
development 7 :
import os
import asyncio
from dotenv import load_dotenv
import discord
from discord import app_commands
from claude_client import ask_claude
load_dotenv()
TEST_GUILD_ID = int(os.environ["TEST_GUILD_ID"])
class ClaudeBot(discord.Client):
def __init__(self):
intents = discord.Intents.default()
super().__init__(intents=intents)
self.tree = app_commands.CommandTree(self)
async def setup_hook(self):
guild = discord.Object(id=TEST_GUILD_ID)
self.tree.copy_global_to(guild=guild)
await self.tree.sync(guild=guild)
bot = ClaudeBot()
@bot.tree.command(name="ask", description="Ask Claude with web-search and calculator tools.")
@app_commands.describe(question="What do you want to ask?")
async def ask(interaction: discord.Interaction, question: str):
await interaction.response.defer(thinking=True)
try:
answer = await asyncio.to_thread(ask_claude, question)
except Exception as exc:
await interaction.followup.send(f"Claude call failed: {exc}")
return
# Discord caps a single message at 2000 chars.
await interaction.followup.send(answer[:1900])
@bot.event
async def on_ready():
print(f"Logged in as {bot.user} (id={bot.user.id})")
if __name__ == "__main__":
bot.run(os.environ["DISCORD_TOKEN"])
Three discord.py specifics worth pointing out. defer(thinking=True)
buys you the full 15-minute followup window — Claude tool loops
that hit the web can take 5-30 seconds, well past the 3-second
initial-response budget Discord enforces 9 .
asyncio.to_thread keeps the synchronous Anthropic SDK call off
the event loop. And the 1,900-character truncation stays under
Discord’s 2,000-character per-message limit with headroom for the
ellipsis fallback.
Step 6 — Test locally
From the project root:
python bot.py
The terminal should print Logged in as claude-tool-bot#NNNN.
In your test server, type /ask — Discord should autocomplete
the command. Ask What is 47 * 198, and what was the top trending GitHub repo today?. Claude should fire the calculator
tool for the multiplication and the web_search tool for the
trending question, then return a combined answer.
If the /ask command does not appear:
- Confirm the bot is in the server (re-invite via the OAuth2 URL Generator if needed).
- Confirm
TEST_GUILD_IDmatches the server. Discord rejects the sync silently if the bot is not a member of the guild. - Restart the Discord client; the command palette caches.
Step 7 — Deploy on Railway
Initialise git and push to GitHub:
git init && git add . && git commit -m "initial bot"
gh repo create claude-discord-bot --private --source=. --push
Sign up at railway.com and create a new project from your
GitHub repository. Railway auto-detects Python via
requirements.txt and runs python bot.py as the start command
when no Procfile is present.
Add a Procfile to be explicit:
worker: python bot.py
worker (not web) is the right process type — a Discord bot
holds an outbound WebSocket; it does not listen for inbound HTTP.
Railway’s free trial caps trial-account projects at 1 GB of RAM
on shared vCPU and up to 5 services per project 10 ,
which is fine for a single bot worker.
In the Railway project’s Variables tab, paste the three
secrets from .env (DISCORD_TOKEN, ANTHROPIC_API_KEY,
TEST_GUILD_ID). Trigger a deploy. The logs should show
Logged in as claude-tool-bot#NNNN once the container boots; the
slash command keeps working from the test server because the
guild-scoped sync from step 5 already registered it with
Discord.
Image: Railway Docs — Free Trial pricing page (docs.railway.com), used for editorial coverage of the trial-credit and deployment-limit claims described.
Cost expectations
Per Anthropic’s tool-use pricing reference, every request with
tools set carries a 346-token system-prompt overhead on Claude
Opus 4.7 (313 tokens when tool_choice is any or
tool) 8 . That overhead is per-request, so a
chatty channel with 100 /ask calls a day adds roughly 35K tokens
of tool-system overhead on top of message tokens. The
web_search server tool also carries per-search charges
separately from token billing 3 ; check the
current per-search rate on the model pricing page before sizing
your monthly budget.
On Railway, third-party trackers report a small Python service running 24/7 burns roughly $0.30-$0.50 per month of Railway credits, so the Free plan’s $1 monthly credit is borderline for a single-bot deployment that stays online continuously 4 . A Hobby plan at $5/month is the next step up if the bot needs to stay up past free-tier limits.
Next steps
- Add more client tools. A
get_user_role(user_id)tool, a channel-history-summariser tool, a Notion / Linear search tool — each follows the samename+description+input_schemashape, with a dispatch branch intools.py11 . - Enable strict mode. Adding
"strict": trueto a tool definition forces Claude’s tool calls to match the input schema exactly 12 , which prevents the bot from hallucinating optional fields. - Persist conversation state. The example above is
stateless per-command. Wire a Postgres or SQLite store keyed on
interaction.user.idto keep multi-turn context between/askcalls.
How this article was made: an autonomous AI pipeline researched, drafted, fact-checked, and reviewed this piece, aggregating publicly-available information from the sources consulted below. AI (artificial intelligence) can make mistakes, so please cross-check the consulted sources before acting on anything here. Neural Tech Daily is not liable for decisions or outcomes based on this article.
Sources consulted
Cited Sources
- 1. Anthropic — Tool use with Claude overview (server tools, client tools, web_search_20260209 type identifier) (accessed ) ↩
- 2. Railway Docs — Free Trial (one-time \$5 credit, 30-day expiry) (accessed ) ↩
- 3. Anthropic — How tool use works (agentic loop, stop_reason behaviour, server-tool pricing) (accessed ) ↩
- 4. Railway Docs — Pricing Plans (Free plan \$1/month credit, no roll-over) (accessed ) ↩
- 5. Discord Developer Docs — Building your first Discord Bot (bot creation, intents, slash command guidance) (accessed ) ↩
- 6. python-dotenv on PyPI (load .env into os.environ at startup) (accessed ) ↩
- 7. discord.py — Interactions API reference, CommandTree.sync (accessed ) ↩
- 8. Anthropic — Tool use pricing table (346/313 token system-prompt overhead by tool_choice) (accessed ) ↩
- 9. discord.py — Interaction.response.defer documentation (accessed ) ↩
- 10. Railway Docs — Free Trial limits (1 GB RAM, 5 services per project on trial) (accessed ) ↩
- 11. Anthropic — Define tools (input_schema shape) (accessed ) ↩
- 12. Anthropic — Strict tool use (strict: true schema enforcement) (accessed ) ↩
Further Reading
- discord.py — discord.ext.commands reference (accessed )
Anonymous · no cookies set