DEV Community

Cover image for Build Discord Agent with MCP or API interface, both?
codepoetn
codepoetn

Posted on

Build Discord Agent with MCP or API interface, both?

TL;DR

  • Discord has no official MCP server. Community servers run on a bot token or, in self-bot variants, a user's account token, which Discord's terms prohibit.
  • OAuth2 user tokens cover identity: profile, server membership, linked accounts, and entitlements. Messages, threads, and moderation run on a bot token, so most Discord agents act as a bot.
  • A bot token has no OAuth scopes and no expiry, and one bot shared across customers reaches every customer's server.
  • The direct API gives you the full surface, plus the 50 requests per second limit, the invalid-request IP ban, and privileged intent review.
  • Scalekit's Discord (OAuth, 34 tools) and Discord Bot (221 tools) connectors vault both token types, log every tool call, and serve over a Virtual MCP server scoped per agent role. ## The decision your Discord agent actually faces

Your agent needs to work inside Discord. It answers questions in a customer's support channel, opens threads for bug reports, or checks which servers a user belongs to before unlocking a feature. Search for a Discord MCP server and you find dozens of repositories, none of them from Discord. The real decision sits one layer down: bot token or OAuth2 user token, and who holds that credential when the agent serves a hundred customer servers instead of one.

What the three Discord paths actually are

A production Discord agent reaches the platform one of three ways. They differ in who wrote the code that holds the credential and in which Discord identity the agent acts with.

Community Discord MCP servers

Community servers are open-source MCP servers written by individual developers. Many run locally over stdio: the MCP client launches the process and passes a token such as DISCORD_TOKEN through the env block of its config file. Others run as a local HTTP service in a container.

They come in two families. Bot-token servers expose tools for sending and reading messages, managing channels and roles, forum posts, reactions, and webhooks across the servers the bot has joined. Self-bot servers ask for a personal account token, sometimes extracted from a logged-in browser session, and several of their own READMEs warn that this violates Discord's terms and can get the account banned.

The Discord API directly

The Discord API has three surfaces: a versioned REST API (v10 today), the Gateway WebSocket for real-time events, and interactions for slash commands and message components.

Authentication splits into two token types. A bot token, sent as Authorization: Bot <token>, authenticates the bot user; Discord's OAuth2 documentation states that bot users have access to most API routes without bearer tokens and can connect to the Gateway. OAuth2 bearer tokens come from the Authorization Code grant, expire after 604,800 seconds (seven days), and ship with a refresh token. The bot authorization flow, which uses the bot scope and a permissions integer, is how a server admin installs your bot.

Scalekit's Discord connectors

Scalekit has two connectors for Discord. The Discord connector runs OAuth 2.0 with PKCE against each user's Discord account and ships 34 tools; Scalekit stores and refreshes the tokens. The Discord Bot connector stores a bot token per connected account and ships 221 tools across messages, threads, members, roles, moderation, webhooks, and interactions.

Your agent calls either one with execute_tool in Python or executeTool in Node.js, or reaches both through a Virtual MCP server that Scalekit hosts. The product pages for the Discord connector and the Discord Bot connector summarize both.

Comparing them where it matters for agents

Three questions decide the path: what the agent can do, which identity it acts with, and how far one credential reaches.

What your agent can actually do

Coverage on Discord follows the token type, not the transport.

Capability Community MCP Discord API Scalekit
User profile, linked accounts Rare OAuth2 bearer discord_get_my_user
User's servers, member record Varies OAuth2 bearer discord_list_my_guilds
Channel message history Bot or self-bot Bot, Message Content intent discordbot_list_channel_messages
Send messages, replies Yes Bot discordbot_create_message
Threads, forum posts Some Bot discordbot_start_thread_from_message
Kick, ban, AutoMod Some Bot, role permissions discordbot_create_guild_ban
Server audit log Rare Bot discordbot_get_guild_audit_log
Slash command responses Rare Gateway or interactions endpoint discordbot_create_interaction_response
Real-time events Needs Gateway Gateway Your own listener
User's own DMs Self-bot only Approved partners Not available

Where the user-token ceiling sits

Discord's OAuth2 scopes describe identity and installation. The only message scope, messages.read, applies to the local RPC server, and dm_channels.read is reserved for approved partners. Reading conversations, and nearly everything that writes to them, runs on the bot token; an incoming webhook can post to one channel but cannot read.

