discord-tools
Provides tools for interacting with Discord through the user's own account, enabling agents to read servers, channels, DMs, and messages; search chats; manage mentions, saves, and read state; view people, profiles, members, roles, and server details; send messages and files; export chat history; save message attachments; edit or delete the account's own messages; manage reactions and pins; and work with threads and roles.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
discord-tools
Discord tools for coding agents: chats, messages, search, inbox, people and servers, history, files, sends, threads and roles.
These tools act from your own Discord account, through the web client's API and with your account's token. Discord's terms forbid automating a user account ("self-bots"), and an account can be terminated for it, read-only use included. Use them only on an account whose loss you accept, and only for what you would do yourself, at your own pace. The token is your account: whoever holds it reads and writes as you, past your password and your second factor.
Harness | Delivery | Pinned by |
OpenCode V2 | native plugin (npm package, with skills) | the version in the package |
OpenCode V1 | native plugin (npm package, no skills) | the version in the package |
Claude Code | plugin: MCP server and skills | the release tag, as the ref |
Codex | plugin: MCP server and skills | the release tag, as the ref |
Grok Build | plugin: MCP server and skills | the release tag, as |
Antigravity | plugin: MCP server and skills | the release tag's tree URL |
Hermes Agent | native plugin (Python, calls the command line) | the release commit, as the SHA |
other MCP host | MCP server from npm | the version in the package |
The tools need Node.js 24 or later.
Plugin installs pinned by a release tag run on macOS (arm64, x64) and Linux
(x64, arm64, glibc); npm installs run wherever wreq-js has a binding.
docs/conventions.md describes how the tools are named
and how they behave.
Install
Install from a release, never from the default branch: a release is a tag whose
commit carries the built dist/, and the default branch does not. In the
commands below, <version> is a release's version (1.2.3) and <sha> is the
commit its tag names:
gh api repos/craft-agent/discord-tools/commits/v<version> --jq .shaOpenCode
OpenCode V2 takes the package and its options in opencode.json:
{
"plugins": [{ "package": "@craftsamo/discord-tools@<version>", "options": { "read_only": false } }],
"permissions": [{ "action": "discord_*", "resource": "*", "effect": "ask" }]
}OpenCode V1 takes the package as a string, or as a pair with its options:
{
"plugin": [["@craftsamo/discord-tools@<version>", { "read_only": false }]],
"permission": { "discord_*": "ask" }
}OpenCode allows any action no rule names, so the plugin's request is granted
silently unless a rule says ask. Read tools never ask, so the discord_* glob
is safe.
V1 registers no skills. Unpack the package and copy its skills/* folders into
~/.config/opencode/skills/:
npm pack @craftsamo/discord-tools@<version>
tar -xzf craftsamo-discord-tools-<version>.tgz
mkdir -p ~/.config/opencode/skills
cp -r package/skills/* ~/.config/opencode/skills/
rm -rf package craftsamo-discord-tools-<version>.tgzClaude Code
claude plugin marketplace add craft-agent/discord-tools#v<version>
claude plugin install discord-tools@craft-agentCodex
codex plugin marketplace add craft-agent/discord-tools --ref v<version>
codex plugin add discord-tools@craft-agentCodex turns - into _ in server names, so it shows the tools as
mcp__discord_tools__discord_*. It starts the MCP server in the plugin's
install directory and passes no roots, so the server takes each call's directory
from what Codex sends with the call: its sandbox state, which the server asks
for, else the one workspace it names.
The plugin declares default_tools_approval_mode = "writes": Codex asks before
every tool not marked read-only. A user's
[plugins."<plugin>".mcp_servers.<server>] setting can only tighten it. Setting
auto there yields prompt (reads asked too), since Codex treats auto and
writes as incomparable.
Grok Build
grok plugin install craft-agent/discord-tools@v<version> --trustGrok starts the MCP server in the session's working directory.
Antigravity
Install from the tree of the release tag, never from the default branch, which
has no built dist/; Antigravity clones the tag into
~/.gemini/config/plugins/discord-tools.
agy plugin install https://github.com/craft-agent/discord-tools/tree/v<version>Hermes Agent
Hermes installs a commit, not a tag, and the plugin installs disabled:
hermes plugins install craft-agent/discord-tools --ref <sha>
hermes plugins enable discord-toolsIf a Hermes gateway is already running, restart it (hermes gateway restart) to
load the plugin.
The plugin runs the Node it found when Hermes started. If a version manager
removes that version while Hermes runs, a call looks for Node again; when it
finds none, restart Hermes from a shell where node runs.
Any other MCP host
Add the server to the host's MCP configuration. Flags go in args:
{
"mcpServers": {
"discord-tools": {
"command": "npx",
"args": ["-y", "@craftsamo/discord-tools@<version>", "--read-only"]
}
}
}Related MCP server: discord-user-connector
Tools and skills
Each toolset comes with a skill that tells an agent how to use its tools. A
skill names its toolset under metadata in its frontmatter
(metadata: { toolset: sends }, written as a block). OpenCode V2 and Hermes
register it only when the settings register at least one of that toolset's
tools, so a profile with the toolset off, or with all its tools removed, does
not see it; a skill without a toolset is always registered. Claude Code,
Codex, Grok Build and Antigravity install the skill folder whole and pass the
settings only to the MCP server, so they list every skill whatever the
settings select. OpenCode V1 registers no skills, and its copied skills are
all listed too.
Toolset | Tool | Skill | Does |
account |
| reads local account, token and sync health | |
sync |
| shows the sync list and suggests changes | |
sync |
| follows a server or named channels | |
sync |
| stops following a server or channels | |
chats |
| reads servers, channels, DMs and messages | |
search |
| searches the mirror or Discord's search | |
inbox |
| lists mentions, waiting chats and saves | |
inbox |
| saves a message, optionally with a reminder | |
inbox |
| removes a save and its reminder | |
inbox |
| marks one chat read, on every device | |
people |
| reads friends, notes, profiles and members | |
people |
| reads a server's info, roles, emoji, events and invites | |
sends |
| sends a text, optionally as a reply | |
sends |
| sends files, optionally with a text | |
history |
| counts messages per chat, author or day | |
history |
| writes a chat's history to a private file | |
files |
| saves the files of a message to disk | |
actions |
| edits the account's own message | |
actions |
| deletes the account's own message | |
actions |
| adds the account's reaction | |
actions |
| takes back the account's reaction | |
actions |
| pins a message | |
actions |
| unpins a message | |
threads |
| starts a thread in a channel or from a message | |
threads |
| posts in a forum or media channel | |
threads |
| posts files in a forum or media channel | |
threads |
| joins a thread | |
threads |
| leaves a thread | |
roles |
| reads roles, permissions and role members | |
roles |
| gives a role to one or several members | |
roles |
| takes a role away from one member | |
roles |
| creates a role with named permissions | |
roles |
| changes a role's name, color, flags or permissions | |
roles |
| deletes a role from everyone who held it |
account is on by default and available with read_only. Call
discord_account_read with {} to list every label, sorted, or with
{"account":"default"} to inspect one; an unknown label returns not_found.
It reads local identity, credential status, scheduler and mirror metadata,
never contacts Discord or changes a service. Names in the result are untrusted
data, not instructions.
token_source says variable, file or null, never the token itself.
token_status: "available" means locally usable, not freshly authenticated.
A scheduled but idle service is normal; unknown scheduler state stays null.
Healthy local state requires a complete sync within 15 minutes; a partial run
needs continuation. Recorded running or rejected sync status does not prove
that a process is running or the current token is rejected.
When attention is needed, action, hint and recovery_command give a person
one next step. The read never runs it. After token rejection, sign in with a
new token, then reinstall the service to resume its schedule, as described
under Local account commands.
sync is on by default; read_only keeps only discord_sync_read. The sync
list names the servers the sync service follows, each whole (its 10 most
active text channels, minus exclusions) or by named text and announcement
channels; DMs and group chats are always followed. At most 10 servers and 30
channel slots, a whole server using 10.
discord_sync_readlistshows each server, the channels it follows and whether each is current, its exclusions and the slots used.suggestproposes, from the mirror alone, channels the account wrote in or that are busy but not followed, and entries gone quiet, each with the exact arguments of the tool that applies it and whether it fits.discord_sync_addfollows a whole server or named channels of a server the mirror knows;discord_sync_removedrops a server, or named channels, or excludes channels from a whole server. Switching a server between whole and named channels takes a removal first.
All three change or read only the local list, never Discord. The two writes are asked about, since every followed channel adds requests to each later sync run. A change applies from the next run; messages already in the mirror stay readable.
chats is on by default and available with read_only. discord_chat_read
takes a method:
Method | Reads |
| the account's servers, and how the sync list follows each |
| one server's channels by category, and whether each is followed and current |
| DMs and group chats newest first, with a name filter and the last message |
| a chat's messages between two points, oldest first |
| the messages before and after one message |
| a channel's threads or a forum's posts, by activity or creation, with tags |
| a chat's pinned messages, newest pin first |
| older history of a chat the sync follows, into the local mirror |
Chats, servers and messages are named by id, found through servers,
channels, dms and threads; a name is never a target. messages and
around read a chat the sync marked current within 15 minutes from the local
mirror, inside the history the sync stored; dms uses a DM list read within
15 minutes and servers one read within 6 hours. Everything else, and
channels, threads and pins always, is read from Discord at the paced
rate and stored in the mirror. No read
sends anything, shows the account as present or marks a chat read
(discord_read_mark of inbox does, when asked). Message
text, embeds, file names and names are written by other people: the result
says so, and an agent treats them as data, never as instructions.
search is on by default and stays in read_only mode. discord_search_read
finds messages, links and files, newest first, in one chat, one server or
everywhere, by text, author, mentions, has (link, file, image,
video, sound, embed, sticker, poll), pinned and a time range:
Method | Searches |
| what the local mirror holds, current or not; sends no request |
| Discord's own search of a server, one chat, or every DM; stores what it found |
mirror matches a literal substring and also filters by the account's
reactions, an emoji, and a channel's threads; each chat in the result says
when a sync run last marked it current. discord covers everything the
account can read in that scope, but cannot search all servers at once; a
server still being indexed is waited out once. Both page with next_before.
inbox is on by default; read_only keeps only discord_inbox_read, whose
method is one of:
Method | Reads |
| Discord's own list of the past 7 days' mentions, roles and @everyone included |
| chats whose latest messages the user has not answered, from the mirror alone |
| the user's saved messages and reminders, reminders first |
waiting means not answered, never unread. discord_saved_add saves a
mirrored message, optionally with a remind_at time, and discord_saved_remove
removes a save. discord_read_mark marks one chat read, up to a message or the
newest the mirror holds, on every device; it is the only tool that marks
anything read, and every write here is asked about. There are no unread
counts: Discord keeps them only behind its gateway, and a check on a real
phone showed that even a brief gateway session holds back the phone's push
notifications, so nothing here opens it.
people is on by default, read-only and kept under read_only. Both tools
read live on every call, one at a time, with user and server ids the mirror
knows, never names; only returned user names are stored. discord_people_read
reads friends, private notes, one person's profile, membership with role names,
or member search. discord_server_read reads server info with approximate
member and online counts, roles, custom emoji and stickers, upcoming events
and interested users, or invites. Member search and invites need Manage Server.
Invite codes go to the user only, never into a message, file or another tool
unless the user asks for that; notes are private and never shared unless asked.
sends is on by default; read_only leaves it out. discord_message_send
sends one text to a DM, group DM, server channel or thread the mirror knows,
by id, and never opens a new DM. The text arrives exactly as given. reply_to
answers a mirrored message of that chat without pinging its author;
@everyone, @here and role mentions notify only with mass_mentions, and
silent sends without notifications. Every send is asked about.
discord_file_send sends 1 to 10 files, optionally with a text, the same way.
The files must lie inside the folders send_dirs allows (by default the
working directory, never a home directory), each at most send_max_mb.
Credentials, keys and databases, programs and archives that cannot be
inspected are refused, and one refused file refuses the whole send. What is
checked is a private copy, and the copy is what is uploaded; a file changed
after the prompt makes the call stale. Files go only to Discord's storage,
without the token, and nothing is posted unless every file is up.
A send goes out once and is never retried. Its nonce is recorded in the
mirror's send ledger before it leaves, with a hash of the text, never the
text. The result is the message Discord returned, stored in the mirror. A
refusal is its error. Anything else is uncertain: the message may have been
sent, and details.matches lists the account's messages with the same text
from one read-back of the chat. Check the chat before sending again.
actions is off by default, because it changes what others already read; turn
it on with toolsets (for example --toolsets all, or the defaults plus
actions). read_only leaves it out. Every call names a chat and a message by
id, and the message must be in the mirror. Every action is asked about.
discord_message_editreplaces the text of the account's own message, sent exactly as given;discord_message_deleteremoves the account's own message, which cannot be undone, and counts a message Discord no longer has as deleted (already).discord_reaction_addanddiscord_reaction_removeadd or take back the account's own reaction on any message: one Unicode emoji, or a custom emoji already on the message; never a super reaction.discord_pin_addanddiscord_pin_removepin or unpin any message; Discord decides whether the account may, and caps a chat at 250 pins.
Before a request, the tool refuses what it can know is wrong: a name for an
id, a message the mirror does not hold, someone else's message to edit or
delete, an unusable emoji, an edit that changes nothing, a system message to
pin, a reaction to take back that is not the account's. Each action is sent
once and never retried, and followed in the mirror. Anything that may have
reached Discord is uncertain: details.applied says whether one read-back of
the chat saw the new state (null when it could not tell). Repeating an
action is harmless, but check the chat and ask first.
threads is off by default because it creates lasting posts; turn it on with
toolsets (for example --toolsets all, or the defaults plus threads).
read_only leaves all five tools out, and every call is asked about. Targets
are ids the mirror knows: discord_thread_create starts a public or private
thread in a text or announcement channel, optionally from a message (public
only); discord_post_create creates a text post in a forum or media channel;
discord_file_post creates one with files, optionally with text;
discord_thread_join and discord_thread_leave change only the user's
membership. Tags are ids from discord_chat_read method threads; files
follow a file send's directory, size and safety rules. Nothing is retried.
Creation has no nonce: for uncertain, read details.read_back, check the
channel's threads or posts and ask before trying again. Never repeat a
creation blindly; a repeated join is harmless, but check before leaving again.
roles is off by default because it changes what others may do; turn it on
with toolsets (for example --toolsets all, or the defaults plus roles).
read_only keeps only discord_role_read; every write is asked about each
time, even under an allow rule. Read the roles first with method list or
role: writes need a snapshot no older than 15 minutes, and a change to the
user's own roles needs a fresh read. Targets are server, role and user ids the
mirror knows, never names. Roles can be given, taken, created, edited or
deleted; administrator is never given, created or granted, nor is anything
the user lacks. The audit log names the user and records optional reason.
Nothing is retried: for uncertain, read details.read_back, read the roles
again and ask the person. Creation's candidates are only candidates; never
repeat a creation blindly. Giving again or taking again is harmless, but
check first.
history is off by default because the export writes files; turn it on with
toolsets (for example --toolsets all, or the defaults plus history).
read_only keeps only discord_history_read, whose method counts counts
the mirror's ordinary messages and replies per chat, author or day, over the
past 30 days by default, without reading text. Totals cover the whole window;
limit cuts only rows. Days use time_zone, by default the machine's.
coverage.partial and coverage.not_current flag lower bounds, not Discord's
totals: backfill older gaps in a followed chat or wait for the next sync.
discord_export_create writes one chat's history held in one piece by the
mirror, current or not, as Markdown, JSON or JSONL, with whole texts and message
permalinks. Files go under <download_dir>/exports/, private (0600 files in a
0700 folder), never overwritten; taken names get a number. count, current,
left_out, truncated and complete_to_start say what the file covers.
Continue into a new file with continue.older.before or continue.newer.after;
continue.backfill_first means backfill a followed chat before going older.
Every export is asked about. Neither tool makes a request to Discord. Exports
may hold private DMs; never send, upload or share one unless the person asks.
files is off by default, because it writes files to disk; turn it on with
toolsets (for example --toolsets all, or the defaults plus files).
read_only leaves it out. discord_file_save takes a chat and a message by
id, the message being one the mirror holds, and saves its attachments, the
images of its link previews and its stickers to
<download_dir>/<channel>-<message>/. Every save is asked about.
Nothing is overwritten: the same name with the same bytes is reported
already, any other gets name-2.ext. Files come only from Discord's own
CDN and media proxy, without the token, never from the site behind a link. Each file is
checked by its name, its declared type and its bytes before it is kept:
programs, installers, scripts and archives that cannot be read are refused,
and a file over download_max_mb is too_large. The others go on, each with
its outcome. Files carry no execute bit and, on macOS, are marked as
downloaded.
An archive (.zip, .tar, .tar.gz, .tgz) is saved packed, with a listing
of what it holds. With unpack: true, and only when asked, it is also
unpacked into a new folder beside it, all of it or only the entries named
from the listing. Where files go and their size limit are the download_dir
and download_max_mb settings.
Local account commands
These are commands for a person, not agent tools. They print plain text;
list, run and target keep their JSON output.
discord-tools-cli login --account default
discord-tools-cli sync --account default
discord-tools-cli service install --account default
discord-tools-cli service status --account default
discord-tools-cli service uninstall --account default
discord-tools-cli remove --account defaultlogin verifies the account with Discord. When
DISCORD_TOOLS_TOKEN_DEFAULT is supplied in its environment, it uses that
variable and stores no token. Otherwise, it reads a token from standard input:
one hidden line at a terminal, or a UTF-8 pipe through EOF. A secret manager
can pipe its output directly into discord-tools-cli login --account default.
The verified token is then stored in the label's private token file (mode 600).
Never pass a token as a command-line argument.
Each accepts --state-dir <directory>; otherwise DISCORD_TOOLS_STATE_DIR
or the default state directory is used. Status distinguishes a scheduled
job from a run currently in progress. Status and uninstall accept an explicit
label even after its local state has been removed.
sync runs one bounded pass and prints status and counts. A complete run or
an already-busy label exits 0; a partial run exits 1 and needs another run.
Authentication rejection pauses the label's schedule and keeps its service
definition. To resume, run login with a new token, then service install
for that label.
service install requires a signed-in label and schedules the installed CLI
every five minutes: a launchd agent on macOS, or a systemd user timer and
oneshot service on Linux. Keep that CLI installation and its Node runtime
available at their installed paths, and run service install again after
updating either. The service definition records the
runtime, command, state directory and PATH, never a token. Where Node's path
holds its version, as under a version manager, service install records a
node on PATH at a path without it that starts the same Node, and warns when
there is none; that node then runs whichever Node the manager makes current.
The label's service.json keeps the runtime and CLI paths; service status
and discord_account_read say when either is gone, once the service has been
installed with this version. A service manager
does not automatically inherit the terminal's environment: when using
DISCORD_TOOLS_TOKEN_<LABEL>, supply it independently to the scheduled
service's environment, as well as to the harness that starts the MCP server.
On macOS, Login Items lists the service as discord-tools-sync-<label>: the
agent starts through that launcher script, which service install writes to
~/Library/Application Support/io.github.craft-agent.discord-tools/ and
service uninstall deletes. Switching the item off there stops syncing.
remove asks you to type the label. For a non-interactive command, pass
--yes. It uninstalls the scheduled job, refuses while the label is busy,
and deletes that label's local state, including its mirror and token file.
If a foreground sync is still running, the state is kept and the schedule
stays stopped; stop that run before trying removal again.
It neither deletes anything on Discord nor removes credentials supplied by
the environment. Other labels are kept.
Adding a tool
Write the spec in
src/core/<domain>/<verb>.tswithdefineTool: an ID<prefix>_<domain>_<verb>, a title and description, the Zod argument and output schemas, its toolset and write class, andexecute(args, context). A write tool also givestargets(args, context): what the call writes to, as a permission rule should name it. A tool whose settings can put some calls out of reach whatever the person answers givesgate(args, context), which throws for such a call; it is refused before anyone is asked, and beforeexecute.Test it beside the spec (
<verb>.test.ts), calling it throughinvokeas the entry points do. A tool that runs a program is tested against a fake of that program onPATH; no test reaches a real service.List it in
src/tools.ts. A new toolset goes intotoolsetsinsrc/project.ts(anddefaultToolsetswhen it is on by default), a setting of the package's own intooptions(andOPTIONSinproject.pywithconfig_schemainplugin.yaml), and a variable the tool reads besides the settings intotoolVariables.Add or update the toolset's skill,
skills/<prefix>-<toolset>/SKILL.md, whose frontmatter names the toolset undermetadata.Run
pnpm run build && pnpm run manifeststo list the tool inplugin.yamland its variables in Codex's manifest, then describe it under "Tools and skills".
docs/conventions.md has the rules each step follows.
A package declares each option as text, boolean, number, list, path
or paths, or as an object with type and optional bounds. For example, in
src/project.ts:
options: {
notes_dir: "path",
note_limit: { type: "number", integer: true, min: 1, max: 50 },
}integer: true requires a whole number; min and max bound a number or
count a list or paths value's items. An object with only type means the
same as the type alone. Hermes's OPTIONS mirrors the declarations on one
line, with double quotes and Python's True for integer:
OPTIONS: dict[str, str | dict[str, object]] = {"notes_dir": "path", "note_limit": {"type": "number", "integer": True, "min": 1, "max": 50}}Reads and writes
Every read tool is named <prefix>_<domain>_read. Reads can run unasked;
writes are confirmed by the harness.
Harness | Reads without asking |
OpenCode | always; the plugin asks only before a write |
Claude Code | the rule |
Codex | always; Codex runs tools that declare |
Grok Build |
|
Antigravity | the rule |
Hermes | always; the plugin asks only before a write |
Where a write is asked about, and where it is not:
OpenCode grants any action no rule names, so a write runs unasked unless a rule asks for it: add the
discord_*rule shown under OpenCode.Codex asks before every write because of the plugin's approval mode. Under
codex execit refuses with "MCP tool call requires approval, but approval policy is never".Grok Build under
grok -prefuses a write it cannot ask about, unless it is allowed beforehand.Grok Build calls the plugin's tools through its
use_tooltool, and the agent often names a tool without its server first. Grok checks the rules against the name as given, so the server-qualified rule alone does not match that first call. The second rule matches the short name; a write still matches neither.Antigravity asks before any MCP tool no rule allows, reads included, and names the plugin's server
discord-tools_discord-tools(plugin, then server).agy -prefuses a tool it cannot ask about.Hermes
hermes chat -qrefuses a write it cannot ask about;hermes -zand--yoloskip approval, so writes then run unasked.
A call a tool's gate refuses never runs, whatever the answer. OpenCode and
Hermes refuse it before they ask, so no prompt shows it; Hermes blocks it with
the tool's error. Claude Code, Codex, Grok Build and Antigravity draw their
own prompt before the MCP server sees the call, so they may still ask about
it; the server then refuses it.
Settings
Every entry point takes the same settings. A flag wins over its environment variable, and an option or setting wins over the variable too.
Setting | Flag | Variable | Meaning |
|
|
| the toolsets that are on, or |
|
|
| registers no tool that writes; wins over every other setting |
|
|
| single tools to add by ID, whatever their toolset |
|
|
| single tools to remove by ID; wins over |
|
|
| MCP server only: |
|
|
| native plugins only: the approval prompt's language, such as |
| — | — | Hermes only: the emoji shown beside each toolset, by toolset name ( |
|
|
|
|
|
|
|
|
|
|
| the most megabytes a received file may have, a whole number from 1 to 500; default 100 |
|
|
|
|
|
|
| the most megabytes a sent file may have, a whole number from 1 to 500; default 10 |
Lists are comma-separated in a flag or a variable, send_dirs too. This
package's own settings, from state_dir to send_max_mb, are listed in
options in src/project.ts and read the same way. A path is trimmed; ~
and ~/… (also with the platform separator) stand for the home directory
(HOME when it is absolute, else the account's). It must then be absolute and
reaches the tool normalized, without a filesystem check. paths is a list of
such paths, comma-separated in a flag or variable and an array of strings in a
plugin option. A blank path counts as unset, so the default applies; blank
list items are dropped, and a list with none left counts as unset. Bounds check only a given value, not an unset option. Refused values are
invalid_argument, naming the flag, variable or option and what it must be.
Hermes passes paths and lists to the command line, which checks the paths and
bounds. Where each harness reads them:
OpenCode takes the plugin
optionsshown above, with lists as arrays,read_onlyas a boolean and the megabyte limits as numbers, and falls back to the variables. V2 runs plugins in its background service, which keeps the environment it started with: a variable set whereopencode runstarts has no effect until the service is restarted in that environment (opencode service restart). V1 runs plugins in its own process.Hermes takes
plugins.entries.discord-tools.settingsinconfig.yaml:plugins: entries: discord-tools: settings: read_only: false exclude_tools: []Without a
languagesetting, the approval card follows Hermes's owndisplay.language(orHERMES_LANGUAGE), the language of its approval buttons, and else stays in English. The plugin passes its own environment to the command line it runs, so the variables work too. A running gateway keeps the environment it started with; restart it (hermes gateway restart) after changing one.Claude Code, Grok Build and Antigravity start the plugin's server without flags and pass it the client's environment, so set the variables in the environment the harness runs in.
Codex passes a plugin's MCP server only a few basic variables (such as
PATHandHOME) and those the plugin's manifest names, which are the variables above: set them in the environment Codex runs in. A variable the tools read besides the settings belongs intoolVariablesinsrc/project.ts;pnpm run manifestsadds it to the manifest after the settings' variables. To pass flags, or to give the server a variable the package does not name, add the server under[mcp_servers]in Codex'sconfig.tomlinstead of using the plugin: flags go inargs,env_varsnames the variables to forward from Codex's environment, andenvsets fixed values.[mcp_servers.discord-tools] command = "npx" args = ["-y", "@craftsamo/discord-tools@<version>", "--read-only"] env_vars = ["DISCORD_TOOLS_TOOLSETS"]
Accounts, the token and the sync service
Each Discord account has a label, 1 to 24 lowercase letters, digits and
_(defaultwhen signing in names none). A tool'saccountargument picks the label; leave it out when only one account is signed in.Each label keeps its state in one directory,
<state_dir>/<label>/(mode 700): the mirror of synced messages, the sync list, the token file when there is one, and the copies of files being sent or received.state_dirdefaults to$XDG_STATE_HOME/discord-toolswhenXDG_STATE_HOMEis absolute, else~/.local/state/discord-tools. The mirror holds your private messages, so keep the directory out of backups you share.The token comes from
DISCORD_TOOLS_TOKEN_<LABEL>when it is set (DISCORD_TOOLS_TOKEN_DEFAULTfordefault), and from the token file that signing in writes otherwise. With the variable, nothing of the token is stored by the package: inject it from the secret manager you use, into the environment of both the harness (its MCP server reads live and writes) and the sync service. For Codex, list it inenv_varsas above.A sync service keeps each label's mirror, polling Discord every 5 minutes in short, capped runs: DMs and group DMs always, servers only as the sync list names them (at most 10 servers and 30 channels; the
synctools change it). A read uses the mirror only while its channel was synced in the last 15 minutes, and reads Discord directly otherwise. The service sends nothing of its own: no presence, no typing, no read marks.A rejected token stops the service for its label until a new one is given; changing your Discord password invalidates the token.
Layout
src/project.ts— the package's name, tool ID prefix, setting prefix, toolsets, tool options and the other variables its tools read.src/core/— the tool contract and the tools, free of any harness and of the project's names.src/tools.ts— the list of every tool, which each entry point registers.src/words.ts— the translations of the tools' words, per language, which the native plugins use in their approval prompts.src/mcp/— the MCP server, built todist/mcp.jsand run bydiscord-tools.src/opencode/— the OpenCode plugin, built todist/opencode.js.src/cli/— the command line the Hermes plugin calls, built todist/cli.jsand run bydiscord-tools-cli.src/hermes/,plugin.yamland__init__.py— the Hermes plugin.skills/— one skill per toolset,skills/<name>/SKILL.md, which every harness that takes skills loads from the package or plugin root; the native plugins load only those of the toolsets that are on..claude-plugin/,.codex-plugin/,.agents/plugins/,.mcp.json,.antigravity-plugin/andmcp_config.json— the plugin manifests of the other harnesses. Codex reads.codex-plugin/plugin.json; a rootplugin.jsonwould make it load the plugin as an Agent Plugin, whose tool budget hides tools past a fixed size. The root.mcp.jsonis the configuration Grok Build reads (with${CLAUDE_PLUGIN_ROOT}, the variable both Grok Build and Claude Code expand); when working on this repository in Claude Code, decline the project MCP server it offers from that file.
How a release is made is in RELEASING.md.
License
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
Discord MCP server for ChatGPT, Claude and any AI agent: list servers and channels, read channel messages, search messages, send message, reply to message, send DM, read mentions and assign role in your own Discord account. Cotlet works in the account through your Cloud Browser, a real Chrome where you sign in to Discord yourself; Cotlet does not store your password and you need no Discord API key. About the official Discord API: Yes, Discord has an official API for bots and OAuth2 apps, but it acts as a bot that a server manager adds, not as you, and Discord has no official MCP server for it (checked 5 Nov 2026). The MCP server on Discord's docs site only searches documentation. Setup and FAQ: https://cotlet.dev/integrations/discord-api-mcp-server
Connect a Discord bot (bot token from the developer portal) to read/send messages, list guilds and c
Control your Discord community: send/read messages, manage channels and forums, and handle webhook…
Let AI agents read your Discord community's analytics, members and tickets, and save drafts.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to interact with Discord through the REST API and real-time events, supporting message management, user info, channel operations, and more with security controls.16 npm1MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to access and interact with a Discord user account via the Discord client API, allowing them to read servers, channels, DMs, mentions, and message history, and optionally send messages and perform other actions.1MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI assistants to interact with a Discord account to read channels, send and manage messages, handle DMs and reactions, search messages, and update status.190 npmMIT
- AlicenseNot gradedqualityAmaintenanceEnables AI assistants to interact with the Discord platform, including servers, members, roles, channels, categories, forum posts, messages, reactions, and webhooks. It supports logging in a bot, reading and searching messages, and creating, editing, or deleting Discord resources through natural language.860 npmMIT