#
tokens: 2859/50000 6/6 files
lines: off (toggle) GitHub
raw markdown copy
# 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();
  }
}

```