That is why the discord connector has no message tools and discordbot has dozens. Treat per-user OAuth as account linking: confirm a user's Discord identity, check server membership before granting access, sync role connections, or read entitlements. Treat the bot as the actor that reads and writes in channels.

The auth path each one puts you on

Community MCP servers put a long-lived token in a file on the machine running the agent. The direct API gives you both token types and nothing around them. Scalekit puts both behind connected accounts: the OAuth path handles consent, storage, and refresh, and the bot path stores one token per connected account and injects it on every call.

The token types behave differently. An OAuth2 access token expires in seven days, refreshes with a refresh token, and revoking one token revokes every token from that authorization. A bot token has no expiry and no OAuth scopes. It stays valid until it is reset, and its reach is the role permissions the bot holds in each server.

One bot token can reach every customer

If you run one Discord application for all customers, its token works in every server the bot has joined. An agent acting for tenant A can post into tenant B's channel the moment it gets B's channel ID, through a prompt injection or a bug.

Neither transport fixes that. Isolation comes from one of two designs: pin server and channel IDs from the tenant record and treat model-supplied IDs as untrusted, or have each customer register its own Discord application so every tenant has a separate token. The single-tenant vs multi-tenant tool calling guide covers the general pattern.

The self-bot shortcut is a policy violation

Discord's OAuth2 documentation tells developers to refrain from automating standard user accounts, and the Discord Developer Policy prohibits requesting passwords or login tokens from users. A product that runs a self-bot MCP server is asking customers for their account token. Flagged accounts can be terminated, and the policy gives Discord grounds to remove your application.

What you own in production

Whichever path you choose, Discord's platform rules apply to your agent the same way they apply to any bot. Four of them shape agent design.

Rate limits and the invalid-request ban

Discord's Rate Limits documentation sets a global limit of 50 requests per second per bot, independent of per-route limits; interaction endpoints are exempt. Separately, an IP address that makes 10,000 invalid requests (401, 403, or 429) within 10 minutes is temporarily restricted from the API.

Agents hit the second limit in ways scripts rarely do. A loop that retries a revoked token, or keeps calling a channel the bot lost access to, burns the budget fast. When every tenant's agent egresses from the same IP, one misbehaving loop can lock out all of them. Stop on 401, check permissions before acting, and honor retry_after.

Privileged intents gate what the bot can read

Discord gates three data types behind privileged intents: Guild Presences, Guild Members, and Message Content. Without Message Content, the content, embeds, attachments, components, and poll fields arrive empty, apart from a few exceptions such as messages that mention the bot.

Since June 10, 2026, apps that reach fewer than 10,000 unique users toggle these intents in the Developer Portal. Past that threshold you get 90 days to apply, and approved apps reapply every year. A support agent that reads channel history needs Message Content, so document the use case from day one. Agents that act only on slash command input can skip it.

Data handling obligations

The Discord Developer Policy limits API Data to what your stated functionality needs and prohibits using message content to train machine learning or AI models, including LLMs, without Discord's express permission. Sending channel messages to a model to answer a question can fall within stated functionality; piping stored transcripts into a fine-tuning set is training. The Developer Terms also require you to notify Discord and affected users of potential unauthorized access to API Data, which can turn a leaked bot token into a disclosure event.

Schema and maintenance drift

Community servers change when their maintainer changes them, often without versioning. The direct API is versioned, but you own every adapter, pagination rule, and deprecation. Scalekit maintains tool schemas against Discord's API; the connector already marks discord_get_invite_deprecated and points agents to discord_resolve_invite. On any path, pin the tool names your agent depends on and test them before each release.

The risks of running an unofficial Discord MCP server

Community servers are a reasonable way to explore what a Discord agent should do. Running one in front of customers moves six specific risks into your product.

Credential exposure

The bot token sits in plaintext in an MCP client config file or a shell environment. Any process, extension, or synced backup that can read that file can take it. A bot token never expires on its own, so a copied token stays valid until someone notices and resets it, and resetting it breaks every deployment that shares it. Whoever holds the token controls the bot.

Unreviewed code holding server-wide permissions

The MCP server process holds the token and decides which API calls to make. An unpinned npx -y launch command can pick up a newly published release without review, so one compromised version can exfiltrate the token or post into every server the bot has joined. Tool descriptions are model input too, and a malicious update can rewrite them to steer the agent. The MCP security risks breakdown covers these supply-chain patterns.

