# 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 |
```