Skip to main content
Glama
discord-tools

discord-tools

by craft-agent

discord-tools

Discord tools for coding agents: chats, messages, search, inbox, people and servers, history, files, sends, threads and roles.

WARNING

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 @ref

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 .sha

OpenCode

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>.tgz

Claude Code

claude plugin marketplace add craft-agent/discord-tools#v<version>
claude plugin install discord-tools@craft-agent

Codex

codex plugin marketplace add craft-agent/discord-tools --ref v<version>
codex plugin add discord-tools@craft-agent

Codex 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> --trust

Grok 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-tools

If 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

discord_account_read

discord-account

reads local account, token and sync health

sync

discord_sync_read

discord-sync

shows the sync list and suggests changes

sync

discord_sync_add

discord-sync

follows a server or named channels

sync

discord_sync_remove

discord-sync

stops following a server or channels

chats

discord_chat_read

discord-chats

reads servers, channels, DMs and messages

search

discord_search_read

discord-search

searches the mirror or Discord's search

inbox

discord_inbox_read

discord-inbox

lists mentions, waiting chats and saves

inbox

discord_saved_add

discord-inbox

saves a message, optionally with a reminder

inbox

discord_saved_remove

discord-inbox

removes a save and its reminder

inbox

discord_read_mark

discord-inbox

marks one chat read, on every device

people

discord_people_read

discord-people

reads friends, notes, profiles and members

people

discord_server_read

discord-people

reads a server's info, roles, emoji, events and invites

sends

discord_message_send

discord-sends

sends a text, optionally as a reply

sends

discord_file_send

discord-sends

sends files, optionally with a text

history

discord_history_read

discord-history

counts messages per chat, author or day

history

discord_export_create

discord-history

writes a chat's history to a private file

files

discord_file_save

discord-files

saves the files of a message to disk

actions

discord_message_edit

discord-actions

edits the account's own message

actions

discord_message_delete

discord-actions

deletes the account's own message

actions

discord_reaction_add

discord-actions

adds the account's reaction

actions

discord_reaction_remove

discord-actions

takes back the account's reaction

actions

discord_pin_add

discord-actions

pins a message

actions

discord_pin_remove

discord-actions

unpins a message

threads

discord_thread_create

discord-threads

starts a thread in a channel or from a message

threads

discord_post_create

discord-threads

posts in a forum or media channel

threads

discord_file_post

discord-threads

posts files in a forum or media channel

threads

discord_thread_join

discord-threads

joins a thread

threads

discord_thread_leave

discord-threads

leaves a thread

roles

discord_role_read

discord-roles

reads roles, permissions and role members

roles

discord_role_assign

discord-roles

gives a role to one or several members

roles

discord_role_unassign

discord-roles

takes a role away from one member

roles

discord_role_create

discord-roles

creates a role with named permissions

roles

discord_role_edit

discord-roles

changes a role's name, color, flags or permissions

roles

discord_role_delete

discord-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_read list shows each server, the channels it follows and whether each is current, its exclusions and the slots used. suggest proposes, 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_add follows a whole server or named channels of a server the mirror knows; discord_sync_remove drops 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

servers

the account's servers, and how the sync list follows each

channels

one server's channels by category, and whether each is followed and current

dms

DMs and group chats newest first, with a name filter and the last message

messages

a chat's messages between two points, oldest first

around

the messages before and after one message

threads

a channel's threads or a forum's posts, by activity or creation, with tags

pins

a chat's pinned messages, newest pin first

backfill

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

mirror

what the local mirror holds, current or not; sends no request

discord

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

mentions

Discord's own list of the past 7 days' mentions, roles and @everyone included

waiting

chats whose latest messages the user has not answered, from the mirror alone

saved

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_edit replaces the text of the account's own message, sent exactly as given; discord_message_delete removes the account's own message, which cannot be undone, and counts a message Discord no longer has as deleted (already).

  • discord_reaction_add and discord_reaction_remove add 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_add and discord_pin_remove pin 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 default