Every server member can write into your agent's context

Discord content is untrusted input. Any member of any server the bot reads can post text designed to redirect the agent toward banning a user or posting a link. A community server that exposes kick, ban, and delete tools next to message reading gives that injected text real reach. Keep destructive tools out of any agent role that reads open channels.

Maintenance and breakage

Most community servers have one maintainer and no support commitment. Discord keeps moving: the privileged intent threshold switched from 100 servers to 10,000 users in June 2026, and endpoints get deprecated. When a server lags, your agent fails at runtime, often with a 401 or 403 that also counts against the invalid-request limit.

No audit trail

Calls go from the MCP process straight to Discord, and nothing records which of your users prompted which action. Discord's server audit log attributes moderation actions to the bot account, not to the person or tenant behind the request. When a customer asks why the bot deleted a thread, there is no answer. Audit trails for agent auth explains what that record needs to contain.

Policy liability

Self-bot servers violate Discord's terms outright. Bot-token servers leave every Developer Policy obligation with you: data minimization, no training on message content, and breach notification. If unreviewed code logs message content to disk or forwards it elsewhere, the violation is yours, not the maintainer's.

When to use each path

Each path has a legitimate use. The deciding questions are whose servers the agent touches and how many.

Use a community MCP server when

  • You are prototyping against your own test server with a throwaway bot that holds only the permissions the prototype needs.
  • You want to learn which tool shapes a Discord agent actually uses before designing the production surface.
  • The agent runs on one developer's machine, touches no customer servers, and never uses a self-bot token.

    Use the Discord API directly when

  • The agent reacts to Gateway events, such as every new message or member join, or owns slash commands end to end.

  • You run one bot for your own community and will operate sharding, rate-limit handling, and intent review yourself.

  • Request volume sits near the 50 requests per second global limit and you need direct control of scheduling across shards.

    Use Scalekit's Discord connectors when

  • You ship a B2B agent that works inside many customers' servers and need one credential per tenant, per-tenant logs, and clean revocation.

  • The same product links each user's Discord identity through OAuth and acts in channels through a bot.

  • The agent spans Discord plus tools like Linear, GitHub, or Zendesk and should reach them through one scoped MCP endpoint.

  • Different agent roles, such as a support responder and a moderator, need different tool surfaces from the same connectors.

    The credential problem that exists on every path

Pick any path and you still hold Discord credentials on behalf of other people. The transport never manages them for you.

N tenants, N bot tokens, N user grants

A B2B agent serving 40 customer communities holds up to 40 bot tokens if customers bring their own application, plus one OAuth grant per linked user. Each bot token needs encrypted storage and a rotation path. Each user grant needs a refresh before its seven-day expiry and cleanup when the user revokes the app. That work is identical whether the agent calls tools over MCP or REST. Token vaults for agent workflows covers the storage model.

Revocation has to reach the agent

A user deauthorizes your app, or a tenant churns. The agent has to stop acting instead of discovering the failure mid-task through a burst of 401s that also count toward the invalid-request ban. For OAuth grants, Scalekit emits connected_account.status_updated and connected_account.token_refresh_failed webhooks so your system can pause the agent and request re-authorization. A bot removed from a server keeps a valid token but loses access, so treat a 403 on a tenant's channel as a stop signal, not a retry.

Where Scalekit fits

Scalekit's Discord connectors handle the OAuth flow, token storage, and refresh for user grants, and vault one bot token per connected account, so the MCP vs API decision doesn't change your credential infrastructure. Credentials are encrypted with AES-256, resolved at request time, and never placed in the model's context.

Connecting Discord to your agent with Scalekit

The target is a community support agent for a tenant called org_acme: a bot that reads a support channel and replies, plus optional user identity linking. Install the SDK with pip install scalekit-sdk-python, then set SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, and SCALEKIT_CLIENT_SECRET from Developers > API Credentials in the Scalekit dashboard.

Set up the Discord connection

Create one application in the Discord Developer Portal. In the Scalekit dashboard, open AgentKit > Connections > Create Connection, pick Discord, and paste the redirect URI it shows into the application's OAuth2 redirects. Discord matches the URI exactly, so a trailing slash fails the flow with invalid_redirect_uri. Add the client ID, client secret, and only the scopes you need, such as identify and guilds. If you also install the bot through this flow, set Bot Permissions explicitly; an empty value grants zero permissions.

