feat: implement multi-transport support and optimize Discord MCP server
This commit is contained in:
parent
21c910a15d
commit
7baf5a03e6
137
README.md
137
README.md
|
@ -77,56 +77,119 @@ npm run build
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
A Discord bot token is required for proper operation. You can provide it in two ways:
|
A Discord bot token is required for proper operation. The server supports two transport methods: stdio and streamable HTTP.
|
||||||
|
|
||||||
|
### Transport Methods
|
||||||
|
|
||||||
|
1. **stdio** (Default)
|
||||||
|
- Traditional stdio transport for basic usage
|
||||||
|
- Suitable for simple integrations
|
||||||
|
|
||||||
|
2. **streamable HTTP**
|
||||||
|
- HTTP-based transport for more advanced scenarios
|
||||||
|
- Supports stateless operation
|
||||||
|
- Configurable port number
|
||||||
|
- Powered by Smithery SDK for improved reliability and performance
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
You can provide configuration in two ways:
|
||||||
|
|
||||||
1. Environment variables:
|
1. Environment variables:
|
||||||
```
|
```bash
|
||||||
DISCORD_TOKEN=your_discord_bot_token
|
DISCORD_TOKEN=your_discord_bot_token
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Using the `--config` parameter when launching:
|
2. Using command line arguments:
|
||||||
```
|
```bash
|
||||||
node path/to/mcp-discord/build/index.js --config "{\"DISCORD_TOKEN\":\"your_discord_bot_token\"}"
|
# For stdio transport (default)
|
||||||
|
node build/index.js --config "your_discord_bot_token"
|
||||||
|
|
||||||
|
# For streamable HTTP transport
|
||||||
|
node build/index.js --transport http --port 3000 --config "your_discord_bot_token"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage with Claude/Cursor
|
## Usage with Claude/Cursor
|
||||||
- Claude
|
|
||||||
|
|
||||||
```json
|
### Claude
|
||||||
{
|
|
||||||
"mcpServers": {
|
1. Using stdio transport:
|
||||||
"discord": {
|
```json
|
||||||
"command": "node",
|
{
|
||||||
"args": [
|
"mcpServers": {
|
||||||
"path/to/mcp-discord/build/index.js"
|
"discord": {
|
||||||
],
|
"command": "node",
|
||||||
"env": {
|
"args": [
|
||||||
"DISCORD_TOKEN": "your_discord_bot_token"
|
"path/to/mcp-discord/build/index.js",
|
||||||
}
|
"--config",
|
||||||
}
|
"your_discord_bot_token"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- Cursor
|
2. Using streamable HTTP transport:
|
||||||
|
```json
|
||||||
```json
|
{
|
||||||
{
|
"mcpServers": {
|
||||||
"mcpServers": {
|
"discord": {
|
||||||
"discord": {
|
"command": "node",
|
||||||
"command": "cmd",
|
"args": [
|
||||||
"args": [
|
"path/to/mcp-discord/build/index.js",
|
||||||
"/c",
|
"--transport",
|
||||||
"node",
|
"http",
|
||||||
"path/to/mcp-discord/build/index.js"
|
"--port",
|
||||||
],
|
"3000",
|
||||||
"env": {
|
"--config",
|
||||||
"DISCORD_TOKEN": "your_discord_bot_token"
|
"your_discord_bot_token"
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cursor
|
||||||
|
|
||||||
|
1. Using stdio transport:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"discord": {
|
||||||
|
"command": "cmd",
|
||||||
|
"args": [
|
||||||
|
"/c",
|
||||||
|
"node",
|
||||||
|
"path/to/mcp-discord/build/index.js",
|
||||||
|
"--config",
|
||||||
|
"your_discord_bot_token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Using streamable HTTP transport:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"discord": {
|
||||||
|
"command": "cmd",
|
||||||
|
"args": [
|
||||||
|
"/c",
|
||||||
|
"node",
|
||||||
|
"path/to/mcp-discord/build/index.js",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
"--port",
|
||||||
|
"3000",
|
||||||
|
"--config",
|
||||||
|
"your_discord_bot_token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Tools Documentation
|
## Tools Documentation
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "mcp-discord",
|
"name": "mcp-discord",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"main": "build/index.js",
|
"main": "build/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"mcp-discord": "build/index.js"
|
"mcp-discord": "build/index.js"
|
||||||
|
@ -10,20 +10,23 @@
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node build/index.js",
|
"start": "node build/index.js",
|
||||||
"dev": "node --loader ts-node/esm src/index.ts"
|
"dev": "node --loader ts-node/esm src/index.ts",
|
||||||
|
"test-api": "node test-api.js"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "",
|
"description": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.8.0",
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||||
|
"@types/express": "^5.0.1",
|
||||||
"@types/node": "^20.17.26",
|
"@types/node": "^20.17.26",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"discord.js": "^14.18.0",
|
"discord.js": "^14.18.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.5.0",
|
||||||
|
"express": "^5.1.0",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
|
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
|
||||||
|
|
||||||
startCommand:
|
version: 1
|
||||||
type: stdio
|
start:
|
||||||
|
type: http
|
||||||
configSchema:
|
configSchema:
|
||||||
# JSON Schema defining the configuration options for the MCP.
|
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- discordToken
|
- discordToken
|
||||||
|
@ -11,13 +11,27 @@ startCommand:
|
||||||
discordToken:
|
discordToken:
|
||||||
type: string
|
type: string
|
||||||
description: Discord bot token. Obtain this from the Discord Developer Portal.
|
description: Discord bot token. Obtain this from the Discord Developer Portal.
|
||||||
commandFunction:
|
port:
|
||||||
# A JS function that produces the CLI command based on the given config to start the MCP on stdio.
|
type: number
|
||||||
|-
|
description: Port number for the HTTP server
|
||||||
(config) => ({
|
default: 3000
|
||||||
command: 'node',
|
command:
|
||||||
args: ['build/index.js'],
|
function: |
|
||||||
env: { DISCORD_TOKEN: config.discordToken }
|
(config) => ({
|
||||||
})
|
command: 'node',
|
||||||
|
args: [
|
||||||
|
'build/index.js',
|
||||||
|
'--transport',
|
||||||
|
'http',
|
||||||
|
'--port',
|
||||||
|
config.port || 3000,
|
||||||
|
'--config',
|
||||||
|
config.discordToken
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production'
|
||||||
|
}
|
||||||
|
})
|
||||||
exampleConfig:
|
exampleConfig:
|
||||||
discordToken: YOUR_DISCORD_BOT_TOKEN
|
discordToken: YOUR_DISCORD_BOT_TOKEN
|
||||||
|
port: 3000
|
||||||
|
|
275
src/index.ts
275
src/index.ts
|
@ -1,73 +1,60 @@
|
||||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { Client, GatewayIntentBits } from "discord.js";
|
import { Client, GatewayIntentBits } from "discord.js";
|
||||||
import {
|
import { config as dotenvConfig } from 'dotenv';
|
||||||
CallToolRequestSchema,
|
import { DiscordMCPServer } from './server.js';
|
||||||
ListToolsRequestSchema,
|
import { StdioTransport, StreamableHttpTransport } from './transport.js';
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
|
||||||
import { toolList } from './toolList.js';
|
|
||||||
import {
|
|
||||||
createToolContext,
|
|
||||||
loginHandler,
|
|
||||||
sendMessageHandler,
|
|
||||||
getForumChannelsHandler,
|
|
||||||
createForumPostHandler,
|
|
||||||
getForumPostHandler,
|
|
||||||
replyToForumHandler,
|
|
||||||
deleteForumPostHandler,
|
|
||||||
createTextChannelHandler,
|
|
||||||
deleteChannelHandler,
|
|
||||||
readMessagesHandler,
|
|
||||||
getServerInfoHandler,
|
|
||||||
addReactionHandler,
|
|
||||||
addMultipleReactionsHandler,
|
|
||||||
removeReactionHandler,
|
|
||||||
deleteMessageHandler,
|
|
||||||
createWebhookHandler,
|
|
||||||
sendWebhookMessageHandler,
|
|
||||||
editWebhookHandler,
|
|
||||||
deleteWebhookHandler
|
|
||||||
} from './tools/tools.js';
|
|
||||||
|
|
||||||
// Configuration parsing
|
// Load environment variables from .env file if exists
|
||||||
let config: any = {};
|
dotenvConfig();
|
||||||
|
|
||||||
// Read configuration from environment variables
|
// Configuration with priority for command line arguments
|
||||||
if (process.env.DISCORD_TOKEN) {
|
const config = {
|
||||||
config.DISCORD_TOKEN = process.env.DISCORD_TOKEN;
|
DISCORD_TOKEN: (() => {
|
||||||
console.log("Config loaded from environment variables. Discord token available:", !!config.DISCORD_TOKEN);
|
|
||||||
if (config.DISCORD_TOKEN) {
|
|
||||||
console.log("Token length:", config.DISCORD_TOKEN.length);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Try to parse configuration from command line arguments (for backward compatibility)
|
|
||||||
const configArgIndex = process.argv.indexOf('--config');
|
|
||||||
if (configArgIndex !== -1 && configArgIndex < process.argv.length - 1) {
|
|
||||||
try {
|
try {
|
||||||
let configStr = process.argv[configArgIndex + 1];
|
// First try to get from command line arguments
|
||||||
|
const configIndex = process.argv.indexOf('--config');
|
||||||
// Print raw configuration string for debugging
|
if (configIndex !== -1 && configIndex + 1 < process.argv.length) {
|
||||||
console.log("Raw config string:", configStr);
|
const configArg = process.argv[configIndex + 1];
|
||||||
|
// Handle both string and object formats
|
||||||
// Try to parse JSON
|
if (typeof configArg === 'string') {
|
||||||
config = JSON.parse(configStr);
|
try {
|
||||||
console.log("Config parsed successfully. Discord token available:", !!config.DISCORD_TOKEN);
|
const parsedConfig = JSON.parse(configArg);
|
||||||
|
return parsedConfig.DISCORD_TOKEN;
|
||||||
if (config.DISCORD_TOKEN) {
|
} catch (e) {
|
||||||
console.log("Token length:", config.DISCORD_TOKEN.length);
|
// If not valid JSON, try using the string directly
|
||||||
|
return configArg;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Then try environment variable
|
||||||
|
return process.env.DISCORD_TOKEN;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to parse config argument:", error);
|
console.error('Error parsing config:', error);
|
||||||
console.error("Raw config argument:", process.argv[configArgIndex + 1]);
|
return null;
|
||||||
|
|
||||||
// Try to read arguments directly (for debugging)
|
|
||||||
console.log("All arguments:", process.argv);
|
|
||||||
}
|
}
|
||||||
} else {
|
})(),
|
||||||
console.warn("No config found in environment variables or command line arguments");
|
TRANSPORT: (() => {
|
||||||
console.log("All arguments:", process.argv);
|
// Check for transport type argument
|
||||||
}
|
const transportIndex = process.argv.indexOf('--transport');
|
||||||
|
if (transportIndex !== -1 && transportIndex + 1 < process.argv.length) {
|
||||||
|
return process.argv[transportIndex + 1];
|
||||||
|
}
|
||||||
|
// Default to stdio
|
||||||
|
return 'stdio';
|
||||||
|
})(),
|
||||||
|
HTTP_PORT: (() => {
|
||||||
|
// Check for port argument
|
||||||
|
const portIndex = process.argv.indexOf('--port');
|
||||||
|
if (portIndex !== -1 && portIndex + 1 < process.argv.length) {
|
||||||
|
return parseInt(process.argv[portIndex + 1]);
|
||||||
|
}
|
||||||
|
// Default port
|
||||||
|
return 3000;
|
||||||
|
})()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!config.DISCORD_TOKEN) {
|
||||||
|
console.error('Discord token not found. Please provide it via --config argument or environment variable.');
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create Discord client
|
// Create Discord client
|
||||||
|
@ -84,141 +71,13 @@ if (config.DISCORD_TOKEN) {
|
||||||
client.token = config.DISCORD_TOKEN;
|
client.token = config.DISCORD_TOKEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create an MCP server
|
|
||||||
const server = new Server(
|
|
||||||
{
|
|
||||||
name: "MCP-Discord",
|
|
||||||
version: "1.0.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
capabilities: {
|
|
||||||
tools: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set up the tool list
|
|
||||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
||||||
return {
|
|
||||||
tools: toolList
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create tool context
|
|
||||||
const toolContext = createToolContext(client);
|
|
||||||
|
|
||||||
// Handle tool execution requests
|
|
||||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
||||||
const { name, arguments: args } = request.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let toolResponse;
|
|
||||||
switch (name) {
|
|
||||||
case "discord_login":
|
|
||||||
toolResponse = await loginHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_send":
|
|
||||||
toolResponse = await sendMessageHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_get_forum_channels":
|
|
||||||
toolResponse = await getForumChannelsHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_create_forum_post":
|
|
||||||
toolResponse = await createForumPostHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_get_forum_post":
|
|
||||||
toolResponse = await getForumPostHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_reply_to_forum":
|
|
||||||
toolResponse = await replyToForumHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_delete_forum_post":
|
|
||||||
toolResponse = await deleteForumPostHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_create_text_channel":
|
|
||||||
toolResponse = await createTextChannelHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_delete_channel":
|
|
||||||
toolResponse = await deleteChannelHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_read_messages":
|
|
||||||
toolResponse = await readMessagesHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_get_server_info":
|
|
||||||
toolResponse = await getServerInfoHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_add_reaction":
|
|
||||||
toolResponse = await addReactionHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_add_multiple_reactions":
|
|
||||||
toolResponse = await addMultipleReactionsHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_remove_reaction":
|
|
||||||
toolResponse = await removeReactionHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_delete_message":
|
|
||||||
toolResponse = await deleteMessageHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_create_webhook":
|
|
||||||
toolResponse = await createWebhookHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_send_webhook_message":
|
|
||||||
toolResponse = await sendWebhookMessageHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_edit_webhook":
|
|
||||||
toolResponse = await editWebhookHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
case "discord_delete_webhook":
|
|
||||||
toolResponse = await deleteWebhookHandler(args, toolContext);
|
|
||||||
return toolResponse;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown tool: ${name}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return {
|
|
||||||
content: [{
|
|
||||||
type: "text",
|
|
||||||
text: `Invalid arguments: ${error.errors
|
|
||||||
.map((e) => `${e.path.join(".")}: ${e.message}`)
|
|
||||||
.join(", ")}`
|
|
||||||
}],
|
|
||||||
isError: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: `Error executing tool: ${error}` }],
|
|
||||||
isError: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto-login on startup if token is available
|
// Auto-login on startup if token is available
|
||||||
const autoLogin = async () => {
|
const autoLogin = async () => {
|
||||||
const token = config.DISCORD_TOKEN;
|
const token = config.DISCORD_TOKEN;
|
||||||
if (token) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
await client.login(token);
|
await client.login(token);
|
||||||
|
console.log('Successfully logged in to Discord');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Auto-login failed:", error);
|
console.error("Auto-login failed:", error);
|
||||||
}
|
}
|
||||||
|
@ -227,8 +86,32 @@ const autoLogin = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start auto-login process
|
// Initialize transport based on configuration
|
||||||
autoLogin();
|
const initializeTransport = () => {
|
||||||
|
switch (config.TRANSPORT.toLowerCase()) {
|
||||||
|
case 'http':
|
||||||
|
console.log(`Initializing HTTP transport on port ${config.HTTP_PORT}`);
|
||||||
|
return new StreamableHttpTransport(config.HTTP_PORT);
|
||||||
|
case 'stdio':
|
||||||
|
console.log('Initializing stdio transport');
|
||||||
|
return new StdioTransport();
|
||||||
|
default:
|
||||||
|
console.error(`Unknown transport type: ${config.TRANSPORT}. Falling back to stdio.`);
|
||||||
|
return new StdioTransport();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const transport = new StdioServerTransport();
|
// Start auto-login process
|
||||||
await server.connect(transport);
|
await autoLogin();
|
||||||
|
|
||||||
|
// Create and start MCP server with selected transport
|
||||||
|
const transport = initializeTransport();
|
||||||
|
const mcpServer = new DiscordMCPServer(client, transport);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mcpServer.start();
|
||||||
|
console.log('MCP server started successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start MCP server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
|
import { Client } from "discord.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import { toolList } from './toolList.js';
|
||||||
|
import {
|
||||||
|
createToolContext,
|
||||||
|
loginHandler,
|
||||||
|
sendMessageHandler,
|
||||||
|
getForumChannelsHandler,
|
||||||
|
createForumPostHandler,
|
||||||
|
getForumPostHandler,
|
||||||
|
replyToForumHandler,
|
||||||
|
deleteForumPostHandler,
|
||||||
|
createTextChannelHandler,
|
||||||
|
deleteChannelHandler,
|
||||||
|
readMessagesHandler,
|
||||||
|
getServerInfoHandler,
|
||||||
|
addReactionHandler,
|
||||||
|
addMultipleReactionsHandler,
|
||||||
|
removeReactionHandler,
|
||||||
|
deleteMessageHandler,
|
||||||
|
createWebhookHandler,
|
||||||
|
sendWebhookMessageHandler,
|
||||||
|
editWebhookHandler,
|
||||||
|
deleteWebhookHandler
|
||||||
|
} from './tools/tools.js';
|
||||||
|
import { MCPTransport } from './transport.js';
|
||||||
|
|
||||||
|
export class DiscordMCPServer {
|
||||||
|
private server: Server;
|
||||||
|
private toolContext: ReturnType<typeof createToolContext>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private client: Client,
|
||||||
|
private transport: MCPTransport
|
||||||
|
) {
|
||||||
|
this.server = new Server(
|
||||||
|
{
|
||||||
|
name: "MCP-Discord",
|
||||||
|
version: "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.toolContext = createToolContext(client);
|
||||||
|
this.setupHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupHandlers() {
|
||||||
|
// Set up the tool list
|
||||||
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: toolList
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle tool execution requests
|
||||||
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { name, arguments: args } = request.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let toolResponse;
|
||||||
|
switch (name) {
|
||||||
|
case "discord_login":
|
||||||
|
toolResponse = await loginHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_send":
|
||||||
|
toolResponse = await sendMessageHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_get_forum_channels":
|
||||||
|
toolResponse = await getForumChannelsHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_create_forum_post":
|
||||||
|
toolResponse = await createForumPostHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_get_forum_post":
|
||||||
|
toolResponse = await getForumPostHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_reply_to_forum":
|
||||||
|
toolResponse = await replyToForumHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_delete_forum_post":
|
||||||
|
toolResponse = await deleteForumPostHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_create_text_channel":
|
||||||
|
toolResponse = await createTextChannelHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_delete_channel":
|
||||||
|
toolResponse = await deleteChannelHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_read_messages":
|
||||||
|
toolResponse = await readMessagesHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_get_server_info":
|
||||||
|
toolResponse = await getServerInfoHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_add_reaction":
|
||||||
|
toolResponse = await addReactionHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_add_multiple_reactions":
|
||||||
|
toolResponse = await addMultipleReactionsHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_remove_reaction":
|
||||||
|
toolResponse = await removeReactionHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_delete_message":
|
||||||
|
toolResponse = await deleteMessageHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_create_webhook":
|
||||||
|
toolResponse = await createWebhookHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_send_webhook_message":
|
||||||
|
toolResponse = await sendWebhookMessageHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_edit_webhook":
|
||||||
|
toolResponse = await editWebhookHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
case "discord_delete_webhook":
|
||||||
|
toolResponse = await deleteWebhookHandler(args, this.toolContext);
|
||||||
|
return toolResponse;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${name}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Invalid arguments: ${error.errors
|
||||||
|
.map((e: z.ZodIssue) => `${e.path.join(".")}: ${e.message}`)
|
||||||
|
.join(", ")}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error executing tool: ${errorMessage}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
// Add client to server context so transport can access it
|
||||||
|
(this.server as any)._context = { client: this.client };
|
||||||
|
(this.server as any).client = this.client;
|
||||||
|
|
||||||
|
await this.transport.start(this.server);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop() {
|
||||||
|
await this.transport.stop();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,375 @@
|
||||||
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import express, { Request, Response } from "express";
|
||||||
|
import { toolList } from './toolList.js';
|
||||||
|
import {
|
||||||
|
createToolContext,
|
||||||
|
loginHandler,
|
||||||
|
sendMessageHandler,
|
||||||
|
getForumChannelsHandler,
|
||||||
|
createForumPostHandler,
|
||||||
|
getForumPostHandler,
|
||||||
|
replyToForumHandler,
|
||||||
|
deleteForumPostHandler,
|
||||||
|
createTextChannelHandler,
|
||||||
|
deleteChannelHandler,
|
||||||
|
readMessagesHandler,
|
||||||
|
getServerInfoHandler,
|
||||||
|
addReactionHandler,
|
||||||
|
addMultipleReactionsHandler,
|
||||||
|
removeReactionHandler,
|
||||||
|
deleteMessageHandler,
|
||||||
|
createWebhookHandler,
|
||||||
|
sendWebhookMessageHandler,
|
||||||
|
editWebhookHandler,
|
||||||
|
deleteWebhookHandler
|
||||||
|
} from './tools/tools.js';
|
||||||
|
import { Client } from "discord.js";
|
||||||
|
|
||||||
|
export interface MCPTransport {
|
||||||
|
start(server: Server): Promise<void>;
|
||||||
|
stop(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StdioTransport implements MCPTransport {
|
||||||
|
private transport: StdioServerTransport | null = null;
|
||||||
|
|
||||||
|
async start(server: Server): Promise<void> {
|
||||||
|
this.transport = new StdioServerTransport();
|
||||||
|
await server.connect(this.transport);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
if (this.transport) {
|
||||||
|
await this.transport.close();
|
||||||
|
this.transport = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StreamableHttpTransport implements MCPTransport {
|
||||||
|
private app: express.Application;
|
||||||
|
private server: Server | null = null;
|
||||||
|
private httpServer: any = null;
|
||||||
|
private transport: StreamableHTTPServerTransport | null = null;
|
||||||
|
private toolContext: ReturnType<typeof createToolContext> | null = null;
|
||||||
|
|
||||||
|
constructor(private port: number = 3000) {
|
||||||
|
this.app = express();
|
||||||
|
this.app.use(express.json());
|
||||||
|
this.setupEndpoints();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupEndpoints() {
|
||||||
|
// Handler for POST requests
|
||||||
|
this.app.post('/mcp', (req: Request, res: Response) => {
|
||||||
|
console.log('Received MCP request:', req.body);
|
||||||
|
this.handleMcpRequest(req, res).catch(error => {
|
||||||
|
console.error('Unhandled error in MCP request:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handler for non-POST methods
|
||||||
|
this.app.all('/mcp', (req: Request, res: Response) => {
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
res.status(405).json({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
error: {
|
||||||
|
code: -32000,
|
||||||
|
message: 'Method not allowed. Use POST.',
|
||||||
|
},
|
||||||
|
id: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleMcpRequest(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
if (!this.server) {
|
||||||
|
return res.status(500).json({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
error: {
|
||||||
|
code: -32603,
|
||||||
|
message: 'Server not initialized',
|
||||||
|
},
|
||||||
|
id: req.body?.id || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Request body:', JSON.stringify(req.body));
|
||||||
|
|
||||||
|
// Handle all tool requests in a generic way
|
||||||
|
if (!req.body.method) {
|
||||||
|
return res.status(400).json({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
error: {
|
||||||
|
code: -32600,
|
||||||
|
message: 'Invalid Request: No method specified',
|
||||||
|
},
|
||||||
|
id: req.body?.id || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle all tools directly with proper error handling
|
||||||
|
try {
|
||||||
|
const method = req.body.method;
|
||||||
|
const params = req.body.params || {};
|
||||||
|
|
||||||
|
// Make sure toolContext is available
|
||||||
|
if (!this.toolContext && method !== 'list_tools') {
|
||||||
|
return res.status(500).json({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
error: {
|
||||||
|
code: -32603,
|
||||||
|
message: 'Tool context not initialized. Service may need to be restarted.',
|
||||||
|
},
|
||||||
|
id: req.body?.id || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
|
// Handle each tool method directly
|
||||||
|
switch (method) {
|
||||||
|
case 'list_tools':
|
||||||
|
result = { tools: toolList };
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_login':
|
||||||
|
result = await loginHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_send':
|
||||||
|
result = await sendMessageHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_get_forum_channels':
|
||||||
|
result = await getForumChannelsHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_create_forum_post':
|
||||||
|
result = await createForumPostHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_get_forum_post':
|
||||||
|
result = await getForumPostHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_reply_to_forum':
|
||||||
|
result = await replyToForumHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_delete_forum_post':
|
||||||
|
result = await deleteForumPostHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_create_text_channel':
|
||||||
|
result = await createTextChannelHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_delete_channel':
|
||||||
|
result = await deleteChannelHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_read_messages':
|
||||||
|
result = await readMessagesHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_get_server_info':
|
||||||
|
result = await getServerInfoHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_add_reaction':
|
||||||
|
result = await addReactionHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_add_multiple_reactions':
|
||||||
|
result = await addMultipleReactionsHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_remove_reaction':
|
||||||
|
result = await removeReactionHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_delete_message':
|
||||||
|
result = await deleteMessageHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_create_webhook':
|
||||||
|
result = await createWebhookHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_send_webhook_message':
|
||||||
|
result = await sendWebhookMessageHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_edit_webhook':
|
||||||
|
result = await editWebhookHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord_delete_webhook':
|
||||||
|
result = await deleteWebhookHandler(params, this.toolContext!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return res.status(400).json({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
error: {
|
||||||
|
code: -32601,
|
||||||
|
message: `Method not found: ${method}`,
|
||||||
|
},
|
||||||
|
id: req.body?.id || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Request for ${method} handled successfully`);
|
||||||
|
|
||||||
|
// Handle the case where tool handlers return { content, isError }
|
||||||
|
if (result && typeof result === 'object' && 'content' in result) {
|
||||||
|
// If it's an error from the tool handler
|
||||||
|
if ('isError' in result && result.isError) {
|
||||||
|
return res.status(400).json({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: req.body.id,
|
||||||
|
error: {
|
||||||
|
code: -32603,
|
||||||
|
message: Array.isArray(result.content)
|
||||||
|
? result.content.map((item: any) => item.text).join(' ')
|
||||||
|
: 'Tool execution error'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return success result but maintain same format as other RPC methods
|
||||||
|
return res.json({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: req.body.id,
|
||||||
|
result: result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard result format
|
||||||
|
return res.json({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: req.body.id,
|
||||||
|
result: result
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing tool request:', error);
|
||||||
|
|
||||||
|
// Handle validation errors
|
||||||
|
if (error && typeof error === 'object' && 'name' in error && error.name === 'ZodError') {
|
||||||
|
return res.status(400).json({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
error: {
|
||||||
|
code: -32602,
|
||||||
|
message: `Invalid parameters: ${error && typeof error === 'object' && 'message' in error ? String(error.message) : 'Unknown validation error'}`,
|
||||||
|
},
|
||||||
|
id: req.body?.id || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle all other errors
|
||||||
|
return res.status(500).json({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
error: {
|
||||||
|
code: -32603,
|
||||||
|
message: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
},
|
||||||
|
id: req.body?.id || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling MCP request:', error);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
error: {
|
||||||
|
code: -32603,
|
||||||
|
message: 'Internal server error',
|
||||||
|
},
|
||||||
|
id: req.body?.id || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(server: Server): Promise<void> {
|
||||||
|
this.server = server;
|
||||||
|
console.log('Starting HTTP transport with server:', !!this.server);
|
||||||
|
|
||||||
|
// Try to get client from the DiscordMCPServer instance
|
||||||
|
// First, check if the server is passed from DiscordMCPServer
|
||||||
|
if (server) {
|
||||||
|
// Try to access client directly from server._context
|
||||||
|
const anyServer = server as any;
|
||||||
|
let client: Client | undefined;
|
||||||
|
|
||||||
|
if (anyServer._context?.client) {
|
||||||
|
client = anyServer._context.client;
|
||||||
|
console.log('Found client in server._context');
|
||||||
|
}
|
||||||
|
// Also check if the server object has client directly
|
||||||
|
else if (anyServer.client instanceof Client) {
|
||||||
|
client = anyServer.client;
|
||||||
|
console.log('Found client directly on server object');
|
||||||
|
}
|
||||||
|
// Look in parent object if available
|
||||||
|
else if (anyServer._parent?.client instanceof Client) {
|
||||||
|
client = anyServer._parent.client;
|
||||||
|
console.log('Found client in server._parent');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
this.toolContext = createToolContext(client);
|
||||||
|
console.log('Tool context initialized with Discord client');
|
||||||
|
} else {
|
||||||
|
// Create a dummy client for testing - allows list_tools to work
|
||||||
|
console.log('Unable to get Discord client. Creating tool context without client.');
|
||||||
|
this.toolContext = createToolContext({} as Client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a stateless transport
|
||||||
|
this.transport = new StreamableHTTPServerTransport({
|
||||||
|
sessionIdGenerator: undefined // set to undefined for stateless servers
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect the transport
|
||||||
|
await this.server.connect(this.transport);
|
||||||
|
console.log('Transport connected');
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.httpServer = this.app.listen(this.port, () => {
|
||||||
|
console.log(`MCP Server listening on port ${this.port}`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
if (this.transport) {
|
||||||
|
await this.transport.close();
|
||||||
|
this.transport = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.server) {
|
||||||
|
await this.server.close();
|
||||||
|
this.server = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.httpServer) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.httpServer.close(() => {
|
||||||
|
console.log('HTTP server closed');
|
||||||
|
this.httpServer = null;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue