Neural Tech Daily
ai-tutorials

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.

~11 min read
Share
Anthropic platform docs tool-use overview page showing the Python SDK example with model claude-opus-4-7 and a server-side web_search tool definition

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 pip available 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 bot and applications.commands scopes. 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.
Discord Developer Docs quick-start Building your first Discord Bot landing page showing the step-by-step tutorial entry point

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.

Anthropic Define tools docs page showing the tool definition JSON schema with name, description, and input_schema fields

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_ID matches 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.

Railway Docs Free Trial pricing page showing the one-time five-dollar credit grant, 30-day expiry, and trial-account resource limits

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 same name + description + input_schema shape, with a dispatch branch in tools.py 11 .
  • Enable strict mode. Adding "strict": true to 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.id to keep multi-turn context between /ask calls.

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. 1. Anthropic — Tool use with Claude overview (server tools, client tools, web_search_20260209 type identifier) (accessed )
  2. 2. Railway Docs — Free Trial (one-time \$5 credit, 30-day expiry) (accessed )
  3. 3. Anthropic — How tool use works (agentic loop, stop_reason behaviour, server-tool pricing) (accessed )
  4. 4. Railway Docs — Pricing Plans (Free plan \$1/month credit, no roll-over) (accessed )
  5. 5. Discord Developer Docs — Building your first Discord Bot (bot creation, intents, slash command guidance) (accessed )
  6. 6. python-dotenv on PyPI (load .env into os.environ at startup) (accessed )
  7. 7. discord.py — Interactions API reference, CommandTree.sync (accessed )
  8. 8. Anthropic — Tool use pricing table (346/313 token system-prompt overhead by tool_choice) (accessed )
  9. 9. discord.py — Interaction.response.defer documentation (accessed )
  10. 10. Railway Docs — Free Trial limits (1 GB RAM, 5 services per project on trial) (accessed )
  11. 11. Anthropic — Define tools (input_schema shape) (accessed )
  12. 12. Anthropic — Strict tool use (strict: true schema enforcement) (accessed )

Further Reading

Anonymous · no cookies set

Report a problem with this article

Articles are produced by an autonomous AI pipeline; mistakes do happen. Tell us what's wrong and the editorial review will revisit the claim.

Category

Found this useful? Share it.