# Directory Structure
```
├── .gitignore
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── discord-relay.ts
│ └── index.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Dependencies
node_modules/
# Build output
build/
dist/
# Environment variables
.env
.env.*
# Personal configuration
config.personal.json
# OS files
.DS_Store
Thumbs.db
# IDE files
.idea/
.vscode/
*.swp
*.swo
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# MCP Relay
This MCP server allows Claude to send messages and prompts to a Discord channel and receive responses.
## Setup Instructions
### 1. Create a Discord Application and Bot
1. Go to the [Discord Developer Portal](https://discord.com/developers/applications)
2. Click "New Application" and give it a name
3. Go to the "Bot" section in the left sidebar
4. Under the bot's token section, click "Reset Token" and copy the new token
- Keep this token secure! Don't share it publicly
5. Under "Privileged Gateway Intents", enable:
- Message Content Intent
- Server Members Intent
- Presence Intent
### 2. Invite the Bot to Your Server
1. Go to the "OAuth2" section in the left sidebar
2. Select "URL Generator"
3. Under "Scopes", select:
- bot
- applications.commands
4. Under "Bot Permissions", select:
- Send Messages
- Embed Links
- Read Message History
5. Copy the generated URL and open it in your browser
6. Select your server and authorize the bot
### 3. Get Channel ID
1. In Discord, enable Developer Mode:
- Go to User Settings > App Settings > Advanced
- Turn on "Developer Mode"
2. Right-click the channel you want to use
3. Click "Copy Channel ID"
### 4. Configure MCP Settings
The server requires configuration in your MCP settings file. Add the following to your configuration file:
```json
{
"mcpServers": {
"discord-relay": {
"command": "node",
"args": [
"/ABSOLUTE/PATH/TO/MCP Relay/build/index.js"
],
"env": {
"DISCORD_TOKEN": "your_bot_token_here",
"DISCORD_CHANNEL_ID": "your_channel_id_here"
}
}
}
}
```
Replace:
- `/ABSOLUTE/PATH/TO/MCP Relay` with the actual path to your MCP Relay project
- `your_bot_token_here` with your Discord bot token
- `your_channel_id_here` with your Discord channel ID
Note: Make sure to use absolute paths in the configuration.
## Usage
The server provides a tool called `send-message` that accepts the following parameters:
```typescript
{
type: 'prompt' | 'notification', // Type of message
title: string, // Message title
content: string, // Message content
actions?: Array<{ // Optional action buttons
label: string, // Button label
value: string // Value returned when clicked
}>,
timeout?: number // Optional timeout in milliseconds
}
```
### Message Types
1. **Notification**: Simple message that doesn't expect a response
```json
{
"type": "notification",
"title": "Hello",
"content": "This is a notification"
}
```
2. **Prompt**: Message that waits for a response
```json
{
"type": "prompt",
"title": "Question",
"content": "Do you want to proceed?",
"actions": [
{ "label": "Yes", "value": "yes" },
{ "label": "No", "value": "no" }
],
"timeout": 60000 // Optional: 1 minute timeout
}
```
Notes:
- Prompts can be answered either by clicking action buttons or sending a text message
- Only one response is accepted per prompt
- If a timeout is specified, the prompt will fail after the timeout period
- Notifications don't wait for responses and return immediately
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "mcp-relay",
"version": "1.0.0",
"description": "Discord relay MCP server",
"type": "module",
"main": "build/index.js",
"bin": {
"mcp-relay": "./build/index.js"
},
"scripts": {
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
"start": "node build/index.js"
},
"keywords": [
"mcp",
"discord",
"relay"
],
"author": "Emilio Bool",
"license": "ISC",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.4.1",
"discord.js": "^14.17.3",
"zod": "^3.24.1"
},
"devDependencies": {
"@types/node": "^22.10.10",
"typescript": "^5.7.3"
},
"files": [
"build"
]
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { DiscordRelay, RelayMessage } from './discord-relay.js';
const DISCORD_TOKEN = process.env.DISCORD_TOKEN;
const CHANNEL_ID = process.env.DISCORD_CHANNEL_ID;
if (!DISCORD_TOKEN || !CHANNEL_ID) {
throw new Error('DISCORD_TOKEN and DISCORD_CHANNEL_ID environment variables are required');
}
const relay = new DiscordRelay({
discordToken: DISCORD_TOKEN,
channelId: CHANNEL_ID
});
const server = new McpServer({
name: 'discord-relay',
version: '1.0.0',
});
// Register the send-message tool
server.tool(
'send-message',
'Send a message to Discord, either prompting for a response or sending a notification',
{
type: z.enum(['prompt', 'notification']),
title: z.string(),
content: z.string(),
actions: z.array(z.object({
label: z.string(),
value: z.string()
})).optional(),
timeout: z.number().optional()
},
async (message: RelayMessage) => {
const response = await relay.sendMessage(message);
if (!response.success) {
return {
content: [
{
type: 'text',
text: response.error || 'Unknown error occurred'
}
],
isError: true
};
}
return {
content: [
{
type: 'text',
text: message.type === 'notification'
? 'Message sent successfully'
: `Response received: ${response.response}`
}
]
};
}
);
async function main() {
try {
await relay.initialize();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Discord relay MCP server running on stdio');
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
// Handle cleanup
process.on('SIGINT', async () => {
await relay.destroy();
process.exit(0);
});
main().catch(console.error);
```
--------------------------------------------------------------------------------
/src/discord-relay.ts:
--------------------------------------------------------------------------------
```typescript
import { Client, TextChannel, Message, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ButtonInteraction } from 'discord.js';
export interface RelayConfig {
discordToken: string;
channelId: string;
}
export interface RelayMessage {
type: 'prompt' | 'notification';
title: string;
content: string;
actions?: Array<{
label: string;
value: string;
}>;
timeout?: number;
}
export interface RelayResponse {
success: boolean;
error?: string;
response?: string;
}
export class DiscordRelay {
private client: Client;
private config: RelayConfig;
private responseMap: Map<string, {
resolve: (response: RelayResponse) => void;
timer?: NodeJS.Timeout;
}>;
constructor(config: RelayConfig) {
this.config = config;
this.client = new Client({
intents: ['Guilds', 'GuildMessages', 'MessageContent']
});
this.responseMap = new Map();
}
async initialize(): Promise<void> {
try {
await this.client.login(this.config.discordToken);
this.setupResponseHandling();
console.error('Discord relay initialized successfully');
} catch (error) {
throw new Error(`Failed to initialize Discord client: ${error}`);
}
}
async sendMessage(message: RelayMessage): Promise<RelayResponse> {
try {
const channel = await this.client.channels.fetch(this.config.channelId);
if (!channel || !(channel instanceof TextChannel)) {
throw new Error('Invalid channel or channel not found');
}
const embed = new EmbedBuilder()
.setTitle(message.title)
.setDescription(message.content)
.setColor(message.type === 'notification' ? 0x00ff00 : 0x0099ff)
.setTimestamp();
const components = [];
if (message.actions && message.actions.length > 0) {
const row = new ActionRowBuilder<ButtonBuilder>();
message.actions.forEach((action, index) => {
row.addComponents(
new ButtonBuilder()
.setCustomId(`action_${index}`)
.setLabel(action.label)
.setStyle(ButtonStyle.Primary)
);
});
components.push(row);
}
const discordMessage = await channel.send({
embeds: [embed],
components: components
});
if (message.type === 'notification') {
return { success: true };
}
return new Promise((resolve) => {
this.responseMap.set(discordMessage.id, {
resolve,
timer: message.timeout ? setTimeout(() => {
this.clearResponse(discordMessage.id);
resolve({
success: false,
error: 'Response timeout'
});
}, message.timeout) : undefined
});
});
} catch (error) {
return {
success: false,
error: `Failed to send message: ${error}`
};
}
}
private setupResponseHandling() {
this.client.on('messageCreate', (message: Message) => {
if (message.channel.id !== this.config.channelId || message.author.bot) {
return;
}
// Find the most recent message that's waiting for a response
const entries = Array.from(this.responseMap.entries());
const lastEntry = entries[entries.length - 1];
if (lastEntry) {
const [messageId, handler] = lastEntry;
this.clearResponse(messageId);
handler.resolve({
success: true,
response: message.content
});
}
});
this.client.on('interactionCreate', async (interaction) => {
if (!interaction.isButton()) return;
const messageId = interaction.message.id;
const handler = this.responseMap.get(messageId);
if (handler) {
const actionIndex = parseInt(interaction.customId.split('_')[1]);
const message = interaction.message;
const embed = message.embeds[0];
if (embed && message.components) {
const actions = message.components[0].components
.filter(comp => comp.type === 2) // Button type
.map((comp: any) => ({
label: comp.label,
value: comp.customId
}));
if (actions[actionIndex]) {
this.clearResponse(messageId);
handler.resolve({
success: true,
response: actions[actionIndex].label
});
}
}
await interaction.deferUpdate();
}
});
}
private clearResponse(messageId: string) {
const handler = this.responseMap.get(messageId);
if (handler?.timer) {
clearTimeout(handler.timer);
}
this.responseMap.delete(messageId);
}
async destroy() {
this.client.destroy();
}
}
```