Set up the Discord Bot connection

On the application's Bot page, reset and copy the token, invite the bot to your test server with the URL generator's bot scope and the smallest permission set that works, and enable the Message Content intent if the agent reads messages. Then create a Discord Bot connection in Scalekit. Every connection_name you pass must match the connection name in the dashboard exactly. The connection names here are discord, discordbot, and linear.

Link a Discord user identity

get_or_create_connected_account returns the user's connected account. If it isn't active, the user completes Discord's consent screen through the authorization link. Once it is active, tools run against that user's token.

import os
from scalekit import ScalekitClient

scalekit_client = ScalekitClient(
    env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
    client_id=os.environ["SCALEKIT_CLIENT_ID"],
    client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
)
actions = scalekit_client.actions

# Must match the connection name in AgentKit > Connections exactly
DISCORD_USER_CONNECTION = "discord"
user_id = "user_123"  # your app's identifier for this user

account = actions.get_or_create_connected_account(
    connection_name=DISCORD_USER_CONNECTION,
    identifier=user_id,
).connected_account

if account.status != "ACTIVE":
    link = actions.get_authorization_link(
        connection_name=DISCORD_USER_CONNECTION,
        identifier=user_id,
    )
    print("Send the user here to connect Discord:", link.link)
else:
    guilds = actions.execute_tool(
        tool_name="discord_list_my_guilds",
        tool_input={"limit": 50},
        connection_name=DISCORD_USER_CONNECTION,
        identifier=user_id,
    )
    print(guilds.execution_id, guilds.data)
Enter fullscreen mode Exit fullscreen mode

Register a tenant's bot token

The bot connector has no consent screen. You write the token to a connected account keyed by your tenant ID, through the API in production or the connection's Connected Accounts tab for testing.

import os
from scalekit import ScalekitClient

scalekit_client = ScalekitClient(
    env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
    client_id=os.environ["SCALEKIT_CLIENT_ID"],
    client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
)
actions = scalekit_client.actions

# Must match the Discord Bot connection name in AgentKit > Connections exactly
DISCORD_BOT_CONNECTION = "discordbot"
TENANT_ID = "org_acme"  # one connected account per tenant bot install

actions.upsert_connected_account(
    connection_name=DISCORD_BOT_CONNECTION,
    identifier=TENANT_ID,
    authorization_details={
        "static_auth": {"api_key": os.environ["ACME_DISCORD_BOT_TOKEN"]}
    },
)

bot = actions.execute_tool(
    tool_name="discordbot_get_current_user",
    tool_input={},
    connection_name=DISCORD_BOT_CONNECTION,
    identifier=TENANT_ID,
)
print(bot.data)
Enter fullscreen mode Exit fullscreen mode

Scope tools and pin the channel with LangChain

The agent does not load the 221-tool catalog. actions.langchain.get_tools returns the tools authorized for this tenant's connected account, filtered to the two the support role needs, as native LangChain tools. A pre_modifier overwrites channel_id before every execution, so the model cannot redirect the bot to another server's channel. Install langchain-anthropic alongside the SDK.

import os
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage
from scalekit import ScalekitClient

scalekit_client = ScalekitClient(
    env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
    client_id=os.environ["SCALEKIT_CLIENT_ID"],
    client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
)
actions = scalekit_client.actions

DISCORD_BOT_CONNECTION = "discordbot"  # must match the dashboard connection name
TENANT_ID = "org_acme"
SUPPORT_CHANNEL_ID = os.environ["ACME_SUPPORT_CHANNEL_ID"]  # from your tenant record

# The channel comes from your tenant record, never from the model
@actions.pre_modifier(
    tool_names=["discordbot_list_channel_messages", "discordbot_create_message"]
)
def pin_support_channel(tool_input):
    tool_input["channel_id"] = SUPPORT_CHANNEL_ID
    return tool_input

tools = actions.langchain.get_tools(
    identifier=TENANT_ID,
    connection_names=[DISCORD_BOT_CONNECTION],
    tool_names=["discordbot_list_channel_messages", "discordbot_create_message"],
)
tool_map = {t.name: t for t in tools}
llm = ChatAnthropic(model="claude-sonnet-5").bind_tools(tools)

messages = [
    SystemMessage(
        f"You answer questions in Discord channel {SUPPORT_CHANNEL_ID}. "
        "Reply to the original message using message_reference."
    ),
    HumanMessage("Read the last 50 messages and answer any unanswered questions."),
]

while True:
    response = llm.invoke(messages)
    messages.append(response)
    if not response.tool_calls:
        print(response.content)
        break
    for call in response.tool_calls:
        result = tool_map[call["name"]].invoke(call["args"])
        messages.append(ToolMessage(content=str(result), tool_call_id=call["id"]))
Enter fullscreen mode Exit fullscreen mode

Resolve the pinned channel per request

Modifiers register on the client and run inside the SDK's execute_tool path. In a multi-tenant server, resolve the channel from request context inside the modifier instead of a module-level constant. The LangChain tool calling guide and the LangChain example go deeper.

Serve Discord over a Virtual MCP server

A Virtual MCP server declares which connections and tools an agent role can see. Create it once per role; its mcp_server_url is static and shared across tenants. This one exposes three Discord Bot tools and one Linear tool, so the tenant also needs an active Linear connected account under the same identifier.

import os
from scalekit import ScalekitClient
from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping

scalekit_client = ScalekitClient(
    env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
    client_id=os.environ["SCALEKIT_CLIENT_ID"],
    client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
)
actions = scalekit_client.actions

# Run once per agent role, not once per tenant or per user
config = actions.mcp.create_config(
    name="discord-community-support",
    connection_tool_mappings=[
        McpConfigConnectionToolMapping(
            connection_name="discordbot",
            tools=[
                "discordbot_list_channel_messages",
                "discordbot_start_thread_from_message",
                "discordbot_create_message",
            ],
        ),
        McpConfigConnectionToolMapping(
            connection_name="linear",
            tools=["linear_issue_create"],
        ),
    ],
).config

print("SCALEKIT_MCP_CONFIG_ID=", config.id, sep="")
print("SCALEKIT_MCP_SERVER_URL=", config.mcp_server_url, sep="")
Enter fullscreen mode Exit fullscreen mode

Connect Claude through the Virtual MCP server

Before each run, confirm the tenant's connections are active and mint a short-lived session token bound to that tenant. The URL is shared; the token carries the identity. Claude's MCP connector (beta) calls the server directly, so there is no tool loop to write. Install anthropic alongside the SDK.

import os
from datetime import timedelta
import anthropic
from scalekit import ScalekitClient

scalekit_client = ScalekitClient(
    env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
    client_id=os.environ["SCALEKIT_CLIENT_ID"],
    client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
)
actions = scalekit_client.actions

config_id = os.environ["SCALEKIT_MCP_CONFIG_ID"]
mcp_url = os.environ["SCALEKIT_MCP_SERVER_URL"]
tenant = "org_acme"

# 1. Every connection on the server must be active for this tenant
state = actions.mcp.list_mcp_connected_accounts(
    config_id=config_id, identifier=tenant, include_auth_link=True
)
inactive = [
    a for a in state.connected_accounts
    if (a.connected_account_status or "").upper() != "ACTIVE"
]
if inactive:
    for a in inactive:
        print(f"{a.connection_name} needs auth: {a.authentication_link}")
    raise SystemExit(1)

# 2. Mint a short-lived session token bound to this tenant
token = actions.mcp.create_session_token(
    mcp_config_id=config_id,
    identifier=tenant,
    expiry=timedelta(minutes=30),
).token

# 3. Hand the static URL and the per-run token to Claude
client = anthropic.Anthropic()
response = client.beta.messages.create(
    model="claude-sonnet-5",
    max_tokens=2048,
    betas=["mcp-client-2025-11-20"],
    mcp_servers=[
        {
            "type": "url",
            "url": mcp_url,
            "name": "scalekit",
            "authorization_token": token,
        }
    ],
    tools=[{"type": "mcp_toolset", "mcp_server_name": "scalekit"}],
    messages=[{
        "role": "user",
        "content": (
            f"Support channel: {os.environ['ACME_SUPPORT_CHANNEL_ID']}. "
            f"Linear team: {os.environ['ACME_LINEAR_TEAM_ID']}. "
            "Find bug reports in the last 50 messages, file each one in Linear, "
            "and reply in a thread under the report with the issue link."
        ),
    }],
)
print("".join(b.text for b in response.content if b.type == "text"))
Enter fullscreen mode Exit fullscreen mode

