Blog

Using Discord and Elastic Agent Builder A2A to build a gaming community support bot

Learn how to connect Discord to Elastic Agent Builder's Agent-to-Agent (A2A) server to create a gaming community support bot.

In this article, we'll build a gaming community support bot that connects Discord to Elastic Agent Builder's Agent-to-Agent (A2A) server. Players will ask questions like, "Who's the best Mage?" or "What's the current meta?" and get real-time answers powered by Elasticsearch Query Language (ES|QL) analytics and semantic search.

Many gaming companies already use Elasticsearch for player data and game analytics. In this tutorial, we'll access that data through Discord, where many users spend their time.

Prerequisites

What we're building

We'll create a Discord bot that uses Elastic Agent Builder's A2A server to answer player questions. The bot will have two types of tools:

  • ES|QL tools for analytics: leaderboards, hero stats, meta reports.

  • Index search tools for knowledge: game mechanics, FAQs.

The architecture looks like this:

Architecture for using Discord with Elastic Agent Builder A2A

Architecture overview

Elastic Agent Builder provides an A2A server to connect with clients. This server exposes the agent with tools that can query Elasticsearch using ES|QL or index search tools. Any A2A-compatible client can connect to it.

The A2A client instantiates a bot that can connect to the Discord server, communicate with Agent Builder, and receive/send messages to the users.

Why A2A instead of MCP?

A2A architecture

Source: https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/

Agent Builder also offers the option to expose the agent's tools via a Model Context Protocol (MCP) server. The key difference is that with MCP, the client can only access the tools and their descriptions, not the agent itself. Additionally, tool selection logic must be implemented on the MCP client side, since all tools from all agents are exposed together.

With A2A, the entire agent, including instructions and tools, is available to the client. This gives you more control from the Agent Builder side, letting you manage behavior centrally rather than in each client.

The right choice depends on where you want that control to live. For this Discord bot, we want to manage the agent's behavior from Elastic, so A2A is the better fit.

Setting up sample game data

Let's create gaming data that our bot can query. We'll set up three indices:

  • player_stats: Player profiles with wins, kills, rank.

  • hero_meta: Hero pick rates and win rates by tier.

  • game_knowledge: FAQs and game mechanics. Uses semantic_text for meaning-based versus keyword matching. Both title and content are copied into semantic_field for a hybrid search solution.

Create the indices

from elasticsearch import Elasticsearch
import os

es = Elasticsearch(
    hosts=[os.getenv("ELASTICSEARCH_URL")],
    api_key=os.environ["ELASTIC_API_KEY"]
)

# Player stats index
es.indices.create(
    index="player_stats",
    mappings={
        "properties": {
            "player_id": {"type": "keyword"},
            "username": {"type": "keyword"},
            "hero": {"type": "keyword"},
            "wins": {"type": "integer"},
            "losses": {"type": "integer"},
            "kills": {"type": "integer"},
            "deaths": {"type": "integer"},
            "rank": {"type": "keyword"},
            "last_played": {"type": "date"}
        }
    }
)

# Hero meta index
es.indices.create(
    index="hero_meta",
    mappings={
        "properties": {
            "hero_name": {"type": "keyword"},
            "pick_rate": {"type": "float"},
            "win_rate": {"type": "float"},
            "tier": {"type": "keyword"},
            "patch_version": {"type": "keyword"}
        }
    }
)

# Game knowledge index (for semantic search)
es.indices.create(
    index="game_knowledge",
    mappings={
        "properties": {
            "title": {"type": "text", "copy_to": "semantic_field"},
            "content": {"type": "text", "copy_to": "semantic_field"},
            "category": {"type": "keyword"},
            "semantic_field": {"type": "semantic_text"} # Semantic search queries this combined field
        }
    }
)

Index sample data

from datetime import datetime, timedelta
from elasticsearch.helpers import bulk
import random

# Sample player data
players = [
    {"player_id": "p001", "username": "DragonSlayer99", "hero": "Warrior", "wins": 342, "losses": 198, "kills": 4521, "deaths": 2103, "rank": "Diamond"},
    {"player_id": "p002", "username": "ShadowMage", "hero": "Mage", "wins": 567, "losses": 234, "kills": 8932, "deaths": 3421, "rank": "Master"},
    {"player_id": "p003", "username": "HealBot3000", "hero": "Healer", "wins": 423, "losses": 187, "kills": 1234, "deaths": 1876, "rank": "Diamond"},
    {"player_id": "p004", "username": "TankMaster", "hero": "Tank", "wins": 298, "losses": 302, "kills": 2341, "deaths": 1543, "rank": "Platinum"},
    {"player_id": "p005", "username": "AssassinX", "hero": "Assassin", "wins": 789, "losses": 156, "kills": 12453, "deaths": 2987, "rank": "Grandmaster"},
]

