mcp-discord-notify — Development
This project is an MCP server that exposes Discord (notifications, channel creation, questions with responses) to AI agents. Use this skill when modifying or extending the mcp-discord-notify server.
Architecture
| File | Role |
|---|---|
| main.py | MCP entrypoint (FastMCP, stdio). Defines tools; each tool uses run_async() to call into the Discord bot. Starts the bot at import time. |
| discord_bot.py | Discord client in a background thread with its own event loop. Handles Gateway, sends messages, creates channels, and handles interactions (buttons, modal). Exposes run_async(coro), start_bot(token), Views, and register_pending_question(question_id) for two-way questions. |
| config.py | Loads DISCORD_BOT_TOKEN / DISCORD_TOKEN, optional DISCORD_GUILD_ID, and channel map from env and/or channels.json. save_channel_map() persists keys for create_channel(..., register_key=...). |
- •Transport: stdio (JSON-RPC). Client spawns
python main.pyand talks over stdin/stdout. - •Threading: Main thread runs MCP stdio; Discord bot runs in a daemon thread. Tools are sync and call
run_async(coro, timeout)to run coroutines on the Discord loop and block for the result. - •Two-way questions:
send_questionregisters aquestion_id→queue.Queue, posts a message with a View (buttons/modal). When a user interacts, the View callback puts the result in the queue; the tool blocks onq.get(timeout=...).
Adding a new MCP tool
- •In main.py, add a
@mcp.tool()function. Use clear docstring (name, args, behavior). - •Resolve channel with
_resolve_channel(channel_id_or_key)when the tool targets a channel. - •Implement the side effect in an
async def _do_it()and callrun_async(_do_it(), timeout=...). Use_get_client()from discord_bot to get the Discord client and callclient.get_channel(cid),guild.create_text_channel(...), etc. - •Return a JSON-serializable result (or
json.dumps({...})for consistency with existing tools).
Adding a new question type
- •In discord_bot.py, add a new
ui.Viewsubclass (e.g.MyQuestionView(question_id, ..., timeout_sec=...)). Add buttons or a button that opens a modal; usecustom_idor passquestion_idso the callback can find_pending_questions[question_id]andq.put({...}). - •Important: Respond to the interaction within 3 seconds. Use
await interaction.response.defer()at the start of the callback, then put the result in the queue. - •In main.py, in
send_question, add a branch for the newquestion_type, instantiate your View, callregister_pending_question(question_id)before posting, post the message withview=view, thenq.get(timeout=wait_timeout_seconds)if waiting.
Config and channel map
- •Token: Required.
DISCORD_BOT_TOKENorDISCORD_TOKEN(see config.py). - •Channel map: Optional. Keys (e.g.
general,alerts) → channel IDs. Loaded fromDISCORD_CHANNELSenv (JSON string) and/orchannels.jsoninDISCORD_MCP_CONFIG_DIR(default: current dir). Agents use keys insend_notification(channel_id_or_key, ...)andsend_question(...). - •Persisting new channels:
create_channel(..., register_key="foo")writes the new channel ID into the in-memory map and callssave_channel_map()sochannels.jsonis updated.
Conventions
- •Keep tools sync; use
run_async()for any Discord API call. The Discord loop runs in another thread. - •Use question_id (e.g.
uuid.uuid4()) to tie a posted question to its queue; register before sending, unregister on timeout or after first response. - •Tool return values: prefer
json.dumps({"error": "..."})for errors andjson.dumps({...})for success so the client gets consistent JSON. - •Views must defer or respond to interactions within 3 seconds; then do any slow work or queue put.
Testing without a real bot
- •Import and unit-test config (load_channel_map, get_token with env set) and discord_bot Views in isolation (no
start_bot). Full integration requires a validDISCORD_BOT_TOKENand a guild/channel the bot can access.
Quick reference
| Change | Where |
|---|---|
| New tool | main.py: @mcp.tool() + run_async(_impl()) |
| New question type | discord_bot.py: new View; main.py: branch in send_question |
| New config env | config.py: add getter; document in README and .env.example |
| Channel map file path | config.py: CHANNELS_FILE, DISCORD_MCP_CONFIG_DIR |