login 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

  1. Write the spec in src/core/<domain>/<verb>.ts with defineTool: an ID <prefix>_<domain>_<verb>, a title and description, the Zod argument and output schemas, its toolset and write class, and execute(args, context). A write tool also gives targets(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 gives gate(args, context), which throws for such a call; it is refused before anyone is asked, and before execute.

  2. Test it beside the spec (<verb>.test.ts), calling it through invoke as the entry points do. A tool that runs a program is tested against a fake of that program on PATH; no test reaches a real service.

  3. List it in src/tools.ts. A new toolset goes into toolsets in src/project.ts (and defaultToolsets when it is on by default), a setting of the package's own into options (and OPTIONS in project.py with config_schema in plugin.yaml), and a variable the tool reads besides the settings into toolVariables.

  4. Add or update the toolset's skill, skills/<prefix>-<toolset>/SKILL.md, whose frontmatter names the toolset under metadata.

  5. Run pnpm run build && pnpm run manifests to list the tool in plugin.yaml and 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 mcp__plugin_discord-tools_discord-tools__*_read in permissions.allow of ~/.claude/settings.json

Codex

always; Codex runs tools that declare readOnlyHint unasked, as the read tools do

Grok Build

grok --allow 'MCPTool(discord-tools__*_read)' --allow 'MCPTool(discord_*_read)' (both rules; see below)

Antigravity

the rule mcp(discord-tools_discord-tools/<tool id>) in permissions.allow, one for each read tool

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 exec it refuses with "MCP tool call requires approval, but approval policy is never".

  • Grok Build under grok -p refuses a write it cannot ask about, unless it is allowed beforehand.

  • Grok Build calls the plugin's tools through its use_tool tool, 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 -p refuses a tool it cannot ask about.

  • Hermes hermes chat -q refuses a write it cannot ask about; hermes -z and --yolo skip 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

toolsets

--toolsets

DISCORD_TOOLS_TOOLSETS

the toolsets that are on, or all; default those src/project.ts names

read_only

--read-only

DISCORD_TOOLS_READ_ONLY

registers no tool that writes; wins over every other setting

tools

--tools

DISCORD_TOOLS_TOOLS

single tools to add by ID, whatever their toolset

exclude_tools

--exclude-tools

DISCORD_TOOLS_EXCLUDE_TOOLS

single tools to remove by ID; wins over tools and toolsets

confirm

--confirm

DISCORD_TOOLS_CONFIRM

MCP server only: harness (default) or elicitation, as the conventions say

language

--language

DISCORD_TOOLS_LANGUAGE

native plugins only: the approval prompt's language, such as ja

emoji

—

—

Hermes only: the emoji shown beside each toolset, by toolset name ("" for none)

state_dir

--state-dir

DISCORD_TOOLS_STATE_DIR

path: where every label's state lives; default $XDG_STATE_HOME/discord-tools

download_dir

--download-dir

DISCORD_TOOLS_DOWNLOAD_DIR

path: where received files are saved; exports go under its private exports/ folder; default ~/Downloads/discord-tools

download_max_mb

--download-max-mb

DISCORD_TOOLS_DOWNLOAD_MAX_MB

the most megabytes a received file may have, a whole number from 1 to 500; default 100

send_dirs

--send-dirs

DISCORD_TOOLS_SEND_DIRS

paths: where files may be sent from, at most 20; default the working directory

send_max_mb

--send-max-mb

DISCORD_TOOLS_SEND_MAX_MB

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 options shown above, with lists as arrays, read_only as 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 where opencode run starts 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.settings in config.yaml:

    plugins:
      entries:
        discord-tools:
          settings:
            read_only: false
            exclude_tools: []

    Without a language setting, the approval card follows Hermes's own display.language (or HERMES_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 PATH and HOME) 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 in toolVariables in src/project.ts; pnpm run manifests adds 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's config.toml instead of using the plugin: flags go in args, env_vars names the variables to forward from Codex's environment, and env sets 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 _ (default when signing in names none). A tool's account argument 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_dir defaults to $XDG_STATE_HOME/discord-tools when XDG_STATE_HOME is 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_DEFAULT for default), 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 in env_vars as 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 sync tools 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 to dist/mcp.js and run by discord-tools.

  • src/opencode/ — the OpenCode plugin, built to dist/opencode.js.

  • src/cli/ — the command line the Hermes plugin calls, built to dist/cli.js and run by discord-tools-cli.

  • src/hermes/, plugin.yaml and __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/ and mcp_config.json — the plugin manifests of the other harnesses. Codex reads .codex-plugin/plugin.json; a root plugin.json would make it load the plugin as an Agent Plugin, whose tool budget hides tools past a fixed size. The root .mcp.json is 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

AGPL-3.0 with Commons Clause

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables 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 npm
      1
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables 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.
      1
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables 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 npm
      MIT
    • A
      license
      Not graded
      quality
      A
      maintenance
      Enables 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 npm
      MIT