for player in players:
    player["last_played"] = datetime.now() - timedelta(hours=random.randint(1, 72))

# Hero meta data
heroes = [
    {"hero_name": "Warrior", "pick_rate": 15.2, "win_rate": 51.3, "tier": "A", "patch_version": "2.4.1"},
    {"hero_name": "Mage", "pick_rate": 22.8, "win_rate": 54.7, "tier": "S", "patch_version": "2.4.1"},
    {"hero_name": "Healer", "pick_rate": 18.5, "win_rate": 52.1, "tier": "A", "patch_version": "2.4.1"},
    {"hero_name": "Tank", "pick_rate": 12.3, "win_rate": 48.9, "tier": "B", "patch_version": "2.4.1"},
    {"hero_name": "Assassin", "pick_rate": 31.2, "win_rate": 49.2, "tier": "A", "patch_version": "2.4.1"},
]

# Game knowledge for semantic search
knowledge = [
    {"title": "How to unlock the Dragon Mount", "content": "Complete the Dragon's Lair dungeon on Nightmare difficulty with all party members alive. The mount has a 15% drop rate.", "category": "mounts"},
    {"title": "Best Mage build for Season 4", "content": "Focus on Intelligence and Critical Chance. Use the Arcane Staff with Frost Runes. Prioritize cooldown reduction for burst damage.", "category": "builds"},
    {"title": "Understanding the ranking system", "content": "Ranks go from Bronze to Grandmaster. You need 100 points to advance. Wins give 25 points, losses subtract 20.", "category": "ranked"},
]

# Bulk index all data
actions = []
for player in players:
    actions.append({"_index": "player_stats", "_source": player})
for hero in heroes:
    actions.append({"_index": "hero_meta", "_source": hero})
for doc in knowledge:
    actions.append({"_index": "game_knowledge", "_source": doc})

success, errors = bulk(es, actions)
print(f"Indexed {success} documents")

es.indices.refresh(index="player_stats,hero_meta,game_knowledge")

Done! Now we have data to query.

Creating ES|QL tools via API

Instead of using the UI, we'll create tools programmatically using the Agent Builder API. This makes it easier to version control and deploy.

First, let's set up our Kibana API connection:

import requests

KIBANA_URL = os.environ["KIBANA_URL"]  # e.g., https://your-deployment.kb.us-central1.gcp.cloud.es.io
KIBANA_API_KEY = os.environ["KIBANA_API_KEY"]

headers = {
    "kbn-xsrf": "true",
    "Authorization": f"ApiKey {KIBANA_API_KEY}",
    "Content-Type": "application/json"
}

Tool 1: Leaderboard

This tool shows top players by kills. We use ES|QL for aggregations:

leaderboard_tool = {
    "id": "leaderboard",
    "type": "esql",
    "description": "Shows top players ranked by kills. Use when someone asks Who is the best? or Show me top players.",
    "configuration": {
        "query": """FROM player_stats
| STATS total_kills = SUM(kills), total_wins = SUM(wins) BY username, hero, rank
| SORT total_kills DESC
| LIMIT 10""",
        "params": {}
    }
}

response = requests.post(
    f"{KIBANA_URL}/api/agent_builder/tools",
    headers=headers,
    json=leaderboard_tool
)
print(f"Leaderboard tool: {response.status_code}")

Tool 2: Hero stats

The hero stats tool returns performance stats for a specific hero using a dynamic parameter:

hero_stats_tool = {
    "id": "hero_stats",
    "type": "esql",
    "description": "Gets win rate, pick rate, and tier for a specific hero. Use when someone asks How good is Mage? or What is the win rate for Warrior?",
    "configuration": {
        "query": """FROM hero_meta
| WHERE hero_name == ?hero
| KEEP hero_name, win_rate, pick_rate, tier, patch_version""",
        "params": {
            "hero": {
                "type": "keyword",
                "description": "The hero name to look up"
            }
        }
    }
}

response = requests.post(
    f"{KIBANA_URL}/api/agent_builder/tools",
    headers=headers,
    json=hero_stats_tool
)
print(f"Hero stats tool: {response.status_code}")

?hero is a dynamic parameter. When the agent calls this tool, it fills in the hero name from the user's question, bridging natural language, “Is Mage strong?” to structured queries, like | WHERE hero_name == “Mage”.

