# Directory Structure
```
├── .gitignore
├── .python-version
├── pyproject.toml
├── README.md
├── requirements.txt
├── src
│ └── mcp_discord_chat
│ ├── __init__.py
│ └── server.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
1 | 3.13
2 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Python-generated files
2 | __pycache__/
3 | *.py[oc]
4 | build/
5 | dist/
6 | wheels/
7 | *.egg-info
8 |
9 | # Virtual environments
10 | .venv
11 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # mcp-discord-chat MCP server
2 |
3 | A MCP server project
4 |
5 | ## Components
6 |
7 | ### Resources
8 |
9 | The server implements a simple note storage system with:
10 | - Custom note:// URI scheme for accessing individual notes
11 | - Each note resource has a name, description and text/plain mimetype
12 |
13 | ### Prompts
14 |
15 | The server provides a single prompt:
16 | - summarize-notes: Creates summaries of all stored notes
17 | - Optional "style" argument to control detail level (brief/detailed)
18 | - Generates prompt combining all current notes with style preference
19 |
20 | ### Tools
21 |
22 | The server implements one tool:
23 | - add-note: Adds a new note to the server
24 | - Takes "name" and "content" as required string arguments
25 | - Updates server state and notifies clients of resource changes
26 |
27 | ## Configuration
28 |
29 | [TODO: Add configuration details specific to your implementation]
30 |
31 | ## Quickstart
32 |
33 | ### Install
34 |
35 | #### Claude Desktop
36 |
37 | On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
38 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
39 |
40 | <details>
41 | <summary>Development/Unpublished Servers Configuration</summary>
42 | ```
43 | "mcpServers": {
44 | "mcp-discord-chat": {
45 | "command": "uv",
46 | "args": [
47 | "--directory",
48 | "/Users/koladev/speakeasy-projects/mcp-discord-chat",
49 | "run",
50 | "mcp-discord-chat"
51 | ]
52 | }
53 | }
54 | ```
55 | </details>
56 |
57 | <details>
58 | <summary>Published Servers Configuration</summary>
59 | ```
60 | "mcpServers": {
61 | "mcp-discord-chat": {
62 | "command": "uvx",
63 | "args": [
64 | "mcp-discord-chat"
65 | ]
66 | }
67 | }
68 | ```
69 | </details>
70 |
71 | ## Development
72 |
73 | ### Building and Publishing
74 |
75 | To prepare the package for distribution:
76 |
77 | 1. Sync dependencies and update lockfile:
78 | ```bash
79 | uv sync
80 | ```
81 |
82 | 2. Build package distributions:
83 | ```bash
84 | uv build
85 | ```
86 |
87 | This will create source and wheel distributions in the `dist/` directory.
88 |
89 | 3. Publish to PyPI:
90 | ```bash
91 | uv publish
92 | ```
93 |
94 | Note: You'll need to set PyPI credentials via environment variables or command flags:
95 | - Token: `--token` or `UV_PUBLISH_TOKEN`
96 | - Or username/password: `--username`/`UV_PUBLISH_USERNAME` and `--password`/`UV_PUBLISH_PASSWORD`
97 |
98 | ### Debugging
99 |
100 | Since MCP servers run over stdio, debugging can be challenging. For the best debugging
101 | experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).
102 |
103 |
104 | You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command:
105 |
106 | ```bash
107 | npx @modelcontextprotocol/inspector uv --directory /Users/speakeasy-projects/mcp-discord-chat run mcp-discord-chat
108 | ```
109 |
110 |
111 | Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging.
112 |
```
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
```
1 | discord.py>=2.3.0
```
--------------------------------------------------------------------------------
/src/mcp_discord_chat/__init__.py:
--------------------------------------------------------------------------------
```python
1 | from . import server
2 | import asyncio
3 |
4 | def main():
5 | """Main entry point for the package."""
6 | asyncio.run(server.main())
7 |
8 | # Optionally expose other important items at package level
9 | __all__ = ['main', 'server']
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [project]
2 | name = "mcp-discord-chat"
3 | version = "0.1.0"
4 | description = "A MCP server project"
5 | readme = "README.md"
6 | requires-python = ">=3.12"
7 | dependencies = [
8 | "discord.py>=2.3.0",
9 | "mcp>=1.2.1",
10 | "audioop-lts; python_version >= '3.13'"
11 | ]
12 | [[project.authors]]
13 | name = "koladev"
14 | email = "[email protected]"
15 |
16 | [build-system]
17 | requires = [ "hatchling",]
18 | build-backend = "hatchling.build"
19 |
20 | [project.scripts]
21 | mcp-discord-chat = "mcp_discord_chat:main"
22 |
```
--------------------------------------------------------------------------------
/src/mcp_discord_chat/server.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 | import asyncio
3 | import logging
4 | from datetime import datetime
5 | from typing import Any, List
6 | from functools import wraps
7 |
8 | import discord
9 | from discord.ext import commands
10 | from mcp.server import Server
11 | from mcp.types import Tool, TextContent
12 | from mcp.server.stdio import stdio_server
13 |
14 | # -----------------------------------------------------------------------------
15 | # Logging Configuration
16 | # -----------------------------------------------------------------------------
17 | # Set up logging to output informational messages. This helps with debugging
18 | # and monitoring the application during runtime.
19 | logging.basicConfig(level=logging.INFO)
20 | logger = logging.getLogger("discord-mcp-server")
21 |
22 | # -----------------------------------------------------------------------------
23 | # Discord Bot Setup
24 | # -----------------------------------------------------------------------------
25 | # Retrieve the Discord token from the environment. This token authenticates the
26 | # bot with Discord's API. The application will exit if the token is not provided.
27 | DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
28 | if not DISCORD_TOKEN:
29 | raise ValueError("DISCORD_TOKEN environment variable is required")
30 |
31 | # Create a Discord bot instance with necessary intents.
32 | # Here, we enable the 'message_content' intent to allow the bot to read message content.
33 | # The 'members' intent is also enabled to access member information.
34 | intents = discord.Intents.default()
35 | intents.message_content = True
36 | intents.members = True
37 | bot = commands.Bot(command_prefix="!", intents=intents)
38 |
39 | # -----------------------------------------------------------------------------
40 | # MCP Server Initialization
41 | # -----------------------------------------------------------------------------
42 | # Create an MCP server instance. The MCP (Model Context Protocol) server will
43 | # allow external calls to registered tools (commands) in this application.
44 | app = Server("discord-server")
45 |
46 | # Global variable to store the Discord client instance once the bot is ready.
47 | discord_client = None
48 |
49 | # -----------------------------------------------------------------------------
50 | # Helper Functions
51 | # -----------------------------------------------------------------------------
52 | def format_reactions(reactions: List[dict]) -> str:
53 | """
54 | Format a list of reaction dictionaries into a human-readable string.
55 | Each reaction is shown as: emoji(count).
56 | If no reactions are present, returns "No reactions".
57 | """
58 | if not reactions:
59 | return "No reactions"
60 | return ", ".join(f"{r['emoji']}({r['count']})" for r in reactions)
61 |
62 | def require_discord_client(func):
63 | """
64 | Decorator to ensure the Discord client is ready before executing a tool.
65 | Raises a RuntimeError if the client is not yet available.
66 | """
67 | @wraps(func)
68 | async def wrapper(*args, **kwargs):
69 | if not discord_client:
70 | raise RuntimeError("Discord client not ready")
71 | return await func(*args, **kwargs)
72 | return wrapper
73 |
74 | # -----------------------------------------------------------------------------
75 | # Discord Bot Events
76 | # -----------------------------------------------------------------------------
77 | @bot.event
78 | async def on_ready():
79 | """
80 | Event handler called when the Discord bot successfully logs in.
81 | Sets the global discord_client variable and logs the bot's username.
82 | """
83 | global discord_client
84 | discord_client = bot
85 | logger.info(f"Logged in as {bot.user.name}")
86 |
87 | # -----------------------------------------------------------------------------
88 | # MCP Tools Registration
89 | # -----------------------------------------------------------------------------
90 | @app.list_tools()
91 | async def list_tools() -> List[Tool]:
92 | """
93 | Register and list the available MCP tools for the Discord server.
94 | Only three tools are registered:
95 | - add_reaction: Adds an emoji reaction to a message.
96 | - send_message: Sends a message to a specified Discord channel.
97 | - read_messages: Retrieves recent messages from a Discord channel.
98 | """
99 | return [
100 | Tool(
101 | name="add_reaction",
102 | description="Add a reaction to a message",
103 | inputSchema={
104 | "type": "object",
105 | "properties": {
106 | "channel_id": {
107 | "type": "string",
108 | "description": "ID of the channel containing the message",
109 | },
110 | "message_id": {
111 | "type": "string",
112 | "description": "ID of the message to react to",
113 | },
114 | "emoji": {
115 | "type": "string",
116 | "description": "Emoji to react with (Unicode or custom emoji ID)",
117 | },
118 | },
119 | "required": ["channel_id", "message_id", "emoji"],
120 | },
121 | ),
122 | Tool(
123 | name="send_message",
124 | description="Send a message to a specific channel",
125 | inputSchema={
126 | "type": "object",
127 | "properties": {
128 | "channel_id": {
129 | "type": "string",
130 | "description": "Discord channel ID where the message will be sent",
131 | },
132 | "content": {
133 | "type": "string",
134 | "description": "Content of the message to send",
135 | },
136 | },
137 | "required": ["channel_id", "content"],
138 | },
139 | ),
140 | Tool(
141 | name="read_messages",
142 | description="Read recent messages from a channel",
143 | inputSchema={
144 | "type": "object",
145 | "properties": {
146 | "channel_id": {
147 | "type": "string",
148 | "description": "Discord channel ID from which to fetch messages",
149 | },
150 | "limit": {
151 | "type": "number",
152 | "description": "Number of messages to fetch (max 100)",
153 | "minimum": 1,
154 | "maximum": 100,
155 | },
156 | },
157 | "required": ["channel_id"],
158 | },
159 | ),
160 | ]
161 |
162 | @app.call_tool()
163 | @require_discord_client
164 | async def call_tool(name: str, arguments: Any) -> List[TextContent]:
165 | """
166 | Dispatch function for tool calls. This function checks the 'name' of the tool
167 | requested and performs the corresponding Discord operation.
168 |
169 | Tools implemented:
170 | - send_message: Sends a message to a channel.
171 | - read_messages: Retrieves recent messages from a channel.
172 | - add_reaction: Adds a reaction (emoji) to a message.
173 |
174 | Returns:
175 | A list of TextContent objects containing the result of the operation.
176 | """
177 | if name == "send_message":
178 | # Retrieve the channel and send the message with the provided content.
179 | channel = await discord_client.fetch_channel(int(arguments["channel_id"]))
180 | message = await channel.send(arguments["content"])
181 | return [
182 | TextContent(
183 | type="text",
184 | text=f"Message sent successfully. Message ID: {message.id}"
185 | )
186 | ]
187 |
188 | elif name == "read_messages":
189 | # Retrieve the channel and fetch a limited number of recent messages.
190 | channel = await discord_client.fetch_channel(int(arguments["channel_id"]))
191 | limit = min(int(arguments.get("limit", 10)), 100)
192 | messages = []
193 | async for message in channel.history(limit=limit):
194 | reaction_data = []
195 | # Iterate through reactions and collect emoji data.
196 | for reaction in message.reactions:
197 | emoji_str = (
198 | str(reaction.emoji.name)
199 | if hasattr(reaction.emoji, "name") and reaction.emoji.name
200 | else (
201 | str(reaction.emoji.id)
202 | if hasattr(reaction.emoji, "id")
203 | else str(reaction.emoji)
204 | )
205 | )
206 | reaction_info = {"emoji": emoji_str, "count": reaction.count}
207 | logger.debug(f"Found reaction: {emoji_str}")
208 | reaction_data.append(reaction_info)
209 | messages.append(
210 | {
211 | "id": str(message.id),
212 | "author": str(message.author),
213 | "content": message.content,
214 | "timestamp": message.created_at.isoformat(),
215 | "reactions": reaction_data,
216 | }
217 | )
218 | # Format the messages for output.
219 | formatted_messages = "\n".join(
220 | f"{m['author']} ({m['timestamp']}): {m['content']}\nReactions: {format_reactions(m['reactions'])}"
221 | for m in messages
222 | )
223 | return [
224 | TextContent(
225 | type="text",
226 | text=f"Retrieved {len(messages)} messages:\n\n{formatted_messages}"
227 | )
228 | ]
229 |
230 | elif name == "add_reaction":
231 | # Retrieve the channel and message, then add the specified reaction.
232 | channel = await discord_client.fetch_channel(int(arguments["channel_id"]))
233 | message = await channel.fetch_message(int(arguments["message_id"]))
234 | await message.add_reaction(arguments["emoji"])
235 | return [
236 | TextContent(
237 | type="text",
238 | text=f"Added reaction '{arguments['emoji']}' to message {message.id}"
239 | )
240 | ]
241 |
242 | # If the tool name is not recognized, raise an error.
243 | raise ValueError(f"Unknown tool: {name}")
244 |
245 | # -----------------------------------------------------------------------------
246 | # Main Function: Starts Discord Bot and MCP Server
247 | # -----------------------------------------------------------------------------
248 | async def main():
249 | """
250 | Main entry point of the application.
251 |
252 | - Starts the Discord bot as a background task.
253 | - Runs the MCP server using standard I/O for communication.
254 | """
255 | # Start the Discord bot in the background so that it can handle events.
256 | asyncio.create_task(bot.start(DISCORD_TOKEN))
257 |
258 | # Open a connection using the stdio server transport and run the MCP server.
259 | async with stdio_server() as (read_stream, write_stream):
260 | await app.run(read_stream, write_stream, app.create_initialization_options())
261 |
262 | # -----------------------------------------------------------------------------
263 | # Application Entry Point
264 | # -----------------------------------------------------------------------------
265 | if __name__ == "__main__":
266 | asyncio.run(main())
267 |
```