#!/usr/bin/env node
import 'dotenv/config';
import express, { Request, Response } from 'express';
import { randomUUID } from 'node:crypto';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { getDiscordClient, destroyClient } from './utils/discord-client.js';
import { registerServerTools } from './tools/server-tools.js';
import { registerChannelTools } from './tools/channel-tools.js';
import { registerMemberTools } from './tools/member-tools.js';
import { registerRoleTools } from './tools/role-tools.js';
import { registerPermissionTools } from './tools/permission-tools.js';
import { registerMessageTools } from './tools/message-tools.js';
import { registerEmojiTools } from './tools/emoji-tools.js';
import { registerWebhookTools } from './tools/webhook-tools.js';
import { registerInviteTools } from './tools/invite-tools.js';
import { registerEventTools } from './tools/event-tools.js';
import { registerThreadTools } from './tools/thread-tools.js';
import { registerAuditTools } from './tools/audit-tools.js';
/**
* Discord MCP Server
*
* This MCP server provides comprehensive tools for managing Discord servers.
* It requires the DISCORD_BOT_TOKEN environment variable to be set.
*
* Available tool categories:
* - Server Management: list_servers, get_server_info, modify_server
* - Channel Management: list_channels, get_channel_info, create_channel, delete_channel, modify_channel
* - Member Management: list_members, get_member_info, modify_member, kick_member, ban_member, unban_member, list_bans
* - Role Management: list_roles, get_role_info, create_role, delete_role, modify_role, assign_role, remove_role
* - Permission Management: get_channel_permissions, set_channel_permission, delete_channel_permission, list_permissions, sync_channel_permissions
* - Message Management: send_message, get_messages, edit_message, delete_message, bulk_delete_messages, pin/unpin, reactions
* - Emoji & Sticker Management: list_emojis, create_emoji, delete_emoji, modify_emoji, sticker operations
* - Webhook Management: list_channel_webhooks, list_guild_webhooks, create_webhook, delete_webhook, modify_webhook, send_webhook_message
* - Invite Management: list_invites, get_invite_info, create_invite, delete_invite
* - Event Management: list_events, get_event_info, create_event, modify_event, delete_event, get_event_subscribers
* - Thread Management: list_threads, create_thread, create_forum_post, modify_thread, delete_thread, join/leave, member management
* - Audit & Moderation: get_audit_logs, list_audit_log_types, list_automod_rules, get_automod_rule, delete_automod_rule, toggle_automod_rule
*/
// Create a configured MCP server instance
function createMcpServer(): McpServer {
const server = new McpServer({
name: 'discord-mcp',
version: '1.0.5',
});
// Register all tool categories
registerServerTools(server);
registerChannelTools(server);
registerMemberTools(server);
registerRoleTools(server);
registerPermissionTools(server);
registerMessageTools(server);
registerEmojiTools(server);
registerWebhookTools(server);
registerInviteTools(server);
registerEventTools(server);
registerThreadTools(server);
registerAuditTools(server);
return server;
}
// Run server with stdio transport (for mcp-proxy and Claude Desktop)
async function runStdioServer() {
// Create MCP server and connect via stdio
// Note: We don't require DISCORD_BOT_TOKEN at startup to allow the server
// to start and respond to capability queries. The token is only required
// when tools are actually called (handled in discord-client.ts).
const server = createMcpServer();
const transport = new StdioServerTransport();
// Setup graceful shutdown
const cleanup = async () => {
await destroyClient();
process.exit(0);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
await server.connect(transport);
}
// Run server with HTTP transport
async function runHttpServer() {
// Check for Discord token
if (!process.env.DISCORD_BOT_TOKEN) {
console.error('Error: DISCORD_BOT_TOKEN environment variable is required');
console.error('Please set it before running the server:');
console.error(' export DISCORD_BOT_TOKEN=your_bot_token_here');
process.exit(1);
}
// Get port from environment or default to 3000
const PORT = parseInt(process.env.PORT || '3000', 10);
// Initialize Discord client
console.log('Initializing Discord client...');
try {
await getDiscordClient();
console.log('Discord client initialized successfully');
} catch (error) {
console.error('Failed to initialize Discord client:', error);
process.exit(1);
}
// Create Express app
const app = express();
app.use(express.json());
// Store active transports by session ID
const transports: Record<string, StreamableHTTPServerTransport> = {};
// Health check endpoint
app.get('/health', (_req: Request, res: Response) => {
res.json({ status: 'ok', service: 'discord-mcp', version: '1.0.5' });
});
// MCP endpoint - POST for requests
app.post('/mcp', async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
// Reuse existing session
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// New session initialization
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => {
transports[id] = transport;
console.log('Session initialized:', id);
},
});
transport.onclose = () => {
if (transport.sessionId) {
delete transports[transport.sessionId];
console.log('Session closed:', transport.sessionId);
}
};
// Create and connect MCP server for this session
const server = createMcpServer();
await server.connect(transport);
} else {
res.status(400).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Invalid session or missing initialization' },
id: null,
});
return;
}
await transport.handleRequest(req, res, req.body);
});
// MCP endpoint - GET for SSE streams
app.get('/mcp', async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string;
const transport = transports[sessionId];
if (transport) {
await transport.handleRequest(req, res);
} else {
res.status(400).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Invalid or missing session' },
id: null,
});
}
});
// MCP endpoint - DELETE for session cleanup
app.delete('/mcp', async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string;
const transport = transports[sessionId];
if (transport) {
await transport.handleRequest(req, res);
} else {
res.status(400).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Invalid or missing session' },
id: null,
});
}
});
// Setup graceful shutdown
const cleanup = async () => {
console.log('Shutting down...');
// Close all active transports
for (const sessionId of Object.keys(transports)) {
try {
await transports[sessionId].close();
} catch {
// Ignore errors during cleanup
}
}
await destroyClient();
process.exit(0);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
// Start HTTP server
app.listen(PORT, () => {
console.log(`Discord MCP HTTP server is running on http://localhost:${PORT}`);
console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);
console.log(`Health check: http://localhost:${PORT}/health`);
});
}
async function main() {
// Check for --http flag or TRANSPORT environment variable
// Default to stdio mode for npm/npx compatibility (standard for MCP servers)
const useHttp = process.argv.includes('--http') || process.env.MCP_TRANSPORT === 'http';
if (useHttp) {
await runHttpServer();
} else {
await runStdioServer();
}
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});