Tool 3: Meta report

This tool shows which heroes are dominating the current patch:

meta_report_tool = {
    "id": "meta_report",
    "type": "esql",
    "description": "Shows all heroes sorted by tier and win rate. Use when someone asks What is the current meta? or Which heroes are S-tier?",
    "configuration": {
        "query": """FROM hero_meta
| SORT tier ASC, win_rate DESC
| KEEP hero_name, tier, win_rate, pick_rate""",
        "params": {}
    }
}

response = requests.post(
    f"{KIBANA_URL}/api/agent_builder/tools",
    headers=headers,
    json=meta_report_tool
)
print(f"Meta report tool: {response.status_code}")

Adding index search tools

For unstructured content, like FAQs and game mechanics, we use index search tools. The pattern parameter specifies which Elasticsearch index to search:

game_knowledge_tool = {
    "id": "game_knowledge",
    "type": "index_search",
    "description": "Searches game guides, FAQs, and mechanics. Use when someone asks How do I...? or What is...? questions about game content.",
    "configuration": {
        "pattern": "game_knowledge"
    }
}

response = requests.post(
    f"{KIBANA_URL}/api/agent_builder/tools",
    headers=headers,
    json=game_knowledge_tool
)
print(f"Game knowledge tool: {response.status_code}")

Keep in mind that ES|QL tools are for structured analytics ("Who has the most kills?"), while index search tools are for unstructured knowledge ("How do I unlock the dragon mount?").

Creating the agent

Let’s create an agent that uses these tools:

agent = {
    "id": "gaming_support_bot",
    "name": "Gaming Support Bot",
    "description": "A gaming community support bot that answers player questions about stats, heroes, and game mechanics.",
    "configuration": {
        "tools": [{"tool_ids": ["leaderboard", "hero_stats", "meta_report", "game_knowledge"]}],
        "instructions": """You are a helpful gaming community bot. Answer player questions about:
- Player stats and leaderboards (use leaderboard tool)
- Hero performance and meta (use hero_stats and meta_report tools)
- Game mechanics and guides (use game_knowledge tool)

Be concise and friendly. Format leaderboards clearly with rankings."""
    }
}

response = requests.post(
    f"{KIBANA_URL}/api/agent_builder/agents",
    headers=headers,
    json=agent
)
print(f"Agent created: {response.status_code}")

The agent now has access to all our tools and can choose one on the user's question.

You can test the connectivity by visiting the Kibana Agent Chat GUI or by sending an API call. By default, Agent Builder uses the Elastic Managed LLM, so no connector configuration is required:

test_message = "Show me all heroes sorted by tier"

response = requests.post(
    f"{KIBANA_URL}/api/agent_builder/converse",
    headers=headers,
    json={
        "agent_id": "gaming_support_bot",
        "input": test_message
    },
    timeout=60
)

print(f"Status: {response.status_code}")
if response.status_code == 200:
    result = response.json()
    print(f"\nAgent used tools: {[step.get('tool_id') for step in result.get('steps', []) if step.get('type') == 'tool_call']}")
    print(f"\nResponse:\n{result.get('response', {}).get('message', 'No message')}")
else:
    print(f"Error: {response.text}")

Setting up a Discord bot

If you don't have a Discord bot yet, you'll need to create one following these steps:

1. Go to the Discord Developer Portal.

2. Click New Application, and give it a name.

Creating a new application in the Discord Developer Portal

3. Go to the Bot section, and click Add Bot.

