#
tokens: 4045/50000 7/7 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | 
```