#
tokens: 4109/50000 6/6 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── discord-relay.ts
│   └── index.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Dependencies
 2 | node_modules/
 3 | 
 4 | # Build output
 5 | build/
 6 | dist/
 7 | 
 8 | # Environment variables
 9 | .env
10 | .env.*
11 | 
12 | # Personal configuration
13 | config.personal.json
14 | 
15 | # OS files
16 | .DS_Store
17 | Thumbs.db
18 | 
19 | # IDE files
20 | .idea/
21 | .vscode/
22 | *.swp
23 | *.swo
24 | 
25 | # Logs
26 | logs/
27 | *.log
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | 
32 | # Optional npm cache directory
33 | .npm
34 | 
35 | # Optional eslint cache
36 | .eslintcache
37 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP Relay
  2 | 
  3 | This MCP server allows Claude to send messages and prompts to a Discord channel and receive responses.
  4 | 
  5 | ## Setup Instructions
  6 | 
  7 | ### 1. Create a Discord Application and Bot
  8 | 
  9 | 1. Go to the [Discord Developer Portal](https://discord.com/developers/applications)
 10 | 2. Click "New Application" and give it a name
 11 | 3. Go to the "Bot" section in the left sidebar
 12 | 4. Under the bot's token section, click "Reset Token" and copy the new token
 13 |    - Keep this token secure! Don't share it publicly
 14 | 5. Under "Privileged Gateway Intents", enable:
 15 |    - Message Content Intent
 16 |    - Server Members Intent
 17 |    - Presence Intent
 18 | 
 19 | ### 2. Invite the Bot to Your Server
 20 | 
 21 | 1. Go to the "OAuth2" section in the left sidebar
 22 | 2. Select "URL Generator"
 23 | 3. Under "Scopes", select:
 24 |    - bot
 25 |    - applications.commands
 26 | 4. Under "Bot Permissions", select:
 27 |    - Send Messages
 28 |    - Embed Links
 29 |    - Read Message History
 30 | 5. Copy the generated URL and open it in your browser
 31 | 6. Select your server and authorize the bot
 32 | 
 33 | ### 3. Get Channel ID
 34 | 
 35 | 1. In Discord, enable Developer Mode:
 36 |    - Go to User Settings > App Settings > Advanced
 37 |    - Turn on "Developer Mode"
 38 | 2. Right-click the channel you want to use
 39 | 3. Click "Copy Channel ID"
 40 | 
 41 | ### 4. Configure MCP Settings
 42 | 
 43 | The server requires configuration in your MCP settings file. Add the following to your configuration file:
 44 | 
 45 | ```json
 46 | {
 47 |     "mcpServers": {
 48 |         "discord-relay": {
 49 |             "command": "node",
 50 |             "args": [
 51 |                 "/ABSOLUTE/PATH/TO/MCP Relay/build/index.js"
 52 |             ],
 53 |             "env": {
 54 |                 "DISCORD_TOKEN": "your_bot_token_here",
 55 |                 "DISCORD_CHANNEL_ID": "your_channel_id_here"
 56 |             }
 57 |         }
 58 |     }
 59 | }
 60 | ```
 61 | 
 62 | Replace:
 63 | - `/ABSOLUTE/PATH/TO/MCP Relay` with the actual path to your MCP Relay project
 64 | - `your_bot_token_here` with your Discord bot token
 65 | - `your_channel_id_here` with your Discord channel ID
 66 | 
 67 | Note: Make sure to use absolute paths in the configuration.
 68 | 
 69 | ## Usage
 70 | 
 71 | The server provides a tool called `send-message` that accepts the following parameters:
 72 | 
 73 | ```typescript
 74 | {
 75 |   type: 'prompt' | 'notification',  // Type of message
 76 |   title: string,                    // Message title
 77 |   content: string,                  // Message content
 78 |   actions?: Array<{                 // Optional action buttons
 79 |     label: string,                  // Button label
 80 |     value: string                   // Value returned when clicked
 81 |   }>,
 82 |   timeout?: number                  // Optional timeout in milliseconds
 83 | }
 84 | ```
 85 | 
 86 | ### Message Types
 87 | 
 88 | 1. **Notification**: Simple message that doesn't expect a response
 89 |    ```json
 90 |    {
 91 |      "type": "notification",
 92 |      "title": "Hello",
 93 |      "content": "This is a notification"
 94 |    }
 95 |    ```
 96 | 
 97 | 2. **Prompt**: Message that waits for a response
 98 |    ```json
 99 |    {
100 |      "type": "prompt",
101 |      "title": "Question",
102 |      "content": "Do you want to proceed?",
103 |      "actions": [
104 |        { "label": "Yes", "value": "yes" },
105 |        { "label": "No", "value": "no" }
106 |      ],
107 |      "timeout": 60000  // Optional: 1 minute timeout
108 |    }
109 |    ```
110 | 
111 | Notes:
112 | - Prompts can be answered either by clicking action buttons or sending a text message
113 | - Only one response is accepted per prompt
114 | - If a timeout is specified, the prompt will fail after the timeout period
115 | - Notifications don't wait for responses and return immediately
116 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "outDir": "./build",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true
12 |   },
13 |   "include": ["src/**/*"],
14 |   "exclude": ["node_modules"]
15 | }
16 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "mcp-relay",
 3 |   "version": "1.0.0",
 4 |   "description": "Discord relay MCP server",
 5 |   "type": "module",
 6 |   "main": "build/index.js",
 7 |   "bin": {
 8 |     "mcp-relay": "./build/index.js"
 9 |   },