Pair tool scope with tenant scope

Virtual MCP scopes which tools exist; it does not rewrite arguments. Pair it with a per-tenant bot token so a wrong channel ID can only reach that tenant's own servers. The Virtual MCP setup guide covers server updates and token reminting.

Connect a Mastra agent in TypeScript

The Node.js SDK does not mint Virtual MCP session tokens yet, so mint the token on a Python backend with create_session_token and pass the URL and token to the Mastra process. Install @mastra/core@1, @mastra/mcp@2, @ai-sdk/openai@4, and dotenv, then run the file with npx tsx agent.mts.

import { Agent } from '@mastra/core/agent';
import { MCPClient } from '@mastra/mcp';
import { openai } from '@ai-sdk/openai';
import 'dotenv/config';

// Minted on your backend for one tenant; never shared across tenants
const mcpServerUrl = process.env.SCALEKIT_MCP_SERVER_URL;
const mcpToken = process.env.SCALEKIT_MCP_SESSION_TOKEN;
const supportChannelId = process.env.ACME_SUPPORT_CHANNEL_ID;
if (!mcpServerUrl || !mcpToken || !supportChannelId) {
  throw new Error('Set SCALEKIT_MCP_SERVER_URL, SCALEKIT_MCP_SESSION_TOKEN and ACME_SUPPORT_CHANNEL_ID');
}

const mcp = new MCPClient({
  servers: {
    scalekit: {
      url: new URL(mcpServerUrl),
      requestInit: { headers: { Authorization: `Bearer ${mcpToken}` } },
    },
  },
});

try {
  // Tools arrive namespaced by server key, e.g. scalekit_discordbot_create_message
  const tools = await mcp.listTools();
  const agent = new Agent({
    id: 'discord-community-support',
    name: 'Discord community support',
    instructions: `You support the community in Discord channel ${supportChannelId}. File confirmed bugs in Linear.`,
    model: openai('gpt-4o'),
    tools,
  });
  const result = await agent.generate('Summarize the unanswered questions from the last 50 messages.');
  console.log(result.text);
} finally {
  await mcp.disconnect();
}
Enter fullscreen mode Exit fullscreen mode

The Mastra tool calling guide and the Mastra example cover the full setup.

Why the Scalekit path holds up in production

Two properties matter once a Discord agent leaves the prototype: seeing what it did, and limiting what it can reach.

Downstream tool-calling logs

Every execute_tool call returns an execution_id. In the dashboard, AgentKit > Connected Accounts shows each account's status, token refresh history, and tool execution logs, so a ticket about a deleted message traces back to a tenant, a connected account, and a specific call. The Discord connector page lists a 90-day audit trail. That is the record Discord's own audit log cannot give you, because it only names the bot.

Scoped tool surfaces

Tool surface is a cost problem and an accuracy problem. At the roughly 200 tokens per tool that Scalekit's Virtual MCP docs use as a rule of thumb, exposing all 221 Discord Bot tools spends about 44,000 tokens of context before the agent does any work. A model choosing among ban, kick, prune, and delete tools on every turn is one bad inference from an incident. The discord-community-support server exposes four tools.

One endpoint for many tenants and tools

One Virtual MCP server definition serves every tenant, and each run gets a session token scoped to one identifier. Adding GitHub or Zendesk to the same role is a config change, not another server to host and patch. Virtual MCP servers explained and when to use a Virtual MCP server cover the model in depth.

Start from a template

The support triage agent and incident response agent templates use the same connected-account pattern with other tools; Discord Bot slots in as the chat surface. The free tier on the pricing page includes 5,000 tool calls a month and unlimited connected accounts. All other connectors live in the connector catalog.

Which one to build against

If the agent lives on your own test server and touches nobody else's data, a community MCP server with a minimal-permission bot is a fine way to learn. Never run it with a self-bot token. If you run one bot for one community and need Gateway events end to end, the direct API gives you full control and full operational ownership.

If your agent works inside customers' Discord servers, the question stops being MCP versus REST. It becomes how many bot tokens and user grants you hold, how each one is isolated, and what record exists when the bot acts. That is credential infrastructure, and it is the same work on every path.

Get help with your Discord agent

Building a Discord agent for customer communities? Talk to us for help with the auth model, or start from the Discord connector and the Discord Bot connector.

Top comments (0)