4. Copy the bot token. (You'll need this later.)

Bot token in Discord

5. Under Privileged Gateway Intents, enable Message Content Intent.

Message Content Intent in Discord

6. Go to OAuth2 > URL Generator, select bot and application.commands scopes and Send Messages + Read Message History permissions.

7. Use the generated URL to invite the bot to your server.

Invite the Discord bot to your server

Security considerations

On the Discord side, request only the minimum permissions:

  • Send Messages.

  • Send Messages in Threads.

  • Read Message History.

  • Create Polls.

This limits what the bot can do if compromised.

On the Elastic side, create an API key with restricted privileges. For this bot, you only need read access to the game indices:

POST /_security/api_key
{
  "name": "gaming-bot-key",
  "role_descriptors": {
    "gaming_bot_role": {
      "cluster": ["monitor"],
      "indices": [
        {
          "names": ["player_stats", "hero_meta", "game_knowledge"],
          "privileges": ["read"]
        }
      ]
    }
  }
}

Configuring the Discord A2A client

The next step is to connect Discord to Agent Builder. We'll use a modified version of the kagent-a2a-discord.

Clone and setup

git clone https://github.com/llermaly/agentbuilder-a2a-discord
cd agentbuilder-a2a-discord

Environment variables

Create an .env file in the root directory:

DISCORD_BOT_TOKEN=your_bot_token
AGENT_BUILDER_URL=https://<kibana_url>/api/agent_builder/a2a/gaming_support_bot
A2A_API_KEY=your_api_key

Run with Python

# Create virtual environment
uv venv

# Install dependencies
uv sync

# Run the bot 
uv run main.py

The bot should come online in your Discord server within a few seconds.

Online bot in Discord server

Testing the bot

Let's test with different query types.

Analytics queries (ES|QL)

User: "Who is the best Mage player?"

Bot: Based on the leaderboard data, ShadowMage is currently the best Mage player in the game. They've reached Master rank with an impressive 8,932 kills and 567 wins…

User: "What's the current meta?"

Bot: The current meta is dominated by Mage, which stands alone in S-tier with an impressive 54.7% win rate and 22.8% pick rate in patch 2.4.1.

Testing Discord A2A client

Semantic queries (index search)

User: "How do I unlock the dragon mount?"

Bot: To unlock the Dragon Mount, you need to:

  1. Complete the Dragon's Lair dungeon on Nightmare difficulty.

  2. Keep all party members alive throughout the entire run.

  3. Cross your fingers for luck - the mount has a 15% drop rate…

Testing Discord bot A2A client

The bot automatically picks the right tool based on the question.

Bidirectional: Giving the agent actions

Beyond answering questions, we can give Agent Builder the ability to trigger Discord actions. With a small modification to the Discord client, we can parse special tags in the agent's response and execute Discord commands.

For example, we added support for a <poll> tag:

<poll>Should Mage be nerfed?|Yes, too strong|No, it's balanced|Need more data</poll>

When the agent includes this in its response, the bot creates a native Discord poll. The agent just needs instructions on when to use it. Run the following to add to the instructions:

agent = {
    "id": "gaming_support_bot",
    "name": "Gaming Support Bot",
    "description": "A gaming community support bot that answers player questions about stats, heroes, and game mechanics.",
    "configuration": {
        "tools": [{"tool_ids": ["leaderboard", "hero_stats", "meta_report", "game_knowledge"]}],
        "instructions": """You are a helpful gaming community bot. Answer player questions about:
- Player stats and leaderboards (use leaderboard tool)
- Hero performance and meta (use hero_stats and meta_report tools)
- Game mechanics and guides (use game_knowledge tool)

When discussing balance topics, create a poll for community input.
Use: <poll>Question|Option1|Option2|Option3</poll>

Be concise and friendly. Format leaderboards clearly with rankings."""
    }
}

response = requests.put(
    f"{KIBANA_URL}/api/agent_builder/agents",
    headers=headers,
    json=agent
)
print(f"Agent created: {response.status_code}")

Now when a user asks "Is Mage overpowered?" the agent can share the stats and create a poll, turning data insights into community engagement.

Discord Bot with Discord and Elastic Agent Builder A2A

The same pattern works for other Discord features, like reactions, role mentions, or scheduled announcements. The agent's response becomes a control channel for Discord actions.

Conclusion

In this article, we successfully built a Discord bot using Elastic Agent Builder's A2A server. The process covered creating ES|QL tools via API for analytics, such as leaderboards, hero stats, and meta reports, and developing index search tools for semantic search against a knowledge base. Furthermore, we demonstrated the use of dynamic parameters, like ?hero for flexible queries, established the connection between Discord and the A2A client, and tested both analytics and semantic query types.

The Agent Builder provides the A2A server, so you only need to focus on creating tools and connecting clients, while ES|QL handles structured analytics and index search handles unstructured knowledge.

Resources

Related Content

Your AI agent doesn't need your API key: OAuth 2.1 for Elasticsearch MCP server authentication

Alex Chalkias

Faster, cheaper support investigations with precomputed context

Abhimanyu Anand

Building context in Elasticsearch: how AI Indices power smarter agents using fewer tokens

Kathleen DeRusso

Your agents have been keeping receipts: turning Elastic Agent Builder's built-in OTel traces into token cost dashboards in Kibana

Meghan Murphy

Ready to build state of the art search experiences?

Sufficiently advanced search isn’t achieved with the efforts of one. Elasticsearch is powered by data scientists, ML ops, engineers, and many more who are just as passionate about search as you are. Let’s connect and work together to build the magical search experience that will get you the results you want.

Try it yourself