Discord MCP Server
by v-3
Verified
- src
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import dotenv from 'dotenv';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { Client, GatewayIntentBits, TextChannel } from 'discord.js';
import { z } from 'zod';
// Load environment variables
dotenv.config();
// Discord client setup
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
// Helper function to find a guild by name or ID
async function findGuild(guildIdentifier?: string) {
if (!guildIdentifier) {
// If no guild specified and bot is only in one guild, use that
if (client.guilds.cache.size === 1) {
return client.guilds.cache.first()!;
}
// List available guilds
const guildList = Array.from(client.guilds.cache.values())
.map(g => `"${g.name}"`).join(', ');
throw new Error(`Bot is in multiple servers. Please specify server name or ID. Available servers: ${guildList}`);
}
// Try to fetch by ID first
try {
const guild = await client.guilds.fetch(guildIdentifier);
if (guild) return guild;
} catch {
// If ID fetch fails, search by name
const guilds = client.guilds.cache.filter(
g => g.name.toLowerCase() === guildIdentifier.toLowerCase()
);
if (guilds.size === 0) {
const availableGuilds = Array.from(client.guilds.cache.values())
.map(g => `"${g.name}"`).join(', ');
throw new Error(`Server "${guildIdentifier}" not found. Available servers: ${availableGuilds}`);
}
if (guilds.size > 1) {
const guildList = guilds.map(g => `${g.name} (ID: ${g.id})`).join(', ');
throw new Error(`Multiple servers found with name "${guildIdentifier}": ${guildList}. Please specify the server ID.`);
}
return guilds.first()!;
}
throw new Error(`Server "${guildIdentifier}" not found`);
}
// Helper function to find a channel by name or ID within a specific guild
async function findChannel(channelIdentifier: string, guildIdentifier?: string): Promise<TextChannel> {
const guild = await findGuild(guildIdentifier);
// First try to fetch by ID
try {
const channel = await client.channels.fetch(channelIdentifier);
if (channel instanceof TextChannel && channel.guild.id === guild.id) {
return channel;
}
} catch {
// If fetching by ID fails, search by name in the specified guild
const channels = guild.channels.cache.filter(
(channel): channel is TextChannel =>
channel instanceof TextChannel &&
(channel.name.toLowerCase() === channelIdentifier.toLowerCase() ||
channel.name.toLowerCase() === channelIdentifier.toLowerCase().replace('#', ''))
);
if (channels.size === 0) {
const availableChannels = guild.channels.cache
.filter((c): c is TextChannel => c instanceof TextChannel)
.map(c => `"#${c.name}"`).join(', ');
throw new Error(`Channel "${channelIdentifier}" not found in server "${guild.name}". Available channels: ${availableChannels}`);
}
if (channels.size > 1) {
const channelList = channels.map(c => `#${c.name} (${c.id})`).join(', ');
throw new Error(`Multiple channels found with name "${channelIdentifier}" in server "${guild.name}": ${channelList}. Please specify the channel ID.`);
}
return channels.first()!;
}
throw new Error(`Channel "${channelIdentifier}" is not a text channel or not found in server "${guild.name}"`);
}
// Updated validation schemas
const SendMessageSchema = z.object({
server: z.string().optional().describe('Server name or ID (optional if bot is only in one server)'),
channel: z.string().describe('Channel name (e.g., "general") or ID'),
message: z.string(),
});
const ReadMessagesSchema = z.object({
server: z.string().optional().describe('Server name or ID (optional if bot is only in one server)'),
channel: z.string().describe('Channel name (e.g., "general") or ID'),
limit: z.number().min(1).max(100).default(50),
});
// Create server instance
const server = new Server(
{
name: "discord",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "send-message",
description: "Send a message to a Discord channel",
inputSchema: {
type: "object",
properties: {
server: {
type: "string",
description: 'Server name or ID (optional if bot is only in one server)',
},
channel: {
type: "string",
description: 'Channel name (e.g., "general") or ID',
},
message: {
type: "string",
description: "Message content to send",
},
},
required: ["channel", "message"],
},
},
{
name: "read-messages",
description: "Read recent messages from a Discord channel",
inputSchema: {
type: "object",
properties: {
server: {
type: "string",
description: 'Server name or ID (optional if bot is only in one server)',
},
channel: {
type: "string",
description: 'Channel name (e.g., "general") or ID',
},
limit: {
type: "number",
description: "Number of messages to fetch (max 100)",
default: 50,
},
},
required: ["channel"],
},
},
],
};
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "send-message": {
const { channel: channelIdentifier, message } = SendMessageSchema.parse(args);
const channel = await findChannel(channelIdentifier);
const sent = await channel.send(message);
return {
content: [{
type: "text",
text: `Message sent successfully to #${channel.name} in ${channel.guild.name}. Message ID: ${sent.id}`,
}],
};
}
case "read-messages": {
const { channel: channelIdentifier, limit } = ReadMessagesSchema.parse(args);
const channel = await findChannel(channelIdentifier);
const messages = await channel.messages.fetch({ limit });
const formattedMessages = Array.from(messages.values()).map(msg => ({
channel: `#${channel.name}`,
server: channel.guild.name,
author: msg.author.tag,
content: msg.content,
timestamp: msg.createdAt.toISOString(),
}));
return {
content: [{
type: "text",
text: JSON.stringify(formattedMessages, null, 2),
}],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(
`Invalid arguments: ${error.errors
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join(", ")}`
);
}
throw error;
}
});
// Discord client login and error handling
client.once('ready', () => {
console.error('Discord bot is ready!');
});
// Start the server
async function main() {
// Check for Discord token
const token = process.env.DISCORD_TOKEN;
if (!token) {
throw new Error('DISCORD_TOKEN environment variable is not set');
}
try {
// Login to Discord
await client.login(token);
// Start MCP server
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Discord MCP Server running on stdio");
} catch (error) {
console.error("Fatal error in main():", error);
process.exit(1);
}
}
main();