10 |   "scripts": {
11 |     "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
12 |     "start": "node build/index.js"
13 |   },
14 |   "keywords": [
15 |     "mcp",
16 |     "discord",
17 |     "relay"
18 |   ],
19 |   "author": "Emilio Bool",
20 |   "license": "ISC",
21 |   "dependencies": {
22 |     "@modelcontextprotocol/sdk": "^1.4.1",
23 |     "discord.js": "^14.17.3",
24 |     "zod": "^3.24.1"
25 |   },
26 |   "devDependencies": {
27 |     "@types/node": "^22.10.10",
28 |     "typescript": "^5.7.3"
29 |   },
30 |   "files": [
31 |     "build"
32 |   ]
33 | }
34 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | #!/usr/bin/env node
 2 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
 4 | import { z } from 'zod';
 5 | import { DiscordRelay, RelayMessage } from './discord-relay.js';
 6 | 
 7 | const DISCORD_TOKEN = process.env.DISCORD_TOKEN;
 8 | const CHANNEL_ID = process.env.DISCORD_CHANNEL_ID;
 9 | 
10 | if (!DISCORD_TOKEN || !CHANNEL_ID) {
11 |   throw new Error('DISCORD_TOKEN and DISCORD_CHANNEL_ID environment variables are required');
12 | }
13 | 
14 | const relay = new DiscordRelay({
15 |   discordToken: DISCORD_TOKEN,
16 |   channelId: CHANNEL_ID
17 | });
18 | 
19 | const server = new McpServer({
20 |   name: 'discord-relay',
21 |   version: '1.0.0',
22 | });
23 | 
24 | // Register the send-message tool
25 | server.tool(
26 |   'send-message',
27 |   'Send a message to Discord, either prompting for a response or sending a notification',
28 |   {
29 |     type: z.enum(['prompt', 'notification']),
30 |     title: z.string(),
31 |     content: z.string(),
32 |     actions: z.array(z.object({
33 |       label: z.string(),
34 |       value: z.string()
35 |     })).optional(),
36 |     timeout: z.number().optional()
37 |   },
38 |   async (message: RelayMessage) => {
39 |     const response = await relay.sendMessage(message);
40 |     
41 |     if (!response.success) {
42 |       return {
43 |         content: [
44 |           {
45 |             type: 'text',
46 |             text: response.error || 'Unknown error occurred'
47 |           }
48 |         ],
49 |         isError: true
50 |       };
51 |     }
52 | 
53 |     return {
54 |       content: [
55 |         {
56 |           type: 'text',
57 |           text: message.type === 'notification' 
58 |             ? 'Message sent successfully'
59 |             : `Response received: ${response.response}`
60 |         }
61 |       ]
62 |     };
63 |   }
64 | );
65 | 
66 | async function main() {
67 |   try {
68 |     await relay.initialize();
69 |     const transport = new StdioServerTransport();
70 |     await server.connect(transport);
71 |     console.error('Discord relay MCP server running on stdio');
72 |   } catch (error) {
73 |     console.error('Failed to start server:', error);
74 |     process.exit(1);
75 |   }
76 | }
77 | 
78 | // Handle cleanup
79 | process.on('SIGINT', async () => {
80 |   await relay.destroy();
81 |   process.exit(0);
82 | });
83 | 
84 | main().catch(console.error);
85 | 
```

--------------------------------------------------------------------------------
/src/discord-relay.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Client, TextChannel, Message, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ButtonInteraction } from 'discord.js';
  2 | 
  3 | export interface RelayConfig {
  4 |   discordToken: string;
  5 |   channelId: string;
  6 | }
  7 | 
  8 | export interface RelayMessage {
  9 |   type: 'prompt' | 'notification';
 10 |   title: string;
 11 |   content: string;
 12 |   actions?: Array<{
 13 |     label: string;
 14 |     value: string;
 15 |   }>;
 16 |   timeout?: number;
 17 | }
 18 | 
 19 | export interface RelayResponse {
 20 |   success: boolean;
 21 |   error?: string;
 22 |   response?: string;
 23 | }
 24 | 
 25 | export class DiscordRelay {
 26 |   private client: Client;
 27 |   private config: RelayConfig;
 28 |   private responseMap: Map<string, {
 29 |     resolve: (response: RelayResponse) => void;
 30 |     timer?: NodeJS.Timeout;
 31 |   }>;
 32 | 
 33 |   constructor(config: RelayConfig) {
 34 |     this.config = config;
 35 |     this.client = new Client({
 36 |       intents: ['Guilds', 'GuildMessages', 'MessageContent']
 37 |     });
 38 |     this.responseMap = new Map();
 39 |   }
 40 | 
 41 |   async initialize(): Promise<void> {
 42 |     try {
 43 |       await this.client.login(this.config.discordToken);
 44 |       this.setupResponseHandling();
 45 |       console.error('Discord relay initialized successfully');
 46 |     } catch (error) {
 47 |       throw new Error(`Failed to initialize Discord client: ${error}`);
 48 |     }
 49 |   }
 50 | 
 51 |   async sendMessage(message: RelayMessage): Promise<RelayResponse> {
 52 |     try {
 53 |       const channel = await this.client.channels.fetch(this.config.channelId);
 54 |       if (!channel || !(channel instanceof TextChannel)) {
 55 |         throw new Error('Invalid channel or channel not found');
 56 |       }
 57 | 
 58 |       const embed = new EmbedBuilder()
 59 |         .setTitle(message.title)
 60 |         .setDescription(message.content)
 61 |         .setColor(message.type === 'notification' ? 0x00ff00 : 0x0099ff)
 62 |         .setTimestamp();
 63 | 
 64 |       const components = [];
 65 |       if (message.actions && message.actions.length > 0) {
 66 |         const row = new ActionRowBuilder<ButtonBuilder>();
 67 |         message.actions.forEach((action, index) => {
 68 |           row.addComponents(
 69 |             new ButtonBuilder()
 70 |               .setCustomId(`action_${index}`)
 71 |               .setLabel(action.label)
 72 |               .setStyle(ButtonStyle.Primary)
 73 |           );
 74 |         });
 75 |         components.push(row);
 76 |       }
 77 | 
 78 |       const discordMessage = await channel.send({
 79 |         embeds: [embed],
 80 |         components: components
 81 |       });
 82 | 
 83 |       if (message.type === 'notification') {
 84 |         return { success: true };
 85 |       }
 86 | 
 87 |       return new Promise((resolve) => {
 88 |         this.responseMap.set(discordMessage.id, {
 89 |           resolve,
 90 |           timer: message.timeout ? setTimeout(() => {
 91 |             this.clearResponse(discordMessage.id);
 92 |             resolve({ 
 93 |               success: false, 
 94 |               error: 'Response timeout' 
 95 |             });
 96 |           }, message.timeout) : undefined
 97 |         });
 98 |       });
 99 |     } catch (error) {
100 |       return {
101 |         success: false,
102 |         error: `Failed to send message: ${error}`
103 |       };
104 |     }
105 |   }
106 | 
107 |   private setupResponseHandling() {
108 |     this.client.on('messageCreate', (message: Message) => {
109 |       if (message.channel.id !== this.config.channelId || message.author.bot) {
110 |         return;
111 |       }
112 | 
113 |       // Find the most recent message that's waiting for a response
114 |       const entries = Array.from(this.responseMap.entries());
115 |       const lastEntry = entries[entries.length - 1];
116 |       
117 |       if (lastEntry) {
118 |         const [messageId, handler] = lastEntry;
119 |         this.clearResponse(messageId);
120 |         handler.resolve({
121 |           success: true,
122 |           response: message.content
123 |         });
124 |       }
125 |     });
126 | 
127 |     this.client.on('interactionCreate', async (interaction) => {
128 |       if (!interaction.isButton()) return;
129 | 
130 |       const messageId = interaction.message.id;
131 |       const handler = this.responseMap.get(messageId);
132 |       
133 |       if (handler) {
134 |         const actionIndex = parseInt(interaction.customId.split('_')[1]);
135 |         const message = interaction.message;
136 |         const embed = message.embeds[0];
137 |         
138 |         if (embed && message.components) {
139 |           const actions = message.components[0].components
140 |             .filter(comp => comp.type === 2) // Button type
141 |             .map((comp: any) => ({
142 |               label: comp.label,
143 |               value: comp.customId
144 |             }));
145 | 
146 |           if (actions[actionIndex]) {
147 |             this.clearResponse(messageId);
148 |             handler.resolve({
149 |               success: true,
150 |               response: actions[actionIndex].label
151 |             });
152 |           }
153 |         }
154 | 
155 |         await interaction.deferUpdate();
156 |       }
157 |     });
158 |   }
159 | 
160 |   private clearResponse(messageId: string) {
161 |     const handler = this.responseMap.get(messageId);
162 |     if (handler?.timer) {
163 |       clearTimeout(handler.timer);
164 |     }
165 |     this.responseMap.delete(messageId);
166 |   }
167 | 
168 |   async destroy() {
169 |     this.client.destroy();
170 |   }
171 | }
172 | 
```